尧图精选

HVP Planner 实战:UVM 验证计划与功能覆盖率收敛

🕒 发布时间:2026/10/2 22:54:34 📁 来源:尧图网络
1. 先把话说清楚HVP Planner 在验证流程里到底站在哪个位置1.1 一块反复贴来贴去的 Excel 说起刚入行那几年我们的验证计划就是一张 Excel左边一列功能点右边几列写“谁负责”“什么时候测”“测完打勾”。项目前期大家还很认真地更新到了中期RTL 一天改三版验证环境一天加五个 case那张表就彻底废了——没人知道表上的“已通过”到底是哪一版代码跑的也没人知道某个功能点对应的 case 到底写在哪个 test 里。等到流片前的覆盖率评审会上leader 问一句“这个 feature 到底覆盖了没有”全场沉默十秒然后开始翻 simv.vdb。HVP Planner 这类东西解决的正是这个痛点。HVP 在不同公司、不同团队的叫法不太一样我见过最通行的解释是Hierarchical Verification Plan分层验证计划它不是一个仿真器选项也不是一条编译命令而是把“功能意图 → 测试用例 → 覆盖率数据”这三段链条串起来的中间层。你可以把它理解成验证工作的“项目管理系统 覆盖率索引”表里记的是人话描述的功能点背后挂着具体的 covergroup、coverpoint、cross还有具体跑出这些覆盖率的 test 名字。仿真跑完工具把 vdb 里的覆盖率数据往计划上一回填哪些功能点真的被打了、哪些只是“纸面通过”一眼就能看出来。我这里说的 planner泛指这类计划管理与覆盖率映射的工具能力。业界常见的有 Verdi 里那套 Verification Plan 管理界面也有团队自己用脚本 YAML/JSON 搭的轻量版本还有直接挂在 CI 上做自动回填的。名字不同内核逻辑是一样的计划是唯一事实来源single source of truth覆盖率数据库是证据两者必须能对上号。1.2 它跟代码覆盖率、功能覆盖率之间是什么关系这里必须先把三个概念掰开不然后面全是糊涂账。代码覆盖率line / toggle / branch / condition / FSM是工具自动生成的你写不写计划它都有反映的是 RTL 被“物理执行”的程度。它的问题是100% 的代码覆盖率完全可能对应一个功能全错的芯片。功能覆盖率是你自己定义的用 covergroup、coverpoint 描述“我想看到什么场景”。它的问题是定义得对不对只有你自己知道工具不会提醒你漏了哪个 feature。HVP 计划站在两者之上回答的是“我原本打算验什么现在验到什么程度了”。举个具体的例子一个 AXI 从机模块功能点写“支持 outstanding 深度 1~16 的乱序响应”。这句话落到计划里是一个 feature 条目落到环境里是outstanding_depth_cp的 16 个 bin再加一个depth_x_burst_len的 cross。计划工具做的事就是跑完 regression去 vdb 里查outstanding_depth_cp的 bin 命中情况把 16 个 bin 里哪几个没打到的信息回填到这条 feature 上标成“部分覆盖”。我个人的习惯是把计划里每个条目的状态分成这么几档写脚本的时候直接照这个状态机来状态含义判定依据Not Implemented计划里有环境里还没有对应的 covergroup 或 test计划条目未绑定任何 coverpoint / testImplemented绑定关系建好了但一次都没跑过绑定了但仍无覆盖率数据Partially Covered跑过bin 只命中了一部分bin 命中率介于 0 与 100% 之间Passed全部 bin 命中且相关 test 全绿bin 全中 test 状态 passFailed覆盖到了但 test 挂了test 状态 failWaived明确评审后决定不覆盖人工标注 评审记录这张表的价值在于它把“主观感觉”变成了“可查询状态”。评审会上不用再争论直接grep一下 Not Implemented 和 Partially Covered 有多少条工作量就摆在那了。1.3 什么样的项目真的需要它什么样的项目纯属折腾不是所有项目都值得上这套东西。我踩过的坑是一个只有两个小模块、验证周期三周的小改版项目硬要搭一套计划管理结果搭框架花了一周写计划花了两天实际跑仿真就五天。这属于典型的用力过猛。我的判断标准大概是这样的模块数量超过 5 个、验证周期超过两个月、参与验证的人超过 2 个、或者有跨版本复用需求比如 IP 要交付给别的团队这几个条件里中两条以上就值得上。反过来如果是一个人从头包到尾的小项目一张写得清楚的 Markdown 表格加一套命名规范效果其实差不多。还有一个容易被忽略的收益计划是给人看的不只是给工具看的。新人接手一个跑了半年的环境最痛苦的不是看不懂 UVM 的 factory override而是不知道这堆 test 到底在验什么。有一份维护良好的计划新人两天就能摸清边界在哪、哪些地方是空白。提示计划文件的版本一定要跟 RTL 版本、环境版本一起管。我见过最离谱的一次事故是计划表停留在三个月前覆盖率报告是最新的评审时按旧表判断“这个功能已经验完了”实际那条 feature 对应的 covergroup 早就被重构删掉了。2. 整体设计思路拆解为什么不能只做一张表2.1 分层结构是这套东西的骨架“Hierarchical”这个词不是随便加的。真正好用的计划一定是分层的一般至少三层第一层是功能域Feature Group粒度粗通常对应一个模块或者一个协议层比如“AXI 读写通道”“中断响应”“低功耗握手”。这一层是给项目经理和系统架构师看的回答“这个模块验了百分之多少”。第二层是功能点Feature / Requirement是计划的最小可评审单元必须能被一句话说清楚并且能被判定“是/否”比如“支持 4KB 边界跨越的 burst 拆包”。写成“验证读写功能正常”这种就是耍流氓因为它永远无法判定通过。第三层是测试点与覆盖点Test Point / Coverage Point这一层才落到具体实现哪个 test、哪个 coverpoint、哪个 checker。一个功能点可能对应三个 test 和两个 covergroup这是正常的。分层的好处是收敛判断可以按层做。项目中期我关心的是“第一层哪些功能域还是零覆盖”进入后期我关心的是“第三层哪些 bin 死活打不到”。如果只有平铺的一张表这两类查询都很难做。2.2 计划与覆盖率数据库之间的映射是最容易做错的一环映射关系怎么建决定了这套东西能不能自动化。常见的三种做法做法一靠命名约定。计划条目 ID 写成AXI_WR_003对应的 covergroup 里的 coverpoint 也带同样的前缀比如cp_axi_wr_003_len。脚本靠字符串匹配把两边对上。优点是零成本缺点是脆——一旦有人改名字映射就断了而且断得很安静不会报错。做法二靠配置文件显式声明。计划文件里直接写明coverage: top.axi_agent.monitor.cg_write.cp_len这样的层次路径再写tests: [axi_wr_basic_test, axi_wr_4k_test]。优点是有据可查缺点是要人工维护路径重构一次要改一片。做法三靠工具原生绑定。在 Verdi 的 Verification Plan 界面里把功能点和 covergroup 直接连起来工具维护这份关系。这种方式目前最省心但要求你的覆盖率数据必须是标准 vdb而且环境编译时要带-cm系列选项否则根本没有数据源。我自己的实践是做法二为主、做法一为辅显式声明路径同时强制命名规约做交叉校验。脚本每次跑完先做一遍一致性检查——计划里声明的路径在 vdb 里找不到直接报 error 而不是 warning。这一点很关键因为静默失配是这套流程最大的杀手。# 轻量级一致性检查脚本骨架 import os, re, sys def load_plan(path): # 计划文件里每条记录形如ID / feature 描述 / cov_path / test 列表 plan [] with open(path, encodingutf-8) as f: for line in f: if not line.strip() or line.startswith(#): continue parts [p.strip() for p in line.split(|)] plan.append({id: parts[0], desc: parts[1], cov_path: parts[2], tests: [t for t in parts[3].split(,) if t]}) return plan def check_mapping(plan, vdb_root): missing [] for item in plan: # vdb 中覆盖率路径通常导出为 urg 的文本报告后再做匹配 if not os.path.exists(os.path.join(vdb_root, cov.txt)): sys.exit(coverage export not found, run urg first) with open(os.path.join(vdb_root, cov.txt), encodingutf-8) as f: content f.read() if item[cov_path] not in content: missing.append(item[id]) return missing if __name__ __main__: bad check_mapping(load_plan(sys.argv[1]), sys.argv[2]) for b in bad: print(MAPPING BROKEN:, b) sys.exit(1 if bad else 0)这段脚本很糙但思路值得抄映射检查必须作为回归流程的一个独立关卡失败了要让 CI 变红而不是打个日志就过去了。2.3 和 UVM 验证平台的衔接位置计划工具本身不关心你用的是 UVM 还是纯 SV TB它只关心两件事覆盖率数据在哪test 名字是什么。但要让衔接顺畅环境这边得配合做几件事。第一顶层环境要统一 dump 覆盖率。VCS 里就是编译加-cm运行加-cm_name。这里最常见的错误是每个 test 手动指定-cm_name导致名字五花八门回归脚本根本认不出来。正确做法是用统一的命名模板比如${TEST_NAME}_${SEED}。第二test 的名字要能被外部枚举出来。UVM 的 test 列表如果只写在UVM_TESTNAME里计划工具是看不到的。我的做法是维护一份testlist.f每行一个 test 名回归脚本和计划检查脚本共用这一份文件。这样“计划里声明的 test 是否真的存在于 testlist”也能自动校验。第三report server 要有结构化的结果输出。UVM 默认打印一堆UVM_INFO人看还行机器解析很痛苦。用UVM_REPORT_DEFAULT_FILE或者自定义 report server把每个 test 的 pass/fail 写成一行 JSON 或者 CSV计划回填的时候直接读比爬日志靠谱得多。注意如果你的环境是混合语言Verilog SV 少量 C 模型覆盖率采集要注意-cm的采集范围别把 C 模型也一起采进来否则代码覆盖率会被拉低到无法收敛评审时解释起来很麻烦。用-cm_hier指定层次范围是常规做法。3. 从零搭一套最小可用流程3.1 目录组织与文件约定先把目录定下来。我用的结构大概是这样verify/ ├── plan/ │ ├── hvp.plan # 计划主文件功能点 映射 │ ├── waivers.txt # 明确 waive 的条目及理由 │ └── testlist.f # 全局 test 列表 ├── env/ # UVM 环境 ├── sim/ │ ├── simv.vdb/ # 每次编译生成的覆盖率数据库 │ └── run/ # 每个 test 的运行目录 ├── regress/ │ ├── run_regress.sh # 回归脚本 │ └── merge_cov.sh # 覆盖率合并 报告 └── reports/ ├── urgReport/ # urg 生成的 HTML/文本报告 └── plan_status.csv # 回填后的计划状态几个约定必须写进团队规约里否则半年后全是烂摊子plan目录跟着 RTL 版本走 tagsimv.vdb每次全量编译要清空不能增量累加否则会出现“覆盖率越跑越高但代码是旧的”这种鬼故事reports目录下的东西不进版本库但每次评审的plan_status.csv要存档方便对比。3.2 计划文件怎么写才不给自己挖坑我倾向用简单的竖线分隔文本原因很实在能 diff能 grep能用 awk改起来不用打开 GUI。计划文件最怕的就是“只有某个人电脑上的那个工具能打开”。下面是一个示例片段覆盖 AXI 从机的一小部分功能# ID | 功能点描述 | 覆盖率路径 | 关联 test AXI_WR_001 | 支持单拍写awlen0 | top.u_slave.u_mon.cg_wr.cp_len.bin_0 | axi_wr_single_test AXI_WR_002 | 支持 INCR 类型 1~16 拍写 | top.u_slave.u_mon.cg_wr.cp_len | axi_wr_burst_test,axi_wr_stress_test AXI_WR_003 | 支持 4KB 边界跨越自动拆包 | top.u_slave.u_mon.cg_wr.cp_4k_cross | axi_wr_4k_test AXI_WR_004 | 支持 WSTRB 任意组合的窄写 | top.u_slave.u_mon.cg_wr.cp_wstrb_x_len | axi_wr_narrow_test AXI_RD_001 | 读通道 outstanding 深度 1~16 | top.u_slave.u_mon.cg_rd.cp_ostd | axi_rd_ostd_test AXI_RD_002 | 读写并发时的响应顺序 | top.u_slave.u_mon.cg_rd.cp_rw_interleave | axi_rdwr_mix_test写这份文件的时候有几个原则都是从踩坑里总结出来的功能点必须可判定。“支持任意组合”这种描述我没法判定得写成“支持 WSTRB 的 2^4 种组合”。可判定的意思就是我能用一句话说出它通过的标准。一个功能点尽量只绑一个覆盖率路径。一个功能点绑七八个 coverpoint 的时候回填脚本判断“这个功能点过了没有”会变成一笔糊涂账——到底是要全部命中还是命中一个就算过如果确实需要多个维度那就把它拆成多个功能点。test 列表里写的名字必须在 testlist.f 里存在。这是脚本能自动查的别偷懒。3.3 编译与仿真命令参数是有讲究的编译阶段最关键的是覆盖率采集选项。我常用的组合是这样vcs -full64 -sverilog -ntb_opts uvm-1.2 \ -debug_accessall -kdb \ -cm linecondfsmtglbranch \ -cm_dir ./simv.vdb \ -cm_hier cm_hier.cfg \ -f filelist.f \ -l compile.log \ -o simv这里每个参数都有理由不是抄来的-cm linecondfsmtglbranch里的类型要按需选。tgl翻转覆盖率在大模块上会让数据库爆炸几百 MB 起步所以我的习惯是中期只开 linebranchcond进入收敛期再加 tgl或者用-cm_tgl portsonly只采端口翻转。fsm只有在你确实有状态机、且它没有被其他覆盖类型间接覆盖时才开。-cm_hier cm_hier.cfg是一个范围控制文件格式大概是tree top.u_slave 0 tree top.u_third_party_ip -tree第一行的意思是采u_slave下面所有的第二行的-tree是明确排除第三方 IP。第三方 IP 的覆盖率通常拿不到 waiver不排除的话永远收敛不了评审会上会成为甩不掉的尾巴。-kdb是给 Verdi 用的生成知识数据库。现在普遍用-debug_accessall -kdb这套组合取代老的-lca之类的选项这样 Verdi 可以直接吃 kdb不用重新解析 RTL打开速度快很多。如果你的流程里 Verdi 还是靠 FSDB 后处理那$fsdbDumpfile/$fsdbDumpvars这套就得留着通常只在 debug 的 test 里开启全量回归开 FSDB 会让磁盘直接撑爆。仿真阶段./simv UVM_TESTNAMEaxi_wr_burst_test \ ntb_random_seed12345 \ -cm linecondfsmbranch \ -cm_name axi_wr_burst_test_12345 \ -cm_dir ./simv.vdb \ -l run.log-cm_name必须唯一否则后跑的会覆盖先跑的。我见过因为-cm_name重复导致回归跑了两千个 case合并报告里只有三百个的数据白跑一星期。3.4 合并覆盖率并生成能回填的报告单个 test 的 vdb 是碎片要先用 urg 合并urg -dir simv.vdb -dir ../run/*/simv.vdb \ -dbname merged.vdb \ -format both \ -report ./reports/urgReport \ -show tests \ -metric linecondfsmtglbranch-show tests这个选项很关键它会在报告里保留“哪个 test 贡献了哪些覆盖率”的映射信息这是回填计划时判断“某个 bin 是哪个 test 打到的”的依据。少了它你只知道覆盖率有了但不知道是谁给的出问题时查起来很痛苦。合并完了之后我一般导出成文本再喂给回填脚本urg -dir merged.vdb -format text -report ./reports/urgText文本报告的结构比较稳定用正则提取 coverpoint 的名字和命中率就够了。整条链路的顺序是编译带 -cm→ 逐 test 仿真唯一 -cm_name→ urg 合并 → 导出文本 → 回填计划 → 生成 plan_status.csv。这条链路我建议写成一个脚本任何人都能一键跑别让流程依赖于某个人的记忆。提示simv.vdb目录下的结构是snps/coverage/db/testdata/cm_name/。想快速确认某个 test 的数据有没有生成直接看这个目录比翻日志快得多。这一步我几乎每次排查覆盖率问题时都会做。4. 覆盖率收敛阶段怎么用这套东西4.1 先分清“打不到”和“没打到”覆盖率上不去只有两种原因环境根本产生不了那个场景或者场景产生了但采样点没抓到。这两类问题的处理方式完全不同前者要改 sequence 或约束后者要改 monitor 或 covergroup。HVP 计划在这里的价值是分流。把plan_status.csv按状态排一下Partially Covered 里那些 bin 命中率长期为零的条目基本可以直接归到“场景产生不了”那一类。我一般的排查动作是先在波形里搜一下这个场景该有的信号比如 4KB 跨越那条直接找awaddr[11:0]有没有接近边界后回绕的时刻。波形里有、覆盖率里没有那就是采样点的问题绝大多数情况是 covergroup 的采样时机绑错了——比如绑在(posedge clk)上但没有加有效条件或者绑在某个 event 上而这个 event 在某些分支下不触发。我印象很深的一次是cp_wstrb_x_len这个 cross 死活打不满。查了两天才发现是 monitor 里采样 covergroup 的位置在awready wready的握手分支里而窄写场景下wvalid会先于awvalid拉高采样点根本没被执行到。把采样挪到事务结束时再采一次性就打满了。这个坑的教训是covergroup 的采样点要绑在“事务完成”这个语义上而不是绑在某个具体信号跳变上。4.2 回归策略怎么排 case 才不浪费机时计划做出来之后回归不再是“把 testlist 全跑一遍”。我会按三层排第一层是冒烟集只跑计划里标记为关键路径的十几个 test半小时出结果用来挡 RTL 的低级错误。RTL 每次小改动后都跑。第二层是功能域回归按计划的第一层分组改动哪个功能域就重点跑那一片加上它的相邻功能域。这一层控制在两三个小时。第三层是全量回归每晚跑。这里有个技巧用计划里的功能点 ID 给 test 打标签回归报告按标签分组展示而不是按字母序排。评审的时候你就能一眼看出“中断相关的功能点整体进度 78%但低功耗那一片只有 30%”这比看一个全局 65% 的数字有用得多。跑完之后我会固定做三件事合并覆盖率、回填计划、把plan_status.csv和历史对比一下。如果某个功能点昨天还是 Passed、今天变成 Partially Covered那基本说明 RTL 改动引入了新的分支路径是个需要立刻看的信号。这种“覆盖率回退”比“覆盖率不涨”更值得警惕。4.3 收敛期最容易被忽视的两件事第一件是排除文件的管理。代码覆盖率里总有一些天然的不可达分支比如default分支、复位期间的路径、assertion 的失败分支。这些要用-cm_hier或者 urg 的 exclusion 文件排除掉。但排除文件必须逐条写理由而且要经过评审。我见过一个项目为了“把数字做好看”一口气排除了两百多条最后代码覆盖率报表很漂亮但没人说得清到底验了多少。这个做法在短期评审里能糊弄过去在真正出问题的时候会很难看。第二件是waive 的记录方式。功能覆盖率的 waive 一定要落成文件写清楚“为什么不需要覆盖”和“谁批的”。计划里我专门留一档状态就是 Waived跟 Not Implemented 严格分开。这两个状态混在一起是后期最大的风险来源——Not Implemented 是欠债Waived 是决策性质完全不同。注意waive 的判断一定要在覆盖率跑到 80% 以上、对问题本质有清晰认识之后再下。前期急着 waive后面发现是环境问题收回来很尴尬。5. 常见问题速查5.1 排查思路先立起来覆盖率类问题我固定的排查顺序是先看数据有没有进来再看数据对不对最后看映射通不通。“数据有没有进来”就是查simv.vdb/snps/coverage/db/testdata/下的目录数量跟 test 数量对不对得上“数据对不对”就是拿单个 test 的 vdb 单独生成一份 urg 报告看它自己的覆盖率是否合理“映射通不通”就是跑第 2.2 节那套一致性检查脚本。这个顺序的好处是绝大多数问题在前两步就定位了不会跑到映射那一步去瞎猜。5.2 常见问题速查表现象常见原因处理方式合并后覆盖率比单个 test 还低-cm_name重复后跑的覆盖了先跑的检查 testdata 目录数量统一命名模板功能覆盖率一直是 0covergroup 没例化或采样事件未触发在波形里确认信号跳变确认 cg 的new被调用覆盖率报告里看不到某个模块-cm_hier把范围排除了检查 cm_hier.cfg 的 tree 配置Verdi 打开 kdb 后跳不到源码编译时缺-debug_access或 kdb 版本不匹配重新编译确认-kdb生效回归跑完但报告里只有部分 testurg 的-dir通配没匹配到嵌套目录用显式路径列表或find出所有 vdb 再传参计划回填后大量条目是 Not Implemented覆盖率路径拼写和实际层次不一致从 urg 文本报告里复制路径别手打某个 bin 死活打不到约束把该组合排除了或采样点位置不对先用波形确认场景是否存在再决定改约束还是改采样代码覆盖率长期停在 95% 上不去不可达分支未排除逐条评估后写 exclusion附理由评审tgl 覆盖率让 vdb 爆到几 GB全层次开了翻转采集用-cm_tgl portsonly或缩小 tree 范围5.3 几条不太写进文档的经验别在项目中期重构 covergroup 结构。一旦开始回填计划covergroup 的层次路径就是契约。中期重构会让历史覆盖率数据全部失配你只能从头再跑一轮才能对比趋势。如果真要做就选在版本节点上做一次性改完并重新建立映射。覆盖率数据的存档要有节奏。我习惯在每个关键节点功能冻结、第一次全量回归全绿、流片前两周把merged.vdb和plan_status.csv一起打包存档。出问题回溯的时候这几份数据比日志有用得多。计划文件的修改要走 code review。听起来有点重但很必要。计划条目被悄悄删掉、状态被手动改成 Passed 的事我见过不止一次。放在版本库里改动有记录谁改的一目了然。给覆盖率数字配一个“人话解释”。每次评审报告里除了 87.3% 这种数字我会额外写三到五句话这次涨了多少、涨在哪个功能域、剩下的卡在哪、预计什么时候解决。这几句话才是评审真正需要的东西数字只是背景。6. 几个我踩过的坑以及后来怎么处理的第一个坑是过度依赖自动化回填。有段时间我把计划状态完全交给脚本脚本说 Passed 就 Passed。后来发现一个问题某个功能点的 coverpoint 全打满了但对应的 scoreboard 是关着的等于覆盖到了但没检查。覆盖率只能证明“场景发生了”不能证明“结果被正确判断了”。从那以后我在计划里加了一列checker明确写出这个功能点靠哪个 checker 判定回填的时候要求“覆盖率满 checker 存在 test 状态 pass”三个条件同时满足才算 Passed。第二个坑是计划颗粒度一开始就定太细。我做过一次把每个信号都列成功能点的事结果三千多条维护成本远超收益。后来我的做法是先粗后细第一版只写功能域和大概二十到三十个核心功能点等项目跑起来、边界越来越清楚再往下拆。计划是长出来的不是一次画完的。第三个坑是用错了关闭时机的判断标准。有段时间团队用“功能覆盖率 100%”作为收工标准结果为了这个数字大家对约束放水、把 case 写得很“温柔”场景是打满了但边角情况其实没激到。后来我们改成双标准功能覆盖率达标同时代码覆盖率里 condition 和 branch 的排除条目必须逐条评审。两个标准一起看才能真正挡住那种“数字好看、风险还在”的情况。最后一个体会是关于人的。这套东西本质上是给团队建立共同语言的工具只是载体。我推过两次 HVP 类的流程第一次失败得很彻底原因是我只顾着搭脚本和目录规范没跟设计、系统那边对齐功能点的描述方式最后计划里写的东西和他们脑子里的东西对不上。第二次就学乖了功能点的描述文本从需求文档里直接摘一个字不改只在后面补覆盖率路径。这样评审的时候各方看的都是同一份描述争论自然就少了。工具能做到的事其实有限真正让流程跑起来的是“大家的描述方式统一了”这件事本身。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →