1. 覆盖率报告里那些"看不见"的盲区是怎么来的
做数字验证的同行大概都有过这种体验:跑完一轮回归,URG报告一打开,行覆盖率98%、条件覆盖率95%,看着挺漂亮,但心里总不踏实——因为你知道那剩下的2%里,藏着最要命的东西。尤其是Toggle覆盖率,经常出现某个信号翻转次数为零,或者某个跨时钟域的信号只翻了一半,这种问题在仿真阶段不暴露,到了后仿或者硅后就变成"偶发性的诡异bug",查起来能耗掉你两周时间。
这篇内容就是围绕VCS覆盖率实战展开的,重点聊两件事:一是怎么高效排查那些没有被测试用例(TC)覆盖到的点,二是怎么优化Toggle收集策略,让覆盖率数据既有意义又不拖慢仿真速度。适合已经跑过完整回归、正在做覆盖率收敛的验证工程师,也适合刚接触VCS覆盖率、想搞清楚URG报告怎么读的入门者。我不会只贴命令,而是把每个操作背后的逻辑讲清楚,让你知道为什么要这么干,以及我踩过哪些坑。
先说一个反直觉的结论:覆盖率数字高不等于验证充分,覆盖率数字低也不一定代表有漏洞。关键在于你收集的覆盖率模型是否合理,以及你有没有真正理解那些未覆盖项背后的原因。很多人一看到未覆盖就急着加TC,结果加了一堆无效用例,仿真时间翻倍,覆盖率纹丝不动。问题出在哪儿?出在没有先做"未覆盖根因分析"。
2. 从URG报告定位未覆盖TC的完整排查链路
2.1 URG报告的正确打开方式
VCS跑完仿真后,会在simv.vdb目录下生成覆盖率数据库。很多人直接用urg -dir simv.vdb生成HTML报告就完事了,但其实URG有很多参数能帮你快速定位问题。我常用的组合是这样的:
urg -dir simv.vdb -format both -report urgReport -show tests -show ratios这里几个参数的作用需要说清楚:
-format both:同时生成HTML和文本格式,文本格式方便用grep搜索,HTML适合整体浏览。-show tests:在报告里显示每个测试用例对覆盖率的贡献,这个非常关键,能帮你快速判断是哪个TC覆盖了哪些点。-show ratios:显示覆盖率比值而不是简单的百分比,有时候比值更能反映问题。
生成报告后,不要急着看总的覆盖率数字。先打开tests.html,按覆盖率贡献排序,看看哪些TC贡献最大,哪些TC几乎没贡献。我遇到过一种情况:某个TC跑了三个小时,结果对覆盖率贡献为零,原因是它的激励根本没打到DUT的有效路径上。这种TC就是典型的"无效用例",留着只会浪费回归时间。
2.2 用URG的exclude功能缩小排查范围
当你面对一个大型SoC项目,覆盖率数据库里可能有几十万个覆盖点,逐个看根本不现实。这时候要用URG的exclude功能,先把已知的、不关心的覆盖点排除掉。
urg -dir simv.vdb -exclude exclude_file.el -report urgReport_filteredexclude文件的格式很简单,每行一个排除规则:
# 排除所有时钟门控单元的toggle toggle:top.u_core.u_clk_gating.* # 排除测试逻辑的覆盖点 line:top.u_dft.* # 排除某个已知不用的配置 cond:top.u_cfg.mode[3]这里有个经验:exclude文件要版本化管理。我见过团队里有人随手改了exclude文件,把一些本该覆盖的点排除了,结果覆盖率虚高,流片后才发现问题。建议把exclude文件纳入Git管理,每次修改都要有review记录。
2.3 定位未覆盖TC的三步法
当你发现某个覆盖点没有被覆盖时,不要急着加TC。按下面三步走:
第一步:确认覆盖点是否合理。有些覆盖点是工具自动生成的,可能根本不需要覆盖。比如一个复位信号的toggle,如果设计上复位只在初始化时拉一次,那toggle覆盖率低是正常的。这时候应该用exclude排除,而不是强行加TC去翻转它。
第二步:确认激励是否到达。用Verdi打开波形,找到对应的信号,看看仿真期间它有没有被激活过。如果波形里信号根本没动,说明激励没打到。这时候要检查你的sequence或者testbench的约束是不是太严了。
第三步:确认覆盖点是否可达。有些覆盖点在当前配置下就是不可达的,比如某个只在特定模式下拉高的信号,而你的TC根本没配置那个模式。这时候要么加配置,要么用exclude排除。
我通常会用这样一个表格来跟踪未覆盖项的处理状态:
| 覆盖点 | 类型 | 未覆盖原因 | 处理方式 | 负责人 | 状态 |
|---|---|---|---|---|---|
| top.u_dma.ch_prio[2] | toggle | 激励未打到高优先级通道 | 新增TC | 张三 | 进行中 |
| top.u_cpu.irq_mask[7] | toggle | 该中断源未启用 | exclude | 李四 | 已完成 |
| top.u_mem.addr[31:28] | toggle | 地址空间未使用 | exclude | 王五 | 已完成 |
这个表格看起来简单,但能帮你把"为什么没覆盖"和"怎么处理"分开,避免一上来就盲目加TC。
2.4 用覆盖率数据库合并来定位回归盲区
VCS支持把多次仿真的覆盖率数据库合并,这个功能在排查未覆盖TC时特别有用:
urg -dir simv1.vdb simv2.vdb simv3.vdb -dbname merged.vdb合并之后,你可以看到哪些覆盖点是所有TC都没覆盖的,哪些是部分TC覆盖的。我一般会做两次合并:一次是全回归的合并,一次是只选"应该覆盖该模块"的TC合并。两次结果的差集,就是真正的盲区。
这里有个坑:合并数据库时要注意仿真配置是否一致。如果两次仿真的编译选项不同(比如一次开了assertion,一次没开),合并后的覆盖率数据可能会失真。建议在Makefile里固定覆盖率编译选项,避免人为失误。
3. Toggle覆盖率收集的优化策略与参数调优
3.1 Toggle覆盖率为什么这么"重"
Toggle覆盖率是VCS覆盖率里最耗资源的一项。原因很简单:它要记录每个信号在仿真期间的翻转情况,包括上升沿、下降沿、以及是否翻转到0和1。对于一个有几十万信号的设计,这个数据量是巨大的。
我做过一个对比测试:同一个DUT,关闭Toggle收集时仿真速度是1.2kHz,打开后降到0.8kHz,慢了33%。如果再加上条件覆盖率和FSM覆盖率,速度可能只有原来的一半。所以Toggle收集必须优化,不能无脑全开。
3.2 用+ntb_random_seed和+rad控制Toggle收集范围
VCS提供了几个编译和运行时选项来控制Toggle收集:
# 编译时指定只收集特定层次的toggle vcs -cm_tgl portsonly -cm_tgl_portsonly top.u_coreportsonly的意思是只收集模块端口的toggle,不收集内部信号的。这个选项在模块级验证时特别有用,因为内部信号的toggle往往可以通过端口推断出来。
另一个常用的是+rad选项,它可以让VCS在运行时动态决定是否收集某个信号的toggle:
simv +rad +cm_tgl+rad的原理是基于活动性分析,如果某个信号在仿真期间很少活动,VCS会自动降低它的收集优先级。这个选项对仿真速度的提升很明显,但要注意:它可能会漏掉一些低频但关键的翻转。所以我在做最终覆盖率签核时,会关掉+rad重新跑一遍,确保数据完整。
3.3 Toggle rate的合理设置
Toggle rate是指信号在单位时间内的翻转次数。VCS允许你设置一个阈值,只有超过这个阈值的信号才会被详细记录:
simv +cm_tgl +tgl_rate=100这个设置的意思是:只有翻转次数超过100的信号才记录详细toggle信息。对于低频信号,只记录是否翻转,不记录翻转次数。
这个参数怎么设?我的经验是:先跑一轮不设阈值的仿真,看看toggle rate的分布。如果大部分信号的翻转次数都在1000以上,那阈值设100就没问题。如果有些关键信号翻转次数只有个位数,那阈值就要调低,或者把这些信号加入白名单。
3.4 用覆盖率分组来管理Toggle收集
大型项目里,我建议按功能模块把Toggle覆盖率分组管理。VCS支持在编译时用-cm_hier指定覆盖率层次:
vcs -cm_hier cm_hier.cfgcm_hier.cfg文件的内容大概是这样:
+tree top.u_core 0 +tree top.u_dma 1 +tree top.u_periph 1 +tree top.u_dft 0这里的0和1表示收集级别,0是不收集,1是收集。这样你可以把DFT逻辑、测试逻辑的Toggle关掉,只收集功能逻辑的,既省时间又让报告更干净。
我通常会维护两份cm_hier.cfg:一份是"全收集"用于最终签核,一份是"精简收集"用于日常回归。日常回归用精简版,速度能快40%左右。
4. 那些年我在覆盖率收敛上踩过的坑
4.1 坑一:覆盖率数据库损坏导致数据丢失
这个坑我踩过两次,都是因为仿真异常终止(比如磁盘满了或者被kill了),导致simv.vdb目录损坏。URG打开时报错"database corrupted",之前跑的几个小时全白费。
解决方案:在仿真脚本里加一个保护机制,每次仿真结束后先检查simv.vdb是否完整,再决定是否删除旧的数据库。我现在的做法是:
# 仿真结束后检查数据库 if [ -d simv.vdb ]; then urg -dir simv.vdb -format text -report /dev/null > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Coverage database is valid" # 备份到带时间戳的目录 cp -r simv.vdb simv.vdb.$(date +%Y%m%d_%H%M%S) else echo "Coverage database is corrupted, skipping backup" fi fi这个检查虽然多花几秒钟,但能避免数据丢失的风险。
4.2 坑二:exclude文件写错导致覆盖率虚高
前面提过exclude文件要版本化,这里再补充一个细节:exclude文件的通配符要小心使用。我见过有人写了toggle:top.u_core.*,本意是排除u_core下某个子模块,结果把整个u_core的toggle都排除了,覆盖率直接从95%跳到99%,但实际上是漏掉了大量未覆盖点。
解决方案:exclude文件写完后,用URG的-show excluded选项检查一下到底排除了哪些点:
urg -dir simv.vdb -show excluded -report exclude_check这个报告会列出所有被排除的覆盖点,你可以逐个确认是否合理。我一般会把这个报告发给模块负责人review,确认无误后再纳入正式流程。
4.3 坑三:Toggle收集导致仿真超时
有一次跑一个大型SoC的回归,开了全量Toggle收集,结果单个TC的仿真时间从2小时涨到6小时,整个回归跑了两天还没跑完。后来发现是某个时钟分频器的内部计数器,每个周期都在翻转,Toggle数据量巨大。
解决方案:对于这种高频翻转的信号,用+tgl_rate设置阈值,或者直接用cm_hier.cfg把它排除。另外,VCS还有一个-cm_tgl_max选项可以限制单个信号的toggle记录条数:
simv +cm_tgl +tgl_max=10000这个选项的意思是:单个信号最多记录10000次翻转,超过后只记录统计信息。对于高频信号,这个限制能大幅减少数据量。
4.4 坑四:合并数据库时配置不一致
前面提过配置不一致的问题,这里展开说一下具体表现。有一次我把模块级验证的数据库和系统级验证的数据库合并,结果发现某些覆盖点在模块级是覆盖的,在系统级是未覆盖的,合并后变成了"部分覆盖",导致报告看起来很奇怪。
根本原因:模块级验证时,某些配置信号被固定了,而系统级验证时这些信号是动态变化的。合并后,VCS无法正确判断这些覆盖点的状态。
解决方案:合并数据库前,先确认两次仿真的编译选项、覆盖率模型、exclude文件是否一致。如果不一致,要么统一配置后重跑,要么分开报告,不要强行合并。
5. 覆盖率签核前的最后一道检查
5.1 用URG的assertion和FSM报告交叉验证
覆盖率签核不能只看数字,还要看质量。我通常会用URG生成几份交叉报告:
# 生成assertion覆盖率报告 urg -dir simv.vdb -report assertion_report -show assertion # 生成FSM覆盖率报告 urg -dir simv.vdb -report fsm_report -show fsmAssertion覆盖率能告诉你哪些断言被激活过,哪些从未触发。如果一个断言从未触发,要么是激励没打到,要么是断言写错了。FSM覆盖率能告诉你状态机的所有状态和跳转是否都覆盖了,未覆盖的跳转往往是bug的高发区。
5.2 用Verdi的覆盖率浏览器做最终确认
URG报告是静态的,Verdi的覆盖率浏览器可以让你在波形上直接看覆盖情况。我一般会这样做:
verdi -cov -covdir simv.vdb打开后,在覆盖率浏览器里找到未覆盖的点,右键选择"Trace to Waveform",Verdi会自动跳转到对应的信号和时刻。这个功能在排查"为什么没覆盖"时特别高效,比对着报告猜要快得多。
5.3 覆盖率签核清单
最后分享一个我用了多年的签核清单,每次覆盖率收敛到最后一轮时,逐项检查:
| 检查项 | 检查方法 | 通过标准 |
|---|---|---|
| 行覆盖率 | URG报告 | >95% |
| 条件覆盖率 | URG报告 | >90% |
| Toggle覆盖率 | URG报告 | >85% |
| FSM覆盖率 | URG报告 | 100%状态和跳转 |
| Assertion覆盖率 | URG报告 | 100%激活 |
| exclude文件review | 人工确认 | 所有排除项有记录 |
| 数据库完整性 | URG打开无报错 | 无corrupted提示 |
| 配置一致性 | 对比编译选项 | 与签核配置一致 |
这个清单看起来简单,但能帮你避免90%的覆盖率签核事故。我见过太多团队在最后一刻发现exclude文件写错了,或者数据库损坏了,导致签核延期。
5.4 一个容易被忽略的细节:覆盖率数据的版本管理
覆盖率数据库和代码一样,需要版本管理。我建议每次签核的覆盖率数据库都打上标签,和对应的RTL版本、TC版本关联起来。这样当流片后发现问题时,可以回溯到当时的覆盖率数据,看看是哪个环节漏掉了。
具体做法是在Makefile里加一个目标:
coverage_tag: @echo "Tagging coverage database with version $(VERSION)" cp -r simv.vdb coverage_$(VERSION)_$(shell date +%Y%m%d) @echo "Coverage database tagged as coverage_$(VERSION)_$(shell date +%Y%m%d)"这个操作只需要几秒钟,但能在关键时刻救你一命。
6. 写在最后的一些个人体会
覆盖率收敛这件事,说到底是一个"平衡"的艺术。你要在覆盖率数字、仿真时间、验证质量之间找到平衡点。我见过有人为了追求100%的Toggle覆盖率,加了无数TC,结果仿真时间翻了三倍,但真正发现的bug还不如之前多。也见过有人为了省时间,把Toggle收集关掉,结果流片后才发现一个跨时钟域的握手信号从来没翻转过。
我的经验是:覆盖率是手段,不是目的。URG报告上的数字只是参考,真正重要的是你有没有理解每个未覆盖项背后的原因。如果一个未覆盖项你能说清楚"为什么不覆盖"和"为什么不需要覆盖",那它就是合理的。如果你说不清楚,那就老老实实加TC或者改激励。
另外,Toggle覆盖率的优化没有银弹。+rad、+tgl_rate、cm_hier.cfg这些工具都有各自的适用场景,你需要根据项目的特点去组合使用。我通常会在项目初期就建立一套覆盖率收集的规范,包括编译选项、exclude规则、分组策略,然后在整个项目周期里持续优化。这样到了签核阶段,就不会手忙脚乱。
最后再分享一个小技巧:定期用URG的-show tests功能检查每个TC的覆盖率贡献。如果某个TC的贡献长期为零,要么是TC写错了,要么是它覆盖的功能已经被其他TC覆盖了。及时清理这些无效TC,能让你的回归效率提升不少。我在最近一个项目里,通过这个方式砍掉了30%的冗余TC,回归时间从8小时降到5小时,覆盖率数字一点没掉。