第5节 功能描述-组合逻辑

5.1 程序语句
5.1.1 assign 语句

assign 语句是连续赋值语句,一般是将一个变量的值不间断地赋值给另一变量,两个变量之间就类似于被导线连在了一起,习惯上当做连线用。 assign 语句的基本格式是:

assign a = b (逻辑运算符) c …;

assign 语句的功能属于组合逻辑的范畴,应用范围可以概括为一下几点:

(1)持续赋值

(2)连线

(3) 对 wire 型变量赋值, wire 是线网,相当于实际的连接线,如果要用 assign 直接连接,就用 wire 型变量, wire 型变量的值随时发生变化。

需要说明的是,多条 assign 连续赋值语句之间互相独立、 并行执行。

5.1.2 always 语句

always 语句是条件循环语句,执行机制是通过对一个称为敏感变量表的事件驱动来实现的,下面会具体讲到。 always 语句的基本格式是:

always @(敏感事件)begin

程序语句

end

always 是“一直、总是”的意思, @后面跟着事件。整个 always 的意思是:当敏感事件的条件满足时,就执行一次“程序语句”。敏感事件每满足一次,就执行“程序语句”一次。 (敏感事件中的敏感条件出现变化时,执行always条件循环语句中的内容

image-20211029113455362

这段程序的意思是: 当信号 a 或者信号 b 或者信号 d 发生变化时,就执行一次下面语句。在执行该段语句时,首先判断信号 sel 是否为 0,如果为 0,则执行第 3 行代码。 如果 sel 不为 0,则执行第 5 行代码。需要强调的是, a、 b、 c 任意一个发生变化一次, 2 行至 5 行也只执行一次,不会执行第二次。

此处需要注意,仅仅 sel 这个信号发生变化是不会执行第 2 行到 5 行代码的, 通常这并不符合设计者的想法。例如,一般设计者的想法是: 当 sel 为 0 时 c 的结果是 a+b;当 sel 不为 0 时 c 的结果是 a+d。但如果触发条件没有发生改变, 虽然 sel 由 0 变 1, 但此时 c 的结果仍是 a+b。 因此, 这并不是一个规范的设计思维。

因此,按照设计者的想法重新对代码进行设计:当信号 a 或者信号 b 或者信号 d 或者信号 sel发生变化时,就执行 2 行至 5 行。这样就可以确保 sel 信号值为 0 时, c 的结果一定为 a+b, 当 sel 不为 0 时, c 的结果一定是 a+d。 因此要在敏感列表中加入 sel, 其代码如下所示。

image-20211029113540684

当敏感信号非常多时很容易就会把敏感信号遗漏,为避免这种情况可以用“ * ”来代替。这个“ *”是指“程序语句”中所有的条件信号, 即 a、 b、 d、 sel(不包括 c) , 也推荐这种写法,其具体代码如下所示。

image-20211029113600878

这种条件信号变化结果立即变化的 always 语句被称为“组合逻辑”。

image-20211029113618567

上述代码敏感列表是**“ posedge clk”,其中 posedge 表示上升沿**。也就是说, 当 clk 由 0 变成1 的瞬间执行一次程序代码,即第 2 至 5 行, 其他时刻 c 的值保持不变。要特别强调的是: 如果 clk没有由 0 变成 1,那么即使 a、 b、 d、 sel 发生变化, c 的值也是不变的。

image-20211029113640663

可以看到上述代码的敏感列表是“ negedge clk”,其中 negedg 表示下降沿。也就是说,当 clk 由 1 变成 0 的瞬间执行一次程序代码,即第 2 至 5 行, 其他时刻 c 的值保持不变。要特别强调的是,如果 clk 没有由 1 变成 0,那么即使 a、 b、 d、 sel 发生变化, c 的值也是不变的。

image-20211029113709007

上述代码的敏感列表是“ posedge clk or negedge rst_n”,也就是说,当 clk 由 0 变成 1 的瞬间,或者 rst_n 由 1 变化 0 的瞬间,执行一次程序代码,即第 2 至 8 行, 其他时刻 c 的值保持不变。这种信号边沿触发,即信号上升沿或者下降沿才变化的 always, 被称为“时序逻辑”, 此时信号 clk 是时钟。注意: 识别信号是不是时钟不是看名称,而是看这个信号放在哪里,只有放在敏感列表并且是边沿触发的才是时钟。而信号 rst_n 是复位信号, 同样也不是看名字来判断,而是放在敏感列表中且同样边沿触发,更关键的是“程序语句”首先判断了 rst_n 的值, 这表示 rst_n 优先级最高,一般都是用于复位。

设计时需要注意以下几点

1、*组合逻辑的 always 语句中敏感变量必须写全,或者用“ ”代替

2、组合逻辑器件的赋值采用阻塞赋值“ =, 时序逻辑器件的赋值语句采用非阻塞赋值“ <=”

具体原因见“阻塞赋值和非阻塞赋值”一节内容。

5.2 数字进制
5.2.1 数字表示方式

在 Verilog 中的数字表示方式,最常用的格式是: <位宽>’<基数><数值>,如 4’b1011。位宽:描述常量所含位数的十进制整数,是可选项。例如 4’b1011 中的 4 就是位宽, 通俗理解就是 4 根线。如果没有这一项可以通过常量的值进行推断。例如’b1011 可知位宽是 4,而’b10010 可推断出位宽为 5。

基数:表示数值是多少进制。可以是 b, B, d, D, o, O, h 或者 H,分别表示二进制、十进制、八进制和十六进制。如果没有此项,则缺省默认为十进制数。例如,二进制的 4’b1011 可以写成十进制的 4’d11,也可以写成十六进制的 4’hb 或者八进制的 4’o13,还可以不写基数直接写成 11。 综上所述,只要二进数相同, 无论写成十进制、八进制和十六进制都是同样的数字。

数值:是由基数所决定的表示常量真实值的一串 ASCII 码。如果基数定义为 b 或 B,数值可以是 0, 1, x, X, z 或 Z。如果基数定义为 o 或 O,数值可以是 2, 3, 4, 5, 6, 7。如果基数定义为h 或 H,数值可以是 8, 9, a, b, c, d, e, f, A, B, C, D, E, F。对于基数为 d 或者 D 的情况,数值符可以是任意的十进制数: 0 到 9, 但不可以是 x 或 z。例如, 4’b12 是错误的,因为 b 表示二进制,数值只能是 0、 1、 x 或者 z,不包含 2。 32’h12 等同于 32’h00000012, 即数值未写完整时,高位补 0。

5.2.2 二进制是基础

在数字电路中如果芯片 A 给芯片 B 传递数据,例如传递 0 或者 1 信息,可以将芯片 A 和芯片 B通过一个管脚进行相连,然后由芯片 A 控制该管脚输出为高电平或者低电平,通过高低电平来表示 0和 1。芯片 B 检测到该管脚为低电平时,表示收到 0, 芯片 B 检测到该管脚为高电平时,表示收到 1。

image-20211029114040717

反之, 如果用低电平表示收到 1,用高电平表示收到 0 可不可以呢?当然可以,只要芯片 A 和芯片 B 事先协定, 芯片 A 要发数字 1 时会将该管脚置为低电平。芯片 B 检测到该管脚为低电平, 表示收到了数字 1,通信完成。

image-20211029114120140

一个管脚拥有高低电平两种状态,可以分别表示数字 0 和 1 的两种情况。如果芯片 A 要发数字0、 1、 2、 3 给芯片 B 又要如何操作呢?

可以让芯片 A 和芯片 B 连接两根管脚,即两条线: a 和 b。当两条线都为低电平时,表示发送数字 0;当 a 为高电平 b 为低电平时,表示发送数字 1;当 a 为低电平 b 为高电平时,表示发送数字 2;当两条线都是高电平时,表示发送数字 3。

image-20211029114157781

按照同样的道理,芯片 A 要发送数据 4, 5, 6, 7 给芯片 B 时,只要再添加一条线就可以了。三根线一共有 8 种状态,可以表示 8 个数字。综上所述,线的不同电平状态可以表示不同的含义, 有多少种不同状态就可以表示多少个数字。

下面来思考一下如果芯片 A 要发送+1, -1, 0, +2 等数字给芯片 B,这里的正负又该如何表示呢?参考前面的思路, 线的高低电平表示的含义是由芯片双方向事先约定好的, 既然如此则可以单用一根线来表示符号,例如低电平表示正数,高电平表示负数。

image-20211029114327334

上图所示的三根线中用线 c 表示正负, 其中 0 表示正数, 1 表示负数。用线 a 和线 b 表示数值,以 3’b111 为例,其可以解释为十进制数 7,也可以解释为有符号数原码“ -3”,也可以解释为有符号数补码“-1”, 如何解释取决于工程师对二进制数的定义。只要该定义不影响到电路之间的通信就不会发生问题。 因此数字中的“ 0”和“1”不仅可以表示字面上的数值含义,也可以表示其他意义,如正负符号等。同样的道理,在数字电路中二进制数是八进制、十进制、十六进制、有符号数、无符号数、小数等其他数制的根本。在 FPGA 设计中,不清楚小数、有符号数的计算方法的最根本原因是不清楚这些数据所对应的二进制值, 只要理解了对应的二进制值,很多问题都可以解决。

下面通过例子让同学们更好的理解这一概念, 很多初学者经常问, FPGA 中如何实现小数计算呢?以“0.5+0.25” 为例, 众所周知 0.5+0.25 的结果为 0.75, 可以考虑 0.5、 0.25 和 0.75 用二进制该如何表示? 具体表示方法取决于工程师的做法,因为这种表示方法有很多种,例如定点小数,浮点小数,甚至如前面所讨论,用几根线自行来定义,只要能正常通信,那就没有问题。假设某工程师用三根线自行定义了二进制值所表示的小数值,如下表所示。

二进制值定义二进制值定义
3’b0000.13’b1000.25
3’b0010.53’b1010.3
3’b0100.753’b1100.8
3’b0110.23’b1110

为了说明二进制值的意义是可以随便定义的,数字顺序为乱序。 那为什么只有这几种小数呢?这是因为假定中的系统就只有这几种数字,如果想表示更多数字增加线的数量就可以了。完成上面定义之后,要实现“ 0.5+0.25”就很容易了,其实就是 3’b001 和 3’b100“相加”,期望得到 3’b010。 但是在该表中直接使用 3’b001 + 3’b100,结果为“101”, 这不是想要的结果,此时可以将代码写为:

image-20211029114740441

当然,这只是其中一种写法, 只要能实现所对应的功能且结果正确,任意写法都可以。

此处可能存在疑虑, 0.1+0.8 应该为 0.9,但上面的表格中并没有 0.9 的表示。这其实是设计者定义的这个表格有缺陷,或者设计者认为不会出现这一情况。 此处要表达的是: 只要定义好对应的二进制数,很多功能都是很容易设计的。

当然,实际的工程中通常会遵守约定成俗的做法,没必要另辟蹊径。例如, 下表是常用的定点小数的定义:

二进制值定义二进制值定义
3’b0000.03’b1000.5
3’b0010.1253’b1010.625
3’b0100.253’b1100.75
3’b0110.37253’b1110.8725

此时如果要实现 0+0.5=0.5,也就是 3’b000 和 3’b100 相加,期望能得到 3’b100。 可以发现直接用二进制 3’b000+3’b100 就能得到 3’b100。同样地, 要实现 0.125+0.75=0.8725,也就是 3’b001 和 3’b110 相加,期望能得到 3’b111。 可以发现直接用二进制 3’b001+3’b110 就能得到 3’b111。

如果要实现 0.5+0.75=1.25 这一计算, 可以看出此时 1.25 已经超出了表示范围, 可以通过增加信号位宽或只表示小数位的做法解决这一问题。如果只是表示小数位则结果就是 0.25,即 3’b100 和3’b110 相加,期望得到 3’b010。不难发现 3’b100 + 3’b110 = 4’b1010,用 3 位表示就是 3’b010,也就是 0.25。综上所述可以看出,定点小数的计算并不复杂,定义好定点小数与二进制值之间的关系后直接进行计算即可。

5.2.3 不定态

前文中讲过数字电路只有高电平和低电平,分别表示 1 和 0。但代码中经常能看到 x 和 z,如 1’bx, 1’bz。那么这个 x 和 z 是什么电平呢?答案是并没有实际的电平来对应两者。 x 和 z 更多地是用来表示设计者的意图或者用于仿真目的, 旨在告诉仿真器和综合器如何解释这段代码。

X 态,称之为不定态, 其常用于判断条件, 从而告诉综合工具设计者不关心它的电平是多少,是0 还是 1 都可以。

image-20211029114938488

上面的例子中可以看出判断条件是 din== 4’b10x0, 该条件等价于 din== 4’b1000||din==4’b1010,其中“||”是“或”符号。

image-20211029115013142

然而在设计中直接写成 din== 4’b1000||din == 4’b1010 要好于写成“din == 4’b10x0”, 因为这样的写法更加直接和简单明了。

在仿真的过程中有些信号产生了不定态,那么设计者就要认真分析这个不定态是不是合理的。如果真的不关心它是 0 还是 1,那么可以不解决。但建议所有信号都不应该处于不定态, 写清楚其是 0还是 1,不要给设计添加“思考”的麻烦。

5.2.4 高阻态

Z 态,一般称之为高阻态, 表示设计者不驱动这个信号(既不给 0 也不给 1),通常用于三态门接口当中。

image-20211030112458042

上图就是三态总线的应用案例, 图中的连接总线对于 CPU 和 FPGA 来说既为输入又为输出,是双向接口。一般的硬件电路中会将该线接上一个上拉电阻(弱上拉)或下拉电阻(弱下拉)。

当 CPU 和 FPGA 都不驱动该总线时, A 点保持为高电平。当 FPGA 不驱动该总线, CPU 驱动该总线时, A 点的值就由 CPU 决定。当 CPU 不驱动该总线, FPGA 驱动该总线时, A 点的值就由 FPGA 决定。 但 FPGA 和 CPU 不能同时驱动该总线,否则 A 的电平就不确定了, 通常 FPGA 和 CPU何时驱动总线是按事先协商的协议进行工作。

image-20211030112556798

上图是典型的 I2C 的时序。 I2C 的总线 SDA 就是一个三态信号。 I2C 协议已规定好上面的时间中,哪段时间是由主设备驱动,哪段时间是由从设备驱动,双方都要遵守协议,不能存在同时驱动的情况。那么 FPGA 在设计中是如何做到“不驱动”这一行为呢?这是因为 FPGA 内部有三态门。

image-20211030112615735

三态门是一个硬件,上图是它的典型结构。三态门有四个接口,如上图所示的写使能 wr_en、写数据 wr_data、读数据 rd_data 以及与外面器件相连的三态信号 data。

需要注意的是写使能信号,当该信号有效时三态门会将 wr_data 的值赋给三态线 data,此时 data 的值由 wr_data 决定,当 wr_data 为 0 时 data 值为 0;当 wr_data 为 1 时 data 值为 1。 而当写使能信号无效时,则不论 wr_data 值是多少都不会对外面的 data 值有影响,也就是不驱动。

在 Verilog 中以上功能是通过如下代码实现的:

image-20211030112644309

当综合器看到这两行代码则知道要综合成三态门了,高阻 z 的作用正在于此。 此外可以注意到硬件上用三态线是为了减少管脚,而在 FPGA 内部没有必要减少连线,所以使用三态信号是没有意义的。 因此, 建议各位在进行设计时不要在 FPGA 内部使用高阻态“ z”, 因为没有必要给自己添加“思考”的麻烦。当然, 如果设计中使用了高阻态也不会报错,也可以实现功能。

总的来说高阻态“z”是表示“不驱动总线”这个行为,实际上数字电路就是高电平或者低电平,不存在其他电平的情况。

5.3 算术运算符

image-20211030112732563

image-20211030145125938

算术运算符包括加法“ +”、减法“-”、乘法“*”、除法“ / ”和求余“ % ”, 其中常用的算术运算符主要有 :加法“ + ”,减法“-”和乘法“ *”。

注意,常用的运算中不包括除法和求余运算符, 这是由于除法和求余不是简单的门逻辑搭建起来的, 其所对应的硬件电路比较大。加减是最简单的运算,而乘法可以拆解成多个加法运算, 因此加减法、乘法所对应的电路都比较小。而除法就不同了, 同学们可以回想一下除法的步骤, 其涉及到多次乘法、移位、加减法,所以除法对应的电路是复杂的,这也同时要求设计师在进行 Verilog 设计时要慎用除法。

5.3.1 加法运算符

首先学习加法运算符, 在 Verilog 代码中可以直接使用符号“+”:

image-20211030112934610

其电路示意图如下所示:

image-20211030112947325

综合器可以识别加法运算符并将其转成如上图所示的电路。二进制的加法运算和十进制的加法相似,十进制是逢十进一,而二进制是逢二进一。二进制加法的基本运算如下:

image-20211030113004606

5.3.2 减法运算符

减法运算符,在 Verilog 代码中可以直接使用符号“-”:

image-20211030113035316

其电路示意图如下所示:

image-20211030113049315

综合器可以识别减法运算符并将其直接转成上图所示的电路。

二进制的减法运算和十进制的减法运算是相似的,也有借位的概念。十进制是借一当十,二进制则是借一当二。 1 位减法基本运算如下:

image-20211030113108106

5.3.3 乘法运算符

乘法运算符,在 Verilog 代码中可以直接使用符号“*”:

image-20211030113141837

其电路示意图如下所示:

image-20211030113153342

综合器可以识别乘法运算符,将其直接转成上图所示的电路。二进制的乘法运算和十进制的乘法运算是相似的,其计算过程是相同的。 1 位乘法基本运算如下:

image-20211030113213925

多位数之间相乘,与十进制计算过程也是相同的。例如 2’b11 * 3’b101 的计算过程如下:

image-20211030113232235

5.3.4 除法和求余运算符

除法运算符,可以在 Verilog 代码中直接使用符号―/‖,而求余运算符是“%”:

image-20211030142720756

除法的电路示意图如下所示:

image-20211030142733602

求余的电路示意图如下所示:

image-20211030142749890

综合器可以识别除法运算符和求余运算符,但是这两种运算符包括大量的乘法、加法和减法操作,所以在 FPGA 里除法器的电路是非常大的,综合器可能无法直接转成上图所示的电路。

此处可能存在疑虑: 为什么除法和求余会占用大量的资源呢?可以来分析一下十进制除法和求余的过程, 以 122 除以 11 为例。

image-20211030142812957

在做上面运算的过程中涉及到多次的移位、乘法、减法等运算。也就是说进行一次除法运算使用 到了多个乘法器、减法器,需要比较大的硬件资源,二进制运算也是同样的道理。

所以,在设计代码中,一般不使用除法和求余。在算法中会想各种办法来避免除法和求余操作。因此在数字信号处理、通信、图像处理中会发现有大量的乘法、加减法等,却很少看到除法和求余运算。但在仿真测试中是可以使用除法和求余的,因为其只是用于仿真测试而不用综合成电路,自然也就不需要关心占用多少资源了。

5.3.5 经验总结

位宽问题

在写代码时,需要注意信号的位宽,最终的结果取决于“ =”号左边信号的位宽,保存低位,丢弃高位。例如:

image-20211030142922330

信号 c 的位宽为 1 位,所以运算的结果最终保留最低 1 位,因此 c 的值为 1’b0。由于 d 的位宽有 2 位,所以运算的结果可以保留低 2 位,因此 d 的值为 2’b10。由于 e 的位宽有 3 位,所以运算的结果可以保留低 3 位,因此 e 的值为 3’b010。“1”默认是 32 位, 1+1 的结果也是 32 位,但由于 f的位宽只有 3 位,所以运算的结果可以保留低 3 位,因此 f 的值为 3’b010。

减法运算也是相同的道理,以如下代码为例:

image-20211030142952611

“ 0-1”得到的二进制值是“ 1111111111… .”,但保存结果取决于“ =”号左边信号的位宽。 c的位宽是 1,保留最低 1 位,所以 c 的值为 1’b1。由于 d 的位宽有 2 位,结果保留低 2 位,所以 d的值为 2’b11。由于 e 的位宽有 3 位,结果保留低 3 位,所以 e 的值为 3’b111。 f 的位宽有 4 位,所以运算的结果可以保留低 4 位,所以 f 的值为 4’b1111。

在写乘法代码时, 同样需要注意信号的位宽,最终的结果取决于“ *”号左边信号的位宽,保存低位,丢弃高位:

image-20211030143016965

“2’b11 * 3’b101”得到的二进制值是“ 4’b1111”,但保存结果取决于“ *”号左边信号的位宽。c 的位宽是 1,保留最低 1 位,所以 c 的值为 1’b1。由于 d 的位宽有 2 位,结果保留低 2 位,所以 d的值为 2’b11。由于 e 的位宽有 3 位,结果保留低 3 位,所以 e 的值为 3’b111。 f 的位宽有 4 位,所以运算的结果可以保留低 4 位,所以 f 的值为 4’b1111。需要注意的是 h,该信号有 5 位, 4’b1111 赋给 5 位信号,结果是高位补 0,所以其结果为 5’b01111。

补码的由来

FPGA 实现各种算法的时候,首要的就是保证运算结果的正确性,否则一切毫无意义。在分析加加法运算符和减法运算符的时候可以发现保存结果的信号位宽是否合理对正确性与否有很大的影响。

例如下面的加法运算:

image-20211030143139276

从上表可以发现,如果不保留进位,当加法出现进位的时候计算的结果是不正确的,只有保留了进位计算的结果才是正确的。由此可以得出一个结论:使用加法的时候,为了保证结果的正确性,必须保存进位,也就是结果要扩展位宽。

例如两个 8 位的数相加,则结果要扩展一位, 将位宽设定为 9 位。

image-20211030143209482

接着再来分析一下减法运算,如下表所示例子:

image-20211030143329162

注意表中和 2’b00-2’b01,结果是 2’b11,对应的十进制值为 3,但期望的结果是“ -1”。同样的道理, 2’b01 - 2’b11,结果是 2’b10,对应的十进制值为 2,而期望的结果是“ -2”,所以上面的结果是不正确的。

当期望结果中有正负之分时,可以通过增加一个符号位来区别结果的正负。业内约定的表示方法为,最高位为 0 时表示正数,最高位值为 1 表示负数。符号位之后的数值用低 2 位表示,结果如下表:

image-20211030143414060

从上表中可以看出增加符号位后还是会存在部分运算结果与预期不符合的问题。例如表中的 2’b00-2’b01,结果是 3’b111,对应的十进制值为-3,但期望的结果是“ -1”。所以上面的结果仍然是不正确的。

现在,重新对二进制数“ 000~111”进行如下转换

a. 正数:保持不变

b. 负数:符号位保持不变,数值取反加 1

也就是说,如果是正数“ +1”,之前是用“001”表示,现在仍然是用“ 001”表示。如果是负数“ -1”,之前是用“101”表示,现在则是用“ 111”表示。负数“ -3”,之前是用“ 111”表示,现在则是用“101”表示。这种表示方式就是补码表示方式

改为用补码来表示后,再来分析下结果:

image-20211030145353945

可以看到上表的结果全部都是正确的,与预期全部一致。 这一过程虽然完全没有对代码进行任何改变, 但通过更改数据的定义就实现了正确的结果。

在之前的讨论中,加数、被加数、减数和被减数的运算过程都没有使用有符号数。现在使用有符号数的补码重新对其进行表示。假设加数、被加数、减数和被减数都是 2 位(范围为-2~1),考虑到进位和借位原因,结果用 3 位来表示(范围为-4~3)。因为结果位宽变为 3 位,所以减数和被减数都扩展成用 3 位表示,列出下表:

image-20211030145455123

image-20211030145507551

总结运算步骤如下:

  1. 根据“人的常识”,预计结果的最大最小值,从而确定结果的信号位宽
  2. 将加数、减数等数据,位宽扩展成结果位宽一致。
  3. 按二进制加减法进行计算。

通过以上方式,得到的就是补码的结果。事实上,在 FPGA 甚至计算机系统中,所有数据的保存的方式都是补码的形式。 如果想要了解更多关于补码的内容可以参阅相关资料。

5.4 逻辑运算符

image-20211030145547059

image-20211030145556624

在 Verilog HDL 语言中存在 3 种逻辑运算符, 它们分别是:

(1)&&:逻辑与

(2)| | :逻辑或

(3)!:逻辑非

5.4.1 逻辑与

”&&“是双目运算符, 其要求有两个操作数,如 a && b。

( 1) 1 位逻辑与

image-20211105212729170

A 和 B 都为 1 时, C 为 1, 否则 C 为 0。

对应硬件电路图如下所示:

image-20211105212800939

( 2)多位逻辑与

image-20211105212816535

A 或 B 都不为 0 时, C 为 1,否则为 0。

image-20211105212839123

5.4.2 逻辑或

“||”是双目运算符, 其要求有两个操作数,如 a||b。
( 1) 1 位逻辑或

image-20211105213147969

A 和 B 其中 1 个为 1, C 为 1, 否则 C 为 0。

对应硬件电路图如下图所示:

image-20211105213306687

( 2)多位逻辑或

image-20211105213322593

A 和 B 其中 1 个非 0, C 为 1, 否则 C 为 0。

对应硬件电路图如下图所示:

image-20211105213347308

5.4.3 逻辑非

“!”是单目运算符,只要求有一个操作数,如!( a>b)。

image-20211105213442864

对操作数 a 需要先判断非 a 是否为真,为真就执行{}内的操作,为假的话就结束操作。
下表为逻辑运算的真值表, 其表示当 a 和 b 的值为不同的组合时各种逻辑运算所得到的值。

image-20211105213506720

逻辑运算符最后的结果只有逻辑真或逻辑假两种,即 1 或 0。一般情况下用逻辑运算符作判断条件时逻辑与操作只能是两个 1 位宽的数,只有两个表达式同时为真才为真,有一个为假则为假。

如果操作数是多位的,则可以将操作数看做整体,若操作数中每一位都是 0 值则为逻辑 0 值;
若操作数中有 1 则为逻辑 1 值。

image-20211105213540379

由于 4’b0111 和 4’b1000 都不是 0,不为 0 则被认为是逻辑真,所以上面的代码等效于如下代码。

image-20211105213557611

也就是结果为 a 为逻辑真, b 为逻辑真, c 为逻辑假。

5.4.4 经验总结

(1)逻辑运算符的优先级

逻辑运算符中―“&&”和―“||”的优先级低于算数运算符;“!”的优先级高于双目逻辑运算符

举例如下:

image-20211105213736040

(2)逻辑运算符两边对应的是 1 比特信号

使用心得逻辑运算符两边对应的是 1 比特信号

image-20211105213859092

注意上文代码, 其中 a 和 b 都是多比特信号, 表示两个多比特信号进行逻辑与。 这句代码的正确理解是:当 a 不等于 0 并且 b 不等于 0 时, d 的值为 1。 然而即使是有过多年工作经验的工程师也很难从直观上直接理解上文代码所隐含的意思。不等于 0 就是表示逻辑真,等于 0 就表示逻辑假的这一概念很容易被忽略。

因此上文代码,虽然从功能上没有错误,但设计师在设计中不应该以炫耀技术为目的进行代码编写, 而且这种写法很容易出现误设计的情况, 例如可能原本要表达 assign d =a & b, 但最后由于设计不够直观而写成了上面的代码,导致设计出现问题。 因此在设计中写出直观能理解,让自己与他者看到代码时可以立刻明白代码的意思非常重要, 所以建议上面代码写成如下形式。

image-20211105213942868

(3)多用括号区分优先级

使用心得 2不要试图记住优先级,而是要多用括号

实际上, 工程师们在工作中并不会记住所有优先级排序,记住所有的优先级的这一工作也并不会大幅度的提升工程师的工作效率。 在设计中可能会遇到如下所示代码:

(1) a < b && c > d ;

(2) a = = b | | c = = d ;

(3)! a | | a > b 。

假如没有记住运算符的优先级,遇到类似这三个例子的情况时势必会花一定的时间来理清思路,思考究竟先判断哪部分。 假如工程师能够记住优先级,也需要就这些代码进行沟通和检查的工作,而且人是容易犯错的, 而这些错误经常会被忽略, 很难被检查出来。

因此,为了提高程序的可读性,明确表达各运算符间的优先关系, 建议在设计时多使用括号。上面的三个例子就可分别写成:

1)( a < b) &&( c > d);

2)( a = = b) | |( c = = d);

3)(!a) | |( a > b)。

(4)少用逻辑非

使用心得 3多用“逻辑与”和“逻辑或”,少用逻辑非

“逻辑与”翻译成中文就是“并且”,“逻辑或”翻译成中文就是“或者”。假设有一个信号 flag, 0 表示空闲, 1 表示忙。“ (!(flag== 0) && a== 4’ b1000)”,读起来就是“在空闲的时候取其反状态,并且当 a 等于 4’ b1000 时条件成立”。这样读起来非常拗口, 并且在阅读这一代码时还需要脑袋还要多转一下弯, 多进行一层思考。为了让代码更加直观, 建议上面的例子写成“ flag== 1 && a==4’ b1000”,读起来就是“在忙并且 a 等于 4’ b1000 的时候”条件成立。

5.5 按位逻辑运算符

image-20211105215609897

image-20211105215735525

注: ~ ^, ^ ~(二元异或非即同或):(相当于同或门运算)。

在 Verilog HDL 语言中有下面几种按位运算符:

~(一元非):(相当于非门运算)

&(二元与):(相当于与门运算)

|(二元或):(相当于或门运算)

^(二元异或):(相当于异或门运算)

这些操作符在输入操作数的对应位上按位操作,并产生向量结果。下图各真值表种显示对于不同按位逻辑运算符按位操作的结果:

image-20211106195003983

5.5.1 单目按位与

单目按位与运算符&, 运算符后为需要进行逻辑运算的信号, 表示对信号进行每位之间相与的操作。例如

Reg[3:0] A,C;

assign C=&A;

上面代码等价于 C = A[3] & A[2] & A[1] & A[0];如果 A=4’b0110, C 的结果为 0。

5.5.2 单目按位或

单目按位或运算符|, 运算符后为需要进行逻辑运算的信号,表示对信号进行每位之间相或的操作。例如

reg[3:0] A,C;

assign C=|A;

上面代码等价于 C = A[3] | A[2] | A[1] | A[0];如果 A=4’b0110, C 的结果为 1。

5.5.4 双目按位与

双目按位与运算符&, 信号位于运算符的左右两边,表示的是对这两个信号进行对应位相与的操作。例如

reg[3:0] A,B,C;

assign C = A & B;

上面的代码等价于: C[0] = A[0] & B[0], C[1] = A[1] & B[1], C[2] = A[2] & B[2], C[3] =A[3] & B[3]。如果 A=4’b0110, B=4’b1010, C 的结果为 4’b0010。

如果操作数长度不相等, 长度较小的操作数在最左侧添 0 补位。例如,

reg[1:0] A;

reg[2:0] B;

reg[3:0] C;

assign C = A & B;

上面的代码等价于: C[0] = A[0] & B[0], C[1] = A[1] & B[1], C[2] = 0& B[2], C[3] = 0 &0。

5.5.5 双目按位或

双目按位或运算符|, 信号位于运算符的左右两边,表示的是对这两个信号进行对应位相或的操作。 例如

reg[3:0] A, B, C;

assign C = A | B;

上面的代码等价于: C[0] = A[0] | B[0], C[1] = A[1] | B[1], C[2] = A[2] | B[2], C[3] = A[3]| B[3]。如果 A=4’b0110, B=4’b1010, C 的结果为 4’b1110。

如果操作数长度不相等, 长度较小的操作数在最左侧添 0 补位。例如,

reg[1:0] A;

reg[2:0] B;

reg[3:0] C;

assign C = A | B;

上面的代码等价于: C[0] = A[0] | B[0], C[1] = A[1] | B[1], C[2] = 0 | B[2], C[3] = 0 | 0。

5.5.6 双目按位异或

双目按位异或运算符^, 信号位于运算符的左右两边,表示的是对这两个信号进行对应位相异或的操作。 异或是指 00=0,11=0,0^1=1,即相同为 0,不同为 1。例如

reg[3:0] A, B, C;

assign C = A ^ B;

上面的代码等价于: C[0] = A[0] ^ B[0], C[1] = A[1] ^ B[1], C[2] = A[2] ^ B[2], C[3] = A[3]
^ B[3]。如果 A=4’b0110, B=4’b1010, C 的结果为 4’b1100。

如果操作数长度不相等, 长度较小的操作数在最左侧添 0 补位。例如,

reg[1:0] A;

reg[2:0] B;

reg[3:0] C;

assign C = A | B;

上面的代码等价于: 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;而“ &”是位运算符,用于两个多位宽数据操作。对于位运算符操作,两个数按位进行相与、相或或者非。

image-20211107101136767

上面运行的结果为: a=1’b1, b=1’b1, c=1’b0, d=4’b0000, e=4’b1111, f=4’b1000。

5.6 关系运算符

image-20211107101351496

image-20211107101503199

关系运算符有: >(大于)、 <(小于)、 >=(不小于)、 <=(不大于)、 == (逻辑相等)和!= (逻辑不等)。

关系操作符的结果为真( 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 中有两种移位运算符,分别为"<<"(左移位运算符)和“>>”(右移位运算符)。

下面分别介绍两者的用法:

image-20211107102901190

image-20211107102911492

5.7.1 左移运算符

在 Verilog HDL 中,用“―”<<表示左移运算符。其一般表达式为:

A << n;

其中, A 代表要进行移位的操作数, n 代表要左移多少位。此表达式的意义是把操作数 A 左移 n位。 左移操作属于逻辑移位,需要用 0 来填补移出的空位,即在低位补 0。左移 n 位,就要补 n 个 0。

image-20211107103016873

以上代码由于左移了 2 位,所以在低位补 2 个零,所以上面代码运行结果是: a = 4’b1100。
左移操作中有以下三点值得注意的地方
(1)左移操作是不消耗逻辑资源的,甚至连与门、非门都不需要,它只是线的连接

image-20211107103040064

上面代码是将信号 b 左移两位并赋给 c, 其所对应的硬件电路如下图:

image-20211107103103978

( 2)左移操作需根据位宽储存结果

在学习过程中可能看到过如下代码:4’b1001<<1=4’b0010 与 4’b1001<<1=5’b10010

为什么操作数同样是 4’b1001,都是左移一位,但结果一个是 4’b0010,一个是 5’b10010 呢?这是因为左移操作后,要看用多少位来存储结果。

image-20211107103702482

上面代码中由于 a 是 4 比特,只能保存 4 位结果,所以 b 左移 1 位赋给 4 bit 的 a,用 0 填补移出的位后结果为 a = 4’b0010 ;

image-20211107103721433

而上面代码中由于 a 是 5 比特,能保存 5 位结果,所以 b 左移 1 位赋给 5 bit 的 a,用 0 填补移出的位后结果为 a = 5’b10010 ;
( 3**)左移操作的操作数可以是常数,也可以是信号**。同样,左移操作的移位数、常数也可以是信号。

image-20211107103747278

上面代码中 cnt 每个时钟加 1,由于是 3 比特,所以值为 0~2。 a 则是 4’b1 左移 cnt 位。当 cnt等于 0 时左移 0 位, a 等于 4’b1;当 cnt 等于 1 时左移 1 位, a 等于 4’b10。 以此类推, a 的每个时钟变化情况如下所示:

image-20211107103805477

需要注意的是, 当移位数是信号时,其综合的电路并不是简单的连线,可能会综合出如下图所示的选择器。然而即便如此,这种硬件电路所消耗的资源依然比较少。

image-20211107103822414

5.7.2 右移运算符

在 Verilog HDL 中,用“>>”表示右移运算符。其一般表达式为:

A >>n;

其中, A 代表要进行移位的操作数, n 代表要右移多少位。此代码表示的意义是把操作数 A 右移n 位。

在右移操作中有以下三点值得注意的地方
( 1)右移操作属于逻辑移位, 需要用 0 来填补移出的空位,即在高位补 0,补多少个 0,取决
于保存结果的信号的位宽

image-20211107103914795

4’b0111 右移两位后的结果为 2’b01,由于 a 是 6 位的, 2 位赋值给 6 位需要在高位补 0, 因此需要补 4 个 0。所以上面代码运行结果是:

a = 6’b0001
( 2)与左移操作相似,右移操作是不消耗逻辑资源的,甚至连与门、非门都不需要, 其只是线
的连接

image-20211107103936593

上面代码是将信号 b 左移两位并赋给 a, 其所对应的硬件电路如下图所示。

image-20211107103951964

( 3)左移操作的操作数可以是常数,也可以是信号。同样,右移操作的移位数可以是常数,也可以是信号

image-20211107104010745

上面代码中, cnt 每个时钟加 1,由于是 3 比特,所以值为 0~2。 a 则是 4’b1000 右移 cnt 位。当 cnt 等于 0 时右移 0 位, a 等于 4’b1000;当 cnt 等于 1 时右移 1 位, a 等于 4’b0100。 以此类推,a 的每个时钟变化情况如下图所示。

image-20211107104037650

与左移操作类似,在右移操作中,如果移位数是信号时,其综合的电路就不是简单的连线,而是有可能会综合出如下图所示的选择器。然而同样在这一情况下,这种硬件电路所消耗的资源依然比较少。

image-20211107104055622

5.7.3 经验总结

通过左移乘法运算

FPGA 中要尽量避免乘法运算, 因为这种计算需要占用较大的硬件资源,并且运算速度较慢。 当不得不使用乘法的时候,尽量乘以 2 的 N 次方,这样在设计中可以利用左移运算来实现该乘法运算,从而大大减少硬件资源。

当乘数是 2 的 N 次方的常数时可以用移位运算来实现乘法。 例如: a2,等价于 a<<1; a4 等价于 a<<2; a*8 等价于 a<<3,依此类推。即使乘数不是 2 的 N 次方的常数,也可以通过移位运算来简化实现。例如:

image-20211107104134562

上面代码中 b 和 c 都可以实现 a*127,但第 1 行消耗了一个乘法,而第 2 行则只用到一个减法
器。

image-20211107104147477

上面代码中, b 和 c 都可以实现 a*67,但第 1 行消耗了一个乘法,而第 2 行则只用到两个加法器,从而节省了资源。

可以注意到,上面两个例子中的乘数都是常数,那么在设计时这种乘法也要花时间和精力来考虑优化吗?其实是不必要的,因为现在综合工具都很强大,当工具发现乘数是常数时会自动按上述过程进行优化,也就是说乘以常数在实质上并不消耗乘法器资源,可以放心使用。

但当出现乘数不是常数的情况时就要注意乘法的使用了。尽量将信号转换为与 2 的 N 次方相关的形式。例如当数据要扩大后来计算时, 不要按照惯有思维将数据扩大 100 倍,而是应该直接将其扩大 128 倍(不能根据常规思维,将小数扩大100、1000倍,根据2的n次方进行扩增,减少了资源空间的占用)。

利用右移实现除法运算

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 条件运算符

image-20211107192312809

image-20211107192334644

5.8.1 三目运算符

Verilog HDL 语法中**条件运算符( ?: )**带有三个操作数(即三目运算符) ,其格式一般表达为:

image-20211107192358180

其含义为:当“条件表达式”为真(即逻辑 1),执行“真表达式”;当“条件表达式”为假(即
逻辑 0),执行“假表达式”。即当 condition_expr 为真(即值为 1),选择 true_expr;如果 conditio
n_expr 为假(值为 0),选择 false_expr。如果 condition_expr 为 x 或 z ,结果将是按以下逻辑 true_expr 和 false_expr 按位操作的值: 0 与 0 得 0, 1 与 1 得 1,其余情况为 x 。

应用举例如下:

image-20211107192422886

在上面的表达式中 s 如果为真,则把 t 赋值给 r;如果 s 为假,则把 u 赋值给 r。

对应硬件电路图如下所示。

image-20211107192454340

条件运算符的使用有以下几点需要注意的地方

( 1)条件表达式的作用实际上类似于多路选择器, 如图 1.3-8 所示。 同时,其可以用 if-else 语句来替代。

image-20211107192527485

( 2)条件运算符可用在数据流建模中的条件赋值, 这种情况下条件表达式的作用相当于控制开关。例如:

image-20211107192550223

其中,表达式 Marks > 18 如果为真,则 Grade_A 赋值为 student;如果 Marks > 18 为假,则
Grade_C 赋值为 student。

对应硬件电路图如下所示。

image-20211107192607991

( 3)条件运算符也可以嵌套使用,每个“真表达式”和“假表达式”本身就可以是一个条件表
达式。例如:

image-20211107192719206

上面代码所代表的含义是:表达式 M == 1 如果为真,则判断 CTL 是否为真,如果 CTL 为真就将 A 赋值给 OUT,如果为假就将 B 赋值给 OUT;如果 M = = 1 为假,则判断 CLT 是否为真,如果CLT 为真就将 C 赋值给 OUT,如果为假就将 D 赋值给 OUT。

对应硬件电路图如下所示:

image-20211107192740274

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 个分支中定义多个分支项,且这些值不需要互斥。缺省分支覆盖所有没有被分支表达式覆盖的其他分支。

image-20211107193238224

书写建议: 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 作为一个可变的数来使用。例如需要设计具有
如下功能的代码:

当 cnt== 0 时, 将 din[7:0]赋值给 data[15:8];当 cnt== 1 时将 din[7:0]赋值给 data[7:0]。

在设计的时候便可以写成: data[15-8cnt -: 8] <= din[7:0]( 此时需要将 15-8cnt 视为一个整
体 a, 其会随着 cnt 变化而发生变化) ,这样一来就完成了对代码的精简工作。

选择语句的硬件电路结构如下图所示, 其本质上是一个选择器。当 cnt== 0 时,选中data[15:8]的锁存器,将 din[7:0]赋值给 data[15:8],而 data[7:0]的锁存器保持输出不变;当 cnt==1 时,选中 data[7:0]的锁存器,将 din[7:0]赋值给 data[7:0],而 data[15:8]的锁存器保持输出不变。

image-20211107193442661

经验总结: 在实际工程中可以多使用选择语句 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 语句写的代码:

image-20211107193601401

image-20211107193618227

其综合后的 RTL 视图如下所示。

image-20211107193643645

从上图中所示的 RTL 图可以看到,此电路中含有两个二选一多路选择器,并且右边的优先级要高于左边(因为 q 的值是直接与右边的二选一选择器连接),当 en[0]不为 1 时才会继续判断 en[1]。也就是说,在 if 语句下综合出的电路含有优先级

接下来分析一下使用 case 语句描述的代码。

image-20211107193718179

其综合后的 RTL 视图如下所示:

image-20211107193742651

可以看出,使用 case 语句编写的逻辑代码综合出的电路是并行的,没有优先级,不影响电路的运行速度

虽然在 RTL 视图中,两种语句综合出的电路存在着较大的差别,但是由于目前的开发工具已经足够智能,在布局布线的时候会自动对电路进行优化,其最终映射到 FPGA 内部的电路之间基本毫无差别

image-20211107193823156

image-20211107193833956

最后总结一下 if 语句和 case 语句之间的区别与联系:

If 语句具有优先级,当 if 下的条件不满足时才执行 else 后面的部分。 而 case 语句是并行的,没有优先级,这在两者综合出来的 RTL 视图中可以明显的观察出来。但由于现在的仿真和综合工具已经足够强大,最后综合后的结果 if…else…与 case…语句其实并无不同, 只不过是两种不同的实现方式而已, 因此基本上不用考虑这两者间的区别。在不影响功能的前提下设计师不需要做局部的优化工作,例如不需要考虑 if/case 语句的资源耗费差异、不需要考虑优化电路。只有在影响功能(即时序约束报错)的前提下,根据提示对电路进行优化。

5.9 拼接运算符

image-20211107193940242

image-20211107193949282

拼接操作是将小表达式合并形成大表达式的操作,其形式如下:

{expr1, expr2, . . ., exprN} ;

拼接符是不消耗任何硬件资源的, 其只是把线换一种组合方式, 可以参照如下实例:

image-20211107194014357

由于非定长常数的长度未知, 不允许连接非定长常数。 因此如下所示代码是不符合语法规定的。{Dbus,5} ; / /不允许连接操作非定长常数

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐