HDL-Verilog 简介
Verilog is a portmanteau of the words “verification” and “logic”
Verilog HDL 是一种硬件描述语言,支持从晶体管级到行为级的数字系统建模。需要注意的是除了电路建模,Verilog 另一个重要功能是逻辑验证,所以学习 Verilog 要同时理解 Verilog 的建模(logic)和验证(verification)。Verilog 入门学习可以参考 verilog 菜鸟教程 / asic-world / HDLBits / step_into_mips / 《轻松成为设计高手》 / 优秀 Verilog 项目汇总 /
- 用 Verilog 进行设计和编程的基本单元是模块 ( module)——包含声明和语句的一个文本文件
- Verilog 设计之初就同时支持建模与仿真
- 寄存器传输级指不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓朴结构等), 通过描述寄存器到寄存器之间的逻辑功能描述电路的 HDL 层次。可以类比普通编程语言中的函数,调用接口时,我们一般只关心函数的功能
语言基础
-
建议在一个 Verilog 文件中,只放一个 module 定义,而且使文件名称和 module 名称一致。这是一个良好的设计习惯
-
在一些工具中,尤其是逻辑综合工具,定义了一些特殊的指令,用于控制工具编译过程。这些指令也是以注释的方式出现的
-
基本语句:initial、always、实例化、连续赋值(assign)
- initial 语句在 0 仿真时间执行, 而且只执行一次;always 语句同样在 0 仿真时间开始执行,但是它将一直循环执行
-
赋值语句有两种:持续赋值语句、过程赋值语句
- 在 always、initial 过程中的赋值语句称为过程赋值语句。凡是在 always 或 initial 语句中赋值的变量,一定是寄存器变量
- 过程赋值语句包含阻塞赋值(=)、**非阻塞赋值(<=)**和过程连续赋值(assign)。非阻塞赋值在整个过程语句结束时才会完成赋值操作;阻塞赋值在该语句结束时就立即完成赋值操作
- assign 为持续赋值语句,在 assign 中赋值的一定是线网变量
- 在 always、initial 过程中的赋值语句称为过程赋值语句。凡是在 always 或 initial 语句中赋值的变量,一定是寄存器变量
-
串行 / 并行。在begin. . .end 中存在的语句,按照 Verilog 的语义,应该是顺序执行的。而在 fork. . .join 中的语句,则是并行执行的
-
Verilog 有三种基本的描述方式:数据流、行为和结构化。RTL 级基本要素:时钟域、时序逻辑(寄存器描述)和组合逻辑
-
线网(Net)类型:wire / tri / wor / trior / wand / triand / trireg / tri0&1 / supply0&1。只有线网类型能在 assign 中赋值
-
寄存器(Reg)类型,寄存器类型变量在 Verilog 语言中通常表示一个存储数据的空间,并不一定对应电路中的存储器(锁存器或者触发器等)。interger / time / real / realtime
-
编译指令:
timescale /define / undef / ifdef / else / endif / include /resetall /
基本类型
在大型设计中,一个模块可能有很多端口,要记住这些端口的顺序是困难的。因此 Verilog 提供了另一种更方便的端口连接方法——命名端口连接,这种方法中端口和相应的外部信号按照其名字进行连接, 而不是按照位置
fullAdder adder(.sum(SUM), .cout(COUT), .a(A), .b(B), .cin(CIN),);
Verilog 通过对 reg 型变量建立数组来对存储器建模(memory),可以描述 RAM、ROM 存储器和寄存器数组;在 Verilog 中用 parameter / localparam 来定义常量
向量
// [MSB : LSB]
reg [4:0] a = 5'b11x01;
reg [31:5] ra; // 27 位的 reg 型向量 ra,其中 ra[31]是最高位,ra[5]是最低位
reg [0:7] rc; // 8 位的 reg 型向量 rc,其中 rc[0]是最高位,rc[7]是最低位
reg scalared [31:0] rega; // 使用关键字 scalared,表示是标量类向量
B = rega[5:2]; // 域选择,将向量 rega 的第 5、4、3、2 位赋值给变量 B
reg [31:0] mem[63:0]; // mem 是深度为 64,字长为 32bit 的存储器
操作符
Verilog 语法与 C 非常类似,不过也有一些特有的运算符,例如全等(===
/ !==
)、缩位运算符、位拼接等。缩位运算符示例如下
\[\begin{array}{|l|l|} \hline \& & \text { 与 } \\ \hline \sim \& & \text { 与非 } \\ \hline \mid & \text { 或 } \\ \hline \sim \mid & \text { 或非 } \\ \hline \wedge & \text { 异或 } \\ \hline \sim \wedge \text { 或 } \wedge ~ & \text { 同或 } \\ \hline \end{array} \]
等式运算符中的 “==” 与“===” 的区别是:对于 “==” 运算,参与比较的两个操作数必须逐位相等,其结果才为 1,如果某些值是不定态 X 或高阻态 Z,那么得到的结果是不定值 X;而对于全等 “===” 运算,则要求对参与运算的操作数中为不定态 X 或高阻态 Z 的位也进行比较,两个操作数必须完全一致,其结果才为 1,否则结果为 03
缩位运算符与位运算的运算符号、逻辑运算法则都是一样的,但是缩位运算符是对单个操作数进行与、或、异或的递推运算,它放在操作数的前面,能够将一个矢量减为一个标量
reg [3:0] a;
b = &a; // 等效于 b = ((a[0] & a[1]) & a[2]) & a[3]
wire [3:0] a = 4'b0011;
wire [3:0] b = 4'b0101; // a&b = 4'b0001,a|b = 4'b0111
位拼接运算符用来将两个或多个信号的某些位拼接起来,或重复信号的某些位({重复次数{被重复数据}}
),示例如下:
input [3:0] ina,inb; // 加法输入
output [3:0] sum; // 加法的和
output cout; // 进位
assign {cout, sum} = ina + inb; // 将和与进位拼接在一起
wire [7:0] Data;
wire [11:0] s_data;
s_data = {{4{Data[7]}},Data}; // s_data = {Data[7],Data[7],Data[7],Data[7],Data}
语义与仿真
仿真算法可以分为三大类4:
- 基于时间的仿真。例如 Spice,以较小的时间步长为仿真间隔,获得一段时间内系统的响应。效率低,精度高
- 基于事件的仿真。例如 Verilog-XL 和 NC Verilog 仿真器。这个是大部分 Verilog 仿真器的实现方式(任务队列)
- 在同一个时间片内发生的事件在硬件上是并行的
- 基于周期的仿真。只关心电路功能不关心时序,精度低但速度快,仅限于同步电路
学习 Verilog 要充分理解其语义与仿真原理,否则难以理解一些现象。这里所讲的 Verilog 仿真不同于综合之后的电路仿真,而是 Verilog 语言自身的仿真特性:①同一个设计,在不同的 Verilog 仿真器中仿真, 结果却不一致;②一个设计模块如果没有加上延时单位,仿真结果就不正确。虽然 Verilog 语言不仅仅是用于仿真,但是它的语义(semantics)是为仿真定义的,其他所有的东西都是根据这一基本的定义抽象得到的1
仿真原理
计算事件和更新事件之间循环往复地互相触发,从而推进仿真时间的前进。“什么时间做什么事” 是 Verillog 语言仿真的原则(时间驱动);事件是指在特定时刻,模型中数值的变化。 Verilog 语言的语义规定了一个事件导致其他事件及时发生的方式(事件驱动)
Verilog 的不确定性:①在零时刻的任意执行顺序;②任务调度之间的随意交叉。为了排查问题,每次执行任务前,可以将任务插入到队列中
过程语句
always & initial
过程赋值只能用于 always / initial 块,包含阻塞(=
,用于组合逻辑)和非阻塞赋值(<=
,用于时序逻辑)。always 块中可用的语法:条件、分支 case/x&z、循环(while&repeat&for)等。编写 Verilog HDL 的目的在于综合并获得有效的电路表示,但一些编码问题会造成综合失败,例如:
- 变量在多个 always 块中赋值。从电路的角度上来看,某个 pin 或者 net 由多个模块驱动,最终结果无法确定
- 不完整的信号敏感性列表。比如
always @(a,b)
失误写成了always @(a)
虽然可以综合,但结果和代码逻辑明显不同。使用always @*
可以避免这种情况 - 不完整分支和不完整输出赋值。Verilog 标准规定,没有赋值的变量则保持原值(这将综合出存储器),故组合逻辑中任何时候输出信号都应该赋值。避免方法:可以在进入分支语句前给变量赋默认值;case 语句中添加 default
module add32 (input wire clk, input wire [31:0] in1, input wire [31:0] in2, output reg [31:0] out);
always @ (posedge clk) //在时钟信号的上升沿会触发 always 中的语句
begin
out = in1 + in2;
end
endmodule
initial begin
for (addr = 0; addr < size; addr = addr + 1)
mem[addr] = 0;
end
编译指令
编译指令细节可以参考5第 19 章
// `define 宏名 变量或名字
`define RstEnable 1'b1 // if(rst == `RstEnable)...
`include "defines.v" // 文件包含语句
`ifdef 宏名
语句序列 1
`else
语句序列 2
`endif
系统函数
Verilog 系统函数表可以参考5第17章,标准提供了显示、文件 IO 和仿真等各种类型的系统函数
函数 | 备注 |
---|---|
$stop() |
1. 仿真控制 |
$readmemh ("rom.data", rom ); / |
1.文件 IO 操作 |
$dumpfile ("module1.dump") / $dumpvars (0, top.mod1, top.mod2.net1); / $dumpflush / $dumplimit(filesize) / $dumpall / … |
1. VCD dump,参考5第 18 章 2. 选择需要记录波形的信号 |
示例代码
理解 HDL 比较好的办法是使用硬件的角度思考,可以使用 yosys 等工具综合并可视化 HDL。Verilog 如下(综合结果如上):
module test (A_in, B_in, C_in, D_out);
input A_in, B_in, C_in;
output D_out;
reg D_out; reg Temp;
always @(A_in or B_in or C_in) begin
D_out = Temp | C_in;
Temp = A_in & B_in;
end
endmodule
开源综合和电路显示工具如 yosys、 netlistsvg 和 svgexport 的安装和使用方法请参考官网。为了方便,可以考虑直接使用在线版的 Digitaljs_online 进行综合,也可以自己搭建 DigitalJS 服务或者用 Vscode digitaljs 插件
参数化
真实场景下译码器的输入和输出端会根据需求发生变化,Verilog 提供了参数化模块的功能,可以在实例化模块时指定参数。下面是译码器的参数化代码:
// 使用示例:decode_n2s #(.N(N),.S(S)) UUT (.A(A),.EN(EN),.Y(Y));
module decode_n2s (A,EN,Y);
parameter N = 3, S = 8;
input [N-1:0] A;
input EN;
output reg [S-1:0] Y;
always @(*) begin
Y = 0;
if (EN == 1) Y[A] = 1;
end
endmodule
TestBench
在仿真的时候 testbench 用来产生测试激励给待验证设计(DUV)或者称为待测设计(DUT),同时检查 DUV 的输出是否与预期的一致,达到验证设计功能的目的。无缺陷的芯片不是设计出来的,而是验证出来的。本小节代码参考3第 2.7 节。包含的文件有
pc_reg.v
,简化版 CPU 指针模块(PC);rom.v
,简化版的 ROM;rom.data
,测试时使用的 ROM 初始化数据inst_fetch.v
,使用前两个文件创建一个实例;inst_fetch_tb.v
,testbench 文件- 相关代码可以从 github 下载
Verilog 设计中的 TestBench 和软件开发中的单元测试类似。Verilog 的测试平台并不对硬件建模,它只是一个程序,模拟器通过执行这个程序,将输入加到一个硬件模型的输入端,并观察其输出,这个硬件模型通常被称为 “待测单元”( UUT, Unit Under Test)
因为本节使用开源工具(iverilog)实现代码仿真,故对 inst_fetch_tb.v
做了简单修改,内容如下
module inst_fetch_tb;
reg CLOCK_50;
reg rst;
wire [31:0] inst;
initial begin
$dumpfile("inst_fetch_tb.vcd");
$dumpvars(0, inst_fetch_tb);
end
initial begin
CLOCK_50 = 1'b0;
forever #10 CLOCK_50 = ~CLOCK_50;
end
initial begin
rst = 1'b1;
#195 rst = 1'b0;
#1000 $stop;
end
inst_fetch inst_fetch0 (
.clk(CLOCK_50),
.rst(rst),
.inst_o(inst)
);
endmodule
测试相关命令记录如下:
iverilog -o iv_inst_fetch_test.out ./*.v
vvp iv_inst_fetch_test.out # finish
使用 dwfv / VCDRom / 可以打开上面 TB 生成的 VCD 文件 inst_fetch_tb.vcd
,验证波形信息是否符合预期