第三章 硬件描述语言verilog(二) 功能描述-组合逻辑(上)

简介: 第三章 硬件描述语言verilog(二) 功能描述-组合逻辑

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


5.1程序语句


5.1.1 assign语句


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

assign a = b (逻辑运算符)c …;//这句话可以通俗说是 b与c逻辑过后的数值与a进行连线。


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

(1)持续赋值;

(2)连线;

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

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


5.1.2 always语句


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


always @(敏感事件)begin

程序语句

end


always 是“一直、总是”的意思,@后面跟着事件。整个 always 的意思是:当敏感事件的条件满足时,就执行一次“程序语句”。敏感事件每满足一次,就执行“程序语句”一次

1670836402857.jpg

这段程序的意思是:当信号 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,其代码如下所示

1670836411455.jpg

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

1670836419208.jpg

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

1670836430881.jpg

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

1670836451339.jpg

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

1670836460789.jpg

上述代码的敏感列表是“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,数值可以是 0,1,x,X,z 或 Z,2,3,4,5,6,7。

如果基数定义为h 或 H,数值可以是0,1,x,X,z 或 Z,2,3,4,5,6,7, 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。

1670836489985.jpg

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

1670836503874.jpg

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

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

1670836512889.jpg

按照同样的道理,芯片 A 要发送数据 4,5,6,7 给芯片 B 时,只要再添加一条线就可以了。

三根线一共有 8 种状态,可以表示 8 个数字。综上所述,线的不同电平状态可以表示不同的含义,有多少种不同状态就可以表示多少个数字。


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

1670836520203.jpg

上图所示的三根线中用线 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 用二进制该如何表示?具体表示方法取决于工程师的做法,因为这种表示方法有很多种,例如定点小数,浮点小数,甚至如前面所讨论,用几根线自行来定义,只要能正常通信,那就没有问题。假设某工程师用三根线自行定义了二进制值所表示的小数值,如下表所示。

1670836528832.jpg

为了说明二进制值的意义是可以随便定义的,数字顺序为乱序。那为什么只有这几种小数呢?这是因为假定中的系统就只有这几种数字,如果想表示更多数字增加线的数量就可以了。完成上面定义之后,要实现“0.5+0.25”就很容易了,其实就是 3’b001 和 3’b100“相加”,期

望得到 3’b010。但是在该表中直接使用 3’b001 + 3’b100,结果为“101”,这不是想要的结果,此时可以将代码写为:

1670836538599.jpg

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

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

1670836545341.jpg

此时如果要实现 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 都可以。

1670836556387.jpg

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

1670836571756.jpg

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

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


5.2.4 高阻态


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

1670836583538.jpg

上图就是三态总线的应用案例,图中的连接总线对于 CPU 和 FPGA 来说既为输入又为输出,是双向接口。一般的硬件电路中会将该线接上一个上拉电阻(弱上拉)或下拉电阻(弱下拉)。当 CPU 和 FPGA 都不驱动该总线时,A 点保持为高电平。当 FPGA 不驱动该总线,CPU 驱动该总线时,A 点的值就由 CPU 决定。当 CPU 不驱动该总线,FPGA 驱动该总线时,A 点的值就由 FPGA 决定。但 FPGA 和 CPU 不能同时驱动该总线,否则 A 的电平就不确定了,通常 FPGA 和 CPU何时驱动总线是按事先协商的协议进行工作。

1670836591648.jpg

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

1670836639831.jpg

三态门是一个硬件,上图是它的典型结构。三态门有四个接口,如上图所示的写使能 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 中以上功能是通过如下

1670836648118.jpg

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

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


5.3算术运算符

1670836660816.jpg


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


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


5.3.1 加法运算符


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

1670836674352.jpg

其电路示意图如下所示:

1670836681848.jpg

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


0 + 0 = 0;

0 + 1 = 1;

1 + 0 = 1;

1 + 1 = 10;


两位的二进制加法


11 + 1 = 100;

11 + 11 = 110;


5.3.2 减法运算符


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

1670836693170.jpg

其电路示意图如下所示:

1670836703990.jpg

综合器可以识别减法运算符并将其直接转成上图所示的电路。二进制的减法运算和十进制的减法运算是相似的,也有借位的概念。十进制是借一当十,二进制则是借一当二。1 位减法基本运算如下:


0 - 0 = 0;

0 - 1 = 1,同时需要借位;

1 - 0 = 1;

1 - 1 = 0 ;


5.3.3 乘法运算符


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

1670836715281.jpg

其电路示意图如下所示:

1670836722861.jpg


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


0 * 0 = 0;

0 * 1 = 0;

1 * 0 = 0;

1 * 1 = 1;


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

1670836732487.jpg


5.3.4 除法和求余运算符


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

1670836743184.jpg

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

1670836752418.jpg

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

1670836758364.jpg

综合器可以识别除法运算符和求余运算符,但是这两种运算符包括大量的乘法、加法和减法操作,所以在 FPGA 里除法器的电路是非常大的,综合器可能无法直接转成上图所示的电路。此处可能存在疑虑:为什么除法和求余会占用大量的资源呢?可以来分析一下十进制除法和求余的过程,以 122 除以 11 为例。

1670836766202.jpg

在做上面运算的过程中涉及到多次的移位、乘法、减法等运算。也就是说进行一次除法运算使用到了多个乘法器、减法器,需要比较大的硬件资源,二进制运算也是同样的道理。所以,在设计代码中,一般不使用除法和求余。在算法中会想各种办法来避免除法和求余操作。

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


5.3.5 经验总结


位宽问题

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

1670836780323.jpg

信号 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。

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

1670836788559.jpg

“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。

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

1670836804477.jpg

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

例如下面的加法运算:

1670836813873.jpg

1670836822285.jpg

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

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

1670836842576.jpg

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

1670836851408.jpg

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

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

1670836859989.jpg

1670836869637.jpg

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

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

a. 正数:保持不变

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

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

现在则是用“101”表示。这种表示方式就是补码表示方式。改为用补码来表示后,再来分析下结果:

1670836877413.jpg

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


相关文章
|
存储 开发工具 异构计算
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑(下)
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑
712 0
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑(下)
|
异构计算
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑(中)
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑
237 0
第三章 硬件描述语言verilog(二) 功能描述-组合逻辑(中)
|
存储 程序员 开发工具
第三章 硬件描述语言verilog(一)
第三章 硬件描述语言verilog(一)
316 0
第三章 硬件描述语言verilog(一)
|
开发工具 异构计算
硬件描述语言Verilog学习(一)(上)
1. 综合与仿真 1.1 综合 1.2 仿真 可综合设计 2. 模块结构 2.1 模块介绍 2.2 模块名和端口定义 2.3 参数定义 2.4 接口定义 2.5 信号类型 2.6 功能描述 2.7 模块例化 3 信号类型 3.1 信号位宽 3.2 线网类型 wire 3.3 寄存器类型 reg 3.4 wire 和reg 的区别
347 0
硬件描述语言Verilog学习(一)(上)