上面的代码等价于:
C[0] = A[0] ^ B[0], C[1] = A[1] ^ B[1], C[2] = 0 ^ B[2], C[3] = 0 ^ 0。
5.5.7 经验总结
逻辑运算符和位运算符的区别
逻辑运算符包括&&、||、!,位运算符包括&、|、~。那么逻辑运算符和位运算符有什么区别呢?
将逻辑与“&&”和按位与“&”进行对比可以看出,逻辑与运算符的运算只有逻辑真或逻辑假两种结果,即 1 或 0;而“&”是位运算符,用于两个多位宽数据操作。对于位运算符操作,两个数按位进行相与、相或或者非。
上面运行的结果为: a=1’b1,b=1’b1,c=1’b0,d=4’b000,e=4’b1111,f=4’b1000。
5.6 关系运算符
关系运算符有:>(大于)、<(小于)、>=(不小于)、<=(不大于)、== (逻辑相等)和!= (逻辑不等)。
关系操作符的结果为真(1)或假(0)。如果操作数中有一位为 x 或 z,那么结果为 x。
例:
23 > 45 :结果为假(0 )。
52 < 8'hxFF:结果为 x 。
如果操作数长度不同,长度较短的操作数在最重要的位方向(左方)添 0 补齐。例如:
'b1000 > = 'b01110 等价于:'b01000 > = 'b01110,结果为假(0)。
在逻辑相等与不等的比较中,只要一个操作数含有 x 或 z,比较结果为未知(x),
如假定 Data = 'b11x0; Addr = 'b11x0;
那么 Data == Addr,
比较结果不定,即结果为 x
5.7 移位运算符
在 Verilog HDL 中有两种移位运算符,分别为“<<”(左移位运算符)和“>>”(右移位运算符)。
下面分别介绍两者的用法:
5.7.1 左移运算符
在 Verilog HDL 中,用“<<”表示左移运算符。其一般表达式为:
A << n;
其中,A 代表要进行移位的操作数,n 代表要左移多少位。此表达式的意义是把操作数 A 左移 n位。左移操作属于逻辑移位,需要用 0 来填补移出的空位,即在低位补 0。左移 n个0。
以上代码由于左移了 2 位,所以在低位补 2 个零,所以上面代码运行结果是:
a = 4’b1100。
左移操作中有以下三点值得注意的地方:
(1)左移操作是不消耗逻辑资源的,甚至连与门、非门都不需要,它只是线的连接。
上面代码是将信号 b 左移两位并赋给 c,其所对应的硬件电路如下图:
(2)左移操作需根据位宽储存结果
在学习过程中可能看到过如下代码:
4’b1001<<1=4’b0010 与 4’b1001<<1=5’b10010
为什么操作数同样是 4’b1001,都是左移一位,但结果一个是 4’b0010,一个是 5’b10010 呢?
这是因为左移操作后,要看用多少位来存储结果。
上面代码中由于 a 是 4 比特,只能保存 4 位结果,所以 b 左移 1 位赋给 4 bit 的 a,用 0 填补移出的位后结果为 a = 4’b0010 ;
而上面代码中由于 a 是 5 比特,能保存 5 位结果,所以 b 左移 1 位赋给 5 bit 的 a,用 0 填补移出的位后结果为 a = 5’b10010
(3)左移操作的操作数可以是常数,也可以是信号。同样,左移操作的移位数、常数也可以是信号。
上面代码中 cnt 每个时钟加 1,由于是 3 比特,所以值为 0~2。a 则是 4’b1 左移 cnt 位。当 cnt等于 0 时左移 0 位,a 等于 4’b1;当 cnt 等于 1 时左移 1 位,a 等于 4’b10。以此类推,a 的每个时钟变化情况如下所示:
需要注意的是,当移位数是信号时,其综合的电路并不是简单的连线,可能会综合出如下图所示的选择器。然而即便如此,这种硬件电路所消耗的资源依然比较少。
5.7.2 右移运算符
在 Verilog HDL 中,用“>>”表示右移运算符。其一般表达式为:
A >>n;
其中,A 代表要进行移位的操作数,n 代表要右移多少位。此代码表示的意义是把操作数 A 右移n 位。在右移操作中有以下三点值得注意的地方:
(1)右移操作属于逻辑移位,需要用 0 来填补移出的空位,即在高位补 0,补多少个 0,取决
于保存结果的信号的位宽。
4’b0111 右移两位后的结果为 2’b01,由于 a 是 6 位的,2 位赋值给 6 位需要在高位补 0,因此
需要补 4 个 0。所以上面代码运行结果是:
a = 6’b0001
(2)与左移操作相似,右移操作是不消耗逻辑资源的,甚至连与门、非门都不需要,其只是线
的连接。
上面代码是将信号 b 左移两位并赋给 a,其所对应的硬件电路如下图所示
(3)左移操作的操作数可以是常数,也可以是信号。同样,右移操作的移位数可以是常数,也可以是信号。
上面代码中,cnt 每个时钟加 1,由于是 3 比特,所以值为 0~2。a 则是 4’b1000 右移 cnt 位。当 cnt 等于 0 时右移 0 位,a 等于 4’b1000;当 cnt 等于 1 时右移 1 位,a 等于 4’b0100。以此类推,a 的每个时钟变化情况如下图所示。
与左移操作类似,在右移操作中,如果移位数是信号时,其综合的电路就不是简单的连线,而是有可能会综合出如下图所示的选择器。然而同样在这一情况下,这种硬件电路所消耗的资源依然比较少。
5.7.3 经验总结
通过左移乘法运算
FPGA 中要尽量避免乘法运算,因为这种计算需要占用较大的硬件资源,并且运算速度较慢。当不得不使用乘法的时候,尽量乘以 2 的 N 次方,这样在设计中可以利用左移运算来实现该乘法运算,从而大大减少硬件资源。
当乘数是 2 的 N 次方的常数时可以用移位运算来实现乘法。例如:a2,等价于 a<<1;a4 等价于 a<<2;a*8 等价于 a<<3,依此类推。
即使乘数不是 2 的 N 次方的常数,也可以通过移位运算来简化实现。例如:
上面代码中 b 和 c 都可以实现 a127,但第 1 行消耗了一个乘法,而第 2 行则只用到一个减法器。
上面代码中,b 和 c 都可以实现 a67,但第 1 行消耗了一个乘法,而第 2 行则只用到两个加法器,从而节省了资源。
可以注意到,上面两个例子中的乘数都是常数,那么在设计时这种乘法也要花时间和精力来考虑优化吗?其实是不必要的,因为现在综合工具都很强大,当工具发现乘数是常数时会自动按上述过程进行优化,也就是说乘以常数在实质上并不消耗乘法器资源,可以放心使用。
但当出现乘数不是常数的情况时就要注意乘法的使用了。尽量将信号转换为与 2 的 N 次方相关的形式。例如当数据要扩大后来计算时,不要按照惯有思维将数据扩大 100 倍,而是应该直接将其扩大 128 倍。
利用右移实现除法运算
FPGA 设计中要极力避免除法,甚至是严禁使用“ /”来用于除法计算。这是由于除法器会占用极大的资源,其占用资源量要多于乘法器,而且很多时候不能在一个时钟周期内得出结果。而当不得不使用除法的时候,应尽量使除法转化为除以 2 的 N 次方形式,这样便可以利用右移运算来实现该除法运算,从而大大减少硬件资源。
当除数是 2 的 N 次方的常数时,就可以用移位运算来实现除法。例如:a/2,等价于 a>>1;a/4 等价于 a>>2;a/8 等价于 a>>3,依此类推。
与左移不同的是,当除数不是 2 的 N 次方的常数时,不能简单地通过移位运算来简化实现。总而言之,在 FPGA设计中应尽力避免除法。
利用左移位产生独热码
独热码,也叫 one-hot code,就是只有 1 个比特为 1,其他全为 0 的一种码制。例如 8’b00010000,8’b1000000 等。
独热码在设计时非常有用,可以用来表示状态机的状态使状态机更健壮,也可以用于多选一的电路中,表示选择其中的一个。
利用左移位操作,可以方便地产生独热码,例如产生 4’b0010,可以是 4’b1 << 1。类似地,也可以产生 1 个比特为 0,其他为 1 的码制。例如产生 4’b1011,可以是~(4’b1 <<2)。利用左移操作,还可以产生其他需要的数字结果:
例如,产生 5’b00111,可以是(5’b1<<3)-1。
例如,产生 5’b11100,可以是~((5’b1<<2)-1)。
5.8 条件运算符
5.8.1 三目运算符
Verilog HDL 语法中条件运算符(?:)带有三个操作数(即三目运算符),其格式一般表达为:
其含义为:当“条件表达式”为真(即逻辑 1),执行“真表达式”;当“条件表达式”为假(即逻辑 0),执行“假表达式”。即当 condition_expr 为真(即值为 1),选择 true_expr;如果 condition_expr 为假(值为 0),选择 false_expr。如果 condition_expr 为 x 或 z ,结果将是按以下逻辑 true_expr 和 false_expr 按位操作的值: 0 与 0 得 0,1 与 1 得 1,其余情况为 x 。
应用举例如下:
在上面的表达式中 s 如果为真,则把 t 赋值给 r;如果 s 为假,则把 u 赋值给 r。
对应硬件电路图如下所示:
条件运算符的使用有以下几点需要注意的地方:
(1)条件表达式的作用实际上类似于多路选择器,如图 1.3-8 所示。同时,其可以用 if-else 语句来替代。
(2)条件运算符可用在数据流建模中的条件赋值,这种情况下条件表达式的作用相当于控制开关。例如:
其中,表达式 Marks > 18 如果为真,则 Grade_A 赋值为 student;如果 Marks > 18 为假,则Grade_C 赋值为 student。
对应硬件电路图如下所示。
(3)条件运算符也可以嵌套使用,每个“真表达式”和“假表达式”本身就可以是一个条件表达式。例如:
上面代码所代表的含义是:表达式 M == 1 如果为真,则判断 CTL 是否为真,如果 CTL 为真就将 A 赋值给 OUT,如果为假就将 B 赋值给 OUT;如果 M = = 1 为假,则判断 CLT 是否为真,如果CLT 为真就将 C 赋值给 OUT,如果为假就将 D 赋值给 OUT。
对应硬件电路图如下所示。
5.8.2 if语句
“if”语句的语法如下:
if(condition_1) procedural_statement_1; {else if(condition_2) procedural_statement_2}; {else procedural_statement_3};
其含义为:如果对 condition_1 条件满足,不管其余的条件是否满足,都执行 procedural_statement_1,procedural_statement_2 和 procedural_statement_3 都不执行。
如果 condition_1 不满足而 condition_2 满足,则执行 procedural_statement_2,而 procedural _statement_1 和 procedural_statement_3 都不执行。
如果 condition_1 不满足并且 condition_2 也不满足时,执行 procedural_statement_3,而 procedural_statement_1 和 procedural_statement_2 都不执行。
通过下面一个例子来具体说明:
if(Sum < 60) begin Grade = C; Total_C = Total _C + 1; end else if(Sum < 75) begin Grade = B; Total_B = Total_B + 1; end else begin Grade = A; Total_A = Total_A + 1; end
注意条件表达式必须总是用括号括起来,如果使用 if - if - else 格式,那么可能会有二义性,如下面的示例所示:
if(Clk) if(Reset) Q = 0; else Q = D;
这里存在一个疑问:最后一个 else 属于哪一个 if 语句? 是属于第一个 if 的条件(Clk)还是属于第二个 if 的条件 (Reset)? 这在 Verilog HDL 中已通过将 else 与最近的没有 else 的 if 语句相关联来解决。在这个例子中, else 与内层 if 语句相关联。
下面再举一个 if 语句的例子:
if(Sum < 100) Sum = Sum + 10; if(Nickel_In) Deposit = 5; Elseif (Dime_In) Deposit = 10; else if(Quarter_In) Deposit = 25; else Deposit = ERROR;
建议:
1、条件表达式需用括号括起来。
2、若为 if - if 语句,请使用块语句 begin — end,如下所示。
if(Clk) begin if(Reset) Q = 0 else Q = D; end
以上两点建议是为了使代码更加清晰,防止出错。
5.8.3 case 语句
case 语句是一个多路条件分支形式,其语法如下:
case(case_expr) case_item_expr{case_item_expr} :procedural_statement . . . . . . [default:procedural_statement] endcase
case 语句下首先对条件表达式 case_expr 求值,然后依次对各分支项求值并进行比较,执行第一个与条件表达式值相匹配的分支中的语句。可以在 1 个分支中定义多个分支项,且这些值不需要互斥。缺省分支覆盖所有没有被分支表达式覆盖的其他分支。
例:
case (HEX) 4'b0001 : LED = 7'b1111001; // 1 4'b0010 : LED = 7'b0100100; // 2 4'b0011 : LED = 7'b0110000; // 3 4'b0100 : LED = 7'b0011001; // 4 4'b0101 : LED = 7'b0010010; // 5 4'b0110 : LED = 7'b0000010; // 6 4'b0111 : LED = 7'b1111000; // 7 4'b1000 : LED = 7'b0000000; // 8 4'b1001 : LED = 7'b0010000; // 9 4'b1010 : LED = 7'b0001000; // A 4'b1011 : LED = 7'b0000011; // B 4'b1100 : LED = 7'b1000110; // C 4'b1101 : LED = 7'b0100001; // D 4'b1110 : LED = 7'b0000110; // E 4'b1111 : LED = 7'b0001110; ,// F default:LED = 7'b1000000; // 0 endcase
书写建议: case 语句的缺省项必须写,防止产生锁存器。
5.8.4 选择语句
Verilog 语法中有一个常用的选择语句,其语法形式为:
vect[a +: b] 或 vect [a -: b];
vect 为变量名字,a 为起始位置,加号或者减号代表着升序或者降序,b 表示进行升序或者降序的宽度。
vect[a +: b]等同于 vect[a : a+b-1],vect 的区间从 a 开始向大于 a 的方向进行 b 次计数;vect[a -: b]等同于 vect[a : a-b+1],vect 的区间从 a 开始向 a 小的方向进行 b 次计数。
a 可以是一个常数也可以是一个可变的数,但 b 必须是一个常数。
例 1:vect[7 +: 3];
其中,起始位置为 7,+代表着升序,宽度为 3。即从 7 开始向比 7 大的方向数 3 个数。其等价形式为:
vect[7 +: 3]==vect[7 : 9]。
例 2:vect[9 -: 4];
其中,起始位置为 9,-代表着降序,宽度为 4。即从 9 开始向比 9 小的方向数 4 个数。其等价形式为:
vect[9 -: 4]==vect[9 : 6]。
在实际使用的过程中该语法最常用的形式是将 a 作为一个可变的数来使用。
例如需要设计具有如下功能的代码:
当 cnt0 时,将 din[7:0]赋值给 data[15:8];当 cnt1 时将 din[7:0]赋值给 data[7:0]。
在设计的时候便可以写成:data[15-8cnt -: 8] <= din[7:0](此时需要将 15-8cnt 视为一个整体 a,其会随着 cnt 变化而发生变化),这样一来就完成了对代码的精简工作。
选择语句的硬件电路结构如下图所示,其本质上是一个选择器。当 cnt0 时,选中 data[15:8]的锁存器,将 din[7:0]赋值给 data[15:8],而 data[7:0]的锁存器保持输出不变;当 cnt1 时,选中 data[7:0]的锁存器,将 din[7:0]赋值给 data[7:0],而 data[15:8]的锁存器保持输出不变
用 Modelsim 对以上例子进行功能仿真后的仿真图如下所示。
可以看出仿真结果满足实际的设计需求。
经验总结:在实际工程中可以多使用选择语句 vect[a +: b]或 vect [a -: b]的形式来进行代码编写,这将有助于精简设计代码。
5.8.5 经验总结
if 语句和 case 语句是 Verilog 里两个非常重要的语句, if 和 case 语句有一定的相关性,也有一定的区别。相同的地方在于两者几乎可以实现一样的功能,下面主要介绍一下两者之间的区别。
if 语句每个分支之间是有优先级的,综合得到的电路是类似级联的结构。case 语句每个分支是平等的,综合得到的电路则是一个多路选择器。因此,多个 if else-if 语句综合得到的逻辑电路延时有可能会比 case 语句稍大。对于初学者而言,在一开始学习 Veriolg 的过程中往往喜欢用 if else-if 语句,因为这种语法表达起来更加直接。但是在运行速度比较关键的项目中,使用 case 语句的效果会更好。
下面通过一个具体的案例来对比一下,使用 if 语句和 case 语句来描述同一功能电路的综合结果。
首先是用 if 语句写的代码。
其综合后的 RTL 视图如下所示。
从上图中所示的 RTL 图可以看到,此电路中含有两个二选一多路选择器,并且右边的优先级要高于左边(因为 q 的值是直接与右边的选一选择器连接),当 en[0]不为 1 时才会继续判断 en[1]。
也就是说,在 if 语句下综合出的电路含有优先级。接下来分析一下使用 case 语句描述的代码:
其综合后的 RTL 视图如下所示:
可以看出,使用 case 语句编写的逻辑代码综合出的电路是并行的,没有优先级,不影响电路的运行速度。
虽然在 RTL 视图中,两种语句综合出的电路存在着较大的差别,但是由于目前的开发工具已经足够智能,在布局布线的时候会自动对电路进行优化,其最终映射到 FPGA 内部的电路之间基本毫无差别。
最后总结一下 if 语句和 case 语句之间的区别与联系:
If 语句具有优先级,当 if 下的条件不满足时才执行 else 后面的部分。而 case 语句是并行的,没有优先级,这在两者综合出来的 RTL 视图中可以明显的观察出来。但由于现在的仿真和综合工具已经足够强大,最后综合后的结果 if…else…与 case…语句其实并无不同,只不过是两种不同的实现方式而已,因此基本上不用考虑这两者间的区别。在不影响功能的前提下设计师不需要做局部的优化工作,例如不需要考虑 if/case 语句的资源耗费差异、不需要考虑优化电路。只有在影响功能(即时序约束报错)的前提下,根据提示对电路进行优化。
5.9拼接运算符
拼接操作是将小表达式合并形成大表达式的操作,其形式如下:
{expr1, expr2, . . .,exprN} ;
拼接符是不消耗任何硬件资源的,其只是把线换一种组合方式,可以参照如下实例:
由于非定长常数的长度未知, 不允许连接非定长常数。因此如下所示代码是不符合语法规定的。
{Dbus,5} ;/ /不允许连接操作非定长常数。