Skip to content

Verilog 语法速览

Verilog Rewritten
Contents

本篇是 2026 年 10 月由 AI 根据后续笔记重写的速查版(2023 年学 Verilog 时的原始语法笔记已遗失),里面的代码都用 iverilog 编译、仿真检查过。

只写 Verilog-2001。SystemVerilog 的 logic、always_ff、enum 这些不在本篇范围内。

1. 模块与端口

module 是 Verilog 的基本单位,除了编译指令,所有代码都写在 module ... endmodule 里。模块对外只露出端口,内部怎么实现,上层看不到。

Verilog-1995 在括号里只写端口名,方向另写在体内(module and2 (a, b, y); input a, b; output y;);Verilog-2001 允许把方向、类型、位宽都写进括号(ANSI 风格),现在基本都用这种:

module and2 (input wire a, input wire b, output wire y);
    assign y = a & b;
endmodule

module top (input wire x1, x2, output wire z1, z2);
    and2 u_a (.a(x1), .b(x2), .y(z1));   // 按名字连:.子模块端口(本模块信号),推荐
    and2 u_b (x1, x2, z2);               // 按位置连:跟端口声明顺序对应,端口一改就乱
endmodule
  • 端口不写类型时默认是 wire。在 always 块里赋值的输出写成 output reg [7:0] q。
  • input reg 不合法:输入由外部连续驱动,Verilog 里只有 net 能被连续驱动。
  • ANSI 风格要在头里一次写全。头里写了 output out,体内又补一句 reg out;,是两种风格混用,2001 标准不允许,iverilog 会报重复声明。
  • 带参数的模块例化时用 #( ) 改参数:fifo #(.DEPTH(16)) u_fifo (...);。
  • 上层可以用层次名引用下层信号,比如 tb.dut.state。testbench 里看内部状态很方便,RTL 里不用。
  • module 之间只能例化,不能嵌套定义。testbench 顶层没有端口:module tb;。

2. 数据类型

wire 和 reg

wire(net)reg(variable)
含义连线,本身不存值变量,保持最后一次被赋的值
谁给它赋值assign、例化模块的输出端口always / initial 里的过程赋值
没人驱动时z仿真开始时是 x

reg 不等于触发器,综合出什么看写法:在 always @(posedge clk) 里赋值是触发器;在 always @* 里每条路径都赋了值是组合逻辑;有路径没赋值就成了 latch(见第 7 节)。其他类型:integer(32 位有符号,做循环变量和计数)、time(64 位无符号)、real(浮点,只用于仿真)。

向量和数组

wire [7:0]  bus;                      // 8 位,bus[7] 是最高位
wire [3:0]  hi    = bus[7:4];         // 部分选择
wire [31:0] word;
wire [7:0]  byte2 = word[2*8 +: 8];   // 从第 16 位往上取 8 位,即 word[23:16]
reg  [7:0]  mem [0:255];              // 数组:256 个字,每个字 8 位
  • +:、-: 是 2001 新加的写法:宽度必须是常数,起点可以是变量,比如 word[sel*8 +: 8]。
  • 数组名前面的 [7:0] 是每个字的位宽,后面的 [0:255] 是字数。一次访问一个字:mem[addr]。2001 起还能直接取字里的几位(mem[addr][3:0]),也支持多维数组。
  • 数组不能整块赋值(mem = 0 不行),清零要用 for 循环逐个写,装初值用 $readmemh。

parameter 和 localparam

module mod_counter #(
    parameter N     = 10,              // 模 N,例化时可以改
    parameter WIDTH = 4                // 要装得下 N-1
) (
    input  wire             clk, rst_n, en,
    output reg  [WIDTH-1:0] cnt
);
    localparam LAST = N - 1;           // 内部派生的常量,外部改不了

    always @(posedge clk or negedge rst_n)
        if (!rst_n)                 cnt <= 0;
        else if (en && cnt == LAST) cnt <= 0;
        else if (en)                cnt <= cnt + 1'b1;
endmodule
  • parameter 放位宽、深度、延时这类要从外面配置的量,例化时用 #(.N(60)) 覆盖。也可以不写 #( ),在模块体里写 parameter N = 10;,同样能改。localparam 不能从外面改,放派生常量和状态编码。
  • Verilog-2001 没有 $clog2(2005 才加)。地址位宽要么当参数传进来,要么自己写一个常量函数:2001 允许在 localparam 里调用只依赖常数的 function。

3. 数值表示

格式是 <位宽>'<进制><数值>,比如 8'b1100_0101、8'hc5、4'd9。

  • 进制有 b、o、d、h,不分大小写;下划线只为好读。位宽永远是二进制位数,跟用哪种进制写无关:8'hc5 也是 8 位。
  • 不写位宽:'hc5 至少 32 位(常见实现就是 32 位)。纯数字 10 是 32 位有符号十进制数,-1 就是 32 个 1。
  • 数值比位宽长时高位被截掉:4'hAB 实际是 4'hB,工具一般只给 warning。习惯上永远写全位宽。
  • x 是不定值,z 是高阻,数字里的 ? 等同于 z(casez 里表示不关心)。
  • 负号写在位宽前面:-8'd5 是 5 的补码 8'b1111_1011;8'd-5 是非法写法。
  • 2001 加了 s 标记:4'sb1011 是有符号数,值为 -5。不带 s 的带进制常数(如 8'd2)都是无符号的,有符号运算里会出坑(见第 15 节)。

数字右对齐,写出来的位数不够时往高位补:最左边写的是 x 或 z 就补 x 或 z,是 0 或 1 就补 0。比如 8'bzx 等于 8'bzzzz_zzzx,8'b1x 等于 8'b0000_001x。这种扩展容易心算错,最好每一位都写出来。

4. 运算符

按位、逻辑、归约

  • 按位 ~ & | ^ ~^:逐位运算,结果和操作数一样宽,宽度不同时短的先扩展。
  • 逻辑 ! && ||:把整个数当真假(非零为真),结果 1 位。
  • 归约 & ~& | ~| ^ ~^:单目运算,把一个向量的所有位合成 1 位。
// a = 4'b1010
~a    // 4'b0101:每一位取反
!a    // 1'b0:a 非零即真,取反得假
&a    // 1'b0:所有位相与
|a    // 1'b1:有一位是 1 就是 1,等价于 a != 0
^a    // 1'b0:偶数个 1 得 0,常用来算奇偶校验

处理数据用按位,写 if 条件用逻辑。多位信号上两者结果不同:flag = 4'b0001 时 ~flag 是 4'b1110,if (~flag) 成立,if (!flag) 不成立。

相等:== 和 ===

  • ==、!=:有 x 或 z 参与、因此判断不出结果时返回 x,比如 4'b1x00 == 4'b1100 得 x;已知的位已经不同就照样返回 0,比如 4'b1x00 == 4'b0100 得 0。
  • ===、!==:把 x、z 当普通值逐位比,结果只有 0 或 1,比如 4'b1x00 === 4'b1x00 得 1。

===、!== 只用在 testbench,不可综合。if 把 x 当成假:testbench 里写 if (dout != exp),dout 是 x 时进不了报错分支,错误就漏了,比对要用 !==。

算术与移位

  • + - * / %,2001 新增乘方 **。/ 和 % 综合出来面积大、路径长,除以 2 的幂直接写成移位。
  • <<、>> 是逻辑移位,空位补 0。<<< 和 << 完全一样;>>> 只有操作数是有符号数时才补符号位,无符号数照样补 0。比如 u 是 reg [7:0]、s 是 reg signed [7:0],都等于 8'b1111_1000 时,u >>> 1 得 8'b0111_1100,s >>> 1 得 8'b1111_1100(-4)。
  • 移位不会自己变宽,结果有多宽看上下文:
wire [3:0] w4 = 4'b1001 << 1;   // 4'b0010:最高位的 1 被移出去了
wire [4:0] w5 = 4'b1001 << 1;   // 5'b10010:先扩到 5 位再移,保住了

拼接、条件运算和优先级

{a, b}            // a 在高位,b 在低位
{4{w}}            // w 重复 4 次
{b, {3{a, b}}}    // 可以嵌套
{{8{x[7]}}, x}    // 把 8 位有符号数 x 符号扩展成 16 位
  • 拼接也能放在赋值左边:assign {cout, sum} = a + b;,进位单独拿出来。
  • 拼接里每个操作数都要有确定的位宽。{1, 0} 标准不允许;有的工具会当 32 位处理,拼出 64 位的数,iverilog 直接报错。要写成 {1'b1, 1'b0}。
  • 条件运算 sel ? a : b,嵌套起来就是带优先级的多路选择。sel 是 x 时结果逐位合并:两边相同的位保留,不同的位变成 x。

优先级从高到低:单目运算 → ** → * / % → + - → 移位 → < <= > >= → == != === !== → & → ^ ~^ → | → && → || → ?:。不用背,拿不准就加括号。&、| 比 == 低,是最常见的坑:

assign hit  = flags & MASK == 0;     // 实际是 flags & (MASK == 0)
assign hit2 = (flags & MASK) == 0;   // 本意
assign y    = x << 1 + n;            // 实际是 x << (1 + n)

5. 赋值

连续赋值和过程赋值

连续赋值过程赋值
写法assign y = a & b;写在 always / initial 块里
左边net(wire)variable(reg、integer)
什么时候执行右边变化就重算,一直有效块被触发时按顺序执行
  • 声明时直接赋值也是连续赋值:wire y = a & b;。
  • reg clk = 1'b0; 是 2001 的变量初值写法,相当于 0 时刻执行一次的 initial,只在 testbench 里用。
  • 同一个二选一,可以写 assign y1 = s ? b : a;(y1 是 wire),也可以写 always @* if (s) y2 = b; else y2 = a;(y2 要声明成 reg)。

阻塞与非阻塞

  • 阻塞 =:执行到这一句立刻更新左边,后面的语句看到的是新值,跟 C 一样按顺序执行。
  • 非阻塞 <=:分两步。先把这一时刻所有 <= 的右边算好,用的全是旧值;等这一时刻的其他事件处理完,再统一写进左边。所以 <= 右边看到的永远是这一拍开始时的旧值,跟同一块里前面写了什么无关。
always @(posedge clk) begin
    a  <= b;      // 交换:<= 能换成功;换成 =,a、b 都等于 b 原来的值
    b  <= a;
    q1 <= d;      // 移位寄存器:<= 是三级;换成 =,q1、q2、q3 同一拍都等于 d
    q2 <= q1;
    q3 <= q2;
end

同一个时钟沿触发了几个 always 块,谁先执行标准没有规定。下面两句用 =,mem 写在指针的旧值还是新值上,要看哪个块先跑;都改成 <=,就一定写在旧指针上,指针下一拍才加一。

always @(posedge clk) if (do_wr) wr_ptr = wr_ptr + 1'b1;   // 错:时序逻辑用了 =
always @(posedge clk) if (do_wr) mem[wr_ptr] = din;

规则:

  1. 时序逻辑(always @(posedge clk ...))用 <=。
  2. 组合逻辑(always @*)用 =。
  3. 同一个 always 块里不混用 = 和 <=。
  4. 一个变量只在一个 always 块里赋值。
  5. 要在同一时刻看到 <= 更新后的值,用 $strobe,不用 $display(见第 10 节)。

6. always、initial 与敏感列表

  • initial:0 时刻开始,执行一遍就结束,多个 initial 之间是并行的。ASIC 综合不认它,FPGA 工具可以拿它做上电初值,算特例。
  • always:执行完又从头开始,一直重复到仿真结束。块里必须有 @ 或 #,否则 0 时刻就原地死循环,仿真卡住。
  • begin ... end 里的语句按顺序执行;fork ... join 里的语句同时开始、全部结束才往下走,只用在 testbench。
always @(posedge clk) q <= d;    // 时序逻辑(带异步复位的写法见第 13 节)
always @* y = (a & b) | c;       // 组合逻辑
always @(a) y_bad = a & b;       // 漏了 b:b 变化时仿真里 y_bad 不更新
  • 2001 起敏感列表可以用逗号代替 or(@(a, b, c)),还可以写 @*(或 @(*)),自动把块里读到的信号都加进去,组合逻辑一律这么写。手写列表漏了信号,仿真里输出不更新;综合工具却不看列表,照样综合成组合逻辑,仿真和综合就对不上了。
  • posedge 除了 0→1,也包括 0→x、x→1 这类变化,negedge 同理。所以 0 时刻 rst_n 从 x 变成 0 也算一次 negedge。
  • 同一个列表里不要混用边沿和电平,@(posedge clk or rst) 综合工具不认。

7. 条件语句与 latch

always @* begin
    if (sel == 2'd0)      y = a;     // if-else 有优先级,先判断前面的条件
    else if (sel == 2'd1) y = b;
    else                  y = c;
    case (sel)                       // case 的各分支是并列的
        2'd0:       z = a;
        2'd1, 2'd2: z = b;           // 几个值共用一个分支
        default:    z = c;
    endcase
end

always @*
    casez (req)                      // ? 和 z 表示不关心,从上往下第一个匹配的生效
        4'b???1: grant = 2'd0;       // req[0] 优先级最高
        4'b??1?: grant = 2'd1;
        4'b?1??: grant = 2'd2;
        4'b1???: grant = 2'd3;
        default: grant = 2'd0;
    endcase
  • case 用 case ... endcase 包住,某个分支里有多条语句时,那个分支才加 begin/end。
  • case 是逐位全等比较,跟 === 一样,x、z 也要一模一样才算匹配。
  • casex 把 x 也当成不关心。仿真时选择信号本身是 x 也会被匹配上,会把 bug 藏起来,一般不用。

组合 always 块里,只要有一条路径没给某个输出赋值,它就得记住旧值,综合工具只能推出一个 latch。latch 让时序分析变难,组合逻辑里出现 latch 基本都是写漏了。

always @* begin
    if (en) q = d;        // en 为 0 时 q 没被赋值:推出 latch
end

always @* begin
    q2 = 1'b0;            // 先给默认值
    if (en) q2 = d;       // 每条路径都有值:纯组合逻辑
end

防 latch:块开头先给所有输出赋默认值(状态机里常写 next = state;);if 配 else,case 配 default。时序块里少写 else 不会出 latch,触发器本来就保持原值,相当于多了一个使能。

8. 循环

for、while、repeat、forever 四种。RTL 里只用次数固定的 for,其他三种留给 testbench。

// 8 位移位相加乘法器:b 的第 i 位是 1,就把 a 左移 i 位累加
module mult8 (
    input  wire [7:0]  a, b,
    output reg  [15:0] p
);
    integer i;

    always @* begin
        p = 16'd0;
        for (i = 0; i < 8; i = i + 1)
            if (b[i]) p = p + ({8'd0, a} << i);
    end
endmodule
  • Verilog-2001 没有 i++,要写 i = i + 1。循环变量声明成 integer,写在块外面,不能在 for 括号里声明。
  • RTL 里的 for 不是一拍一拍地跑 8 次,而是综合时展开成 8 份并行的硬件,所以次数必须是常数。练习里同一个乘法器也用 repeat (8) 配合移位写过,仿真结果一样,但要综合还是写成 for。
  • testbench 里:initial forever #5 clk = ~clk; 产生时钟;repeat (5) @(posedge clk); 等 5 个上升沿;while (!done) @(posedge clk); 每拍查一次,直到 done 为 1。

9. task、function 和 generate

task 和 function

functiontask
返回通过函数名返回一个值,可以写在表达式里,包括 assign 右边没有返回值,当一条语句调用,结果从 output 带出来
参数至少一个 input,不能有 output、inoutinput、output、inout 都行,也可以没有参数
时序控制不能有 #、@、wait,零时间算完可以有,能跨越仿真时间
调用别的只能调 functiontask、function 都能调
综合可综合,就是一块组合逻辑不带时序控制时多数工具能综合;带延时的只用在 testbench
function [3:0] count_zeros;          // 统计 8 位数里有几个 0,返回值 4 位
    input [7:0] x;
    integer i;
    begin
        count_zeros = 4'd0;          // 给函数名赋值就是返回值,2001 没有 return
        for (i = 0; i < 8; i = i + 1)
            if (x[i] == 1'b0) count_zeros = count_zeros + 4'd1;
    end
endfunction

assign zeros = count_zeros(data);    // function 可以直接用在 assign 里

task sort2;                          // 冒泡排序里的比较、交换,inout 让变量进出
    inout [3:0] x, y;
    reg   [3:0] t;
    begin
        if (x > y) begin
            t = x;  x = y;  y = t;
        end
    end
endtask

四个数排序就是在 always @* 里把输入拷进 va、vb、vc、vd,再依次调用 sort2(va, vc)、sort2(vb, vd)、sort2(va, vb)、sort2(vc, vd)、sort2(vb, vc),排完 va 最小、vd 最大。

task 和 function 默认是 static,所有调用共用一份内部变量。testbench 里几个 initial 同时调用同一个 task,或者要递归,就声明成 task automatic / function automatic(2001 新增)。

generate

generate 在 elaboration 阶段按参数复制或者挑选硬件结构,例化、assign、always 都能放进去,循环变量用 genvar。

module bit_reverse #(parameter N = 8, parameter USE_REG = 0) (   // USE_REG = 1 时输出再打一拍
    input  wire         clk,
    input  wire [N-1:0] a,
    output wire [N-1:0] y
);
    wire [N-1:0] r;
    genvar i;
    generate
        for (i = 0; i < N; i = i + 1) begin : g_rev    // 复制 N 份
            assign r[i] = a[N-1-i];
        end

        if (USE_REG) begin : g_ff                     // 按参数二选一
            reg [N-1:0] q;
            always @(posedge clk) q <= r;
            assign y = q;
        end else begin : g_comb
            assign y = r;
        end
    endgenerate
endmodule
  • 2001 里 generate / endgenerate 必须写(2005 才允许省略),for 循环里的 begin 块要起名。
  • generate for 复制的是结构,展开后每一份有自己的层次名(如 g_rev[3]);always 里的 for 只是在一个块里展开语句。

10. 常用系统任务

$display("t=%0t a=%b b=%h c=%0d", $time, a, b, c);   // 立即打印,自动换行
$write("no newline ");                               // 不换行
$strobe("q=%b", q);                                  // 等这一时刻的赋值全部完成再打印
$monitor("%0t clk=%b q=%b", $time, clk, q);          // 参数一变就打印
  • 格式符:%b %o %d %h 对应进制,%t 时间,%s 字符串,%m 当前模块的层次名。%0d、%0h 按最小宽度打印,去掉前面补齐的空格或 0。
  • 在时钟沿上用 $display 看 <= 赋值的寄存器,看到的还是旧值;$strobe 看到的是新值。
  • $monitor 同一时间只能有一个,再调用一次会顶掉前一个;$monitoroff / $monitoron 暂停、恢复。$time 本身变化不会触发它。
  • $time 返回 64 位整数,按调用它的模块的时间单位换算并四舍五入;$realtime 返回实数。例如在 `timescale 10ns/1ns 下 #1.55 是 15.5ns,按精度取整成 16ns,这时 $time 是 2,$realtime 是 1.6。
  • $random 返回 32 位有符号数:$random % 60 的范围是 -59 到 59;{$random} % 60 先用拼接变成无符号数,范围是 0 到 59。$random(seed) 用一个 integer 变量做种子,种子相同,序列就能复现。
  • $finish 结束仿真;$stop 暂停,进入交互模式。波形用 $dumpfile("wave.vcd"); $dumpvars(0, tb); 导出,这是标准的 VCD。$fsdbDumpfile / $fsdbDumpvars 是 Verdi 提供的,不属于标准 Verilog。
  • $readmemh / $readmemb 把文本文件里的十六进制 / 二进制数装进数组,文件里的数用空白隔开,可以写注释,用 @地址 跳到指定地址。对 reg [7:0] mem [1:256]:$readmemh("mem.data", mem) 从地址 1 装到 256;$readmemh("mem.data", mem, 16) 从 16 开始往上装;$readmemh("mem.data", mem, 128, 1) 从 128 往下装到 1。

11. 编译指令

编译指令以反引号开头,在编译之前做文本处理,行尾不加分号。

`timescale 1ns/1ps                       // 时间单位 / 时间精度
`include "defines.vh"                    // 把这个文件的内容原样插到这里
`define WIDTH 8                          // 宏,用的时候写 `WIDTH,比如 wire [`WIDTH-1:0] x;
`define MAX(a, b) ((a) > (b) ? (a) : (b))
`ifdef SIM                               // 编译时加 +define+SIM 走这一支
    `define DEPTH 16
`else
    `define DEPTH 1024
`endif
  • `define 从定义处开始,对后面编译的所有文件都生效,结果跟编译顺序有关;`undef 取消。条件编译有 `ifdef、`ifndef、`elsif、`else、`endif,其中 `ifndef 和 `elsif 是 2001 新增的。
  • `include 一次只能包含一个文件。被包含的文件不在当前目录时,与其写绝对路径,不如在编译命令里加搜索路径:VCS 用 +incdir+目录,iverilog 用 -I 目录。
  • `timescale 的精度不能比单位粗(10ns/1ns 可以,1ns/10ns 不行)。它一直作用到下一个 `timescale,跨文件也一样,最省心的做法是每个文件开头都写同一个。只影响仿真,综合时忽略。
  • `default_nettype none 关掉隐式 wire(见第 15 节),文件末尾再写 `default_nettype wire 恢复默认,免得影响别的文件。

12. 状态机

当前状态存在寄存器里,下一状态由组合逻辑算出来,时钟沿一到,下一状态写进当前状态,两者永远差一拍。三段式写法:第 1 段是状态寄存器,always @(posedge clk or negedge rst_n) 里用 <=;第 2 段是次态逻辑,always @* 里用 =,开头写 next = state;,case 配 default;第 3 段是输出逻辑,Moore 型只看当前状态。

两段式就是把第 2、3 段合进一个组合块。三段式分工清楚,也最不容易写出 latch。Verilog-2001 没有 enum,状态编码用 localparam。下面是交通灯,绿、黄、红循环,每种颜色停若干拍:

module traffic_light #(
    parameter G_DUR = 4, Y_DUR = 2, R_DUR = 5    // 每种颜色停几拍,不超过 16
) (
    input  wire       clk, rst_n,
    output reg  [1:0] light                      // 0 绿,1 黄,2 红
);
    localparam GREEN = 2'd0, YELLOW = 2'd1, RED = 2'd2;
    reg [1:0] state, next;
    reg [3:0] cnt;                               // 当前状态已经停了几拍

    always @(posedge clk or negedge rst_n)       // 第 1 段:状态寄存器,计时器一起
        if (!rst_n) begin
            state <= GREEN;
            cnt   <= 4'd0;
        end else begin
            state <= next;
            cnt   <= (next != state) ? 4'd0 : cnt + 4'd1;   // 换状态时清零
        end

    always @* begin                              // 第 2 段:次态逻辑
        next = state;                            // 默认留在原状态
        case (state)
            GREEN:   if (cnt == G_DUR - 1) next = YELLOW;
            YELLOW:  if (cnt == Y_DUR - 1) next = RED;
            RED:     if (cnt == R_DUR - 1) next = GREEN;
            default: next = GREEN;
        endcase
    end

    always @*                                    // 第 3 段:输出逻辑,只看当前状态
        case (state)
            YELLOW:  light = 2'd1;
            RED:     light = 2'd2;
            default: light = 2'd0;
        endcase
endmodule
  • Moore:输出只由当前状态决定,跟着状态寄存器变,比较干净,比如 assign busy = (state != IDLE);。
  • Mealy:输出由当前状态和输入一起决定,比如 assign hit = (state == S3) && din;。输入一变输出当拍就变,比 Moore 早一拍反应,有时还能少几个状态;代价是输入上的毛刺会直接传到输出。
  • 饮料机状态机那个练习里,hold、drink_ind 只看当前状态,是 Moore 型输出;charge_ind 还要看 cancel_flag,是 Mealy 型输出。输出要干净,可以再打一拍寄存器,把毛刺挡在寄存器前面,代价是晚一拍。

13. 复位

always @(posedge clk)                    // 同步复位:只在时钟沿检查
    if (!rst_n) q1 <= 1'b0;
    else        q1 <= d;

always @(posedge clk or negedge rst_n)   // 异步复位:rst_n 一拉低就复位,不等时钟
    if (!rst_n) q2 <= 1'b0;
    else        q2 <= d;
  • 同步复位:时序分析简单,复位线上的毛刺被滤掉;缺点是时钟不跑就复位不了,复位信号还要跟数据一起进组合逻辑。
  • 异步复位:敏感列表里的 negedge rst_n 让复位一有效就触发这个块,块里第一个判断必须是复位,极性跟列表一致。寄存器自带异步清零端,响应快。风险在释放:rst_n 回到 1 的时刻离时钟沿太近(不满足 recovery / removal 时间),寄存器可能进入亚稳态。

异步复位、同步释放

复位来的时候异步生效;撤掉的时候,先用两级寄存器同步到时钟上,再送给后面的逻辑。

module reset_sync (
    input  wire clk,
    input  wire rst_n_async,         // 外部来的异步复位
    output reg  rst_n_sync           // 给后面所有逻辑用的复位
);
    reg rst_n_d1;

    always @(posedge clk or negedge rst_n_async)
        if (!rst_n_async) begin      // 复位:两级同时立刻清零
            rst_n_d1   <= 1'b0;
            rst_n_sync <= 1'b0;
        end else begin               // 释放:1 要被时钟连采两次才出去
            rst_n_d1   <= 1'b1;
            rst_n_sync <= rst_n_d1;
        end
endmodule
  • 复位:rst_n_async 一拉低,rst_n_sync 立刻变 0,不等时钟。
  • 释放:rst_n_async 变 1 以后,rst_n_sync 在第二个时钟沿才变 1,而且一定对齐时钟沿。第一级就算因为释放离时钟太近进了亚稳态,也有一整拍时间稳定下来。后面的逻辑照常写异步复位,敏感列表里换成 negedge rst_n_sync。
  • testbench 里,0 时刻把 rst_n 从 x 拉到 0 也算一次 negedge,但它和 always 块谁先启动没有保证。让复位保持一两个时钟周期最稳,第一个时钟沿来时块里照样会判到 !rst_n。

14. 可综合与 testbench

可综合与不可综合

综合把 RTL 翻译成标准单元连成的门级网表(netlist),只有能对应到硬件的写法才能综合。RTL 里放心用的有:assign、always @(posedge clk ...)、always @*、if、case、casez、次数固定的 for、generate、function、不带时序控制的 task、parameter、localparam,以及位运算、比较、移位、拼接和 + - *(/、% 尽量只用在常数上)。

只能用在 testbench原因
initial硬件没有“只执行一次”(FPGA 上电初值是特例)
#10 这类延时综合时被悄悄忽略、不报错,仿真对了综合后行为却变了
fork ... join并行进程,硬件里没有对应物
forever、while、repeat部分工具能综合次数固定的 while、repeat,但不通用
$display、$time、$finish 等仿真器的功能
===、!==硬件分不出 x 和 z
force、release、wait、event、real仿真控制,或硬件里没有对应的东西

testbench

testbench 是一个没有端口的顶层模块:产生时钟和复位,给 DUT 加激励,自己算期望值、自己比较,最后报 PASS 或 FAIL。下面测第 2 节的 mod_counter:

`timescale 1ns/1ps
module tb_mod_counter;
    reg        clk = 1'b0;                // 一定要有初值,否则 ~x 还是 x,时钟不跳
    reg        rst_n, en;                 // 驱动 DUT 输入用 reg
    wire [3:0] cnt;                       // 接 DUT 输出用 wire
    integer    exp_cnt = 0, err = 0, i;

    mod_counter #(.N(10), .WIDTH(4)) dut (.clk(clk), .rst_n(rst_n), .en(en), .cnt(cnt));

    always #5 clk = ~clk;                 // 每 5ns 翻一次,周期 10ns

    initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb_mod_counter);
        rst_n = 1'b0;  en = 1'b0;         // 上电先复位
        #20 rst_n = 1'b1;                 // 保持两个周期,在下降沿释放
        for (i = 0; i < 200; i = i + 1) begin
            @(negedge clk) en = $random;  // 下降沿给激励
            @(posedge clk); #1;           // 上升沿之后 1ns 再检查
            if (en) exp_cnt = (exp_cnt == 9) ? 0 : exp_cnt + 1;   // 参考模型
            if (cnt !== exp_cnt) begin    // 用 !==,cnt 是 x 也能抓到
                $display("[ERR] %0t cnt=%0d exp=%0d", $time, cnt, exp_cnt);
                err = err + 1;
            end
        end
        if (err == 0) $display("[PASS]");
        else          $display("[FAIL] %0d mismatches", err);
        $finish;
    end

    initial #100000 $finish;              // 超时保护,防止仿真卡死
endmodule
  • 延时和事件:#10 等 10 个时间单位;@(posedge clk) 等下一个上升沿;wait (done); 是电平等待,done 已经是 1 就直接往下走。
  • 在下降沿改输入:如果在上升沿改,testbench 的赋值和 DUT 在同一个沿上的采样谁先谁后没有保证,结果可能差一拍。检查也要避开采样的那个沿。
  • 参考模型用最直接的写法,跟 DUT 的实现方式不同,才能互相查错。

15. 常见坑速查

隐式 wire

没声明的名字出现在例化的端口连接里,Verilog 会自动给它造一根 1 位的 wire,不报错:

wire [7:0] data_bus;
sub u0 (.din(x), .dout(data_buss));   // 名字拼错:多出一根 1 位的 data_buss
assign y = data_bus;                  // data_bus 没人驱动,一直是 z

工具最多给个 warning。文件开头写 `default_nettype none,没声明的名字直接报错;这时端口最好也显式写出 wire,有的工具会对没写类型的端口报错。

位宽不匹配

Verilog 对位宽很宽容:右边比左边宽就截掉高位,窄就在高位补(无符号补 0,有符号补符号位),一般只有 warning。

wire [7:0] sum8 = a + b;       // a、b 都是 8 位:进位丢了
wire [8:0] sum9 = a + b;       // 先扩到 9 位再加:进位保住
assign {cout, sum} = a + b;    // 进位单独拿出来

拼接里的表达式按自身宽度算:wire [8:0] s = {a + b}; 只有 8 位结果,进位照样丢。

有符号运算

  • reg、wire 默认无符号,integer 有符号。要有符号数就声明成 reg signed [7:0],或者用 $signed() 转。
  • 一个表达式里只要有一个操作数是无符号的,整个表达式都按无符号算。
  • 位选择、部分选择、拼接的结果都是无符号的,哪怕选的是有符号数的全部位。
// x 是 reg signed [7:0],值为 -3
x < 8'd2                   // 0:8'd2 无符号,整个比较按无符号算,x 被当成 253
x < 2                      // 1:不带进制的 2 是有符号数
x < 8'sd2                  // 1
// sa 是 reg signed [7:0],值为 -2;ub 是 reg [7:0],值为 3;结果放进 16 位
sa * ub                    // 762:混了无符号数,sa 被当成 254
sa * $signed({1'b0, ub})   // -6:ub 先补一个 0 再转有符号,不会被当成负数

>>> 也要操作数是有符号数才补符号位(见第 4 节)。$signed(a) 是 Verilog-2001 的写法,signed'(a) 是 SystemVerilog 的写法。

其他

  • 多驱动:同一个 reg 在两个 always 块里赋值,仿真时谁后执行听谁的,综合工具会报多驱动;两个 assign 驱动同一根 wire,值不同的位变成 x。一个信号只在一个地方赋值。
  • 时序块用 <=,组合块用 =;组合逻辑一律 @*,块里每条路径都要赋值。
  • RTL 里不写 #;testbench 比对用 !==,时钟变量先给初值。
  • i++、logic、always_ff、$clog2 都不是 Verilog-2001:前三个是 SystemVerilog,$clog2 是 2005 加的。

参考

Discussion

Loading comments…

Checking sign-in…