本篇是 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;
规则:
- 时序逻辑(
always @(posedge clk ...))用<=。 - 组合逻辑(
always @*)用=。 - 同一个 always 块里不混用
=和<=。 - 一个变量只在一个 always 块里赋值。
- 要在同一时刻看到
<=更新后的值,用$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
| function | task | |
|---|---|---|
| 返回 | 通过函数名返回一个值,可以写在表达式里,包括 assign 右边 | 没有返回值,当一条语句调用,结果从 output 带出来 |
| 参数 | 至少一个 input,不能有 output、inout | input、output、inout 都行,也可以没有参数 |
| 时序控制 | 不能有 #、@、wait,零时间算完 | 可以有,能跨越仿真时间 |
| 调用别的 | 只能调 function | task、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…