08. 函数、任务、编译指令与 testbench
写出 RTL 只完成了一半工作。你还要用 testbench 主动制造输入、观察输出,并让仿真自动判定 PASS/FAIL。纯 Verilog-2001 的 $finish 通常不保证返回非零进程状态,因此 CI 还应检查日志中是否存在唯一的 PASS;支持 SystemVerilog 的仿真器可在失败时使用 $fatal。
function:有一个返回值的组合计算
跳转到“function:有一个返回值的组合计算”Verilog 函数不能消耗仿真时间,不能包含 #、@ 或 wait 等时序控制。它适合表达可复用的组合计算。
function [3:0] count_ones8; input [7:0] value; integer i; begin count_ones8 = 4'd0; for (i = 0; i < 8; i = i + 1) count_ones8 = count_ones8 + value[i]; endendfunction
wire [3:0] ones = count_ones8(data);函数名本身是返回值。综合后函数通常被内联为调用位置的组合硬件,不是软件中的共享处理器。
task:多个输入输出的过程复用
跳转到“task:多个输入输出的过程复用”任务可以有 input/output/inout 参数,也可以在 testbench 中包含时间控制:
task drive_and_check; input [7:0] test_a; input [7:0] test_b; input [8:0] expected; begin a = test_a; b = test_b; #1; if (sum !== expected) begin $display("ERROR a=%0d b=%0d sum=%0d expected=%0d", a, b, sum, expected); errors = errors + 1; end endendtask带 #1 的任务只能用于仿真。可综合 task 必须遵守综合工具支持的 RTL 子集,但即使可综合,调用也表示逻辑展开,不代表硬件复用。
`timescale
跳转到“`timescale”`timescale 1ns/1ps前一个值是时间单位,后一个值是时间精度。此设置下:
#10表示 10 ns;- 仿真时间可量化到 1 ps。
不同文件的时间尺度不一致会导致难以发现的仿真问题。工程应统一设置,并了解工具的全局覆盖规则。
宏与条件编译
跳转到“宏与条件编译”`define DATA_WIDTH 8
wire [`DATA_WIDTH-1:0] data;宏是预处理文本替换,没有模块参数那样的类型和层次语义。可配置模块优先使用 parameter;宏适合全局编译开关或少量公共定义。
`ifdef SIMULATION initial $display("simulation build");`endif不要让大量条件编译把设计拆成难以验证的多个隐形版本。
testbench 的基本结构
跳转到“testbench 的基本结构”待测设计 DUT(design under test)输入由 testbench 的 reg 驱动,DUT 输出接 testbench 的 wire:
`timescale 1ns/1ps
module adder_tb;
reg [7:0] a; reg [7:0] b; wire [8:0] sum;
adder #( .WIDTH(8) ) dut ( .a (a), .b (b), .sum (sum) );
initial begin a = 8'd0; b = 8'd0;
#10 a = 8'd5; b = 8'd9; #10 a = 8'd255; b = 8'd1; #10 $finish; end
endmoduletestbench 通常没有端口,因为它是仿真世界的顶层。
产生时钟和复位
跳转到“产生时钟和复位”initial begin clk = 1'b0; forever #5 clk = ~clk; // 10 ns 周期,100 MHzend
initial begin rst_n = 1'b0; repeat (3) @(posedge clk); rst_n = 1'b1;end复位释放靠近时钟沿可能制造 testbench 竞态。更严谨的激励可在负边沿改变同步输入,让 DUT 在下一个正边沿稳定采样:
@(negedge clk);enable = 1'b1;自动检查,而不是只看波形
跳转到“自动检查,而不是只看波形”integer errors;
initial begin errors = 0; a = 0; b = 0;
drive_and_check(8'd5, 8'd9, 9'd14); drive_and_check(8'd255, 8'd1, 9'd256);
if (errors == 0) $display("PASS"); else $display("FAIL: %0d errors", errors);
$finish;end波形适合调查原因,自动检查适合稳定回归。两者不是二选一。
常用系统任务
跳转到“常用系统任务”$display:立即打印一行;$monitor:参数变化时自动打印;$time:当前仿真时间;$finish:正常结束仿真;$stop:暂停并进入交互环境;$dumpfile/$dumpvars:某些模拟器中生成 VCD 波形。
这些系统任务大多只用于仿真。
仿真要覆盖什么
跳转到“仿真要覆盖什么”至少包含:
- 复位期间输出;
- 复位释放后的第一个有效周期;
- 正常输入;
- 最小值、最大值和临界值;
- 同时发生的条件及其优先级;
- 连续多周期行为;
- 非法输入或默认分支;
- 最终自动 PASS/FAIL。
仿真正常仍可能有问题
跳转到“仿真正常仍可能有问题”RTL 仿真通常不会自动发现:
- 目标频率下的建立/保持时间违例;
- 异步输入带来的亚稳态;
- 错误的引脚约束和 I/O 电气标准;
- 电源、时钟质量和板级连接问题。
正确流程是:RTL 仿真验证功能,综合与实现报告检查资源和时序,上板验证真实系统。
练习
跳转到“练习”为第 5 篇的 4 路 MUX 写自检查 testbench:给四个数据端口不同固定值,遍历 sel=0..3,每次用 if (y !== expected) 统计错误,最后打印 PASS/FAIL。