Main

视频链接:从电路设计的角度入门VerilogHDL_哔哩哔哩_bilibili

概述

VerilogHDL(HDL – Hardware Description Language)
高层次综合语言 – HLS、Chisel
VerilogHDL 设计基本流程如下
|500
Placement & Routing 与 Synthesis/Netlist 最大的区别在于前者在板子上已经有了坐标了,而后者阶段还没有。
Verilog 既可以用来进行算法级设计,也可以用在底层来直接进行描述。(有点类似于 C++ 和汇编混合在一起的样子)。但可综合语句仅占 Verilog 中很小的子集,如上图粉色背景部分描述的。其中很大部分用来描述电路一般并没有掌握或者学习到(类似汇编部分)
务必注意数字电路设计的规模会变得越来越大,重用性和可扩展性。(原理图形式设计基本不可用)

相关的 EDA 工具

|500

VerilogHDL 的缺陷

|500
针对这些缺陷有很多挑战者,最知名的如 HLS,Chisel 以及 SpinalHDL.

编写方法

电路设计对象

|500
Reference 和 Cell :有时候我们会把同一个电路复制几份,最原始的那个叫作 Reference,每复制一个就叫做一个 Cell,一个电路上 Cell 的数量是远远大于 Reference 的。 结合上图和下图简单理解就是,我定义了一个 ENCODER module,这是一个设计文件。之后利用它实例化了其他模块如 U1。那么这个最初的参考的设计文件就称为 Reference,而利用它实例化出来的模块就称为 Cell。

关于 Port,Pin 以及 Net,课程中所说感觉不太好理解和记忆,chatgpt 总结了下他们大致区别是:

  • Port(端口):Port是模块(module)的接口,用于与其他模块进行通信。在Verilog中,端口可以是输入(input)、输出(output)或双向(inout)。端口定义了模块与外部世界之间的通信接口。
  • Pin(引脚):Pin通常指的是芯片或电路板上的物理引脚,用于连接到其他芯片或电路板。在Verilog中,引脚通常用于描述模块实例与电路板上的物理引脚之间的连接关系。
  • Net(网络):Net是连接模块内部元素的电气连接。在Verilog中,net用于连接模块内部的信号线,可以是wire、reg等数据类型。net用于传输信号和数据,连接模块内部的各个元素。

对应上图中描述的代码应该是这个样的
|475

综上所述,port是模块的接口,用于模块之间的通信(A,B,C,D,OUT1) ;pin是物理引脚,用于连接模块实例与电路板上的引脚(clk);net是连接模块内部元素的电气连接(INV1, INV0, bus1, bus0),用于传输信号和数据(如果 module 实例化失败,则与之相关的 net signal 可能都会被 optimized away)。

执行顺序

在 Verilog 的 module 结构中,所有描述语句(包括连续赋值语句、行为语句块 always initial 以及模块实例化等)都是并行发生的,而 begin…end 中存在的语句是内部串行的。

变量

  • wire 型:表示电路模块中的连线,仿真波形中不可见;
  • reg 型:占用仿真环境的物理内存,均显示在仿真波形中;
    两个凡是:
  • 凡是在 always initial 语句中赋值的变量,一定是 reg 型;
  • 凡是在 assign 语句中赋值的变量,一定是 wire 型;
    务必注意:reg 变量仅仅是语法定义,不等于电路中的寄存器。只有时序电路中的 reg 变量才会被逻辑综合工具认为是寄存器

补充一个知识点,在Verilog中,当你定义一个模块时,如果你没有明确指定端口类型,默认情况下端口类型被假定为wire。那么什么时候需要显式地将端口定义为 reg 类型呢?大致有下面两种:
第一种,输出端口需要在时钟的上升沿或下降沿触发时进行赋值。在这种情况下,输出端口应该被定义为reg类型,以便在时序逻辑中使用。如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module example_module (
input clk,
input rst,
input [7:0] data_in,
output reg [7:0] data_out
);
always @(posedge clk or posedge rst) begin
if (rst) begin
data_out <= 8'b0; // 在复位时对输出端口进行赋值
end else begin
data_out <= data_in; // 在时钟上升沿触发时对输出端口进行赋值
end
end
endmodule

第二种,当端口需要在组合逻辑中存储中间结果时,你也需要将端口定义为reg类型。如下代码所示

1
2
3
4
5
6
7
8
9
module example_module (
input [7:0] a,
input [7:0] b,
output reg [7:0] result
);
always @* begin
result = a + b; // 将输出端口定义为reg类型,以存储中间计算结果
end
endmodule

可综合描述四个关键字

always + If-else + assign + case
禁止:function + for + fork-join + while
assign 关键字主要用来对信号进行连接,重命名,简单组合逻辑(复杂的难以阅读)等,“=” 右边的任何变化都会被立即计算并驱动给等号左边
always@(*) 会自动将该 always 块中所有引用的信号都自动添加到敏感列表中(适用于敏感信号特别多的情况)

只使用这四个关键字编写的程序一定是可综合的

电路结构描述方法

MUX – 多路选择器(输出结果由输入的选择条件决定)

完全可以用 if-else 来描述
下图是一个选择加法器设计,同一个功能由两种不一样的逻辑实现,既可以先加后选,也可以先选后加,如下图所示
|500
|500
对于数字电路而言,加法器是比较复杂的,所以先选后加面积会比较小。可以看到不同代码产生的逻辑电路是完全不一样的,在进行代码优化时可以考虑性能优先还是面积优先来设计(类似 C 语言代码产生的汇编一样,也是有性能差异,只是编译器做了很多优化工作所以差异甚小)。

触发器与锁存器

两者对比如下图所示(这里寄存器应该是指的触发器)
|500
这里锁存器“输入-输出透明”是指有效期间 D 和 Q 是完全相同的,这种透明就很容易引入一个问题。假设此时 D 信号是有毛刺的,那么就会原封不动地搬给 Q,容易传播毛刺。

有两种容易引入 latch 的途径,它们的本质都是因为使用了不完备的条件判断语句(这里所说的引入 latch 我个人理解可能更多地是指“锁存”这种行为,并不全是指锁存器这个元件)
如果引入了意外的锁存器,可能会导致设计的行为与预期不符,甚至会产生时序故障或逻辑错误。而在另外一些情况下,可以有意地引入锁存器,来实现特定的功能或者优化设计。
|500
上面两种情况都是因为缺少完备的判断条件,这会导致一些条件下没有赋值语句执行,在这些条件下触发器或锁存器的值不会更新,最后导致意外的行为。也就是说,如果某些条件不满足,触发器或锁存器的值可能会保持不变,而不是按预期的方式更新。
对于触发器,需要注意的是输出信号会比输入信号要晚一个时钟周期,每使用一个触发器,时钟周期就会晚一拍。
这点是由于触发器的原理导致的,CMU 产生的时钟信号的上升沿或下降沿会触发触发器的状态变化,从而将输入信号暂时存储起来,并在下一个时钟周期的上升沿或下降沿将数据输出。因此输出的信号会比输入信号晚一个时钟周期。由此可以联想到,当使用多个触发器时,每个触发器的时序延迟会累加,导致输出信号相对于输入信号的延迟时间增加。
提醒:时序是在 Verilog 编程得到实际电路的过程中设计出来的,而不是通过仿真得到的。

组合逻辑

组合逻辑描述多使用 assign: ? 语句来描述,适用于比较简单的组合逻辑。对于复杂的组合逻辑,太多的 assign 类型语句不易解读,此时推荐使用 always 块来描述,二者效果是等价的。
组合逻辑赋值必须使用 = 阻塞赋值
组合逻辑无保存或者锁存功能,因此,没有复位信号与相关的复位逻辑

时序逻辑

时序逻辑其实就是"组合逻辑+触发器"的实现,如下图所示,可以把他们两个拆开来写,也可以组合来写
|500

存储器

主要指常见的单口 RAM、双口 RAM 和 ROM 等类型的存储器。Verilog 语法中基本的存储单元定义格式如下

1
reg [WORD_WIDTH-1:0] MemoryName [DEPTH-1:0];

举例如定义一个数据位宽为 8 bit,地址为 64 位宽的 RAM 8x64,则可以定义为

1
reg [7:0] RAM8x64 [63:0];

在使用时需要注意,不能直接引用存储器某地址的某比特位值,而是应该先将存储单元赋值给某个寄存器,然后再对该寄存器的某位进行相关操作。(有点类似于软件编程时先将硬盘数据读入寄存器再进行操作)
课程中提到不推荐使用 Verilog 直接建模 RAM,推荐使用内嵌的 IP 生成器,在 GUI 中配置生成。

流水线设计

不考虑其他因素的情况下,流水线级数越多,工作效率越高
下图是流水线设计的一个例子
|500
左边框图里只使用了一个DFF,逻辑路径上的操作包括 + 和 *,此时他们两个的执行是串行关系。当在其中插入一个 DFF 变成一级流水之后,如右边框图所示,此时虽然第一次计算 Q 在第二个周期才得到计算结果(因为第一次时只能等加法计算完成后才能计算乘),但之后运算时二者都是并行进行处理的。简单理解就是在同一个时刻,乘法器在计算上一个周期的 f 与 d 相乘时,加法器正在计算当前 a 和 b 的加法,之后执行也是如此,因此逻辑路径上的延迟相当于减少了原来的二分之一。

参数化和实例化

参数化

如下代码所示,其中 # 之后的延迟其实是仿真时可见时序的延迟,在实际综合时只有真实门电路的延迟

1
2
3
4
parameter and_delay = 2;
parameter xor_delay = 4;
and #and_delay u1(co, a, b);
xor #xor_delay u2(sum, a, b);

这里可以通过 parameter(类似 C 语言中的 define)来实现后续的改写,常用在位宽修改
define 多用在全局的定义上,而 parameter 更多使用在局部定义上

仿真

波形文件

常见的波形文件格式主要如下三个:

  • VCD(Value Change Dump),标准波形文件,所有仿真器都必须支持
  • fsdb(Fast Signal DataBase),Verdi 支持
  • WLF(Wave Log File),modelsim 产生

各家仿真或调试工具支持的波形文件类型,互不通用,但基本都可以由 VCD 文件转换而来(其实就是 VCD 文件的压缩版),有的还提供与 VCD 文件的互转换功能)

推荐调试方法:使用各种仿真器后台完成仿真,生成 fsdb 波形,然后使用 verdi 查看波形与调试(功能非常强大)
VCD 是 VerilogHDL 语言标准的一部分,因此所有的 Verilog 仿真器都能够查看该文件,允许用户在 Verilog 代码中通过系统函数来 dump VCD 文件。并且其包含了信号的变化信息,记录了整个仿真的信息。其优点是可以通过 VCD 文件来估计设计的功耗,这一点其他波形文件不具备。缺点是体积巨大

一般来说需要只抽取需要的信号来生成 VCD 查看,不然文件太大
|500
这种方法比较常见,也更加轻量级
|500
关于实际使用和测试,参考课程中给的链接进行:一文学会使用全球第四大数字芯片仿真器iverilog - 知乎 (zhihu.com)

Q & A

1. 为什么在大规模电路设计上 Verilog 渐渐力不从心?