形式验证:先建模环境,再证明设计
形式验证(Formal Verification)把 RTL、环境约束与属性转成逻辑问题,尝试在可达状态空间中证明规则恒成立,或给出一条反例路径。它和随机仿真互补:仿真适合运行真实软件场景,形式验证适合检查难以覆盖的极端状态、异常组合和控制路径。
本文按一次功能形式验证(FPV)的工作顺序说明关键要点。不同 EDA 工具的命令与报告名称会变化,但建模原则相同。
适合先用形式验证的问题
- 握手协议、FIFO、仲裁、状态机和权限控制等控制密集逻辑;
- reset、enable、异常响应、仲裁优先级等极端条件;
- 需要证明“不可能发生”的安全属性,例如不会溢出、不会两个 grant 同时有效;
- 修改 RTL 之后希望确认关键行为没有回退的场景;
- 仿真难以覆盖的深层组合、死锁或不可达状态。
对大规模数据通路、长软件驱动流程或模拟模型,先进行模块切分、抽象和目标排序通常更有效。
正确的起点:定义验证宇宙
形式引擎默认会探索所有未受约束的输入组合。若不定义时钟、复位和环境协议,得到的反例可能只是“现实中不会出现的输入”;约束太强又会把真实 bug 排除掉。
因此一开始应回答四个问题:
- 哪些时钟存在,彼此有什么关系?
- 复位的极性、持续时间与释放顺序是什么?
- 哪些输入由外部环境控制,它们必须遵守什么接口约定?
- 哪些状态在上电后可达,哪些是刻意未初始化的?
环境约束应描述外部真实能保证的最小事实。例如,AXI master 发送地址后应保持地址与 VALID 直到握手;这可以作为 slave 验证的环境假设。相反,“master 永远不会发送某个困难组合”通常不是合理约束。
一条可重复的 FPV 工作流
1. 建立最小可运行环境
先加载最小 DUT、必要的接口 wrapper 和断言文件,定义时钟与复位,再跑一个小深度检查。这个阶段的目标不是覆盖全部功能,而是确认顶层、宏、时钟和 reset 处理规则没有错。
伪代码示意:
# 命令名称以实际工具版本为准
read_design -top traffic -f filelist.f
read_sva ../sva/traffic.sva
create_clock clk -period 10
create_reset rst_n -sense low
set_app_mode FPV
check_properties
脚本中的顶层名、文件列表和路径应来自可复现的构建配置,而不是开发机上的绝对路径。真实项目还需要把宏定义、库文件和 black-box 策略纳入版本管理。
2. 先证明小而硬的属性
优先检查低耦合、高价值的规则:状态机编码、FIFO 空/满状态切换、grant one-hot、请求应答上界、寄存器复位值。小属性的反例更短,也更容易区分设计 bug、断言错误和环境缺失。
assert property (@(posedge clk) disable iff (!rst_n)
$onehot0(grant));
assert property (@(posedge clk) disable iff (!rst_n)
pop && empty |-> !pop_accept);
3. 分析每一条失败
失败并不等同于 RTL 有 bug。通常有三种可能:
| 现象 | 常见原因 | 下一步 |
|---|---|---|
| 反例违反真实接口协议 | 环境约束不足 | 补上有依据的 assume,并记录依据 |
| 反例符合接口协议但违背预期 | RTL 设计缺陷 | 修复 RTL,保留断言作为回归保护 |
| 规则本身没有表达真实意图 | 属性过强、时序偏移或 reset 处理错误 | 重写属性并增加 cover 验证 |
应先从反例的第一个分叉点观察输入、状态和时钟域,而不是只盯住最终失败周期。
4. 处理超时与难证明目标
证明超时不表示属性成立或失败,只表示当前引擎与资源尚未得出结论。常用处理顺序是:
- 检查是否遗漏了不变量、复位约束或合法输入范围;
- 将长时序目标拆成局部引理(lemma);
- 对无关数据通路做抽象,减少状态空间;
- 按时钟域、子模块或功能阶段分解;
- 记录证明深度、timeout 和假设,避免“看似通过”的误判。
任何为求通过而添加的约束,都应能回答“这是系统接口保证的吗?”。
覆盖率的作用
断言证明回答“某规则是否总成立”,形式覆盖回答“某状态或路径是否可达”。两者缺一不可。
cover property (@(posedge clk) disable iff (!rst_n)
req ##[1:3] ack ##1 done);
如果 cover 无法命中,可能是目标确实不可达,也可能是约束过强、建模有误或请求前件根本不会发生。对不可达目标应给出设计依据后再排除;不要把未理解的 cover 直接从报告中隐藏。
仿真覆盖与形式覆盖也不应简单相加:前者反映测试执行过的路径,后者反映在约束模型下的可达性。两份结果结合,才能更完整地说明验证完成情况。
让结果可信的几个习惯
- 将断言、约束、黑盒假设和证明配置与 RTL 一起评审、一起提交。
- 把关键 assumption 当作设计接口的一部分写下注释或链接到规格。
- 修改 reset、时钟、CDC 接口或接口定义时,优先回归相关属性。
- 为关键规则补 cover,避免只证明了 vacuous truth(前件从未发生)。
- 给每类超时目标保留分解路径,而不是把 timeout 当作绿色结果。
形式验证的产出不只是“通过”或“失败”,还包括可审计的设计约束:环境允许什么、模块保证什么、哪些状态与条件已经被探索过。