iverilog 回归脚本流程:遍历用例、PASS/FAIL 判定与非零退出接 CI
更新时间:2026-11-03。单个 testbench 手动敲一次 iverilog 编译加 vvp 运行 很轻松,但用例涨到四个以上、每次改 RTL 都要全部重跑时,靠人工逐个敲、肉眼扫输出必然漏判。本篇把这件事工程化:用一个三十来行的 bash 脚本串起「遍历用例 → 编译 → 运行 → 判定 → 汇总」整条 iverilog 回归脚本流程,退出码直接给 CI 用。更偏模板填空的写法见 iverilog 回归脚本模板篇,带 scoreboard 的随机回归实例见 同步 FIFO 的 iverilog 回归实测。
本文要回答的问题
- 一个最小可用的回归脚本,内部必须有哪几段、顺序是什么?
- 为什么判定不能只看
vvp的退出码,还要 grep 关键串? - 某一个用例编不过或跑挂时,怎样保证后面的用例继续跑完?
- 脚本的退出码怎么约定,CI(Jenkins/GitLab CI/GitHub Actions)才能直接接住?
整条流程五步,先给一张总览卡,再逐段展开:
读这张卡抓住主干:循环体里前三步是机械执行,第四步是判定,计数和退出码放在循环外收口。新手脚本最常见的毛病恰恰是让编译失败直接 exit——第一个坏用例之后的结果全部丢失。
一、回归脚本在仿真流程里的位置
仿真是「写完 RTL、做综合之前」的第一道验证,而回归脚本位于单个仿真命令的上一层:它不产生任何新的验证方法,只负责把同一条 iverilog/vvp 命令对一组用例反复执行,并把散在各终端里的判定收敛成一个退出码。上游喂给它的是 rtl/ 下的设计与 tests/ 下的自校验 testbench,下游消费它的是开发者的眼睛和 CI 系统。
读这张图注意两个边界:脚本与 CI 之间唯一的契约就是退出码加标准输出,不依赖任何专有工具;脚本与单个用例之间的契约是日志关键串(TEST PASS/TEST FAIL),testbench 必须有纪律地打印它们。也就是说,testbench 自己要做自校验——回归脚本只负责收集判决,不替你判断波形对错。
二、run.sh 逐段解读:参数与机制
下面就是脚本本体,配合 Makefile 与 Shell 的管理分工 里的定位看:Makefile 管「有哪些目标」,Shell 管「一批用例怎么跑」。
#!/usr/bin/env bash
set -u
TEST_DIR="${TEST_DIR:-tests}"
BUILD_DIR="${BUILD_DIR:-build}"
mkdir -p "$BUILD_DIR"
pass=0; fail=0; failed_cases=""
for tb in "$TEST_DIR"/*.v; do
name=$(basename "$tb" .v)
sim="$BUILD_DIR/$name.vvp"; log="$BUILD_DIR/$name.log"
echo "== [CASE] $name"
if ! iverilog -g2012 -Wall -o "$sim" rtl/*.v "$tb" 2> "$log"; then
echo " COMPILE FAIL (see $log)"; fail=$((fail + 1))
failed_cases="$failed_cases $name(compile)"; continue
fi
vvp -n "$sim" > "$log" 2>&1
rc=$?
if [ "$rc" -eq 0 ] && grep -q "TEST PASS" "$log" \
&& ! grep -q "TEST FAIL" "$log"; then
echo " PASS"; pass=$((pass + 1))
else
echo " FAIL (rc=$rc, log=$log)"
tail -n 3 "$log" | sed 's/^/ | /'
fail=$((fail + 1)); failed_cases="$failed_cases $name"
fi
done
echo "SUMMARY: pass=$pass fail=$fail total=$((pass + fail))"
if [ "$fail" -gt 0 ]; then echo "FAILED CASES:$failed_cases"; exit 1; fi
echo "ALL TESTS PASS"; exit 0逐段拆。开头只开 set -u 不开 set -e:set -u 让拼错变量名立刻报错,白捡的安全带;set -e 则会让循环里任何非零返回(vvp 跑挂、grep 没匹配到)直接终结脚本,后面的用例没机会跑,汇总也打不出来。回归脚本的语义恰恰是「失败也要跑完再汇报」,错误处理全部手写。
编译段:iverilog -g2012 按 SystemVerilog 兼容语法解析,-Wall 打开缺 timescale、位宽截断等告警,-o 显式命名产物而不是默认的 a.out;rtl/*.v 是共用设计,循环变量 $tb 是当前用例。编不过走 continue 记失败后进入下一例,而不是退出。
运行段:vvp -n 的 -n 表示 testbench 执行到 $finish 后直接退出、不进交互模式——脚本里少了它整轮回归会挂住等输入。输出重定向到每例独立的 log,$? 立刻取回退出码(下一条命令会覆盖它,所以必须先存)。
判定段三重条件:只看退出码不够,因为 testbench 里检测到失配后调用 $finish,vvp 的退出码照样是 0——仿真器认为自己「正常结束」了,它不知道结果对不对。所以还要日志里出现 TEST PASS 且不出现 TEST FAIL。关键串带上 TEST 前缀而不用裸 PASS,是为了避开 RTL 或告警里偶然出现的同名单词误命中。失败时用 tail -n 3 把失配明细直接贴在终端,不打开日志就能看到第一现场。
汇总段:计数在循环外收口,退出码只分两档——0 全过、1 有失败(含编译失败)。CI 只需要 ./run.sh 一条步骤,非零即红灯,失败清单已经打在输出末尾。
三、输入输出文件:吃什么、吐什么
| 文件 | 角色 | 内容约定 |
|---|---|---|
rtl/counter4.v | 共用 DUT | 4 位带使能、进位输出的计数器,所有用例例化同一份设计 |
tests/tb_*.v | 输入:用例 | 每个文件一个顶层,自校验后只打印 TEST PASS tb_xxx 或 TEST FAIL ... errors=N |
build/<名>.vvp | 产物:仿真字节码 | iverilog 输出,只能交同版本 vvp 执行,整个 build/ 可随时删 |
build/<名>.log | 产物:逐例日志 | vvp 的全部终端输出,失败后按用例名直接定位 |
testbench 侧的契约只有两行系统任务:成功打 TEST PASS、任何失配先打明细再打 TEST FAIL。本文四个用例分别覆盖异步复位清零、使能计数与停使能保持、数到 15 后 carry 拉高并回卷到 0、纯停使能保持。写激励时有一个真实踩点:en 这类输入要在两个时钟沿之间改变(如 #3 后赋值、沿后再 #1 拉低),在 posedge 同一时刻与 DUT 竞争采样,会时灵时不灵。
四、常见错与检查点
| 现象 | 先查什么 | 纠正 |
|---|---|---|
| 第一个用例失败后脚本直接退出,后面全没跑 | 是否图省事开了 set -e,或编译失败写成 exit | 去掉 set -e;编译失败用 continue 跳到下一例 |
| 用例明明失配了,汇总却判 PASS | TB 失配后是否只打了提示、没打 TEST FAIL;是否只看退出码 | TB 统一打印两个关键串;判定坚持三重条件 |
| CI 上回归整轮挂住不结束 | vvp 是否少了 -n;TB 是否所有分支都有 $finish | 加 vvp -n,检查 TB 激励收尾 |
| 用例多时找不到是哪个挂的 | 输出是否全糊在终端、没有逐例日志 | 每例独立 build/<名>.log,终端只贴失败尾部三行 |
| 编译告警长期没人看 | 是否没开 -Wall,或把告警重定向到了 /dev/null | 保留 -Wall,编译 stderr 也落日志 |
这些都是正常流程上的检查点,不是报错排查大全;工具本身报错的处理见 timescale 继承问题 与 重名模块报错。另外回归频率不必每次提交都跑全量,怎么安排节奏见 回归该跑多勤。
五、真实跑一遍:注入失败再修复
四个用例在 Icarus Verilog 13.0 下实跑两轮。第一轮把 tb_enable 的保持期望值故意从 4'd3 错写成 4'd7,验证失败汇总路径;第二轮一条 sed 改回正确期望重跑。完整终端实录如下:
读这份实录抓四个点。第一轮 tb_count、tb_reset、tb_wrap 照常 PASS,tb_enable 被判 FAIL 时终端直接贴出失配行 enable-hold: q=3 expect 7——退出码是 0(tb 正常 $finish),单看 vvp 返回值根本发现不了,这就是必须加关键串判定的活证据。失败没有中断循环,四个用例全部跑完后才打印 SUMMARY: pass=3 fail=1 与 FAILED CASES: tb_enable,整轮退出码 1。第二轮改回期望值后 pass=4 fail=0、ALL TESTS PASS、退出码 0——CI 接到的就是这两个数字的差别。失败用例的完整上下文留在 build/tb_enable.log,不必回放整轮输出。
终端卡记录的是注入失败的首轮,修正后第二轮回归的汇总段原样摘录如下,四个用例全部 PASS、退出码 0:
== [CASE] tb_enable
PASS
== [CASE] tb_reset
PASS
== [CASE] tb_wrap
PASS
----------------------------------------
SUMMARY: pass=4 fail=0 total=4
ALL TESTS PASS
exit_code=0实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon)
- 工具版本:Icarus Verilog 13.0(stable,原生安装于
/usr/local/bin,iverilog -V输出Icarus Verilog version 13.0 (stable) (v13_0)),bash 3.2 - 日期:2026-11-03
- 完整命令:
./run.sh(首轮,注入tb_enable期望错误,退出码 1)→sed -i '' "s/4'd7/4'd3/; s/expect 7/expect 3/" tests/tb_enable.v→./run.sh(次轮全绿,退出码 0)
代码获取
完整 RTL、四个自校验 testbench、回归脚本与 Makefile 已同步到公开仓库:geek-doc.cn/experiments/flow-iverilog-regression-script,克隆后进入该目录执行 make run 即可复现全绿汇总(退出码 0),make clean 删除 build/ 产物;想复现失败路径,把 tests/tb_enable.v 里的期望值 4'd3 改成 4'd7 再跑一次即可看到 exit 1。
这一篇在知识体系里的位置
- 单条仿真命令的四步闭环(写 TB、编译、vvp、VCD):读 仿真是什么;
- 本脚本的「随机种子循环」增强版与超时、并行改造:读 仿真回归 Shell 脚本模板;
- 带 scoreboard 的随机回归完整实例:读 同步 FIFO 的 iverilog 回归实测;
- Makefile 在工程里管什么、与 shell 怎么分工:读 仿真 Makefile 模板。
总结
iverilog 回归脚本流程就是五步:for 循环枚举 tests/*.v,逐例 iverilog -g2012 -Wall 编译(失败只记录不退出),vvp -n 运行并把输出落逐例日志,用「退出码 0 且日志有 TEST PASS 且无 TEST FAIL」三重条件判定,循环外累计 pass/fail、打印失败清单并以 0/1 退出码收口。只开 set -u 不开 set -e,是为了任何单用例失败都不牺牲整轮汇总;testbench 负责打印判决关键串,脚本负责收集判决,CI 只认退出码——三段契约定清楚,这个三十行的脚本就能直接挂进任何流水线。