7.3 功能覆盖与回归 本节摘要:代码覆盖率回答「代码行被执行过没有」,功能覆盖率回答「验证意图被触及过没有」——两者都不是「测够了」的充分条件,但缺了它们,回答无从谈起。回归把验证从一次性动作变成持续流水,种子管理与机器判卷是它的两大支柱。 测试台会提问、会判卷了,最后一问是终极问题:测够了没有。这一节讲覆盖率的三种口径、纯 Verilog 环境下功能覆盖点的手写方法,以及把验证组织成回归流水线的工程做法。 覆盖率的两种口径 代码覆盖率由仿真器自动统计:语句覆盖(每行执行过没有)、分支覆盖(每个分支两个方向都走过没有)、翻转覆盖(每个位翻转过 0 到 1 与 1 到 0 没有)、状态机覆盖(每个状态到过、每条边走过没有)。它是「测过什么」的客观下界,工具直接出报告。
本节摘要:代码覆盖率回答「代码行被执行过没有」,功能覆盖率回答「验证意图被触及过没有」——两者都不是「测够了」的充分条件,但缺了它们,回答无从谈起。回归把验证从一次性动作变成持续流水,种子管理与机器判卷是它的两大支柱。
测试台会提问、会判卷了,最后一问是终极问题:测够了没有。这一节讲覆盖率的三种口径、纯 Verilog 环境下功能覆盖点的手写方法,以及把验证组织成回归流水线的工程做法。
代码覆盖率由仿真器自动统计:语句覆盖(每行执行过没有)、分支覆盖(每个分支两个方向都走过没有)、翻转覆盖(每个位翻转过 0 到 1 与 1 到 0 没有)、状态机覆盖(每个状态到过、每条边走过没有)。它是「测过什么」的客观下界,工具直接出报告。
但代码覆盖率有一个致命盲区:它衡量代码被执行,不衡量错误被检查。一行故意写错的代码被执行一万次,覆盖照样漂亮。功能覆盖率从验证意图出发:设计要支持的那些「情形」——协议的每种事务类型、仲裁的每种请求组合、计数器的溢出边界——各设一个覆盖点,统计验证过程中触及了几个。代码覆盖答「代码跑过没有」,功能覆盖答「该测的情形测过没有」,两者对照才构成完整的覆盖图景。
| 口径 | 统计者 | 回答的问题 | 盲区 |
|---|---|---|---|
| 代码覆盖 | 仿真器自动 | 代码是否被执行 | 不关心行为对错 |
| 功能覆盖 | 验证者定义 | 验证意图是否触及 | 覆盖点写漏就测不到 |
| 断言通过率 | 断言工具 | 不变量是否始终成立 | 断言本身可能写错 |
SystemVerilog 有专门的覆盖组语法,纯 Verilog 没有,但覆盖点的本质是计数器,手写即可:
// 仲裁器用例的功能覆盖点 module arb_coverage( input wire clk, input wire rst_n, input wire [3:0] req, input wire [3:0] grant, input wire sample // 用例在关键时刻拉高采样 ); integer cov_all_req; // 覆盖点:四路请求同时出现 integer cov_each_0; // 覆盖点:各路单独获得授权 integer cov_each_1; integer cov_each_2; integer cov_each_3; integer cov_empty; // 覆盖点:无请求通过 integer cov_switch; // 覆盖点:授权在相邻两次间易主 reg [3:0] grant_d; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cov_all_req = 0; cov_each_0 = 0; cov_each_1 = 0; cov_each_2 = 0; cov_each_3 = 0; cov_empty = 0; cov_switch = 0; grant_d = 4'b0000; end else begin grant_d <= grant; if (sample) begin if (req == 4'b1111) cov_all_req = cov_all_req + 1; if (req == 4'b0000) cov_empty = cov_empty + 1; if (grant == 4'b0001) cov_each_0 = cov_each_0 + 1; if (grant == 4'b0010) cov_each_1 = cov_each_1 + 1; if (grant == 4'b0100) cov_each_2 = cov_each_2 + 1; if (grant == 4'b1000) cov_each_3 = cov_each_3 + 1; end if (grant != 4'b0000 && grant_d != 4'b0000 && grant != grant_d) cov_switch = cov_switch + 1; end end endmodule
写覆盖点的手艺在「情形怎么定义」:不是照抄接口信号组合,而是从设计规格里提炼「必须被验证证明过的行为」。上述各覆盖点分别对应仲裁器的几个交付承诺——并发承受力、空载行为、各路可达性、切换正确性。覆盖点写错了或写漏了,覆盖率再高也是假象,所以覆盖点清单本身要过评审——它与测试用例清单同样重要。

案例展开:覆盖率平顶的真相。 背景:某仲裁器回归十几轮后代码覆盖停在九成附近不再上涨,团队一度以为是激励不够随机。操作:对照功能覆盖点清单逐项核对,发现「授权易主」覆盖点从未命中——检查用例发现所有用例都是单请求者独占场景,「切换」这个行为根本没有被任何用例构造出来;补两条双请求者交叠用例后,代码覆盖与功能覆盖同步突破。结果:定位为「用例维度缺失」而非「随机不足」,补的定向用例同时揪出一个授权保持逻辑的边界错误。解读:覆盖率停滞有两种病因——激励没到(随机能救)与情形构造不出来(只能定向用例);区分两者靠功能覆盖点的逐项核对,这也是功能覆盖比代码覆盖值钱的原因:它把「哪里没测到」翻译成了可行动的清单。变式:把覆盖点计数接进回归汇总脚本,每轮回归输出覆盖增量,增量为零的轮次自动标记为「该做定向补强」——覆盖率数据从报表变成调度依据。
单次仿真只能覆盖一条轨迹,回归是让仿真流水线化运转的组织方式。三个支柱:
种子管理:随机激励以种子为源头,每个用例的每轮回归记录所用种子;失败用例用原种子重跑即可复现,「不可复现的随机 bug」在规范化的种子管理下不应该存在。工程做法是种子随用例清单统一分配,回归脚本逐例传入。
机器判卷:每个用例的测试台按 7.2 节的判卷协议打印通过与失败计数,回归脚本收集退出码与计数,汇总成一张表:用例名、种子、结果、失败计数、日志路径。人只看失败行。
失败复现:失败用例用原种子单独重跑,开波形记录,缩小到失败事务的时刻附近分析。回归的价值不在跑得多快,而在「每个失败都能被锁定、被复现、被归因」。
回归的节奏也有讲究:设计每次提交触发冒烟回归(少量核心用例),每夜跑全量回归,里程碑节点跑长随机回归(多种子长时间)。三层节奏让反馈延迟与计算开销达成平衡——这是验证团队运转的心跳。
验证流程到此闭环。下一章「判决执行」:代码通过庭审之后,如何被综合成门电路、走上 FPGA 与 ASIC 的实现之路。