Verilator 接入 CI 流程:lint 与仿真两段式门禁的最小配置
更新时间:2026-11-03。本地手敲 Verilator lint 入门流程 只能管住自己的机器,团队协作时要让「代码不过检查就合不进来」成为机械事实,就得把它挂到 CI 上。本篇给出 Verilator 接入 CI 流程的最小可用配置:一条 pull request 从提交到合并,先后穿过 lint、仿真两道门禁,任一道非零退出即红灯。单步命令的含义见 lint 篇与 Verilator --cc 编译仿真全流程,芯片项目更完整的回归体系见 ASIC CI/CD 与回归流程。
本文要回答的问题
- 为什么 CI 要拆成 lint 和 sim 两个 job,而不是一个脚本一把跑完?
- GitHub Actions 的最小 yaml 每一段是什么意思,
needs、容器、退出码怎么串起来? - 两个 job 各自吃什么文件、吐什么东西,为什么不缓存
obj_dir? - 本地没有 GitHub 也想验证这份配置,怎样用 docker 把两道门禁原样演一遍?
整条流水线六个环节,先给总览,再逐段拆:
读这张卡抓住主干:lint 是「便宜的快闸」,sim 是「稍贵的行为闸」,两者串行而不是并行——静态问题在第一道就拦下,不浪费编译和仿真时间。
一、这一步在开发流程里的位置
在整条 EDA 流程里,CI 位于「开发者写完 RTL、代码进入主干之前」。上游是工作区里的 RTL 与 C++ testbench,下游是受保护的主干分支。它不产生设计数据,只把开发者本地本该跑的两条命令搬到干净的 runner 上重跑,并把结果收敛成红绿信号灯。
读这张图注意颜色表达的语义:蓝色是入口,红色节点是静态判决,橙色是真正执行仿真,绿色是放行,灰色是失败去向。CI 与命令之间唯一的契约就是退出码——这也是为什么 iverilog 回归脚本流程 收尾必须精心约定 exit 0/1,Verilator 这边则省了一半心:它的告警默认就是 fatal,检测到问题自动非零退出,不用自己包装。
二、workflow yaml 与本地脚本逐段解读
CI 定义放在 .github/workflows/verilator-ci.yml,全文三十行出头:
name: verilator-ci
on:
push:
branches: [main]
pull_request:
jobs:
lint:
name: lint gate (--lint-only -Wall)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install Verilator
run: sudo apt-get update && sudo apt-get install -y verilator
- name: Verilator static lint
run: verilator --lint-only -Wall --top-module counter rtl/counter.v
sim:
needs: lint
name: sim gate (--cc --exe C++ harness)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install Verilator and a C++ toolchain
run: sudo apt-get update && sudo apt-get install -y verilator build-essential
- name: Build and run C++ simulation
run: |
verilator --cc --exe --build --top-module counter \
rtl/counter.v sim/tb_counter.cpp
./obj_dir/Vcounter触发段:on: push 盯主干、pull_request 盯合入请求。PR 是关键场景——分支保护规则勾选「该 job 必须成功」后,红灯的 PR 连合并按钮都是灰的。
为什么拆两个 job:第一,成本不同。--lint-only 不生成 obj_dir、不调 C++ 编译器,小工程秒级结束;--cc --exe --build 要生成 C++、调 g++ 编译再运行,贵一个数量级。静态问题(未用信号、位宽截断、推断锁存)占日常失误的大头,应在最便宜的一层拦下。第二,失败定位不同。lint 红说明 RTL 本身不过关,与 testbench 质量无关;sim 红才需要查行为和激励。第三,needs: lint 让 sim 在 lint 失败时根本不排队,runner 额度不浪费。
环境段:这里用 ubuntu-24.04 加 apt 安装 Verilator,是为了让配置不依赖任何私有镜像、克隆即可跑;代价是 apt 版本(Ubuntu 24.04 对应 5.018)与本文实测的 5.020 有小版本差异,告警措辞可能略有出入,退出码语义完全一致。团队若要严格锁版本,把 runs-on 换成 container: <自建镜像> 即可,job 内容不动。sim job 额外装 build-essential,因为 --build 需要本机 g++/make。
退出码:整条链路没有手写任何 exit。-Wall 打开的告警在 Verilator 里默认 fatal——看到告警就非零退出,GitHub Actions 对任何非零退出直接判该 step 失败。不要为了「让 CI 变绿」随手加 -Wno-fatal:那等于把报警器电池抠了,确属误报的信号应在源码里用 lint_off/lint_on 局部豁免,做法见 lint waiver 用法。
缓存:这个最小配置故意不缓存。每次运行都是全新虚拟 runner,obj_dir 随生随灭,且它是从生成的 C++ 编译来的,换一次 RTL 就大面积失效,缓存命中率极低;apt 装一个 Verilator 也就几十秒。等到 DUT 上规模、编译要几分钟时,再引入 ccache 缓存 C++ 编译结果(把 CCACHE_DIR 指向 actions/cache 的缓存目录,并让 g++ 走 ccache),而不是缓存 obj_dir 本身。
本地复演用同一个思路写成 run.sh:检测到本机有 verilator 就原生跑,否则用 VERILATOR_IMG 指定的容器跑;lint 非零立刻 exit 1 并跳过 sim,与 needs 语义一致。
三、输入输出文件:吃什么、吐什么
| 文件 | 谁消费它 | 内容约定 |
|---|---|---|
.github/workflows/verilator-ci.yml | GitHub Actions | 两个 job 的定义,是 CI 唯一的配置入口 |
rtl/counter.v | lint 与 sim 共同的 DUT | 8 位计数器:异步复位、使能、并行加载 |
sim/tb_counter.cpp | 仅 sim job | 自校验 C++ harness,失配打印明细并返回 1,全对打印 TEST PASS counter |
obj_dir/Vcounter | sim job 内部 | 生成并编译出的仿真可执行文件,跑完即弃,不进版本库 |
| CI 日志(网页端) | 开发者 | 红灯时 Verilator 的告警行直接回贴在 PR 上 |
两份输入的契约要分清:lint 只看 RTL,不碰 testbench,所以它不需要 C++ 工具链;harness 自己负责判决对错——它检查复位、连数 5 拍、停使能保持、加载优先、255 回卷到 0 五件事,任何一件不符就打印失配行并以返回码 1 退出。CI 不理解波形,它只看 ./obj_dir/Vcounter 的返回码,这与 iverilog 回归里「testbench 给判决、脚本只收集」的分工是同一条纪律。
四、常见错与检查点
| 现象 | 先查什么 | 纠正 |
|---|---|---|
| lint job 秒绿,但明显的未用信号没报 | 是否漏了 -Wall,或加了 -Wno-fatal | 固定 --lint-only -Wall;误报用源码内 waiver 局部处理 |
| sim job 在本地过、在 CI 编译失败 | runner 是否缺 g++/make | apt 安装时带上 build-essential |
| 红灯 PR 照样被合并 | 分支保护是否没勾「job 必须成功」 | 在仓库设置里把两个 job 设为必需检查项 |
| 两个 job 同时失败、排队时间翻倍 | 是否把 needs: lint 写成了并行 | 保留串行,让便宜的快闸先拦 |
| 同事换平台后告警数量不一样 | CI 用 apt 版、本地用容器版,小版本差异 | 团队统一版本(自建容器或固定发行版),差异只应在措辞 |
这些是正常配置上的检查点;工具本身报错的含义查 Verilator 找不到模块 等报错页,系统的 lint 方法论见 verilator lint 静态检查实操。
五、真实跑一遍:注入一个未用信号,红转绿
不真的推 GitHub,用容器在本地把两道门禁原样演一遍。先在 DUT 的 endmodule 前注入一行未使用的 wire [7:0] next_q,跑 run.sh;再删掉该行重跑。工具为 geek-eda 容器内的 Verilator 5.020:
读这份实录抓三个点。第一轮 lint gate 直接报 %Warning-UNUSEDSIGNAL: rtl/counter.v:18:16: Signal is not used: 'next_q',随后 %Error: Exiting due to 1 warning(s),脚本打印 lint gate FAILED -> sim gate skipped,整轮 pipeline rc=1——注意 sim 一步没跑,这正是 needs: lint 的本地等价行为。第二轮删掉那行后,lint rc=0 放行,sim 编译(输出收在 build/compile.log,终端保持干净)并运行,harness 自检五项全过后打出 TEST PASS counter,ALL GATES PASS、pipeline rc=0。GitHub 网页端看到的红绿差别,就是这两个退出码。
终端卡覆盖了红灯阶段,删除未用信号后第二轮两道门禁的收尾输出如下,pipeline 退出码回到 0:
$ ./run.sh # expect GREEN on both gates
== [gate 1/2] lint: verilator --lint-only -Wall ==
lint gate OK (rc=0)
== [gate 2/2] sim: verilator --cc --exe --build, then run ==
TEST PASS counter
sim gate OK (rc=0)
ALL GATES PASS
pipeline rc=0实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon),本地通过 Docker Desktop 运行容器
- 工具版本:Verilator 5.020(
geek-eda:0.3-sta容器,verilator --version输出Verilator 5.020 2024-01-01 rev (Debian 5.020-1));容器内含 g++/make - 日期:2026-11-03
- 完整命令:
perl -0pi -e "s/^endmodule/ wire [7:0] next_q = q + 8'd1; ...\nendmodule/m" rtl/counter.v(注入未用信号)→./run.sh(lint 红,rc=1,sim 跳过)→perl -0pi -e 's/^ wire \[7:0\] next_q.*\n//m' rtl/counter.v(删除)→./run.sh(双绿,rc=0)
代码获取
完整工程(DUT、自校验 C++ harness、GitHub Actions workflow、本地复演脚本)已同步到公开仓库:geek-doc.cn/experiments/flow-verilator-into-ci,克隆后进入该目录,本机装有 Verilator 5.x 与 g++ 时执行 make run 即可复演双绿;用容器则执行 make run VERILATOR_IMG=geek-eda:0.3-sta,make clean 删除 obj_dir/ 与 build/。想复演红灯,在 rtl/counter.v 的 endmodule 前加一行未使用的 wire 再跑即可看到 rc=1。
这一篇在知识体系里的位置
- 第一道门禁在本地怎么跑、各类告警怎么读:Verilator lint 入门流程;
- 第二道门禁的四步细节(生成 C++、harness、make、运行):Verilator --cc 编译仿真全流程;
- 仿真回归为什么坚持退出码与自校验关键串:仿真是什么;
- 放大到芯片项目级的 CI/CD 与回归体系:ASIC CI/CD 与回归流程。
总结
Verilator 接入 CI 流程的最小形态就是两段式门禁:push/PR 触发后,lint job 跑 verilator --lint-only -Wall --top-module,纯静态、秒级、有告警就非零退出;sim job 以 needs: lint 串行在后,--cc --exe --build 编译运行自校验 C++ harness,看可执行文件返回码。两道全绿退出码 0 才允许合并,任一红灯即阻断,CI 与命令之间只靠退出码说话。最小配置不缓存 obj_dir、不锁版本,求的是克隆即跑;团队统一版本可换自建容器,上规模再加 ccache。本地用同一个 Verilator 镜像把两条命令演成红转绿,这份 yaml 推上 GitHub 之前就已经被验证过了。