形式化验证:用数学证明补上芯片仿真覆盖不到的漏洞
干了十几年芯片验证我一直觉得“芯片验证”和“数学证明”这两个词放在一起特别有味道。很多刚入行的朋友觉得验证就是搭个测试平台、撒一堆随机激励、看覆盖率达到多少流片前达成签核标准就万事大吉。但真正经历过几个项目之后你会发现仿真验证本质上是在“采样”它永远不可能遍历一颗芯片的所有状态。而“数学证明”在验证领域对应的那套方法论——形式化验证Formal Verification走的是完全不同的路它不靠测试激励去撞运气而是用逻辑推导证明一个设计在数学意义上满足它的规格。这篇文章我想把这些年对形式化验证的理解、实践和踩坑经验完整地落下来聊聊它如何补上仿真覆盖不到的漏洞也聊聊什么时候该上它、上了之后怎么避免被反例折腾到怀疑人生。适合所有验证工程师、芯片设计工程师以及正准备入行验证方向的学生参考。1. 芯片验证的困局为什么仿真测不出真相1.1 验证到底在“验证”什么芯片验证这个行当日常干的事情就是回答一个问题我拿到的这个 RTL 设计行为是否符合规格书描述的功能这听起来简单做起来极其庞大。验证工程师要搭 UVM 验证平台写约束随机的激励开发参考模型用 scoreboard 比对结果还要写 functional coverage 去量化“验了多少”。这一整套流程覆盖了功能仿真、时序仿真、低功耗仿真、形式化检查、FPGA 原型验证等等环节。但仿真有个天然的天花板时间是均匀采样状态是有限枚举。一个稍微复杂的模块比如一个支持乱序执行的 CPU 重排序缓冲区或者一颗带着几十个 master 的 NoC 路由器它的状态空间是指数级的。仿真实测的时候哪怕激励生成了几十亿个周期跑了几百个回归用例真正被踩到的状态也仅仅是整个空间里的一小撮。验证团队追求的百分之九十五、九十九的覆盖率本质上是在拿“测过多少”来推估“还剩多少没测”。这个推估在工程上很有效但它不是证明。打破这层天花板的就是标题里说的“数学证明”。形式化验证把设计建模成一个数学对象——通常是有限状态机或者符号迁移系统然后把规格写成逻辑属性最后用求解器去证明“这个状态机在所有合法输入序列下都满足这条属性”。它枚举的不是几条波形而是状态空间本身。只要模型和属性建得正确证明通过就是真的通过不存在“这条路径我没跑到所以没发现”的问题。1.2 穷举的幻觉32 位加法器背后的指数爆炸很多人问仿真跑久一点、约束随机覆盖广一点不就能逼近穷举了吗我拿一个极简单的例子来算一笔账。一个普通的 32 位加法器两个输入各 32 位单看组合逻辑的输入就有 2 的 64 次方种组合这是一个大约 1.84 乘以 10 的 19 次方的天文数字。就算你有一个每秒能跑一亿次加法仿真的平台不吃不喝也得跑大约五千八百多年才能测完所有输入对。这还只是一个加法器放到一颗 SoC 上任何比较严肃的模块状态数都远远超过可穷举的范围。这就是为什么仿真验证始终是一个“不等式”问题。覆盖率报告只能告诉你哪些代码行被翻转了、哪些分支被进过了但“没翻转的代码行”和“翻转了但只在合法路径之外翻转的代码行”才是真正的风险区。形式化验证恰好把这个问题变成了“等式”问题它是严格的全称量化遍历是数学归纳法对无限状态步进的一种有限化处理。它证明的不是某几个用例的行为正确而是整个设计对所有可达到状态的行为正确。1.3 逃逸一个 bug 的真实代价我以前在一家做车规芯片的公司待过车规认证里有一句话让我印象极其深刻每一次流片失败除了几百万上千万人民币的直接损失还有少则三个月多则半年的项目延期以及最要命的客户信任损耗。芯片一旦量产出问题召回和失效分析的隐形成本更高。相比之下在验证阶段多投入资源把形式化验证工具用起来即便是最贵的商业形式化工具 license成本也只是流片失败的一个零头。行业里有一种说法功能 bug 发现得越晚修复成本呈指数上升。RTL 阶段改一行代码可能只要一天到了 netlist 阶段就要重新综合、重新做等价性检查、重新跑回归到了硅片阶段则要改版、重新流片。形式化验证之所以值得认真对待就是因为它能在 RTL 阶段、甚至在架构阶段就把某些“数学意义上不可能满足”的属性揪出来从源头把风险压到最低。这不是说仿真不重要而是说仿真和形式化应该像左右手一样配合使用。2. 数学证明如何把芯片逻辑“算”清楚2.1 把芯片当成一台有限状态机来“推理”形式化验证的第一个核心动作是建模。芯片里的时序逻辑本质上就是一组寄存器和组合逻辑的交互从数学上看就是一个有限状态机。每个 register 的取值组合构成一个状态时钟沿到来时组合逻辑根据当前状态和输入计算出下一状态整个系统就在这个有穷状态空间里游走。形式化的“证明”依赖时序逻辑属性最常见的是安全属性Safety Property和活性属性Liveness Property。安全属性用一句话说就是“坏事情永远不发生”比如“总线不可能同时被两个 master 驱动”“FIFO 满信号拉高时写指针不会再前进”。活性属性则是“好事情终究会发生”比如“每个请求最终都会得到响应”“仲裁器不会让某个请求者永远等不到总线”。前者可以用有界模型检验的思路快速找反例后者往往需要更深入的不动点计算。打个比方你就懂了。仿真验证像在操场上随机抽查学生有没有穿校服你抽查了五千人全合格了但你没法保证剩下那两万人里没人违规。形式化验证把整个操场检查了一遍还顺手用逻辑证明了“按照值日表任何一个学生进入操场前都必须在校门口刷校服条码”所以不需要抽查结论是全局的。这就是“证明”和“测试”的本质区别。2.2 模型检验与 SAT/SMT 求解器在背后干了什么形式化验证的主流引擎是模型检验Model Checking。它把设计转换成一种叫 Kripke 结构的带标记状态迁移系统把规格翻译成计算树逻辑CTL或线性时序逻辑LTL的公式然后通过不动点计算在 BDD二叉决策图或者命题可满足性求解器上验证公式成立与否。具体来说符号模型检验用 BDD 来紧凑表示状态集合这适用于控制逻辑密集、状态数几十亿的模块。而近十几年来SAT/SMT 求解器驱动的有界模型检验BMC越来越主流。BMC 的做法是把属性 P 和设计展开到 k 个时钟周期构造一个 SAT 问题如果求解器返回“可满足”那就意味着找到了一个长度在 k 以内的反例能给出具体波形让工程师去分析如果不可满足说明在 k 步以内没有违反属性。再往上叠加数学归纳法就能把有界结论扩展成真正无界的安全属性证明。你也可以理解成SAT 求解器是那台不停“试算”的机器而归纳法给了它“算到第 k 步能代表所有步”的底气。实际使用商业形式化工具时你不需要自己写 BDD 或者 SAT 求解器但你必须理解引擎的行为逻辑。比如 JasperGold 或 VC Formal 里一个属性迟迟证明不了往深了想往往是抽象不够好、约束写得太弱、或者资源上限设得太保守。这些都是后面要聊的实操问题。2.3 用 SVA 断言把“规格”翻译成机器可证明的语言形式化验证离不开断言。SystemVerilog AssertionSVA是行业中通用的描述属性语言它既能用于仿真平台的动态断言比对也能被形式化工具静态证明。我最早接触 SVA 的时候总觉得它就是用来在仿真里抓时序违例的后来才知道它在形式化里的价值更大因为形式化工具吃的就是这些“属性”。写一条典型的 SVA 安全属性比如“请求信号 req 拉高后三个时钟周期内必须收到响应 ack”property p_req_ack; (posedge clk) disable iff (!rst_n) req | ack within 3; endproperty assert property (p_req_ack);这段断言告诉形式化引擎三个信息时钟沿是什么、复位条件是什么、希望验证的时序行为是什么。引擎就会去枚举所有能让 req 拉高的状态然后检查后续三个周期 ack 是否永远存在。如果有一个路径上 ack 没来引擎会给出一个从 req 拉高开始到 ack 缺失为止的“反例波形”验证工程师顺着波形找 RTL 问题或者找约束问题。形式化契约的思想是这种方式的高级形态把模块的接口行为描述成前置条件和后置条件输入侧断言、输出侧断言这样可以在不跑完整 SoC 仿真的情况下单独验证一个 IP。这也是为什么不少大公司在复杂 IP 上、甚至在跨时钟域CDC边界上大量使用形式化验证因为纯仿真是真测不完所有交错时机的。3. 把形式化验证落进真实芯片项目的实操路径3.1 哪些模块“值得”上形式化验证你得先有个判断不是什么模块都适合上形式化。我的经验是三类东西最适合第一类是控制逻辑密集、状态空间巨大但数据通路简单的模块典型如总线仲裁器、中断控制器、Cache 一致性协议处理单元、电源状态机。这类模块是仿真覆盖率的重灾区因为随机激励很难命中某些罕见的仲裁优先级组合但形式化枚举状态机恰恰有优势。第二类是虽然简单但绝对不允许出错的模块比如复位逻辑、时钟门控使能、DFT 扫描链切换逻辑这些逻辑出问题整颗芯片都白给值得用数学证明把安全属性锁死。第三类是有明确、可形式化接口契约的模块比如带协议握手的总线桥、信用计数的流控模块这类模块规格清晰写断言相对容易。反过来说数据通路极其庞大的模块比如 DSP 的乘加阵列、图形处理器的像素处理流水线形式化引擎处理起来很吃力状态空间被大位宽数据撑爆证明时间会非常难看。这类模块更合适的路径是等价性检查EC加仿真回归EC 只做综合前后或者 ECO 前后的逻辑对比不需要处理大型状态空间效率非常高。3.2 工具选型商业三大家与开源路线市场上主流的商业形式化工具主要是 Cadence JasperGold、Synopsys VC Formal 和 Siemens EDA 的 Questa Formal。这三家各有侧重点JasperGold 在并发属性验证和形式化覆盖率收敛方面口碑很好适合做深度形式化VC Formal 和 Synopsys 的传统仿真流程集成度高对 full flow 用户友好Questa Formal 则在 CDC 检查和协议属性验证上有不少成熟场景。选择的时候别只看品牌要结合你团队现有流程来评估最好在准备上项目的模块上各跑一轮 demo看属性收敛时间和反例可读性。如果预算紧张或者想先学习形式化验证思想开源路线也能跑通。Yosys 配套 SymbiYosys 以及背后的求解器工具链比如基于 SAT 的 Solver是可以处理系统级 Verilog 模块的免费形式化验证方案配合 Z3 做 SMT 求解能解决不少中小粒度模块的属性证明问题。对个人学习和小型 FPGA 项目来说这已经够了。还有一个方向是定理证明器比如 Coq、Isabelle/HOL 和 Lean这不是给 RTL 验证团队日常用的更偏重架构层面的形式化建模比如验证一个 Cache 一致性协议算法本身但在芯片公司里通常是专门的验证研究团队在维护。我个人给团队的配置建议是大项目买一到两套商业工具跑关键模块同时搭一条开源形式化流程做轻量级快速筛查版权、成本、覆盖三方面都相对均衡。3.3 从约束到证明的一套完整落地方案纯讲空话没用我给你一套我在项目里用过的标准流程。第一步定边界。把待验证模块从 SoC 里切出来输入信号哪些是自由变量、哪些要固定成常量时钟和复位怎么处理这一步叫环境建模做得好能极大地降低状态空间复杂度。第二步写属性。从规格书里挑 10 到 20 条最关键的安全属性和活性属性不要一上来就写几百条先跑通再扩展。第三步写约束。约束和属性同样重要要把合法输入的集合描述清楚否则形式化引擎会“帮”你找到一堆不现实的反例那就是典型的环境太宽导致的无效反例。第四步跑证明。先跑 BMC 看有限步数内有没有反例没有的话再启用无界证明引擎同时设定合理的资源上限比如单属性跑两个小时跑不完就标记存疑去优化约束和抽象。关于引擎参数几个最常调的最大展开步数k 上限、内存上限、超时时间。我们常用 BMC 从 k 等于 20 起步逐步加大到 80 到 100对于复杂属性把引擎切到归纳模式并开启“推断不变式”功能很多时候能靠自动推导出的辅助不变式把证明收敛下来。抽象策略上用 cut-point 抽象把某些内部数据通路替换成自由符号值可以把证明资源集中在控制逻辑上。最后别忘了建立形式化验证报告。每条属性都要有明确状态Proven证明通过、Falsified找到反例、Inconclusive资源穷尽但没结论、Vacuous属性自身恒真但没有实际约束力。后面两种状态不能算作签核依据必须后续跟进。4. 踩坑实录形式化验证工程师的真实体验4.1 高频问题与排查速查表这几年在形式化验证上踩过的坑总结成一张高频问题速查表应该能帮你省下很多时间。现象可能原因排查与解决思路属性报反例但仿真完全正常约束环境建宽了引擎自由变量组合出了不可能状态增加输入约束明确协议合法序列在反例窗口检查是否有信号组合违反环境约束证明超时资源全部耗尽抽象粒度太大状态空间还是爆炸增加 cut-point 抽象把数据通路切开把属性拆成更细的小属性逐条证明属性显示 Vacuous空洞属性前置条件太强导致引擎根本找不到激活路径检查 disable iff 条件和前置表达式用 assume 约束激活条件SVA 断言在仿真里有效形式化工具直接说不可解断言中有外部函数调用、动态行为或不可综合的构造重写为纯时序逻辑表达去掉 charge 到类 C 函数和层次引用跨时钟域属性大量误报CDC 信号没有做同步处理建模在约束中屏蔽跨时钟域异步抖动窗口或者单独用 CDC 专用检查流程活性属性永远证明不了缺少公平性约束引擎总在循环路径里绕增加公平性约束fairness强制引擎排除无限重复某个不公平状态的路径等价性检查不通过综合脚本插入了 DFT 逻辑或时钟门控导致逻辑锥不匹配核对两类设计之间是否有关键同步元件命名不匹配暂停 DFT 插入后复跑看这张表你会发现一个规律绝大多数“形式化工具乱报”的场面最后都指向环境约束没写对而不是工具本身傻了。形式化引擎的本质是一个极其较真的数学机器它不懂“这个输入序列在真实系统里不会出现”你不告诉它它就当作合法输入给你找出反例。所以写好约束文件永远是形式化验证里投入产出比最高的事。4.2 团队协作与工作流里的三个经验教训第一个经验是别把形式化验证当成一个人单干的活。最好的模式是设计工程师、验证工程师一起审属性定义。设计工程师最清楚哪些状态时不可能的验证工程师最清楚怎么把数学逻辑翻译成属性语言。我们曾经有一个属性设计看了就说这条件永远不会成立结果反例还真被找出来了后来发现是设计自己漏了一条复位路径。这种对话越早越频繁整个模块的收敛速度越快。第二个经验是形式化验证不是仿真验证的替代品你对它的定位会影响流程效率。仿真负责大规模场景和软件层面的行为正确性检验形式化负责把关键断言从“采样验证”升级成“数学证明”两者并行不悖。我见过一个团队把整个 SoC 的顶层互联属性全都丢给形式化引擎去证明结果三天出不了结果这不是形式化不行是用法错了。顶层互联适合在仿真环境里做事务级验证形式化应该下沉到关键模块和关键边界接口上。第三个经验是形式化属性的维护成本。属性本身就是一种极其精确的规格记录它是一个活的文档。随着设计迭代每隔一段时间重跑全量形式化回归非常有用很多 ECO 引入的边角问题都靠这种回归提前暴露。我们项目里把形式化回归做成 nightly 任务每天提交代码后自动跑一遍全量属性和 BMC第二天早上查反例报告长期下来能积累一张非常宝贵的属性库。4.3 开源流程也能救急一套最小可用的查错案例商业工具不是每个项目都买得起尤其创业团队和个人开发者可能完全买不起。开源的 Yosys SymbiYosys 流程完全可以作为入门和轻量级查错的主力。我举一个最小例子一个同步 FIFO 模块你要验证“读指针永远不会超过写指针”。用 SymbiYosys 只需要把 RTL 和一个顶层 wrapper 放进去wrapper 里用 SVA 定义这条属性然后在 sby 文件里指定模式为 prove求解器选 z3就能跑起来。跑出来如果有反例它会输出一个 VCD 波形文件你用 GTKWave 打开看具体是哪个时序阶段出的问题。这个流程对我是有纪念意义的。有一次做 FPGA 里一个通信接口模块仿真怎么跑都稳定但上板之后极低概率出现丢包。传统 debug 很难复现我把接口的握手协议写成属性丢给 SymbiYosys 跑了一个小时反例波形清清楚楚地指出了一个跨模块的 signal 竞态。那次之后我毫不犹豫地把开源形式化流程列进了每一个项目的必做清单哪怕只是跑最核心的十条属性都发现相当于给设计买了一份额外的保险。最后说几句实在话形式化验证不是银弹它不会把仿真工程师的工作取代掉但它确实是一套应该被认真理解的数学武器。芯片验证与数学证明组合在一起意味着你最高可以做到“设计正确性由逻辑定义来保证”而不再只是借由仿真边界来推断。我对团队的要求是关键模块必须有一组经过形式化证明的断言新进工程师半年内必须能独立写出可被证明的 SVA 属性。这条路走下来之后你会慢慢发现签核的信心变得不太一样因为你不再只是相信覆盖率报告而是知道有一批属性已经从数学上被锁死了。这种踏实感是用多少次全芯片回归都换不来的。