7.2 显示、监控与自检查 本节摘要:display 家族、monitor 与波形文件是仿真的三套记录仪,各自有不同的采样时机与适用场景;自检查把「期望值比对」写进测试台,让错误在仿真结束时自动浮出水面。人眼盯波形是最低效的验证方式,能自动判卷就绝不人肉。 法庭里最不可靠的证人是肉眼。这一节讲三件事:怎么把仿真过程记录下来(显示与波形)、记录的时机有什么讲究(调度区对齐)、以及怎么让测试台自己当法官(自检查)。 display 家族与 monitor 的分工 系统任务按「何时采样」分成两派。display 在执行到它的当下立即求值输出——打印的是那一刻的瞬时值,适合放在关键事件点做标记;monitor 声明一次后持续值守,任何被监视信号变化时自动打印一行——适合做全程流水记录;
本节摘要:display 家族、monitor 与波形文件是仿真的三套记录仪,各自有不同的采样时机与适用场景;自检查把「期望值比对」写进测试台,让错误在仿真结束时自动浮出水面。人眼盯波形是最低效的验证方式,能自动判卷就绝不人肉。
法庭里最不可靠的证人是肉眼。这一节讲三件事:怎么把仿真过程记录下来(显示与波形)、记录的时机有什么讲究(调度区对齐)、以及怎么让测试台自己当法官(自检查)。
系统任务按「何时采样」分成两派。display 在执行到它的当下立即求值输出——打印的是那一刻的瞬时值,适合放在关键事件点做标记;monitor 声明一次后持续值守,任何被监视信号变化时自动打印一行——适合做全程流水记录;strobe 在本时间步末尾求值——打印的是本拍最终值,专门治非阻塞赋值「看不到更新」的困惑(4.1 节对质过这一幕)。
initial $monitor("[%0t] rx=%b data=%h done=%b", $time, rx, data, done); // 全程流水:任一信号变化即打印 always @(posedge clk) begin if (done) begin $display("[%0t] 收到字节 %h(瞬时打印)", $time, data); $strobe ("[%0t] 收到字节 %h(本拍终值)", $time, data); end end
格式符与 C 的 printf 同源:十六进制、十进制、二进制各有对应字母,加数字零前缀可压掉前导空格。仿真时间配合时间系统函数打印,日志按时间排序后就是一份诉讼笔录。
波形文件是另一路记录:开启转储的系统任务把指定层次的所有信号变化写进标准格式的波形文件,仿真结束后用波形查看器打开。日志适合「事件级」复盘,波形适合「时序级」复盘,两者互补:
initial begin $dumpfile("tb_uart_rx.vcd"); // 波形文件名 $dumpvars(0, tb_uart_rx); // 层次参数取零表示本级以下全部记录 end
大设计全量记录波形会让文件巨大、仿真变慢,工程做法是平时只记录关键层次,复现问题再定向打开——波形记录是有代价的观测手段,不是免费装饰。
把三套记录仪放在同一个事务的时间轴上,各自的采样时机一目了然:
这张时间线的实用结论只有一条:打印语句的位置决定它看到什么。同一个变量,display 看到「过去」,strobe 看到「现在」。调试时怀疑「打印出来的值不对」,先检查打印语句挂在调度区的哪一层,再怀疑设计。
人肉验证的流程是「跑仿真、开波形、看输出对不对」,一次只能看一个用例,且依赖观察者的注意力。自检查把判断写进代码:测试台内建一个参考模型(或期望值序列),DUT 每输出一次就比对一次,错误计数器累加,仿真结束时宣判:
// 检查器:独立模块,与激励分离 module uart_rx_check #(parameter CLKS_PER_BIT = 8)( input wire clk, rst_n, rx, input wire [7:0] data, input wire done ); integer errors; integer checks; reg [7:0] expect_q; initial begin errors = 0; checks = 0; expect_q = 8'hA5; // 期望序列的首元素 end // 期望值独立生成:这里用预置序列轮转, // 也可以是按协议独立解码的黄金模型 always @(posedge clk) begin if (done) begin checks = checks + 1; if (data !== expect_q) begin errors = errors + 1; $display("[%0t] 错误:期望 %h 实得 %h", $time, expect_q, data); end else begin $display("[%0t] 通过:%h", $time, data); end expect_q <= next_expect(expect_q); // 推进期望序列 end end function [7:0] next_expect(input [7:0] cur); next_expect = {cur[6:0], cur[7]}; // 示例:循环左移作为下一期望 endfunction // 计数通过层次引用向顶层测试台暴露, // 顶层在仿真结束时读取并打印结案报告 endmodule
三个要点。其一,比对用全等不等号(四个等号的运算符)而不是普通不等号——前者把 x 与 z 也算不匹配,期望值对不上未知态时不会被糊弄过去。其二,期望值的生成要与 DUT 的实现相互独立(独立实现的手动解码、黄金模型、预先录制的期望序列都行),用同一份代码当自己的参考等于没检查。其三,错误要带时间戳打印并在结尾汇总,回归脚本靠这个计数判卷。
顶层测试台一侧的结案与看门狗长这样:
// 结案:等检查计数达标,打印判卷结果 initial begin wait (chk.checks >= 3); // 三个字节都收齐 #1000; // 留出收尾时间 if (chk.errors == 0) $display("判卷:全部通过,共 %0d 项检查", chk.checks); else $display("判卷:失败 %0d 项,共 %0d 项检查", chk.errors, chk.checks); $finish; end // 看门狗:激励挂死时的兜底出口 initial begin #50_000_000; $display("超时强制结束:checks=%0d errors=%0d", chk.checks, chk.errors); $finish; end
案例展开:一次位序错误的自动捕获。 背景:某人改写串口接收位序(低位在先改成高位在先),忘了同步修改发送端测试台。操作:跑自检查测试台,仿真心跳若干拍后日志打印「期望 a5 实得 5a」——位序反转的特征模式一眼可辨;定位到接收模块的位序分支,改回后全部用例通过。结果:人没有看一眼波形,错误从仿真日志直接定位到根因。解读:自检查的价值不只在「不用人看」,更在「错误信息携带诊断线索」——期望值与实际值的差异模式本身就在指认嫌疑(位序反转、位宽截断、差一拍各有各的指纹)。变式:把常见错误模式写成自动归因规则(差一拍、位反转、位截断各匹配什么差异特征),错误日志从「有错」升级到「疑似某类错」,这是轻量级的智能诊断,纯 Verilog 就能做。
测试台的结束要交代清楚三件事:正常结束在最后打印通过与失败计数再结束仿真;异常超时用看门狗兜底(超时强制结束并报错),防止激励 bug 让仿真挂死;失败计数非零时退出前打印醒目的失败横幅,回归脚本按退出状态与计数双重判卷。一套固定的判卷协议让上百个用例的结果可以机器汇总——这是下一节回归流程的地基。
记录与判决就位,还差最后一个问题:测到什么程度算测够了。下一节用覆盖率与回归来回答。