第一次把覆盖率报告投到评审会上,被问了一句"这个 87% 到底对应哪些功能点、剩下 13% 是谁的责任",当场语塞——那是我刚开始接触 VCS 覆盖率流程时最狼狈的一次。后来才明白,VCS 采集出来的 line/cond/fsm/tgl/branch 这些数字,回答的是"代码被跑到了多少",而不是"功能被验证到什么程度"。这两件事之间的桥,就是 HVP(Hierarchical Verification Plan,层级化验证计划)。所谓 VCS hvp planner,我个人的理解是:用 Verdi/VCS 这套工具链把验证计划本身变成结构化、可追踪、能和覆盖率数据挂钩的活文档,而不是一张躺在共享盘里三个月没人更新的 Excel。这篇内容偏向数字 IC 验证方向,适合正在用 VCS 跑仿真、已经开始被覆盖率收敛问题折磨的验证工程师,也适合想搞清楚覆盖率与验证计划怎么打通的朋友。
1. 先把 HVP 在 VCS 覆盖率体系里放对位置
1.1 覆盖率数据库跑完之后,真正缺的是什么
VCS 的覆盖率流程本身并不复杂:编译时打开采集开关,仿真时把数据写进数据库,最后用 URG 合并出一份报告。这套流程跑通以后,你会拿到一堆以百分数呈现的数字,颗粒度细到某一行 RTL、某个条件分支的某个操作数组合、某个状态机的某条跳转。问题恰恰出在这个"细"字上——数字太底层了,它不知道自己在验证计划里对应哪一条需求。
一个典型的场景:某个模块条件覆盖率卡在 92% 上不去,你打开报告一看,是十几个分散在不同 always 块里的条件表达式没被完整触发。这十几个点里,有的属于正常功能路径,有的属于异常保护逻辑,有的根本是冗余代码。报告不会告诉你哪些该管、哪些可以豁免,更不会告诉你这些点分别是哪个测试用例该负责的。于是每周的覆盖率评审就变成了逐行读 RTL 的体力活,标出几十行"已确认无法覆盖",下次回归又冒出来新的。
HVP 要解决的就是这一层信息断层。它把"验证计划"这个本来只存在于文档里的人造物,搬进工具链,变成一个可以被工具解析、可以被版本管理、可以自动和覆盖率数据库做关联的实体。你在计划树上写的每个节点,都能挂上具体的覆盖率条目和测试用例,工具读一遍数据库,就能告诉你每个节点当前是什么状态。
1.2 HVP 的实质:一份能被工具读懂的计划文本
HVP 全称 Hierarchical Verification Plan,关键词在 hierarchical 上。它不是一张平铺的清单,而是一棵有父子关系的树:顶层是模块或者大功能域,往下是特性、子特性,最底层的叶子节点才是真正可执行、可判定的条目。这个结构和我们写验证计划时的思维方式是对得上的,只不过以前是用 Word 的多级标题来体现,现在是工具里真实的树形结构。
从文件角度看,HVP 通常以一个独立文件的形式存在,内容偏结构化(很多版本里是类 XML 的文本格式),这意味着它可以进版本库。这一点在我看来价值极高:计划变更留下 diff,谁在什么时候把一个节点从"已验证"改回"进行中"、为什么改,都留痕。相比一份靠邮件来回传递的 Excel,可控性完全不是一个量级。
需要说清楚的是,各家的叫法和入口在不同版本里会有差别,有的版本把这块功能放在 Verdi 的覆盖率视图下,有的通过命令行选项拉起计划视图,参数名也换过几轮。我的习惯是:上手先跑一遍-help,把当前版本里实际存在的选项抄到自己的笔记里,别直接照抄三年前论坛上的帖子命令。
1.3 和 Excel 版验证计划表的真实差异
很多人会问,计划表用 Excel 维护得好好的,为什么要换工具。我在两个项目上分别用过两种方式,差异可以列成下面这张表。
| 对比维度 | Excel 计划表 | HVP 计划树 |
|---|---|---|
| 与覆盖率的关系 | 手工把百分比抄进去 | 工具读取数据库自动回填状态 |
| 变更历史 | 靠文件名加日期,容易乱 | 文件进版本库,diff 可查 |
| 更新成本 | 每次回归后人工更新,易滞后 | 跑一条命令就能刷新 |
| 团队协作 | 同一份文件多人编辑易冲突 | 结构化文本,冲突范围小且可合并 |
| 与波形/报告的联动 | 无,靠人肉对照 | 可以从节点直接跳到覆盖率视图 |
| 适合的阶段 | 项目早期需求梳理 | 中后期覆盖率收敛与签核 |
真正让我下决心推 HVP 的,是某次 tapeout 前的覆盖率评审。Excel 表上写着某特性"已验证",但去查覆盖率数据库,对应 covergroup 的 cross bin 只覆盖了 60%,只是当时更新表格的人抄错了版本号。这种错误在人工维护的流程里几乎无法杜绝,而工具回填天然不会出现。
1.4 什么阶段用它最划算
我的建议是不要一上来就强推。项目启动初期,需求还在变,验证计划本身一天改三版,这时候用工具建树反而是负担,一张白板加一份文档更快。等 DUT 的接口和主要特性稳定下来、覆盖率流程能稳定产出数据库了,再把计划往 HVP 里搬,效率收益最明显。如果是从零开始的新项目,可以一开始就建,但节点粒度不要切太细,允许它跟着需求长。
2. 从覆盖率采集到计划回填的完整链路
2.1 编译期的覆盖率开关与数据库命名约定
HVP 要工作,前提是覆盖率数据库得稳定产出。而数据库最容易出问题的地方就是命名混乱——每个人用自己的目录、自己的名字,最后合并的时候谁也说不清哪个 vdb 是哪个测试跑出来的。
我固定下来的做法是:编译阶段只产生一份 compile 数据库,运行阶段每个测试一份独立的运行库,目录结构按测试名分。命令大致是这样:
# 编译阶段:打开需要的覆盖率类型,固定编译库路径 vcs -full64 -sverilog -debug_access+all \ -cm line+cond+fsm+tgl+branch \ -cm_dir ./cov/simv.vdb \ -cm_hier ./cfg/cov_hier.cfg \ -f ./flist/filelist.f \ -o ./sim/simv # 运行阶段:每个测试单独一个库,用 -cm_name 打标签 ./sim/simv -cm line+cond+fsm+tgl+branch \ -cm_dir ./cov/run/smoke_001.vdb \ -cm_name smoke_001 \ -l ./log/smoke_001.log这里有几个细节值得展开。-cm后面的类型不是开得越多越好,cond和branch在大规模设计上会让数据库和运行时间都明显膨胀,如果当前阶段主要关注功能覆盖,可以考虑先只开line+tgl加功能覆盖率,等收敛后期再补齐。-cm_name这个标签很重要,它会写进数据库里,URG 合并后的报告能按测试名拆分,你的计划节点绑定测试时才有可读的标识。
-cm_hier指向的层级配置文件,用来限定采集范围,格式大致如下:
# cov_hier.cfg 示意 +tree tb_top.u_dut.u_core -node tb_top.u_dut.u_core.u_mem_model -module tb_top.u_dut.u_core.u_legacy_block +tree tb_top.u_dut.u_arb+tree表示纳入,-node和-module表示排除。为什么一定要写这个文件?典型的 DUT 里总会有一些第三方 IP、存储器模型、或者纯粹的流水线打拍逻辑,把这些纳入采集只会让报告充满永远无法覆盖的项,评审时白白消耗注意力。我一般会给每个项目维护一份这样的配置文件,进版本库,改的时候在提交信息里写明原因。
2.2 用 URG 做合并与报告,为回填准备数据
一堆分散的 vdb 没法直接喂给计划树,得先合并。合并命令本身很简单:
urg -dir ./cov/run/smoke_001.vdb ./cov/run/featureA.vdb \ -dbname ./cov/merged/merged.vdb \ -format both \ -report ./cov/merged/urgReport \ -parallel-parallel在测试数量多的时候能省不少时间,值得加上。-format both会同时生成文本和 HTML 两套报告,文本的报告方便脚本解析,HTML 的方便人看。
合并这一步最常踩的坑是数据库版本不一致。如果某次回归用了修改过编译选项的 simv,产生的 vdb 结构和别的库对不上,URG 会报错或者静默丢弃一部分数据。我的处理方式是:在回归脚本里记录每次编译的 simv 指纹(比如文件的 md5)和一个版本号,只有指纹一致的库才允许合并到同一个 merged.vdb 里。这个约束听起来苛刻,但能省掉大量"报告数字莫名其妙"的排查时间。
2.3 计划树的节点怎么搭:一个可落地的层次结构
HVP 文件本身不建议手搓,尤其不要手搓后再手工维护,那样还不如用 Excel。正确姿势是在工具里建节点、连线、填属性,让工具自己写文件。但你在动手之前得先想清楚树长什么样。我通常用三层:
验证计划(结构示意) ├── 数据通路 │ ├── 仲裁优先级的边界组合 [已验证] 关联 cond + cross bin │ ├── 背压连续 N 拍的场景 [进行中] 关联 fsm 跳转 + covergroup │ └── 复位期间的请求丢弃 [未开始] 关联 line + assert ├── 控制通路 │ ├── 配置寄存器的读写回环 [已验证] │ └── 非法配置的拒绝路径 [进行中] └── 异常处理 ├── 超时保护触发 [未开始] └── 错误状态的自恢复 [未开始]第一层是功能域,第二层是具体特性,第三层一般就不往下切了,直接在特性节点上挂覆盖率条目。切得太深会让维护成本陡增,而工具能提供的价值并不会随深度线性增长。
节点上我固定填四类信息:名字(用动词短语,比如"背压连续 N 拍的场景",而不是"背压")、描述(一句话说清判定标准)、责任人(具体到人,不写组名)、关联项(覆盖率条目、测试名、相关 bug 编号)。判定标准这一栏最容易被省,也最要命——如果一条计划节点的状态定义是模糊的,那它永远会在"进行中"里挂着。
2.4 覆盖率回填:把数据库和计划树对上
回填的本质是让工具拿覆盖率数据库去匹配计划节点上挂的条目。匹配的键通常是 RTL 的层级路径、covergroup 实例名、cross 名之类。这里有个很现实的约束:RTL 层级路径一旦因为代码重构发生变化,历史挂接就会失效,回填时表现为某些节点突然变成"未覆盖"。
应对办法有两个。一是挂接时尽量挂在 covergroup/cross 这种位于验证环境的条目上,而不是 RTL 内部路径,验证环境的稳定性通常高于 RTL 内部结构。二是把 RTL 路径挂接的范围限定在模块的某个固定层级以下,避免路径前缀因为例化层次调整而整体漂移。
回填完成后,你会得到每个节点的状态和实际覆盖率数值的对照。不要只看状态,数值也要看:一个标着"已验证"但覆盖率只有 82% 的节点,比一个标着"进行中"但覆盖率 100% 的节点危险得多。
3. 层级怎么切才不白干
3.1 按 DUT 结构切还是按功能特性切
这是建计划树时第一个要做的决定,也是我最开始纠结最久的地方。按 DUT 结构切,树形和 RTL 层次一一对应,路径好挂、责任人好分,但缺点也很明显:同一个功能特性如果横跨几个模块,你会在好几个分支下重复描述它,覆盖率条目也会被拆得七零八落。按功能特性切,语义清晰、评审时一目了然,但对验证环境的结构依赖更重,需要你事先把 covergroup 的组织方式也按功能域对齐。
我最后采用的是混合方式:第一层按功能域(比如数据通路、配置、中断、异常),第二层开始按特性细分,RTL 层级信息不作为组织维度,只在挂接覆盖率条目时体现。这样评审时讲的是功能,回填时匹配的是代码,两边都不别扭。
3.2 叶子节点的粒度:细到什么程度算合适
粒度太粗,节点状态没有信息量,"数据通路已验证"这种话没人敢签字。粒度太细,节点的数量会爆炸,维护成本压过收益。我摸索出来的参考线是:一个叶子节点应当对应"一个可以被单个或有数几个测试用例判定通过与否的特性判定点",并且它挂接的覆盖率条目数量控制在个位数。
举几个例子感受一下。"仲裁器功能正常"太粗了,这不是判定点,是一句口号。"仲裁器在多个请求同优先级时按轮询顺序授权"就合适,它能挂一个 covergroup 的 cross,也能明确定义通过标准。"仲裁器所有可能的优先级组合"又太细了,那更像是一个覆盖率模型而不是一条计划条目。这个尺度感只能靠项目里磨,我从第一版建树到比较顺手,大概调整了三轮。
3.3 节点和测试用例的绑定:别把映射做死
计划节点和测试名之间是多对多的关系:一个节点可能要靠好几个测试才覆盖得全,一个测试也可能同时为多个节点提供覆盖。所以映射一定要做成多对多,别指望"一条计划对一个测试"这种整齐的结构。
还有一个实践上的细节:不要在节点里写死测试的完整路径或者编号,写测试在回归列表里的逻辑名即可。回归脚本换了执行环境、测试被重命名的时候,只改一处映射就够。我见过有人把带绝对路径的测试名写进去,一次目录迁移就让整棵树的绑定关系全断,重建花了两天。
3.4 状态字段该定义到什么程度
状态字段看似简单,实则是最容易引发扯皮的地方。我用的四态定义是这样的:
| 状态 | 判定标准 | 谁能改 |
|---|---|---|
| 未开始 | 无任何测试针对性构造激励 | 特性责任人 |
| 进行中 | 有测试在跑,但覆盖率条目存在未命中项 | 特性责任人 |
| 待复核 | 覆盖率条目全部命中,等待他人确认 | 特性责任人提交,复核人确认 |
| 已验证 | 复核通过,且连续若干次回归保持稳定 | 复核人 |
关键在于"已验证"和"待复核"的拆分。这两者合并成一个状态,结果就是自己给自己签字,覆盖率数字漂亮但没人真正检查过用例质量。加一道复核,多花的时间非常有限,但能挡掉相当一部分"数字达标、功能没测"的情况。至于"连续若干次回归保持稳定"这条,是为了防止覆盖率抖动——有些测试存在随机性或者时序依赖,某一次跑过了不代表稳定覆盖。
4. 实测中最容易翻车的几个地方
4.1 数据库不完整,回填结果比实际好看
这是最危险的一类问题,因为它给你的是偏乐观的结论。常见成因有三种:回归跑挂了没发现,对应的 vdb 是残缺的;合并时用了不同编译版本的库被静默丢弃;-cm_hier配置在某个节点上误排除了一部分层级,导致那部分永远显示为已覆盖(因为它压根没被采集)。
排查的思路是拿数据库里的测试清单和回归计划清单做对账。URG 的文本报告里会列出参与合并的每个测试,把这份清单和实际提交的回归列表 diff 一遍,少一个都别放过。另外,每次合并后记录 merged.vdb 里的实例总数和覆盖率采集范围,和上一次对比,出现明显跳变就停下来查原因,别直接拿去汇报。
4.2 层级路径写错,映射静默失效
挂接覆盖率条目时写错路径,工具一般不会大声报警,最多是在回填时把这个条目算作未命中。于是你看到的现象是"覆盖率一直上不去",但真正的原因是路径根本对不上。这种问题在几十个节点的树上,靠肉眼找非常痛苦。
我的做法是每次改完挂接关系,先跑一次回填,重点看两件事:新增节点有没有立刻从"未开始"变成有数值的状态;覆盖率条目里有没有"零命中"的项。如果某个节点刚挂上条目、相关测试也跑过了,状态却纹丝不动,八成就是路径问题。另外可以养成习惯:挂接优先选用工具里的选择器从数据库里挑条目,而不是手打字符串,能挡掉绝大部分拼写错误。
4.3 分支与条件覆盖率的"数字收敛、功能没收"
条件覆盖率的数值是可以通过构造极端激励快速刷上去的,比如把某些操作数组合硬凑出来。数字到了 100%,但你其实并没有验证这条路径上的功能行为是否正确。这类"刷覆盖率"在紧张的项目后期特别常见。
防堵手段是把功能覆盖率和结构覆盖率交叉看:一个叶子节点如果结构覆盖率满了、功能覆盖率还空着,说明激励构造出来了但检查没跟上,这时候该补的是断言和参考模型比对,不是继续加激励。反过来,功能覆盖率满了但结构覆盖率有明显缺口,可能是 DUT 里存在死代码或者被配置屏蔽的逻辑,需要判断是否豁免。两种情况的处理方向完全不同,只看单一指标会走错路。
4.4 多人协作下的计划树合并冲突
计划文件进版本库之后,多人同时编辑必然产生冲突。好在结构化文本的冲突范围通常局限在单个节点块内,比 Excel 二进制文件的整文件冲突好处理得多。我固定下来几条纪律:按功能域分工,一个人负责的子树不交叉;提交前先在本地跑一次合并回填,确认没有语法或关联性破坏;节点的新增和删除尽量单独成一次提交,不和其他修改混在一起。
4.5 工具版本升级带来的入口与参数变化
这类工具链的选项在不同版本之间会调整,我遇到过升级之后原来的命令行选项报错、图形入口挪位置的情况。比较稳妥的习惯是:把自己的常用命令写成一个脚本,脚本里对关键选项加注释说明用途,升级后先跑脚本验证,报错就去查该版本的帮助信息,改完更新注释。这样升级的代价是一次性排查,而不是每次用的时候现场猜。
5. 和 Verdi 配合:把未覆盖的点落到波形上
5.1 从计划节点反查到具体的覆盖率视图
计划树本身只告诉你"哪条没过",接下来要定位"为什么没过"。这一步我是用 Verdi 的覆盖率视图来接的:把合并后的数据库加载进去,从报告里的未命中条目标识出发,直接跳到对应的 RTL 或者 covergroup 定义处,看它需要什么条件才能命中。
# 加载合并后的覆盖率数据库做分析 verdi -cov -covdir ./cov/merged/merged.vdb & # 代码与波形联动调试 verdi -ssf ./wave/smoke_001.fsdb -nologo &两条链路配合起来效率提升很明显:覆盖率视图负责告诉你哪个 bin 是空的,波形负责告诉你仿真里到底发生了什么。缺了前者,你不知道该看哪段波形;缺了后者,你只知道缺了什么,不知道怎么写激励补上。
5.2 用波形反推未被触发的激励条件
具体做法是:在覆盖率视图里选中一个未命中的条件项或 cross bin,查看它期望的属性组合,然后回到波形上找相关信号,看这些信号的取值组合在仿真里出现过哪些、缺哪些。这一步特别需要耐心,但经常能发现一些有意思的问题,比如某个条件分支需要信号 A 为高且 B 为低同时出现,而现有的测试序列里这两个信号天然互斥,根本构不出这个组合——这时候问题就上升到了设计或者验证环境层面,不是简单加激励能解决的。
我还用这招发现过几次覆盖率模型的定义和 RTL 实现不一致的情况:覆盖率模型里写的是"三拍连续背压",RTL 实际实现只判断了两拍。这类问题如果只盯数字,很可能被当成"暂时覆盖不到"挂在那里,直到流片后才暴露。
5.3 大规模回归下的存储与耗时控制
覆盖率数据库很占空间,一个中等规模项目的完整回归,vdb 目录能到几十甚至上百 GB。几个能明显省资源的手段:跑完就把原始 vdb 压缩归档,只保留合并后的库;-cm_hier严格排除掉不需要采集的层级;在不需要细粒度条件的阶段用较粗的采集类型;合并时用-parallel。另外建议给覆盖率目录单独挂一块盘,别和波形、日志挤在一起,否则清理的时候容易误删。
6. 几条我在项目里固定下来的习惯
说几条踩过坑之后不再动摇的做法。第一,计划树的节点责任人一律写到具体的人,不写组名——写组名的节点,三个月后没人认领。第二,每次覆盖率评审之前一定重新回填一次,不允许用上次的数据,哪怕只多跑了一个测试,因为覆盖率评审会上扯数据版本的问题最浪费时间。第三,节点从"进行中"改到"待复核"的时候,顺手在提交信息里写清楚是哪次回归、哪个数据库,几个月后回溯的时候这一行信息能救命。第四,别追求一次性把树建完美,第一版粗糙没关系,重要的是让它跟着项目持续更新,一棵三个月没动过的计划树,状态比 Excel 还不可信。
最后分享一个真实的小教训。我曾经在一个项目上把计划树建得非常细致,节点数接近三百个,结果中期需求变更,一半节点的描述和判定标准都需要重写,维护成本高到没人愿意碰,最后整棵树废掉重来。后来我改成"先粗后细":初期只建到特性层,等这一层基本稳定、覆盖率流程跑顺了,再对重点特性往下加判定点。这个节奏下,计划树是跟着验证进度一起长起来的,而不是变成一个一开始就要还清的技术债。