文章目录
本文摘选自《FPGA之道》,一起看看作者总结的VHDL编写的注意事项。
VHDL编写注意事项大小写不敏感
使用VHDL编程的时候,一定要注意,这是一种case insensitive的语言,即大小写不敏感的语言。例如如下几条信号声明的语句对于VHDL来说其实是完全一样的。
signal abc : std_logic;
signal Abc : std_logic;
signal ABC : std_logic;
SIGnal abC : std_LoGic;
SIGNAL ABC : STD_LOGIC;
所以如果你想通过大小写来区分你的信号或变量,肯定是会出错的。例如你想定义两个变量——A和a,这时,编译器会报告一个redefinition(重定义)的错误。
但是,至今为止发现的唯一一个例外就是高阻状态的表示——‘Z’,一定要写成大写,如果写成小写,很多编译器会报错,例如:
data <= a when en = ‘1’ else ‘z’; – error!
data <= a when en = ‘1’ else ‘Z’; – ok!
VHDL中的关键字
命名不能与关键字相同,这几乎是所有编程语言的要求,而VHDL由于是不区分大小写的语言,所以更是需要注意。以下列举了VHDL中的一些关键字,供大家参考:
abs access after alias all and architecture array assert attribute
begin block body buffer bus case component configuration constant
disconnect downto else elsif end entity exit file for function generate generic group guarded if impure in inertial inout is label library linkage literal loop map mod nand new next nor not null of on open or others
out package port postponed procedure process pure range record register
reject rem report return rol ror select severity signal shared sla sll
sra srl subtype then to transport type unaffected untis until use variable
wait when while with xnor xor active ascending ascending base delayed
driving driving_value event falling_edge high image last_active write
last_event last_value left leftof length low pos pred quiet writeline
reverse_range right rightof rising_edge simple_name stable succ transaction
val value bit bit_vector boolean character integer line natural positive
real signed std_logic std_logic_vector string text time unsigned endfile
file_close file_open read readline
多余的符号
在VHDL的语法中,经常有需要添加列表的地方,例如entity中的port list、generic list,process的sensitive signal list,instance的map list等等。这些list中的表达式往往都以“;”或者“,”分隔开来,但是请注意,仅仅是分隔开来,所以最后一个表达式后面都是不需要加分隔符的。例如:
port (
a : in std_logic;
b : in std_logic;
c : out std_logic -- no semicolon here
);
如果你加了,那么这就是一个多余的符号,编译的时候会报语法错误。
纠结的downto 与to
VHDL中的downto与to是表示范围的两个关键字,分别对应从左至右的索引是从大到小还是从小到大排列,它们主要用在定义逻辑向量和数组方面。在这里,我强烈建议大家在代码设计中只使用其中一个关键字,最好是只使用downto,因为它和2进制数的表示一致。混用两种关键字,会带来许多隐患,并且在需要对逻辑向量进行按位操作时,容易选择错误的索引值。当然,这世界上总是有人勇于尝试,乐于探险,为了让他们死的不是很惨,也为了能够深刻大家对downto和to的语法认识,我在这里给大家进行一些分析。
数组范围混用
如果仅仅是在表示数组的范围上面出现了混用,一般倒不太会出什么问题,因为:
type arrayType0 is array (7 downto 0) of <element_type>;
signal testArray0 : arrayType0;
type arrayType1 is array (0 to 7) of <element_type>; -- same element type with arrayType0
signal testArray1 : arrayType1;
testArray0 <= testArray1; -- will report error!!!!
实际上,即使上述的两个数组arrayType0和arrayType1的定义完全一模一样,上述赋值也一定是不能成功的,因为A和B长得一样,并不代表A就是B,B就是A。编译器只知道arrayType0是一个类型,arrayType1是另一个类型,至于它们两个有多像,编译器不关心。所以,数组中的范围混用不会造成太大问题,因为访问数组的时候一般都需要给出数组的索引值,有了索引值的话,范围的定义形式也就无所谓了。
不过这里需要提及一点,有些时候,编译器只会从downto定义范围的数组中,推断出RAM或者ROM的存在,以及正确识别初值语句为RAM或者ROM设定的初值。
逻辑向量范围混用
先看这样一个例子:
signal a :std_logic_vector(3 downto 0) := "1000";
signal b :std_logic_vector(3 downto 0);
signal c,d,e :std_logic_vector(0 to 3);
b <= "1000";
d <= "1000";
c <= a; -- successful assignment
e <= a(0 to 3) -- will report error
为了搞清楚downto和to带来的隐患,我们需要分析a、b、c、d四个逻辑向量在赋值后的排列结果。
先看b,我之前说过,downto和二进制的表示形式一致,即MSB(权重最大的位)在左,LSB(权重最小的位)在右,所以
b <= “1000”;
相当于
b(3)&b(2)& b(1)&b(0) <= “1000”; – 这个赋值是不合法的,只是为了形象说明
因此,b(3) 为‘1’,其他为’0’;
再看d,
d <= “1000”;
相当于
d(0)&d(1)& d(2)&d(3) <= “1000”; – 这个赋值仍然是个形象的说明
因此,d(0) 为‘1’,其他为’0’;
再看c,根据对b的分析,可知a(3)为’1’,其余为’0’,因此
c <= a;
相当于
c(0)&c(1)& c(2)&c(3) <= a(3)&a(2)& a(1)&a(0); --这个赋值仍然是个形象的说明
所以,c(0)为’1’,其余为’0’。
最后看e,
从注释中我们可以看出这句赋值语句报错了,因为逻辑向量a是用downto定义的,但是在赋值的时候却用to访问,这是不允许的。
同样的道理,我们可以推广到任意索引范围的赋值,例如
signal a : std_logic_vector(4 downto 1) := “1011”;
signal b : std_logic_vector(2 to 5);
b <= a; – b(2) = ‘1’, b(3) = ‘0’, b(4) = ‘1’, b(5) = ‘1’
通过以上例子,我们可以清楚认识到downto与to表示的范围与2进制数据之间的按位对应关系。按照这种关系去分析问题,可以帮助我们弄明白一些混用设计的行为结果。好了,无谓的分析就做到这里,只要大家统一代码风格,只用downto来表示范围,就可以避免隐患的存在,减少脑细胞的死亡。
范围中的变量
这里顺便提一下,在早期的版本中,范围中是不支持变量的。例如,
signal a : std_logic_vector(3 downto 0);
signal b : std_logic_vector(2 downto 0);
signal i: integer;
i <= 3;
b <= a(i downto i-2); – may be report error
遇到这种情况,只有改成
for i in 2 downto 0 loop
b(i) <= a(i+1);
end loop;
不过随着版本的进化,范围中的变量已经慢慢可以支持了。从上述例子我们还可以看出,for循环中也有一个范围定义,当然了,这个范围定义跟C语言类似,一般不会造成混淆。
仿真雷区
我们这里的仿真雷区主要针对功能仿真讨论,因为此时输入仿真器的是我们的VHDL代码设计,而仿真器仅仅是按照VHDL语言的表面解释来进行仿真的,而并不是按照对应的具体数字电路来仿真的,所以,如果我们在编写代码时不够仔细,忽略或者丢掉了一些信息,这个时候,虽然最终的FPGA行为是对的,但是仿真的时候会给出错误的结果;亦或仿真的时候给出正确的结果而最终的FPGA行为是错误的也有可能。以下简单列举几条会引起仿真问题的情况供大家参考。
进程敏感量表缺失
Process中的敏感量表对于最终实现的数字电路来说,可以说是可有可无的,因为目前的编译器一般会自动根据process的内部功能代码来完成综合,而并不依赖敏感量表,所以一般不需要担心敏感量表的缺失会引入锁存器。可是仿真器就不一样,它需要等待敏感量表中的信号上发生的事件后才会进入process中执行一次,如果漏写会造成仿真行为不正确。例如:
process© – should be “process(c, d)”
begin
a <= c;
b <= d;
end process;
由于敏感量表中缺少了信号量d,那么仿真的时候,即是d不停的变化,只要c不变,那么b的值就不会改变,显然这不是我们想要的。
进程间语句顺序颠倒
进程内的语句是串行的,因此仿真器会按照进程中的语句是顺序执行的思路来理解代码,因此,一旦代码写的位置不对,就可能会造成仿真的出错。对于描述时序逻辑的进程来说,代码的位置颠倒其实问题不大,因为时序逻辑本身的赋值就是要到下一周期才生效的,而对于组合逻辑,则极有可能会出错。例如:
A : process(a)
begin
c <= b;
b <= a;
end process;
与
B : process(a)
begin
b <= a;
c <= b;
end process;
这两个process的仿真行为会大不一样的,设a初始化为’1’而b初始化为’0’,那么仿真最开始的时候,A进程c=‘0’,而B进程c=‘1’;稍后,若a变为’0’,A进程c=‘1’,而B进程c=‘0’。
所以建议大家在编写代码的时候,尽量按照数据流的传递顺序来安排代码的顺序,这样能避免造成仿真问题。对于以上的例子,其实还有一个解决方法,那就是把中介信号b也添加到敏感量表中去,这样,进程A在a的变化驱动下执行一次,然后这次执行又引起了进程A的另一次执行,从而通过两次执行来给出正确的仿真结果。相比之下,进程B只需要执行一次即给出了正确结果,效率要高的多。尤其是当代码复杂了以后,这种仿真性能的区别就会更加明显。
上例中,若中介b是变量,那么进程A仿真必然出错,并且还没有避免方法,例如:
A : process(a)
variable b : std_logic;
begin
c <= b;
b := a;
end process;
因为variable为进程内部变量无法添加到敏感列表中去。
仿真死循环
组合逻辑如果表述的不好,很容易造成仿真死循环,例如,类似如下的代码都是有问题的:
a <= not a;
b <= m + b;
以上代码如果出现在时序逻辑中,不会有任何问题,因为时序逻辑中的赋值要到下一个周期才会生效,并且每次执行都需要等待时钟事件。但是如果用在组合逻辑中,由于赋值马上生效,一旦生效后的值与之前的值不同,又会激发下一次赋值,如此往复而无穷尽也,所以仿真器会卡在这一时刻无法继续下去。
类似上述的功能代码,对应到数字电路结构上倒是有具体的电路,只不过这种电路的行为是不确定的。
当然,并不是说组合逻辑中一定不能用反馈。其实,时序电路中的触发器和锁存器,就可以利用基本门电路通过一定的反馈连接来实现的。
总结如下,如果组合逻辑中引入的反馈是正反馈(即反馈值会使当前值稳定不变),那么电路结果稳定且有意义;如果引入的反馈是负反馈(即反馈值会使得当前值改变),那么电路结果不稳定也无意义,因此也就无法仿真。
少用生僻语句
正如我们在本篇开头所说,语言在不断发展,仿真器也在不断发展,那么肯定是越常用的语法,越规范的语句,仿真器支持的越好。而那些比较生僻的语句,即便仿真器支持,编译器还不一定支持,所以,建议大家多多使用常用的语法结构进行编程,请放心,这完全不会影响你去实现一个非常非常复杂的设计,反而这才是最好的做法。
危险的variable
VHDL中的变量是一个奇怪的东西,它不具有特定的物理意义。例如,在一个进程中,如果对一个信号多次赋值,那么,只有最后一个值才是有效的;而如果对变量多次赋值,那么每次赋值都是有效的。这也就是说,一个变量有可能对应多个不同的物理连线,这显然不是一种好的表达方式。对于这种情况,我们完全可以用多个信号来对应多段物理连线,这样代码的描述才会和最终的电路更加一致。
除此以外,变量带来的问题还不仅仅于此,由于它的赋值立即生效,是纯软件思路的赋值,因此,对于它的翻译也取决于编译器、仿真器的理解,如果编译器或者仿真器对于这种描述找不到非常正确的硬件结构来对应,那么会出许多莫名其妙的问题。
因此,建议大家在使用VHDL编程的时候,最好不要使用variable。