VHDL语言与其他高级语言一样,编写程序时也要遵循一定的语法规则。下面介绍VHDL的语言要素及其规则。
VHDL标识符分为短标识符和扩展标识符两类。其中短标识符由VHDL87版本规定,在此基础上,VHDL93版本进行了扩展,提出了扩展标识符。
(1)短标识符短标识符由26个大小写英文字母、数字0~9以及下画线“_”中的字符构成,首字符必须是英文字母,“_”前后必须有英文字母或数字,不分大小写,且VHDL定义的保留字或关键字不能用作标识符。多个标识符间用“,”隔开。例如:
Mux21a, mux21a, mux21_a
(2)扩展标识符
扩展标识符由反斜杠界定,反斜杠内除了可以包含26个大小写英文字母、数字0~9以及下画线“_”外,还允许包含图形符号、空格符、关键字或保留字,且扩展标识符区分大小写。例如:
\Mux21a\, \mux21a\, \21mux_a\, \21_mux_a\
注意\Mux21a\和\mux21a\不同。
数据对象是指程序中可以被赋值的载体。VHDL的数据对象有常数(constant)、信号(signal)、变量(variable)和文件(files)四种类型,前三种属于可综合的数据对象,第四种是为传输大量数据而定义的一种数据载体,仅在行为仿真时使用。VHDL的数据对象在使用前必须说明。
1)常数
常数为全局量,相当于电路中的恒定电平,如gnd和vcc。常数说明的一般格式如下:
constant常数名表: 数据类型[: =表达式];
例如:
constant bus: bit_vector :="01011"; constant bus_width: integer :=8;
常数一般要赋初始值,主要用于在entity、architecture、package、process、procedure、function中保持静态数据,以改善程序的可读性,并使修改程序容易。
2)信号
信号具有全局性,主要用于在entity、architecture和package中定义端口或内部连线,在元件间起互联作用;或作为一种数据容器,以保留历史值或当前值。信号传输过程具有延迟特性。信号说明的一般格式如下:
signal信号名表: 数据类型[: =表达式];
其中表达式用于为信号赋初值,通常不建议采用,初值仅在仿真时有用,在实际综合时将被忽略。信号的赋值采用如下格式:
目标信号名<=表达式;
例如:
signal count: bit_vector (3 downto 0); count <=count+1;
信号的赋值既可以采用整体赋值,也可以采用部分赋值,例如:
注意信号的赋值不是立刻发生的,需要经过一定的延时,具体延时取决于所用FPGA的物理性质,即器件的固有延时。仿真时,仿真模拟器会附加一个仿真器的最小分辨时间δ,δ延时后执行赋值操作。
3)变量
变量属于局部量,只能在进程和子程序(函数、过程)中使用,用于计算或暂存中间数据。变量说明的一般格式如下:
variable变量名表: 数据类型[: =表达式];
变量赋值采用如下格式:
目标变量名 : =表达式;
举例如下:
信号与变量作为VHDL电路设计中经常要用到的数据对象,都与一定的物理对象相对应,相当于组合电路中门与门之间的连线,以及连线上的信号值,但是两者之间也有许多不同之处,使用时应当特别注意。主要表现在以下四点:
(1)物理意义不同:信号是全局量,可以作为模块间的信息载体,对应电路设计中一条硬件连接线,可以用于进程之间的联系;变量是局部量,只能作为局部的信息载体,用来暂存某些值。
(2)赋值符号不同:信号赋值用“<=”符号;变量赋值用“:=”符号。
(3)定义位置不同:信号应当在结构体(architecture)、包(package)、实体(entity)的说明语句部分定义。变量则在进程(process)、函数(function)和过程(procedure)的说明部分定义。
(4)附加延时不同:信号赋值语句执行时有附加延时;变量的赋值是一种理想化的数据传输,没有延时,立刻执行。
另外注意在VHDL中有下述所示的信号赋值语句,例如:
a <=b after 5 ns;
把b的值延迟5ns后赋给信号a。此类带有明确延迟时间的语句在大多数的综合器中是不支持的,其中的时间延迟将会被忽略,部分综合器必须去掉“after时间表达式”部分,此部分仅仅在仿真时的测试程序中可用,因此带延迟时间的语句,诸如“after xx ns”、“wait for xx ns”在综合时,要尽量避免使用。
下面两个进程描述语句,进一步说明信号与变量的不同。
进程1中信号d有两条赋值语句,即有两个驱动源:a和c。当进程执行时,具有多个驱动源的赋值语句只有最后一个起作用,所以d的数值是c,程序执行的结果是:x=b+c;y=b+c。进程2中由于d是变量,没有延时立即执行,因此执行语句d:=a后,a的值赋给d,所以在执行语句x <=b+d后,x=b+a;接着又执行语句d:=c,c的值又赋给d,所以执行语句y <=b+d之后,y=b+c。程序执行的结果是:x=b+a;y=b+c。
VHDL属于强类型语言,每个数据对象都具有特定的数据类型,数据对象进行的操作必须与其数据类型相匹配。VHDL提供了多种标准数据类型以及用户自定义的数据类型。
1)标准数据类型
VHDL的标准数据类型为VHDL预定义数据类型,包含10种类型,如表2-3所示。
表2-3 标准数据类型
对于标准数据类型,有几点需要注意:
(1)实数类型在书写时,一定要有小数。
(2)不是所有的综合工具都支持上述10种标准数据类型。Quartus Prime综合工具不支持实数、时间、错误等级和字符串等数据类型,这些类型只在VHDL仿真器中使用。
(3)数据除定义类型外,有时还需要定义约束范围,限定数据的取值范围,否则综合器不予综合。例如下面的代码中,黑体部分描述的均为约束范围。
variable a: integer range-63 to 63; signal b: bit_vector (7 downto 0) signal c: real range 2.0 to 10.0
(4)字符类型在使用时,用单引号括起来,且对大小写敏感,例如:'B'不同于'b'。
(5)时间类型的范围和整型一样,表达时要包括数值和单位两部分,且整数数值和单位之间应有空格,单位如表2-3所示,例如:5ns、10ps,时间类型一般用于仿真。
(6)错误等级用于在仿真时表示系统工作的状态。
下面对整数和矢量的表示形式作一下说明:
(1)整数类型的表示形式除了默认的十进制数字的表示形式外,还有数字基数的表示形式。以十进制数230为例,
默认的十进制表示形式为:230、23E1、2_30。
基数表示形式为:10#230、10#23#1、2#1110_0110#、8#346#、16#E6#。
完整的基数表示形式由五部分组成:十进制数表示的进制基数、数制隔离符#、整数数值、指数隔离符#、十进制数表示的指数部分。
(2)矢量(bit_vector、std_logic_vector)的表示形式除了默认的二进制串表示形式外,也可采用八进制或十六进制串表示,三种进制的数分别用基数符号‘B’、‘O’、‘X’表示,例如上例中的变量赋值b:="01001011"也可写为如下三种形式:
b : =B"01001011"; b : =O"113"; b : =X"4B";
2)用户自定义的数据类型
在VHDL语言的使用过程中,可以由用户自己定义数据类型。用户定义的数据类型书写格式为:
type数据类型名 is 数据类型定义;
可以由用户定义的数据类型有枚举类型、整数类型、实数和浮点数类型、数组类型、存取类型(ACCESS)、文件类型(FILE)、记录类型和物理类型等。这里只介绍Quartus Prime综合工具支持的常用用户定义数据类型。
(1)整数类型
用户定义的整数类型可以认为是VHDL预定义整数的一个子类。定义格式如下:
type数据类型名 is 数据类型定义约束范围;
例如:
type a is integer range-63 to 63;
(2)枚举类型
枚举类型的所有值都由设计者自己定义,枚举类型常用来建立抽象的模型,例如定义状态机中的状态。定义格式如下:
type数据类型名 is (元素, 元素, …)
例如:
枚举类型可以用于信号和变量的说明,例如:
(3)数组类型
相同类型的数据集合形成的数据类型就是数组类型,分一维数组和二维数组、限定性和非限定性数组,定义格式如下:
type数组类型名 is array (范围)of 原数据类型名;
例如:
type byte is array (7 downto 0)of bit; type word is array (63 downto 0)of byte; type bit_vector is array (integer range <>)of bit;
定义了byte为一个长度为8的一维数组,数组中的每一个元素的类型为bit类型,实际上,byte是一个位长为8的bit_vector;定义word为一个长度为64的数组,数组中的每一个元素的类型为byte类型,相当于定义了一个两维数组;定义bit_vector为一个非限定性数组。
数组元素的排列既可以用升序(to),也可以用降序(downto),推荐使用降序。另外可以利用关键字“subtype”对已有的数据类型加以约束,定义一些子类型,如上例对已定义的数据类型做一些范围限制而形成的一种新的数据类型,子类型的名称通常采用用户容易理解的名字。子类型定义的一般格式为
subtype子类型名 is 数据类型名[范围];
例如:
subtype my_vector is bit_vector (0 to 15);
指定my_vector为上述定义的非限定性数组bit_vector的子类型。
通过指定下标或给定下标范围,可以访问数组中的单个元素或访问部分数组,例如:byte(2)、word(3 downto 1)等。
数组常在总线、ROM和RAM中使用。
(4)记录类型
记录类型是将不同类型数据和数据名组织在一起形成的新类型。记录类型定义的形式如下:
type数据类型名 is record 元素名: 数据类型名; 元素名: 数据类型名; … end record;
例如:
记录经常用于描述总线和通信协议。
通常,用户定义的数据类型和子类型都放在程序包中定义,通过use语句调用。
3)IEEE标准数据类型“STD_LOGIC”和“STD_LOGIC_VECTOR”
在IEEE的std_logic_1164程序包中,定义了两种符合数字电路设计工业标准的逻辑类型:STD_LOGIC和STD_LOGIC_VECTOR。以STD_LOGIC为例,它包含9种取值,分别为:U(未初始化)、X(强未知)、0(强0)、1(强1)、Z(高阻)、W(弱未知)、L(弱0)、H(弱1)、-(忽略)。其中,U、X、W三种取值只用于仿真,其他取值既可用于仿真,又可用于综合。
STD_LOGIC和STD_LOGIC_VECTOR类型更接近于物理实际,增加了VHDL语言编程、综合和仿真的灵活,若电路中有三态逻辑(Z),则必须用STD_LOGIC和STD_LOGIC_VECTOR类型。
在IEEE的std_logic_arith程序包中,定义了整数的两种数据类型:无符号数unsigned、有符号数signed。例如十进制数5若定义为无符号数,则综合器将此数解释为二进制数“101”,若定义为有符号数,则综合器将此数解释为二进制数“0101”,其中最高位‘0’为符号位,表示正数,十进制数-5则解释为补码表示的二进制数“1011”,最高位‘1’为符号位,表示负数。两种类型的定义举例如下:
signal a: unsigned (3 downto 0); variable b: signed (3 downto 0);
其中a(3)为a的最高位,b(3)为b的符号位,b(2)为b的最高位。
4)数据类型的转换
在VHDL语言中,数据类型的定义是非常严格的,不同数据类型的数据不能进行运算和直接代入。为了进行运算和代入操作,必要时需要进行数据类型之间的转换。数据类型的转换函数如表2-4所示,转换函数通常由VHDL包集合提供,因此在使用转换函数之前,需要使用library和use语句,使包集合可以使用。
表2-4 数据类型转换函数
例如,假设信号data是一个3位宽的逻辑向量,定义如下:
signal data: std_logic_vector (2 downto 0);
我们想通过数据类型转换函数conv_integer把它转换成一个整型数,并赋值给信号num。则程序设计中必须包含如下部分:
(1)包含转换函数conv_integer的库的调用
(2)根据逻辑向量的范围确定整数num的范围
由于data的位宽为3位,所以num的数值最大为7,即可满足数据转换的要求。因此在结构体的说明部分,我们对整数num定义如下:
signal num: integer range 0 to 7;
(3)类型转换函数的调用
在结构体中,通过如下的函数调用,即可实现数据类型的转换。
num <=conv_integer (data);
属性提供的是关于信号、类型等的指定特性,属性的一般书写格式为
客体'属性名
用单引号'指定属性,单引号后面跟属性名,单引号前面是所附属性的对象。信号又可以分为值类属性和函数类属性两种。
1)值类属性
值类属性用于返回有关数据类型或数组类型的特定值,还可返回数组的长度或者类型的最底边界,常用的有'left、'right、'high、'low、'length、'range等。属性'left生成一个类型最左边的值;属性'right是生成一个类型最右边的值;属性'high生成一个类型的最大值;属性'low生成类型的最小值;属性'length生成限制性数组中的元素数;属性'range生成限制性数组对象的范围。例如,如果
type count is integer range 0 to 127;
则有
count'left=0, count'right=127, count'high=127, count'low=0, count'length=128, count'range= 0 to 127;
2)函数类属性
函数类属性可以用来得到信号的行为信息和功能信息。例如信号是否发生了值的变化、信号最后一次变化到现在经历的时间、信号变化之前的值等,常用的有'event、'active、'last_event、'last_value、'last_active。
signal'event表明如果在当前相当小的一段时间间隔内,信号signal有事件发生,则'event属性函数返回一个“true”的布尔量,否则返回“false”,常用来检查时钟边沿是否有效。例如:
如图2-6所示,信号在运行的过程中,有一些未知的状态,例如“X”状态,如果我们将上升沿的判断再加一个限定条件,则可以增加电路运行的确定性。改善后的上升沿判断语句为:
图2-6 信号值的变化
if clk'event and clk='1'and clk'last_value='0'then
修改以后的if语句可以避免“X→1”状态变化引起的误触发。
signal'active表明若在当前仿真周期中,信号signal上有一个事务,则signal'active返回“true”值,否则返回“false”值。
signal'last_event属性函数返回信号最后一次发生的事件到现在时刻所经历的时间。
signal'last_value属性函数返回信号最后一次变化前的值。
signal'last_active返回一个时间值,即从信号最后一次发生的事务(即上一次信号活跃)到现在的时间长度。
3)由属性生成信号
利用VHDL的属性,还可以生成一类特别的信号,以所加的属性函数为基础和规则而形成,即带属性函数的信号,包含了属性函数所增加的有关信息。此类信号主要用在仿真环节中,主要有以下几种。
(1)signal 'delayed[(time)]属性函数将产生一个延时的信号,该信号在signal经过time表达式所确定的时间延时后得到。
(2)signal'stable[(time)]表示若在表达式time规定的时间内,信号signal是稳定的,没有事件发生,则返回一个“真”值,否则返回“假”值。
(3)signal'quiet[(time)]表示若信号signal在时间表达式time指定的时间内没有事务要处理,则返回一个“真”值,否则返回“假”值。
(4)signal'transaction属性将建立一个bit类型的信号,当属性所加的信号有事务时,其值都将发生变化。信号signal'transaction上的一个事件表明在signal上有一个事务。
图2-7 由a生成的b和c信号
例如:
b <=a'delayed (4ns); c <=a'stable (10ns);
a、b、c信号如图2-7所示。
需要注意的是,EDA综合软件不同对预定义属性的支持程度也各不相同,使用时应参考特定的综合工具说明。
VHDL定义了丰富的运算符,主要有算术运算符、关系运算符、逻辑运算符、赋值运算符、关联运算符和其他运算符。需要注意的是,操作数的数据类型应当与操作符所要求的数据类型一致。到目前为止,VHDL共有3个版本:VHDL87、VHDL93和VHDL2002。不同的版本对操作符的支持程度不同,具体可参见VHDL的参考手册。
1)算术运算符(见表2-5)
表2-5 算术运算符
乘方运算的左边可以是整数或实数,右边必须是整数,且只有左边为实数时,其右边才可以为负数。乘方运算只有在操作数是常数或2的乘方时,才能被综合。除法运算只有在除数为2的幂次时,才能被综合。
2)关系运算符(见表2-6)
表2-6 关系运算符
要注意从程序的上下文区别关系运算符小于或等于“<=”和信号赋值运算符的不同。
3)逻辑运算符(见表2-7)
表2-7 逻辑运算符
其中sll将逻辑型数据左移,右端空出来的位置填充“0”;srl将逻辑型数据右移,左端空出来的位置填充“0”。而sla将逻辑型数据左移,同时复制最右端的位,在数据左移操作后填充在右端空出的位置上;sra将逻辑型数据右移,同时复制最左端的位,在数据右移操作后填充在左端空出的位置上。
循环逻辑左移rol将数据左移,同时从左端移出的位依次填充到右端空出的位置上,循环逻辑右移ror将数据右移,同时从右端移出的位依次填充到左端空出的位置上。其语法结构为:
<左操作数> <移位操作符> <右操作数>
其中,左操作数必须是bit_vector类型或boolean型的一维数组,右操作数必须是integer类型(前面可以加正负号)。
例如:
在此需要注意的是移位运算符是在VHDL93中引入的,如果在VHDL87下编译,则会提示出现操作符未定义的错误。
4)其他运算符(见表2-8)
表2-8 其他运算符
并置运算符“&”用于位连接,例如:
a <="1001"; b <="1100"; c <=a&b;
则:c为"10011100"。
在所有的运算符中,其优先级顺序按照从高到低依次为
(1)乘方(**)、取绝对值(abs)、非(not)运算;
(2)乘(*)、除(/)、取模(mod)、求余(rem)运算;
(3)正(+)、负(-)号运算符;
(4)加(+)、减(-)、并置运算符;
(5)移位运算符;
(6)关系运算符;
(7)逻辑运算符。
在同一项中,运算符的优先级相同,在表达式中按“从左到右”的顺序依次计算,因此为了防止出错,建议在表达式中,不同的运算符之间尽量使用圆括号“()”。
另外,运算符在使用时,需要注意以下几个问题:
(1)尽可能用加法运算实现其他算术运算,以节约硬件资源。例如乘法运算符常常使逻辑门数大大增加。
(2)操作数的长度必须一致。操作符不同时,尽量加括号。
(3)移位操作的操作数必须是一维数组,数组中的元素必须是bit或布尔数据类型。
需要注意的是,EDA综合软件对运算符支持程度各不相同,使用时应参考综合工具的说明。