Buck Blog · 博客正文

返回技术分享首页
FPGA仿真中的断言和覆盖率统计

FPGA仿真中的断言和覆盖率统计

> 目标:讲清楚在 FPGA 仿真中,为什么要用断言(Assertion)和覆盖率(Coverage),以及如何把它们真正用起来。

> 读完后你应能做到:

> 1)知道“什么问题该用断言抓”;

> 2)知道“覆盖率该怎么建、怎么看、怎么补”;

> 3)把断言 + 覆盖率纳入你的日常仿真流程。

---

1. 为什么 FPGA 仿真不能只看波形

很多项目只靠波形看“功能大致对了”,但会漏掉三类高风险问题:

1. 时序关系错误:例如握手顺序错一拍、复位释放时机不对。

2. 边界条件遗漏:例如 FIFO 满/空、计数器回绕、突发长度极值。

3. 测试盲区:你以为测到了,其实某些状态或分支从未触发。

波形适合“定位问题”,不适合“系统证明没问题”。

断言和覆盖率就是把“经验判断”升级成“可量化验证”。

---

2. 断言(Assertion)的作用与意义

2.1 断言是什么

断言本质是“可执行的设计规则”。

可以理解为:

• 你定义一个规则(例如:valid 拉高后,ready 必须在 N 拍内响应)

• 仿真器每拍自动检查

• 违反规则就立即报错

2.2 为什么断言很关键

1. 尽早暴露协议错误:比事后看波形快得多。

2. 可复用:同类接口规则可在多个模块复用。

3. 可回归:每次改代码都自动检查,不靠人工记忆。

4. 定位清晰:报错时带时间点和规则名,定位效率高。

2.3 断言主要类型

• Immediate Assertion:用于过程块里即时检查(常见于简单条件)。

• Concurrent Assertion(SVA):时序关系检查主力(跨周期)。

在 FPGA 项目中,最有价值的是并发断言(时序协议)。

2.4 断言落地伪代码模板(通用)

// 模板1:规则定义
property P_rule_name:
   @(posedge clk) disable iff(!rst_n)
      trigger_condition |-> expected_timing_behavior;

assert property(P_rule_name)
   else error("P_rule_name failed", time, key_signals);

// 模板2:覆盖断言触发(可统计命中)
cover property(P_rule_name);

说明:

• trigger_condition 写“什么时候开始检查”。

• expected_timing_behavior 写“之后必须发生什么”。

---

3. 在仿真中应优先添加哪些断言

建议按“复位 -> 接口协议 -> 数据一致性 -> 状态机合法性”四层添加。

3.1 复位相关断言

重点检查:

• 复位期间输出是否处于已知安全值

• 复位释放后是否在预期拍数内进入工作态

• 不允许出现 X/Z 传播到关键控制信号

典型规则示例(文字描述):

• rst_n=0 时,valid 必须为 0。

• rst_n 上升沿后 1~3 拍内,状态机必须进入 IDLE。

伪代码范例:

// 复位期间输出安全值
assert: on every clk
   if rst_n == 0:
      require(valid == 0 && busy == 0)

// 复位释放后 1~3 拍进入 IDLE
property p_reset_to_idle:
   (rst_n rises) -> within [1..3] cycles (state == IDLE)
assert p_reset_to_idle

3.2 握手协议断言(valid/ready)

重点检查:

• valid 拉高后数据保持稳定直到握手成功

• 不允许“假握手”(valid=0 却当成传输成功)

• backpressure 场景下不丢包、不重复包

典型规则:

• 当 valid=1 && ready=0 时,data 保持不变。

• 只有 valid && ready 才允许计数器递增。

伪代码范例:

// backpressure 下数据必须稳定
property p_data_stable_when_wait:
   if (valid == 1 && ready == 0)
      next_cycle(data == $past(data)) until (ready == 1)
assert p_data_stable_when_wait

// 只有握手成功才允许计数器+1
assert: on every clk
   if (pkt_cnt != $past(pkt_cnt)):
      require($past(valid && ready) == 1)

3.3 FIFO/缓存断言

重点检查:

• 空读/满写禁止

• 读写指针关系合法

• 深度计数不越界

典型规则:

• fifo_empty 时不允许 rd_en。

• fifo_full 时不允许 wr_en。

伪代码范例:

assert: on every clk
   if fifo_empty:
      require(rd_en == 0)

assert: on every clk
   if fifo_full:
      require(wr_en == 0)

assert: on every clk
   require(fifo_level >= 0 && fifo_level <= FIFO_DEPTH)

3.4 状态机断言

重点检查:

• 状态转移路径合法

• 不进入非法状态编码

• 关键状态在限定时间内可达

典型规则:

• IDLE -> BUSY -> DONE -> IDLE,不允许跳过 DONE。

• BUSY 超过超时阈值必须触发 error/回退。

3.5 数值与范围断言

重点检查:

• 地址越界

• 长度字段超规格

• 算法结果溢出(如定点乘加)

---

4. 覆盖率(Coverage)的作用与意义

4.1 覆盖率是什么

覆盖率回答的问题是:

• 你“到底测到了多少”

• 哪些逻辑、状态、场景还没被激活

4.2 常见覆盖率类型

1. 代码覆盖率(Code Coverage)

• line / branch / condition / toggle / FSM

2. 功能覆盖率(Functional Coverage)

• 按规格定义的场景覆盖(最关键)

3. 断言覆盖率(Assertion Coverage)

• 断言触发次数、通过/失败统计

4.3 为什么只看代码覆盖率不够

代码覆盖率高,不代表规格覆盖完整。

例如:分支都跑到了,但“突发长度=最大值 + backpressure + 复位插入”的组合场景可能完全没测。

结论:

• 代码覆盖率用于发现“代码盲区”

• 功能覆盖率用于发现“规格盲区”

4.4 代码覆盖率与功能覆盖率的区别(举例)

本质区别

| 对比项 | 代码覆盖率 | 功能覆盖率 |

|---|---|---|

| 关注对象 | RTL 代码结构(行、分支、条件、翻转、FSM) | 规格场景与业务行为 |

| 典型问题 | “代码跑到了,但不一定验证正确” | “模型定义不全会漏关键场景” |

| 常用目的 | 发现代码盲区 | 发现规格盲区 |

例子1:FIFO 模块

• 代码覆盖率高:empty/full/normal 三个分支都执行到了。

• 功能覆盖率缺口:almost_full 与 rd_en 同时出现时的边界行为未覆盖。

结论:

• 代码覆盖率告诉你“代码执行过”;

• 功能覆盖率告诉你“关键规格场景验证过”。

例子2:AXI 写通道

• 代码覆盖率高:AW/W/B 通道状态都跑过。

• 功能覆盖率低:WVALID 早到 + backpressure + reset 插入 的交叉场景没有命中。

结论:

• 高代码覆盖 ≠ 仿真完备。

---

5. 如何做“有效覆盖率统计”

5.1 先建立覆盖模型(Coverage Model)

先从规格里抽取关键维度:

• 数据长度(最小/常规/最大)

• 地址对齐(对齐/非对齐)

• 操作类型(读/写/突发)

• 流控状态(无阻塞/轻阻塞/重阻塞)

• 异常事件(超时/重试/复位插入)

把这些维度做成覆盖点(coverpoint)和交叉覆盖(cross)。

伪代码范例(覆盖模型):

covergroup cg_tx @(posedge clk);
   cp_len      : coverpoint tx_len    { bins min, mid, max; }
   cp_align    : coverpoint addr_lsb  { bins aligned, unaligned; }
   cp_type     : coverpoint tx_type   { bins read, write, burst; }
   cp_bp       : coverpoint bp_level  { bins none, light, heavy; }
   cp_rst_evt  : coverpoint rst_event { bins no_rst, with_rst; }

   cross_len_bp_rst : cross cp_len, cp_bp, cp_rst_evt;
   cross_type_align : cross cp_type, cp_align;
endgroup

解释:

• cross_len_bp_rst 用来捕捉“极值长度 + 回压 + 复位插入”这类高风险组合。

• 如果该交叉覆盖长期为 0,说明测试策略存在盲区。

5.2 覆盖率分层策略

• 基础层:功能通路是否跑通(smoke)

• 边界层:极值和异常

• 组合层:多维交叉(最难但最有价值)

5.3 覆盖率目标建议

不同项目目标不同,但可参考:

• 代码覆盖率:line/branch/toggle/FSM 都需要达标

• 功能覆盖率:核心场景和交叉场景优先达标

• 断言失败率:回归中必须为 0(或全部有明确豁免)

5.4 在 ModelSim 中实现代码覆盖率和功能覆盖率(步骤)

Step 1:编译并打开覆盖率选项

vlib work
vlog -sv -cover bcesft ../rtl/*.sv ../tb/*.sv

说明:

• bcesft 通常对应 branch/condition/expression/state/fsm/toggle(按版本确认)。

Step 2:启动仿真并采集覆盖率

vsim -coverage tb_top
run -all

Step 3:保存覆盖率数据库(UCDB)

coverage save run_001.ucdb

Step 4:查看代码覆盖率报告

vcover report -details run_001.ucdb

Step 5:查看功能覆盖率与断言覆盖率

vcover report -details -cvg run_001.ucdb

Step 6:多回归覆盖率合并

vcover merge merged.ucdb run_001.ucdb run_002.ucdb run_003.ucdb
vcover report -details merged.ucdb

Step 7:定位覆盖空洞

• 在 ModelSim GUI 的 Coverage 视图按模块/实例定位低覆盖节点。

• 对照 covergroup,找未命中的 bins/cross。

• 结合断言报告确定是“场景缺失”还是“设计异常”。

---

6. 断言 + 覆盖率如何协同(核心)

最有效的方法不是二选一,而是闭环:

1. 用断言抓“规则是否被违反”

2. 用覆盖率看“关键场景是否被激活”

3. 根据覆盖空洞补测试,再用断言保底

可执行闭环:

• 第一步:先加关键断言(复位/握手/FIFO/状态机)

• 第二步:跑回归拿覆盖率

• 第三步:定位覆盖空洞并补 directed/random 测试

• 第四步:收敛到“断言稳定 + 覆盖达标”

伪代码范例(闭环流程脚本思路):

while not signoff:
   run_regression(test_suite)

   a_fail = parse_assert_failures(report_assert)
   cov    = parse_coverage(report_cov)

   if a_fail.count > 0:
      triage_assertions(a_fail)
      fix_design_or_tb(a_fail)
      continue

   holes = find_cov_holes(cov, target_cov_model)
   if holes.not_empty:
      new_tests = generate_directed_tests(holes)
      test_suite.add(new_tests)
      continue

   signoff = true

6.1 覆盖率达到什么程度,才说明仿真“基本完备”

建议使用“三重门禁”而不是单一百分比:

门禁A:代码覆盖率达标

• line/branch/toggle/FSM 达到项目目标;

• 低覆盖代码有合理解释(不可达分支、配置关闭路径等)。

门禁B:功能覆盖率达标

• 规格中的必测场景全部命中;

• 高风险交叉场景(边界+异常+回压)达到目标;

• 不存在关键 coverpoint 长期 0 命中。

门禁C:质量状态达标

• 断言失败为 0(或有正式 Waiver);

• 无未关闭关键 bug;

• 最近多轮回归结果稳定。

伪代码判据:

if code_cov >= CODE_TARGET
   and func_cov >= FUNC_TARGET
   and assert_fail_cnt == 0
   and critical_bug_open == 0:
      signoff = PASS
else:
      signoff = FAIL

6.2 覆盖率没满足时,按什么规则增加测试 case

规则1:按风险优先级补测

优先级顺序:

1. 协议关键路径

2. 边界值场景

3. 异常注入场景

4. 普通组合场景

规则2:按“空洞类型”选“补测类型”

| 空洞类型 | 推荐补测方式 |

|---|---|

| 单一 bin 未命中 | Directed case |

| 多维 cross 未命中 | 约束随机 + 定向 seed |

| FSM/toggle 低覆盖 | 状态驱动序列 |

| 异常路径未命中 | fault/reset/backpressure 注入 |

规则3:一轮只补一类空洞

• 每轮新增少量 case,观察覆盖率增量。

• 避免同时改太多导致收益来源不可追踪。

规则4:建立“空洞-用例”映射台账

hole_id, hole_desc, risk, planned_case, owner, status
H001, cross_len_bp_rst.max_heavy_with_rst, HIGH, tc_len_max_bp_rst, A, DONE
H002, fsm ERROR state not hit, MEDIUM, tc_timeout_inject, B, DOING

规则5:收敛停止条件

• 连续多轮新增 case 覆盖率增益很小;

• 剩余空洞均为低风险且有 Waiver 文档;

• 关键断言与关键场景持续稳定。

---

7. 实战范例对比

范例A:只看波形 vs 加断言

做法1(不推荐)

• 波形看起来 valid/ready 大部分正常,手动抽查几段。

问题:

• 偶发 1 拍握手抖动难发现,回归中随机才触发。

做法2(推荐)

• 增加“valid 保持数据稳定”“握手成功才计数”的断言。

效果:

• 违规立刻报错,定位点精准,回归可重复验证。

伪代码对比:

// 不优做法:只看波形,不自动检查
run_test()
open_waveform_and_manual_check()

// 优化做法:自动断言守护
run_test()
assert_handshake_rules()
if assertion_failed: auto_fail_test()

范例B:只跑随机测试 vs 建功能覆盖模型

做法1(不推荐)

• 大量 random case,但不统计关键场景是否覆盖。

问题:

• 跑很久也可能没覆盖到“最大突发+回压+复位插入”。

做法2(推荐)

• 建 covergroup,设置长度/流控/复位事件交叉覆盖。

效果:

• 明确知道“哪些场景没测到”,补测方向清晰。

范例C:覆盖率100%但仍有 bug

原因常见于:

• 覆盖点设计太粗,没覆盖关键交叉。

• 断言缺失,非法时序未被捕获。

改进:

• 补“协议时序断言 + 交叉覆盖”,而不是只追百分比数字。

---

8. 在 FPGA 项目中落地的推荐步骤

阶段1:最小可用验证框架

• 搭建 testbench

• 增加基础断言(复位、握手)

• 打通冒烟测试

阶段2:覆盖模型建立

• 从规格提取覆盖维度

• 建功能覆盖点

• 加关键交叉覆盖

阶段3:回归与收敛

• 每天跑回归

• 输出断言报告 + 覆盖率报告

• 对未覆盖项补测试

阶段4:签核前检查

• 断言无未处理失败

• 覆盖率达成项目阈值

• 对豁免项有文档说明(为什么可接受)

---

9. 常见误区与规避

1. 误区:断言越多越好

规避:优先关键路径和高风险规则,避免噪声断言。

2. 误区:覆盖率越高越安全

规避:看覆盖“质量”,尤其是交叉覆盖和异常场景。

3. 误区:断言失败先屏蔽

规避:先定位根因,屏蔽必须有严格豁免流程。

4. 误区:只在末期做覆盖率

规避:从项目初期就建立覆盖模型,边开发边收敛。

5. 误区:只验证功能,不验证协议时序

规避:接口协议断言必须是标配。

---

10. 初学者可直接照做的实践清单

第一周

• 完成 testbench 基础框架

• 增加 5 条核心断言(复位、握手、FIFO 空满)

第二周

• 建立基础功能覆盖点(长度、类型、状态)

• 跑回归并生成第一版覆盖报告

第三周

• 增加 2~3 个关键交叉覆盖

• 补定向测试覆盖空洞

第四周

• 输出“断言清单 + 覆盖率清单 + 豁免清单”

• 固化为团队模板

配套伪代码(周度执行脚本)

week1:
   add_assertions([reset, handshake, fifo])
   run_smoke()

week2:
   build_cover_model()
   run_regression()
   dump_cov_report()

week3:
   holes = analyze_cov_holes()
   add_directed_tests(holes)
   rerun_regression()

week4:
   freeze_assert_list()
   freeze_cov_list()
   freeze_waiver_list()

---

11. 建议的断言与覆盖率清单模板(文档化)

建议你在项目里维护 3 张表:

1. Assertion List

• 断言名称

• 检查规则

• 所属模块

• 失败处理策略

2. Coverage List

• 覆盖点名称

• 对应规格条目

• 当前覆盖率

• 未覆盖原因

3. Waiver List(豁免清单)

• 豁免项

• 原因

• 风险评估

• 审核人

---

12. 一页总结

在 FPGA 仿真中:

• 断言解决“规则是否被违反”;

• 覆盖率解决“场景是否被测到”;

• 两者结合才能建立可信的验证闭环。

真正高质量的仿真不是“跑过了”,而是:

1. 关键规则有断言兜底;

2. 关键场景有覆盖率证明;

3. 失败与空洞都可追踪、可解释、可收敛。

---

附录:简化版实施路线(可贴在项目 README)

1. 先加关键断言:复位、握手、FIFO、状态机。

2. 再建覆盖模型:长度/流控/异常三大维度。

3. 每次回归同时看:断言失败 + 覆盖空洞。

4. 用补测试而不是“调阈值”去提升覆盖质量。

> 说明:不同仿真器与流程(VCS/Questa/Xcelium)对覆盖率开关和报告命令略有差异,请结合你的工具链脚本配置。