Verilator 覆盖率收集流程:--coverage 采集行/翻转覆盖,verilator_coverage 合并出报告
Verilator 的覆盖率不是事后从波形里算出来的,而是编译期插桩:verilator 在生成 C++ 时给每个可执行行、分支和信号翻转点埋计数器,仿真跑完由 harness 主动把计数写进 coverage.dat,再交给随 Verilator 一起发布的 verilator_coverage 工具汇总。整条链路与商业仿真器的 urg/verdi 覆盖率流程对应,但全部是纯文本产物,CI 里直接可用。本篇按正常流程走通这条链路;代码覆盖率与功能覆盖率的分工见 功能覆盖率和代码覆盖率是一回事吗,编译运行第一个 Verilator 仿真的步骤见同簇 Verilator C++ 仿真流程:--cc --exe --build。
本文要回答的问题
- 想让 Verilator 仿真产出覆盖率数据,编译开关和 C++ harness 要各加什么?
- 行覆盖、分支覆盖、翻转覆盖分别统计什么,
coverage.dat文件里长什么样? verilator_coverage的汇总、--rank、--annotate、--write各解决什么问题?- 多个测试用例各跑一份数据,为什么必须合并而不是只看最后一份?怎样按未覆盖行回头补激励?
这张图把流程拆成两个阶段:1、2 步发生在仿真侧,决定数据采不采得到;3、4、5 步发生在报告侧,只消费文本数据,可以脱离仿真反复重跑。新手最常见的误区是只做了一侧——编译加了开关却不写盘,或者跑完一份数据就以为覆盖率分析只能看到一个干巴巴的总数。
一、覆盖率采集在流程里的位置
覆盖率这一步的上游是「能跑出正确结果的仿真」,下游是「按空洞补测试」的验证闭环。它不改变激励写法,也不替代自检,只回答一个问题:这套激励到底碰过设计的哪些地方。
读这张图抓住一点:覆盖计数器在编译期就埋进了 obj_dir 里的 C++,不是运行时外挂。所以「忘了加开关」没有任何补救手段——必须重新 verilator 编译。开关打开后,仿真本身不打印覆盖率,数据只在 harness 调用写盘函数时落地;之后所有分析都围绕那份文本数据展开。
二、命令逐段解读
采集侧的完整命令分两段。第一段编译,在普通的 --cc --exe --build 基础上加两个开关:
verilator --cc counter.v --exe tb.cpp --top-module counter \
--coverage-line --coverage-toggle \
--build -j 2--coverage-line:行/块覆盖。每个可执行的语句行(以及if拆出的块)埋一个计数器,报告页类型是v_line与v_branch。不加它,报告里永远不会出现行命中信息。--coverage-toggle:信号翻转覆盖。给每个信号位分别记录 0→1 与 1→0 两个方向的命中,页类型是v_toggle。它比行覆盖贵:一个 32 位总线就是 64 个计数点。只想快速看行覆盖时可以不开。--coverage:以上两类再加用户自定义覆盖点(/*verilator&32; coverage...*/元注释)的总开关,等价于三者全开。教学演示建议分开写,报告读起来不容易混。--build -j 2:让 verilator 直接调 make 完成 C++ 编译,-j并行度按机器给。
第二段在 harness 里。Verilator 没有 testbench 系统任务,落盘靠 C++ 代码,两处缺一不可:
#include "verilated_cov.h" // 文件顶部
// 仿真结束、delete dut 之前
VerilatedCov::write("coverage.dat");不写 write() 会怎样:计数器在内存里正常累加,但进程一退出数据全部消失,目录下找不到任何 .dat。文件名可以传参数改成每次用例一个名字(如 partial.dat);不传参数默认就是 coverage.dat。
报告侧四个用法:
| 命令 | 作用 | 什么时候用 |
|---|---|---|
verilator_coverage x.dat | 打印总覆盖点/命中数与百分比 | 每次回归后快速看数 |
verilator_coverage --rank x.dat | 按未覆盖点数给文件排名 | 多文件设计,先补最差的那个 |
verilator_coverage --annotate logs x.dat | 在 logs/ 下生成逐行带命中次数的源码副本 | 定位具体哪一行、哪个信号位没碰到 |
verilator_coverage --write merged.dat a.dat b.dat | 把多份数据按同名计数点取并集写出 | 多个测试用例各跑一份,合并后再报告 |
另外 -write-info report.info 能导出 lcov 格式,接 genhtml 生成网页报告;本文只用纯文本产物。
三、输入输出文件
整条链吃两类、吐三类文件。输入是 RTL(counter.v)和 harness(tb.cpp)。仿真吐出的 coverage.dat 是纯文本,头部一行版本标识,之后每行一个计数点(下例中的 <0x02> 表示实际文件里不可见的 0x02 分隔字节):
# SystemC::Coverage-3
C 'f<0x02>counter.vl<0x02>10n<0x02>30page<0x02>v_toggle/countero<0x02>q[0]h<0x02>TOP.counter' 15
C 'f<0x02>counter.vl<0x02>12n<0x02>5page<0x02>v_line/countero<0x02>blockS<0x02>12h<0x02>TOP.counter' 18字段用 0x02 分隔,人读的关键有四个:f 源文件名、l 行号、page 计数点类型(v_line/v_branch/v_toggle)、行尾的整数命中次数。合并的本质就是把多份文件里「同一 f/l/page/o/h 元组」的次数相加,所以手工 diff 这份文件也能对账。
--annotate logs x.dat 吐出的是整份源码副本,每行左侧多一列命中次数:空格开头的 000025 表示该行命中 25 次,%000000 表示零命中。这是定位空洞最直接的产物,零在哪一行、属于哪个端口位,一眼可见。
四、常见错与检查点
- 跑完没有 coverage.dat。 先查 harness 是否
#include "verilated_cov.h"并在退出前调了VerilatedCov::write();再查编译时是否真的带了--coverage-*——看obj_dir编译命令里有没有-DVM_COVERAGE=1,没有就是开关没生效,必须重新 verilator 编译。 - 多跑一个用例,前一个用例的数据没了。 默认文件名都是
coverage.dat,同目录运行直接互相覆盖。正确做法是write("case1.dat")、write("case2.dat")分开存,最后--write合并;绝不能只留最后一份,否则覆盖点数只会少不会多。 - 只盯总数百分比。 工具默认输出的
Total coverage把行、分支、翻转点混在一起加权,小设计里翻转点占大头,一个没加载的输入能把总数压得很难看,但它不代表「大部分代码没跑到」。要分v_line、v_branch、v_toggle三类分别读,annotate 产物里也按行分开。 - 改了 harness 以为改了设计。 覆盖率只由 RTL 结构和激励决定,harness 里重排打印不影响任何计数;数字不变先查激励有没有真正改变 DUT 输入。
五、真实跑一遍:一个计数器的两版 harness
被测设计是 4 位可加载计数器:异步复位 rst_n,load 优先于 en,加载值走 d。第一版 harness 只做复位加 10 拍计数,从不拉 load:
分类型读数:行覆盖 v_line 2/3,分支 2/2,翻转 7/12。annotate 把空洞标得很具体——end else if (load) 与 q <= d 两行是 %000000,输入端口 load 和整个 d 总线也从没翻转过。这正是「激励没构造加载场景」的直接证据,比肉眼审 testbench 可靠。
按空洞补激励:第二版 harness 在计数 10 拍后拉一拍 load=1, d=0xA,再继续计数 5 拍。重新编译运行后行覆盖升到 3/3,但出现了一个反直觉现象——分支覆盖反而从 2/2 掉到 1/2:新 harness 每个时钟沿都至少有一个使能条件成立,if(en) 的隐式 else(保持)分支再没出现过。这说明单份数据不能互相替代,必须合并。
合并命令是 verilator_coverage --write merged.dat partial.dat full.dat,同名计数点次数相加:加载分支来自 full、保持分支来自 partial,合并后行 3/3、分支 2/2、翻转 10/12。最后剩的两个零翻转点是 d[0] 与 d[2]——加载值 0xA 是 1010,这两位从来没被驱动成 1。下一轮补测的动作非常具体:再加一个加载奇数(如 d=0x5 或 0xF)的用例,翻转覆盖即可收口。覆盖率数据就这样驱动测试增量,而不是盲目加长仿真。
实验环境
- 机器/系统:Mac mini / macOS,Docker 运行
geek-eda:0.3-sta镜像(Debian bookworm) - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1),含 verilator_coverage
- 日期:2026-11-03
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash run.sh(两版 harness 分别编译运行,各存一份 .dat,再做--rank、--annotate、--write合并与分指标计数)
代码获取
本文全部实验代码(counter.v、tb_partial.cpp、tb_full.cpp、Makefile、run.sh)已同步到公开仓库 geek-doc.cn/experiments/flow-verilator-coverage,克隆后在装有 geek-eda:0.3-sta 镜像的机器上执行 make run 即可复现两版 harness 的覆盖率数据、annotate 源码与合并报告。
这一篇在知识体系里的位置
本篇是开源 EDA 仿真链路中 Verilator 路线的「覆盖率采集」正向操作说明。仿真与 testbench 的基本概念见 仿真是什么;代码覆盖率与功能覆盖率如何分工见 功能覆盖率和代码覆盖率是一回事吗,把特性点映射到覆盖组的规划表见 功能覆盖率矩阵模板;商业仿真器上的 UVM 覆盖率合并与回归管理见 UVM 回归与覆盖率收敛;Verilator 与商业事件驱动仿真器的取舍见 Verilator 和 VCS 主要差别。
总结
Verilator 覆盖率收集流程是一条「编译期插桩 + 退出时写盘 + 离线分析」的纯文本链路:编译加 --coverage-line/--coverage-toggle,harness 结尾调 VerilatedCov::write() 得到 coverage.dat,再用 verilator_coverage 做汇总、--rank 排名、--annotate 逐行注释(%000000 即零命中)、--write 把多用例数据按计数点取并集合并。同一计数器实测:不加载的 harness 行覆盖仅 2/3,补 load 激励后行覆盖 3/3 但分支覆盖反降到 1/2,两份合并才同时拿到行 3/3、分支 2/2、翻转 10/12,剩余空洞精确指向 d[0]、d[2] 两位从未翻转,下一步补一个加载奇数的用例即可收口。