1. 参考
白栎旸. 数字IC设计入门(微课视频版)[M]. 北京: 清华大学出版社,2023-09-01:第2章.
2. 数字器件与Verilog语法
虽然 Foundry 提供的可用元器件种类较多,但数字设计师并不特别在意这些元器件,在其头脑中,只存在 10 种简单元器件:
- 与门、或门、非门、异或门、
- 加法器、乘法器、移位器、
- 选择器、比较器、
- 触发器
注意,虽然加法器、乘法器、比较器等元器件可以由逻辑门生成,但是从Verilog常用的表达式来看,一般直接用“+”这个符号表示加法,而很少用门电路去直接搭建。因为现代芯片规模庞大,功能复杂,工程师应该将主要精力投入到重点难题的实现中,而对于如何实现加法器等最底层的问题,应该交给综合步骤自动完成。
基本元器件中不包括除法,因为除法的实现不同于乘法,它受到被除数、除数、商的数值范围限制,有时需要用到迭代等复杂方法实现,还有分母为 0 等异常情况需要报告,所以不属于Verilog中常用的直接运算方式。
务必注意,数字前端写的Verilog仅仅是代码,而非程序,其代码是代替电路图的一种文本语言描述。与或非加乘等指的是元器件。写Verilog时要有电路概貌和时序。
10种数字器件的符号表示及Verilog表示方法见表格:
![20250117214703](https://i.postimg.cc/B65vgDMk/20250117214703.jpg)
真正的元器件库中有很多复杂的元器件,如图所示:
![20250117214659](https://i.postimg.cc/6QSLdsyT/20250117214659.jpg)
但是这些元器件都可以看作以上10种基础元器件的组合,不会超出原有的功能范围。所以设计时,头脑中只需要有以上10种元器件。
数字IC设计又称为数字逻辑设计,因为其本身就是逻辑的,只有0
和1
两种逻辑。
- 组合逻辑:电平输入和电平输出。元器件结构简单,但问题是如果输入含有毛刺,输出就有毛刺。
![20250117215056](https://i.postimg.cc/C1s5wkTY/20250117215056.jpg)
![20250117214654](https://i.postimg.cc/NMH8DNHD/20250117214654.jpg)
- 时序逻辑:以时钟为驱动源。一个触发器,在时钟的驱动(边沿触发)下,将 D 输入端的信号送到 Q 端输出。
![20250117214649](https://i.postimg.cc/BbfTVx50/20250117214649.jpg)
触发器也可以叫寄存器(register, reg),因为如果没有时钟驱动,那么Q端会保持原有状态不变,也就寄存了上一次触发时的D端信息。而组合逻辑,输出端是无法寄存信息的。
时序逻辑是数字电路的基础。10 种元器件中,只有触发器属于时序逻辑器件,所以触发器是整个数字电路的基础。从 RTL 的名称可以知晓,RTL 意为寄存器传输层,直译过来就是:从一个触发器的输出到另一个触发器的输入,通过触发器的层层传递,最终实现了一个功能完整的数字电路。
数字电路的时序分析,主要是分析两个触发器之间的路径延迟。
进行前仿时,看到的仿真波形会和本图一样,是理想的,而使用版图网表进行后仿时,仿真波形是带延迟的。
上图中触发器的符号表示,它共有4个引脚,除输入的D端和输出的Q
端外,三角形位置表示时钟,下方的rst_n
表示复位,其上的圆圈表示0
电平有效,即rst_n
等于0
时,寄存器处于复位状态。此时,Q
端保持0
,即使时钟和D
端有动作,Q
端也不会变化,只有当rst_n
等于1
时,才解除复位状态,寄存器方能正常工作。
3. 可综合的Verilog设计语法
能变成电路的Verilog表达叫做可综合,在设计电路时,只能使用可综合的语法表述。而在仿真时,由于只在计算机上运行,不留片,可使用不能综合的高级语法,以增加语言表达的灵活度和复杂度。
可综合的电路表述只有两种:
assign
always
与门:
1 | assign z = a & b; |
触发器:
1 | always @(posedge clk or negedge rst_n) |
符号说明:
<=
: 非阻塞赋值,凡是时序逻辑,都用非阻塞赋值;=
: 阻塞赋值,凡是组合逻辑,都用阻塞赋值;@(...)
: 括号中的列表叫敏感列表,意思是,always
块输出的Q
对列表中信号保持敏感,如果敏感信号动,则Q也会动。posedge clk
: 意思是时钟的上升沿;negedge rst_n
: 意思是时钟的下降沿。
always
不仅可以表示时序逻辑,也可以表示组合逻辑。如下是与门的另一种表示:
1 | always @(*) |
其中,@(*)
中的*
是省略表述的敏感列表,综合器会自动在always
块中寻找与输出z
相关的输入信号,自动填入敏感列表中。本例中,会自动将a
和b
作为输入填入。这种让工具自动填入的方式是可靠且推荐的。
Verilog的语法规律:
- 时序逻辑,必须使用
always
块,并同时使用<=
非阻塞赋值。在其敏感列表中,必须出现时钟信号的边沿和复位信号的边沿。 - 组合逻辑,可以使用
assign
,也可以使用always
块,但是它们的赋值是=
阻塞赋值。若使用always
块,则敏感列表中使用*
。若遇到敏感列表中带有*
,则可以直接判定为组合逻辑。
再次强调,Verilog的语法表达,描述的都是电路,因此例子中的 z
、a
、b
、clk
、rst_n
、Q
、D
都称为信号,在电路中都是实实在在的金属连线,切勿称为变量。
4. 对寄存器的深度解读
一般会使用时钟上升沿来驱动寄存器。对于同样的功能需求,双沿触发需要的时钟慢,但要求时钟是50%
占空比,而单沿触发,对时钟的要求快一倍,但对时钟形状的要求降低很多。
复位信号rst_n
,以0
电平作为复位电平,1电平解复位,是通用标准,很少有反过来使用的。原因是,数字电路的复位信号是模拟电路给的,通常,模拟电路将其命名为POR(Power On Reset)
,即上电复位信号。芯片刚通电时,电压小,逐渐上升到要求的电压,例如1.8V
,POR
本质上是一个电压上升的标志,模拟电路放一个比较器,将输入电压与0.9V
比较,电压小于0.9V
,POR
为0
,电压大于0.9V
,POR
为1
。因而复位信号上电时总是先0
后1
,数字寄存器需要在复位信号为0
的阶段保持复位态,不能运行,因为此时芯片电压不足,不能保证正常运行,而复位信号变成1
,说明上电完毕,电压充足,寄存器解除复位进行正常运行是安全的。
需要特别澄清的是语句negedge rst_n
,但是经过仿真和与模拟工程师确认,复位信号对寄存器的作用不是通过信号沿来驱动的,而是通过电平来驱动,也就是 0
信号具有绝对控制权,只要 rst_n
为 0
,那么立即复位。
5. 非阻塞赋值和阻塞赋值的区别
非阻塞赋值的意思是该句表达不会阻塞后续表达的执行。如下例中,X <= 0
的执行,不会阻碍到Y <= 0
的执行,它们是同时发生的:
1 | always @(posedge clk or negedge rst_n) |
而阻塞赋值,意思是如果前一句不执行,后一句就无法执行,前一句会阻塞后一句。对于可综合的Verilog来讲,其实并不会阻塞。在下例中, always
块的目的是创造 z
和 k
两个信号。 k = 3 * z
和 z = a & b
是两个不同的电路, k = 3 * z
电路不会被 z = a & b
阻塞。
1 | always @(*) |
本例对应的原理图如图所示:
![20250117224729](https://i.postimg.cc/MTs1Dhsh/20250117224729.jpg)
可见,对于电路描述来讲,语法只是表示一种连接关系,并没有执行先后顺序的说法,但如果本例使用非阻塞赋值,语法检查会报错,因此,这是一种惯用方法。阻塞赋值在Verilog中真正体现阻塞,是在仿真使用的不可综合语法中,到第3章再做解释。
6. 组合逻辑的表达式
对于一个组合逻辑电路,应该在什么情况下用assign,在什么情况下用always呢?
比较简单的逻辑适合使用assign方式,较为复杂的逻辑应使用always块。下例给出了一个适合用always块的较复杂例子:
1 | always @(*) |
同样的功能若改用assign,则为下例所示。很明显,用always块表达意思更加清晰。
1 | assign a = s1 ? 1 : (s2 ? 2 : (s3 ? 3 : 0)); |
前面解释了敏感列表中的 *
在组合逻辑 always
块中的作用。如果读者使用过一些老IP,则可能还会看到下例所示的表达,这种表达已随着综合器的进步渐渐被淘汰了,不建议初学者使用。
1 | always @(s1 or s2 or s3) |
7. 组合逻辑中的选择器
二选一 MUX 如何表达?
![20250117225844](https://i.postimg.cc/L6vnwrjX/20250117225844.jpg)
1 | // 第 1 种表达, assign 完整表达 |
多选一MUX,又该如何表示呢?
因为使用 assign
表示显然会过于复杂,所以需要用 always
块表示。表示方法有两种,注意两种表达综合出来的电路是不同的。
其一如下例所示。
1 | always @(*) |
综合出来的电路如图所示:
![20250117230859](https://i.postimg.cc/x84BbfW8/20250117230859.jpg)
可见,使用 if
表述的选择关系,综合的电路是一层一层逐渐展开的,写在 if
最前面的语句,掌握着最终的选择权,因而优先级最高,再往后优先级逐层下降,而使用 case
表述的MUX,每个选择都是并列的,优先级相同,见下文。
其二如下例所示。
1 | always @(*) |
所综合的电路如图所示:
![20250117230221](https://i.postimg.cc/PrfqW20k/20250117230221.jpg)
使用 if
表述,有可能出现隐藏逻辑,即设计者没有考虑到,但实际会被综合出来的逻辑门。隐藏逻辑是设计的隐患,设计者在写代码时应该清楚其逻辑含义,尽量避免出现隐藏逻辑。为了避免设计中出现隐藏逻辑,在实际项目中往往会提倡使用 case
语句来表达。
下例中反映出 if
的优先级特征,条件 s < 5
包含 s == 4
的情况,因为 s < 5
优先,因而当 s==4
时, z
的赋值是 a
而不是 b
。如果设计意图是要在 s == 4
时使 z = b
,则应当将其写在 s < 5
之前。
1 | always @(*) |
case有一种变体是casez,它可以拓展case的使用范围:
1 | always @(*) |
其中,问号的意思是0或1都能匹配,类似计算机语言中的通配符。
虽然很多项目提倡使用 case
或 casez
来表述选择器,但究竟是使用 if
还是 case
,仍然取决于表达的需要。总体而言, case
便于判断是否相等的情况,而 if
适合判断大于或小于关系,不同情况用不同的表达,可以使Verilog逻辑更加清晰,也更便于维护。
注意组合逻辑中的 if
和 else if
,最后必须跟一句 else
,使整体逻辑完整。若没有else,则该电路会综合出一个锁存器(Latch
)。锁存器不属于10种基本元器件之一。在设计中,凡有寄存需求,应尽量使用触发器,避免使用锁存器,特别要避免不写 else
引起的隐藏逻辑。