FPGA入门之功能描述-组合逻辑

电子说

1.2w人已加入

描述

第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条件循环语句中的内容)

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

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

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

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

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

上述代码的敏感列表是“ 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。

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

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

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

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

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

上图所示的三根线中用线 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’b000 0.1 3’b100 0.25

3’b001 0.5 3’b101 0.3

3’b010 0.75 3’b110 0.8

3’b011 0.2 3’b111 0

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

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

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

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

二进制值 定义 二进制值 定义

3’b000 0.0 3’b100 0.5

3’b001 0.125 3’b101 0.625

3’b010 0.25 3’b110 0.75

3’b011 0.3725 3’b111 0.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 都可以。

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

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

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

5.2.4 高阻态

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

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

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

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

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

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

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

5.3 算术运算符

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

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

5.3.1 加法运算符

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

其电路示意图如下所示:

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

5.3.2 减法运算符

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

其电路示意图如下所示:

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

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

5.3.3 乘法运算符

————————————————

版权声明:本文为CSDN博主「原来如此呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/Royalic/article/details/121196365

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分