FPGA 进阶 21:高级 Testbench——自检 TB、参考模型、文件读写与受约束随机
本文是
fpga/进阶层第 25 篇。入门 TB 是「给激励 → dump 波形 → 人眼对波形」,规模稍大就靠不住:人对不了百万周期的波形,改一次也不想再看一遍。高级 testbench 的目标是让测试自动判对错、自动凑场景、能反复回归——自检比对、参考模型/记分板、文件数据驱动、受约束随机、SVA 断言。配合上一篇的覆盖率,才能做到「改完跑一遍就敢信」。
本文要回答的问题
- 自检 testbench 怎么自动判对错,不靠人眼?
- 参考模型(golden model)怎么写、怎么和流水线 DUT 对齐比对(记分板)?
- 怎么用 $readmemh/文件读写做数据驱动测试?
- 受约束随机(constrained random)强在哪、随机了怎么复现?
- SVA 断言检查哪些协议属性?
一、自检 TB:让测试自己说 PASS/FAIL
核心转变:人眼对波形 → TB 内自动比对。结构上分几块:

// 时钟复位 + 自检比对骨架
logic clk=0; always #5 clk=~clk;
logic rstn=0; initial begin #20 rstn=1; end
// 记分板:期望值队列,吸收 DUT 流水线延迟
logic [31:0] exp_q[$];
task automatic drive(input logic [31:0] in);
// 1) 同一输入喂 DUT
din <= in; din_valid <= 1; @(posedge clk); din_valid <= 0;
// 2) 同一输入喂参考模型,期望结果入队
exp_q.push_back(golden_func(in));
endtask
// DUT 输出有效时,弹出队首期望比对
always @(posedge clk) begin
if (dout_valid) begin
logic [31:0] exp;
exp = exp_q.pop_front();
if (dout !== exp)
$error("[%0t] MISMATCH: got %08h exp %08h", $time, dout, exp);
else
ok_count++;
end
end
initial begin
wait(rstn);
for (int i=0;i<1000;i++) drive($random); // 随机激励
repeat(20) @(posedge clk); // 等流水线排空
if (exp_q.size()!=0) $error("还有 %0d 笔期望未收到输出", exp_q.size());
$display("==== TEST %s ====", ok_count==1000 ? "PASS":"FAIL");
$finish;
end关键:报错要带时间戳和实际/期望值($time、%h),跑完打印 PASS/FAIL 汇总,这样人只在 FAIL 后去看那段波形。
二、参考模型与记分板:处理流水线延迟
参考模型(golden/reference model)用行为级写法(SystemVerilog 行为、或 C/Python 经 DPI),只保证功能正确、不模拟时序——所以它比 DUT 早出结果。用**队列/信箱(mailbox)**对齐:发激励时把「期望结果」push 进队列;DUT 流水输出时 pop 队首比对。队列天然吸收任意拍延迟和气泡;乱序接口(如 AXI 带 ID)要用「按 ID 索引的期望值表」匹配,不能简单 FIFO。
三、文件读写:数据驱动测试
向量多时放外部文件,加用例不改 TB:
// 1) $readmemh 把 hex 文件读进数组(初始化 ROM / 加载输入向量)
reg [31:0] vec_mem [0:1023];
initial $readmemh("input_vectors.hex", vec_mem);
// 2) 逐行读「输入 期望输出」对
integer fd, code; reg [31:0] a, exp;
initial begin
fd = $fopen("cases.txt","r");
while (!$feof(fd)) begin
code = $fscanf(fd, "%h %h\n", a, exp); // 每行:输入 期望
drive_and_check(a, exp);
end
$fclose(fd);
end好处:可把真实系统抓回的数据(ADC 采样、抓包、图像)回放进仿真,做真实场景回归。
四、受约束随机测试(CRT)
定向用例只能覆盖你想得到的场景;受约束随机让工具自动凑边角组合:
class pkt_t;
rand bit [7:0] data[];
rand bit back2back; // 背靠背 or 间隔空闲
constraint len_c { data.size() inside {1, 2, 16, 255, 256}; } // 含边界长度
constraint gap_c { !back2back -> data.size() < 100; }
endclass
pkt_t p = new();
int seed = 32'd12345; // 失败时记下这个种子
initial begin
process::self().srandom(seed); // 固定种子 → 可复现
repeat(10000) begin
assert(p.randomize()); // 自动生成合法随机包
drive_pkt(p);
end
end- 随机长度/内容/握手间隔/背靠背与空闲,自动撞出「满 FIFO 时恰好来最大 burst」这类边角;
- 可复现靠种子:失败记下 seed,
srandom(seed)重放即精确重现;回归固定一批 seed,另定期换 seed 探新场景; - 配合功能覆盖率确认随机确实演到了目标场景(不是随机了但没覆盖到边界)。
五、SVA 断言:检查「永远必须成立」的属性
自检比对查「数据对不对」,断言查「协议/时序合不合规」——全程在线,违例立刻报:
// valid 拉高后,在 ready 到来前 data 必须保持稳定(AXI-Stream 基本规则)
property p_valid_hold;
@(posedge clk) disable iff (!rstn)
s_tvalid && !s_tready |=> $stable(s_tdata) && s_tvalid;
endproperty
assert property (p_valid_hold) else $error("valid 未握手就改 data");
// FIFO 满时不允许写
property p_no_write_full;
@(posedge clk) disable iff (!rstn) fifo_full |-> !wr_en;
endproperty
assert property (p_no_write_full) else $error("FIFO 满时还在写");断言放接口/边界处(握手、FIFO、状态机非法转移、X 传播),比事后看波形更早抓到违规时刻。
六、边界用例清单(定向部分至少覆盖)
- FIFO/缓冲:空读、满写、将满连续写、将空连续读、同时读写;
- 长度:最小(0/1)、最大、最大+1 边界、随机;
- 时序:背靠背、最大间隔空闲、复位中途、刚复位完立刻来数据;
- 异常:错误响应、反压拉死、下游不 ready、奇偶/校验错;
- 初始化:所有寄存器显式初始化,别留 X(仿真里 X 会掩盖问题、上板是随机值)。
思路再往上走就是 UVM:把驱动(driver)、监控(monitor)、记分板(scoreboard)、代理(agent)、序列(sequence)分层标准化,适合大团队/复杂协议。中小模块用本篇的「自检 TB + 参考模型 + 受约束随机」已经足够,不必一上来套 UVM。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 靠人眼对长波形 | 漏错、无法回归 | 自检 TB 自动比对 + PASS/FAIL |
| 参考模型和 DUT 直接同拍比对 | 流水线延迟对不上误报 | 队列/记分板吸收延迟,乱序按 ID 匹配 |
| 报错不带时间/值 | 定位困难 | $error 带 $time、实际值、期望值 |
| 随机无种子 | bug 无法复现 | 固定/记录 seed,可重放 |
| 只随机不看覆盖率 | 随机了但没覆盖边界 | 配合功能覆盖率收敛 |
| 寄存器/存储未初始化 | X 传播掩盖真问题 | 显式初始化所有 reg/mem |
| 用例全硬编码在 TB 里 | 加用例要改代码 | $readmemh/文件数据驱动 |
相关与延伸
前置/配套:Testbench 与仿真入门、仿真加速与覆盖率(功能覆盖率/CRT 收敛)、valid/ready 握手 与 FIFO(断言常查的对象)。 大规模验证方法学(UVM/形式验证)属专家线;接口协议调试案例见规划 #44。
一句话总结
高级 testbench 的目标是「自动判对错、自动凑场景、能反复回归」:写自检 TB——每拍/每事务自动比对 DUT 输出与期望、违例 $error 带时间和值、结束打印 PASS/FAIL,人只在 FAIL 后看波形;参考模型用行为级/C 写标准答案、不模拟时序,靠记分板队列吸收流水线延迟逐笔比对(乱序按 ID 匹配);大量向量用 $readmemh/$fscanf 文件数据驱动,还能回放真实抓回的数据;受约束随机(rand+constraint)自动凑边角组合、靠随机种子复现,配功能覆盖率确认演到边界;SVA 断言全程盯协议属性(valid 握手数据稳定、FIFO 满不写)。定向用例至少覆盖空/满、最小/最大长度、背靠背/空闲、复位中途等边界,所有寄存器显式初始化别留 X;再大规模才上 UVM 分层。