HDL-Verilog 简介

2022-04-07
8 min read

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 项目汇总 /

  1. 用 Verilog 进行设计和编程的基本单元是模块 ( module)——包含声明和语句的一个文本文件
  2. Verilog 设计之初就同时支持建模与仿真
    1. 寄存器类型变量在 Verilog 语言中通常表示一个存储数据的空间(状态保持),综合后并不一定对应着存储器1。在 Verilog 仿真器中,寄存器类型变量通常占据一个仿真内存空间
    2. Verilog 不是所有组件都可以综合,细节可以参考2第六章
  3. 寄存器传输级指不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓朴结构等), 通过描述寄存器到寄存器之间的逻辑功能描述电路的 HDL 层次。可以类比普通编程语言中的函数,调用接口时,我们一般只关心函数的功能

语言基础

  1. 建议在一个 Verilog 文件中,只放一个 module 定义,而且使文件名称和 module 名称一致。这是一个良好的设计习惯

  2. 在一些工具中,尤其是逻辑综合工具,定义了一些特殊的指令,用于控制工具编译过程。这些指令也是以注释的方式出现的

  3. 基本语句:initial、always、实例化、连续赋值(assign)

    1. initial 语句在 0 仿真时间执行, 而且只执行一次;always 语句同样在 0 仿真时间开始执行,但是它将一直循环执行
  4. 赋值语句有两种:持续赋值语句、过程赋值语句

    1. 在 always、initial 过程中的赋值语句称为过程赋值语句。凡是在 always 或 initial 语句中赋值的变量,一定是寄存器变量
      1. 过程赋值语句包含阻塞赋值(=)、**非阻塞赋值(<=)**和过程连续赋值(assign)。非阻塞赋值在整个过程语句结束时才会完成赋值操作;阻塞赋值在该语句结束时就立即完成赋值操作
    2. assign 为持续赋值语句,在 assign 中赋值的一定是线网变量
  5. 串行 / 并行。在begin. . .end 中存在的语句,按照 Verilog 的语义,应该是顺序执行的。而在 fork. . .join 中的语句,则是并行执行的

  6. Verilog 有三种基本的描述方式:数据流、行为和结构化。RTL 级基本要素:时钟域、时序逻辑(寄存器描述)和组合逻辑

  7. 线网(Net)类型:wire / tri / wor / trior / wand / triand / trireg / tri0&1 / supply0&1。只有线网类型能在 assign 中赋值

  8. 寄存器(Reg)类型,寄存器类型变量在 Verilog 语言中通常表示一个存储数据的空间,并不一定对应电路中的存储器(锁存器或者触发器等)。interger / time / real / realtime

  9. 编译指令: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

  1. 基于时间的仿真。例如 Spice,以较小的时间步长为仿真间隔,获得一段时间内系统的响应。效率低,精度高
  2. 基于事件的仿真。例如 Verilog-XL 和 NC Verilog 仿真器。这个是大部分 Verilog 仿真器的实现方式(任务队列)
    1. 在同一个时间片内发生的事件在硬件上是并行的
  3. 基于周期的仿真。只关心电路功能不关心时序,精度低但速度快,仅限于同步电路

学习 Verilog 要充分理解其语义与仿真原理,否则难以理解一些现象。这里所讲的 Verilog 仿真不同于综合之后的电路仿真,而是 Verilog 语言自身的仿真特性:①同一个设计,在不同的 Verilog 仿真器中仿真, 结果却不一致;②一个设计模块如果没有加上延时单位,仿真结果就不正确。虽然 Verilog 语言不仅仅是用于仿真,但是它的语义(semantics)是为仿真定义的,其他所有的东西都是根据这一基本的定义抽象得到的1

仿真原理

计算事件和更新事件之间循环往复地互相触发,从而推进仿真时间的前进。“什么时间做什么事” 是 Verillog 语言仿真的原则(时间驱动);事件是指在特定时刻,模型中数值的变化。 Verilog 语言的语义规定了一个事件导致其他事件及时发生的方式(事件驱动)

Verilog 的不确定性:①在零时刻的任意执行顺序;②任务调度之间的随意交叉。为了排查问题,每次执行任务前,可以将任务插入到队列中

过程语句

always & initial

过程赋值只能用于 always / initial 块,包含阻塞(=,用于组合逻辑)和非阻塞赋值(<=,用于时序逻辑)。always 块中可用的语法:条件分支 case/x&z循环(while&repeat&for)等。编写 Verilog HDL 的目的在于综合并获得有效的电路表示,但一些编码问题会造成综合失败,例如:

  1. 变量在多个 always 块中赋值。从电路的角度上来看,某个 pin 或者 net 由多个模块驱动,最终结果无法确定
  2. 不完整的信号敏感性列表。比如 always @(a,b) 失误写成了 always @(a) 虽然可以综合,但结果和代码逻辑明显不同。使用 always @* 可以避免这种情况
  3. 不完整分支和不完整输出赋值。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

开源综合和电路显示工具如 yosysnetlistsvgsvgexport 的安装和使用方法请参考官网。为了方便,可以考虑直接使用在线版的 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 节。包含的文件有

  1. pc_reg.v,简化版 CPU 指针模块(PC);rom.v,简化版的 ROM;rom.data,测试时使用的 ROM 初始化数据
  2. inst_fetch.v,使用前两个文件创建一个实例;inst_fetch_tb.v,testbench 文件
  3. 相关代码可以从 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,验证波形信息是否符合预期


  1. EDA先锋工作室. 轻松成为设计高手. 北京航空航天大学出版社, 2012. ↩︎

  2. 王金明, and 杨吉斌. 数字系统设计与Verilog HDL. 电子工业出版社, 2019. ↩︎

  3. 雷思磊. 自己动手写CPU. 电子工业出版社, 2014. ↩︎

  4. 数字集成电路设计入门–从HDL到版图 ↩︎

  5. Automation, Design , and S. Committee . “IEEE Standard for Verilog Hardware Description Language.” IEEE Std 1364-2005 (Revision of IEEE Std 1364-2001) IEEE, 2006. ↩︎

Previous HDL 基础