拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

形式化验证:用数学证明补上芯片仿真覆盖不到的漏洞

形式化验证:用数学证明补上芯片仿真覆盖不到的漏洞

干了十几年芯片验证,我一直觉得“芯片验证”和“数学证明”这两个词放在一起特别有味道。很多刚入行的朋友觉得验证就是搭个测试平台、撒一堆随机激励、看覆盖率达到多少,流片前达成签核标准就万事大吉。但真正经历过几个项目之后你会发现,仿真验证本质上是在“采样”,它永远不可能遍历一颗芯片的所有状态。而“数学证明”在验证领域对应的那套方法论——形式化验证(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 Assertion(SVA)是行业中通用的描述属性语言,它既能用于仿真平台的动态断言比对,也能被形式化工具静态证明。我最早接触 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 属性。这条路走下来之后,你会慢慢发现签核的信心变得不太一样,因为你不再只是相信覆盖率报告,而是知道有一批属性已经从数学上被锁死了。这种踏实感,是用多少次全芯片回归都换不来的。

返回列表