1. 先分清这个“VC Formal”到底是不是你以为的那个“VC”
1.1 名词祛魅:它和 Visual C++、vCenter 不是一回事
“VC Formal 实例”这几个字扔进搜索引擎,出来的结果大概率会让人一头雾水:先是 Microsoft Visual C++ 运行库的修复教程,再是 VMware vCenter 的集群告警,甚至还夹着微信模板消息里miniprogramState = "formal"的跳转配置说明。你会以为要么是工具装坏了,要么是自己搜错了关键词。
其实都没错,只是此 VC 非彼 VC。在数字芯片验证这个圈子里,VC Formal 是 Synopsys 的形式化验证工具,全称里包含“Formal”的意思是它跟纯粹靠跑仿真打激励的验证方式完全不同。它做的是数学层面的证明:给你一段 RTL、一组用 SVA(SystemVerilog Assertions)写的属性,它在所有可达状态空间里搜索,看看有没有违反这条属性的路径。有,就给你一个反例;没有,就告诉你这条属性已经被证明成立。
这种“证明”思维和“跑一万个用例没出错”的思维差着整整一个维度。动态仿真再充分,也只能覆盖你写得出、跑得完的那部分场景;形式化验证虽然不能把整个 SoC 级别设计直接甩给它,但对关键模块的某些核心属性,它给的是完备性保证。这也是为什么验证团队里经常有人说:仿真在找 bug,formal 在断案。
我自己最开始接触 VC Formal 的时候也犯过迷糊,把文档里的 VC 当成 Visual C++,顺手就去查运行库怎么装。直到同事拍我肩膀说,这个工具不吃那套环境,你装的是芯片验证工具链,这才反应过来。所以如果你是因为“VC Formal”这个缩写点进来的,建议先确认一下自己到底找的是哪个领域的工具,别搞混了再往后看。
1.2 什么项目会真正用到 VC Formal
VC Formal 不是用来做整个芯片端到端验证的。像大型 SoC 这种上千万门的规模,直接铺 formal,求解器第一天就会给你脸色看。但它特别适合当中段验证的尖刀部队,常见的使用场景我列在下面:
- 模块级属性验证:比如 FIFO 控制器的溢出、下溢、读空写满时序,状态机的非法状态跳转,仲裁器的公平性,总线桥的读写一致性。
- 寄存器通路验证:验证寄存器的读写属性是否和 spec 一致,复位值是否匹配,硬件和软件视角能不能对上。
- 连接性检查:SOC 集成阶段验证各个模块的端口信号连接是否正确,时钟域交叉以后信号能不能正确采样。
- 数据通路验证:比如浮点运算、压缩加速器里的数据转换逻辑,这类设计行为很规律,特别适合形式化去穷举。
- CDC 结构性检查:做同步结构、握手协议属性验证,这个方向很多团队已经做成 regression 的一部分。
在这些场景里,VC Formal 的优势是能在 RTL 阶段就发现深层次 bug,而不是等验证环境全部搭好、回归跑了几千个用例后才暴雷。功耗、时序、面积这些前端问题它不管,但功能正确性它能管。很多 IP 级验证团队已经把 formality 属性库作为交付项之一,没有通过 formal 证明的模块,sign-off 都不能算完整。
1.3 为什么网上搜出来的结果,看着熟又对不上
再回到搜索结果的事。微信小程序那边有个miniprogramState参数,取值里确实有formal,表示跳转的是正式版小程序;VMware vCenter 经常会弹“主机和 vc 之间的时间已同步”的集群告警;Windows 上修 VC 环境大多指的是 Microsoft Visual C++ Redistributable。这些搜索词里的“VC”和“formal”都是独立的通用词,唯一的共同点是它们碰巧纠缠在了一起。
加上“VC Formal”这个工具本身的用户集中在验证工程师圈子里,普通互联网内容极少。这就导致搜索时看到的结果特别分裂。我写这篇文章,就是想把一个能直接抄作业的“vc formal实例”摆出来,让想上手的人不用再去翻一大堆文档拼凑信息。
2. 我的实例选型:为什么拿一个 FIFO 控制器来说事
2.1 从项目需求里挑一个“状态空间友好”的设计
形式化验证最怕的就是状态爆炸。一个模块的状态空间大小取决于内部寄存器数量、输入端口数量、组合逻辑复杂度。如果一上来就拿一个跨时钟域的千级触发器模块练手,先不说跑不跑得完,光是你就很难判断反例是设计 bug 还是断言写歪了。所以我选了一个非常经典、行为规律、状态空间足够小的同步 FIFO 控制器。
FIFO 控制器在每一个数字芯片里几乎都会出现,读指针、写指针、空满标志、almost_full/almost_empty 这些信号大家都不陌生。它虽然简单,但非常考验时序边界,什么时候能写、什么时候能读、读写同时发生时空满信号怎么变,这些边界条件正好是形式化验证最拿手的地方。用这个例子,能把 formal 的价值讲得很清楚,也方便后面排查和收敛。
如果你在公司里真正负责一个模块的验证,选第一个 formal 用例时也应该照这个思路来:优先选状态逻辑清晰、输入端口不多、行为边界明确的模块。不要贪大,先跑通一条完整链路,建立形式化验证的信心和流程,再逐步扩大范围。
2.2 我给它定义的 RTL 规格和需要证明的性质
这个例子里,我设计了一个参数可调的同步 FIFO,深度 16,位宽 32。核心逻辑分三部分:写指针递增、读指针递增、根据指针差值和读写事件产生空满标志。为了贴近真实设计,我额外加了两件事:一是在写满之后屏蔽写使能,二是读写同时使能且 FIFO 已经为满的时候屏蔽读使能,避免指针产生非预期跳变。
代码可以精简成下面的样子,完整的可综合代码比这个长,但关键行为全在这里:
module fifo_ctrl #( parameter DEPTH = 16, parameter ADDR_WIDTH = 4 )( input logic clk, input logic rst_n, input logic wr_en, input logic rd_en, input logic [31:0] wr_data, output logic [31:0] rd_data, output logic w_full, output logic w_empty, output logic w_almost_full ); localparam ALMOST_FULL_TH = DEPTH - 4; logic [ADDR_WIDTH:0] usedw; // 额外位用来存深度 logic [ADDR_WIDTH-1:0] wr_ptr, rd_ptr; assign w_full = (usedw == DEPTH); assign w_empty = (usedw == 0); assign w_almost_full = (usedw >= ALMOST_FULL_TH); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= '0; rd_ptr <= '0; usedw <= '0; end else begin case ({wr_en && !w_full, rd_en && !w_empty}) 2'b10: usedw <= usedw + 1; 2'b01: usedw <= usedw - 1; default: usedw <= usedw; endcase if (wr_en && !w_full) wr_ptr <= wr_ptr + 1; if (rd_en && !w_empty) rd_ptr <= rd_ptr + 1; end end endmodule这里usedw用 5 bit 存深度计数,比较巧妙的一点是,表现完“同时读写”时深度不变,表现完“只写”时深度加一,条件里提前判断空满,就不会出现负值或者超过深度的情况。真正要证明的性质不是这条实现代码本身,而是它的对外行为是否符合规范。
2.3 动态仿真与 formal 的分工,别把一个工具当万金油
有人可能会问:这个 FIFO 这么简单,仿真跑个几百个用例不也能查出来吗?为什么非得用 formal?
道理在于覆盖完备性。仿真里的每一笔激励都是你主观构造的,哪怕你写了随机约束,也只能保证你约束到的场景被访问过。但 “FIFO 在写满那一拍如果同时来了读使能,写指针到底会不会推进” 这种边界,随机跑一圈不可能保证每个时序组合都被覆盖。VC Formal 的做法不一样,它把所有可达状态都纳入求解范围,只要这条属性在某一拍被违反,它就能顺着状态转移把反例找出来。这是一种确定性的穷举,不是概率上的采样。
所以在项目里的正确分工是:formal 用来证明那些可以被严格定义的、数量适中的关键属性;动态仿真用来做大量交互场景的长时间回归和上层数据一致性检查。formal 不替代仿真,仿真也别硬撑着去追求边界完备性,两个工具互相配合才是效率最高的验证策略。
3. 可复现的工程骨架:目录、脚本与形式化约束
3.1 一套能一次跑通的目录结构
很多人第一次用 VC Formal,拿到手就急着写断言,结果连编译环境都没理顺,后面每一步都在跟路径和加载顺序搏斗。我习惯先把工程目录划好,再动手写任何代码。
formal_fifo/ ├── rtl/ │ ├── fifo_ctrl.sv │ └── fifo_ram.sv ├── bind/ │ └── fifo_bind.sv ├── props/ │ └── fifo_props.sv ├── scripts/ │ ├── setup.tcl │ ├── properties.tcl │ └── run_fv.sh ├── logs/ └── reports/RTL 单独放、属性文件单独放、绑定文件单独放,这个习惯帮我省掉过很多次“改了属性文件结果把验证源文件搞脏”的烦心事。重点说下bind文件,它是 SystemVerilog 里非常实用的构造,可以把断言模块用小钩子绑到被测设计上,而不需要修改原始 RTL 一行代码。
3.2 编译与建立设计的 Tcl 脚本要点
VC Formal 的命令行启动方式随版本略有差异,但核心流程是稳定的:读文件、elaboration、设置时钟复位、建立设计、跑属性。我用的精简 Tcl 脚本骨架大概长这样:
set TOP formal_fifo read_file -sv -top $TOP {rtl/fifo_ctrl.sv rtl/fifo_ram.sv} read_file -sv {props/fifo_props.sv} read_file -sv {bind/fifo_bind.sv} elaborate -top $TOP # 给求解器指定时钟复位信号 set_clock clk set_reset rst_n -low # 建立设计后读取属性列表 create_property_sets add_property_set -set main_props -file props/fifo_props.sv -bind bind/fifo_bind.sv # 跑证明并输出报告 prove -property_set main_props report_properties -output reports/fifo_props.rpt report_proofs -output reports/fifo_proofs.rpt脚本里每一项都有讲究。-top指定顶层模块,VC Formal 会以这个模块的接口作为边界,未被约束的输入端口在 formal 里会被当成自由信号,可以任意取值,这一步非常关键;set_clock告诉工具哪个是时钟信号,形式化工具的“时间”和仿真的 timeunit 不是一回事,它理解的是时钟沿转移关系;set_reset -low说明复位是低有效,工具会从复位释放后的状态开始展开。
如果你跑的不是同步设计,或者有多个时钟域,可别直接照抄这三行,还要把异步域的处理逻辑单独列出来。我见过不少初学者在跨时钟设计上强行用单一时钟约束,结果求解器给出的反例全是“另一个时钟域的输入端任意翻转”导致的假失败,非常误导。
3.3 时钟复位约束:最容易让形式化求解器“疯掉”的地方
再展开说下时钟复位的处理。VC Formal 不是真的在仿真你的时钟波形,它默认所有内部寄存器都是从某个抽象初始状态开始,在每一个时钟沿上做状态迁移。如果你不告诉它复位信号何时有效,它会默认寄存器初始值可以是任意 0/1 组合,这意味着设计可能从“复位还没释放、内部状态完全未知”的节点开始搜索,很多断言会因为这种任意初值而失败。
标准做法是显式声明复位信号和复位极性,同时给一个“复位释放若干拍后再评估断言”的假设:
assume property (@(posedge clk) disable iff (!rst_n) $rose(rst_n) |-> ##2 1'b1);这种写法的意思是:复位信号从低变高的那一刻开始,至少再等两个时钟周期,我才开始检查属性。CPU 里各种单元在复位释放后都需要一个稳定窗口才能进入正常工作状态,在仿真里你可能靠环境序列控制,但在 formal 里必须用显式假设,否则求解器会在复位释放后的第一个可用周期就去挑战你的属性,产生大量没有工程意义的反例。
4. 属性编写:SVA 断言在 VC Formal 里的正确打开方式
4.1 从仿真断言到形式化断言的思维切换
平时在 testbench 里写断言,背后是“我发这拍激励,下一拍采信号”,这是把一个具体时刻的行为钉死。形式化验证里写断言,面对的是所有可能序列,它不是在检查某一个时刻,而是在整棵状态树上检查这个表达式是否恒真。所以写 formal 属性的时候,必须从“这一拍的这个路径对不对”跳到“任意合法序列下,这个行为模式都成立”。
这个思维切换是最难的。很多人第一次写 formal 断言时,总是忍不住手贱去写具体的wr_en值或者rd_en值,结果把通用属性写成了具体的测试向量,既跑不出反例,也证明不了什么。正确写法是把输入当成自由变量,属性里只描述不变量和时序关系。
4.2 实例中三条关键 SVA 的语义拆解
回到 FIFO 实例,我挑了三条最有代表性的属性。
第一条是“写满时不能再写入”:
property p_no_write_when_full; @(posedge clk) disable iff (!rst_n) w_full |-> !wr_en; endproperty这条的含义是:出现了w_full高电平,则同一拍以及后续拍都不能出现wr_en有效。用|->表示重叠蕴含,也就是前提成立的当前拍就检查结论。它保证了 FIFO 不会在满状态下继续写数据造成覆盖,这是数据完整性里最核心的一条。
第二条是“空状态下读使能会被屏蔽”:
property p_no_read_when_empty; @(posedge clk) disable iff (!rst_n) w_empty |-> !rd_en; endproperty如果设计自身已经把rd_en在空状态下屏蔽掉了,这条属性会立即证明成功。可它依然重要,因为从模块外部接口看,总线主设备可能根本不知道 FIFO 现在是空是满,它发了一个读请求,如果控制逻辑漏了屏蔽,就会把无效数据输出出去。
第三条稍复杂一点,是“almost_full 信号在达到阈值后必须拉高”:
property p_almost_full_assert; @(posedge clk) disable iff (!rst_n) (usedw >= ALMOST_FULL_TH) |-> w_almost_full; endproperty这条看着像句废话,因为 RTL 里assign w_almost_full = (usedw >= ALMOST_FULL_TH),是组合逻辑,所以每次拍都会是同一结果。真正的调试价值在别处:如果设计者把usedw的寄存器更新逻辑和w_almost_full的组合判断写在不同模块,该拉高的信号延迟了一拍才拉高,这条属性就会在边界拍抓到反例。这就是 formal 的价值——它能把组合路径上的延迟和不一致揪出来,而普通仿真往往因为激励序列碰巧没跑到这个边界而放过。
4.3 cover 属性就是你的形式化用例“验收单”
做形式化验证,光证明还不够,你还要回答另一个问题:这条特性到底有没有被刺激到?如果一条断言被证明成立,但设计里从来没有任何合法序列能让该相关信号翻转,那证明成功也可能是因为你根本没抓住这个功能点。
这时候需要 cover 属性。比如我想确认“从空到满的整条增长路径确实可被走到”:
cover property (@(posedge clk) disable iff (!rst_n) w_empty && w_full);这个属性有问题,前面是 empty,后面是 full,中间完全没有时间关系。正确写法应该是“从 empty 到 full 至少需要经历 DEPTH 个写时钟周期”:
cover property (@(posedge clk) disable iff (!rst_n) w_empty ##[1:$] w_full);这里##[1:$]表示在将来的任意一个时钟周期,FIFO 能够从空走到满。如果这条 cover 属性查不到 witness,只有两个原因:要么写满的条件永远不可能满足,要么设计里存在卡死状态。不管哪种,都值得你回头查设计,而不是自我安慰说“证明全过了就行”。formal 的证明结果和覆盖报告是一起看的,拿到报告时会先扫一眼证明列表,然后再扫覆盖列表,两边都对得上,才算这个模块真的验证干净了。
4.4 不要让求解器做“不可能完成”的运算
有一类坑特别隐蔽:属性本身没写错,但由于表达式过于复杂,导致求解器一直跑不出结果。最典型的是把内部状态变量深度引用到断言里,比如把两个内部 FIFO 的深度差值嵌进一个属性里,还会同时描述跨多拍的复杂行为。VC Formal 的求解器虽然聪明,但它不是神,任何形式化工具的算力都有上限。
我的经验是保持属性足够“原子”。一条属性只描述一个行为模式,不要把一个复杂的时序流程用超长前缀和超长后缀塞在一起。如果属性太长,先拆成若干个子属性,每个子属性覆盖流程里的一个关键闭环。这不仅让求解器好过,排查反例时也更清楚是哪一步出了问题。
5. 反例排查:从 Falsified 到修设计或修约束的完整链路
5.1 拿到反例先做三件事
VC Formal 跑完后,报告里如果一个属性显示Falsified,你手里会得到一个反例波形。这个波形是求解器从某个初始状态开始、逐步推进到违法状态的一条路径。别急着去改 RTL,先按顺序做三件事。
第一件事,打开反例波形,看起点状态是什么,确认起点是否在合法初始状态空间内。很多反例是从“寄存器初值为全 0 以外的奇怪组合”开始的。这种反例需要对照你的 reset 假定,看清它是不是在复位释放后立刻发生的。
第二件事,看反例路径上输入信号的变化是否合理。VC Formal 把所有外部输入都当作自由变量,它会故意挑那些“现实中接受不到”的输入组合来尝试突破约束。如果你的 mock 模块或者上层接口逻辑在反向 Y 处给了特定约束,VC Formal 可能没识别你设计者心里的约束,因此给出了一个“理论上合法、实测里非法”的序列。
第三件事,把反例波形导入现有的仿真环境里回放一遍。VC Formal 能以fsdb或者vpd格式导出波形,用波形对比工具打开,看看同样的输入序列跑动态仿真时是不是真的会触发同样的失败。这一步就是传说中的“形式化与仿真互相佐证”,能过滤掉一大半假反例。
5.2 一次 almost_full 断言失败的真实排查过程
我实际跑这个 FIFO 例子时,就故意在usedw更新逻辑里埋了一个延迟两拍的错误,模拟设计者写出来的 FSM 在接近满阈值时没有立刻拉高信号的情况。结果p_almost_full_assert果然报Falsified。当时反例波形里,usedw在某一拍已经等于 13,而 ALMOST_FULL_TH 是 12,w_almost_full仍然保持低电平,直到两拍之后才拉起来。
排查过程是这样的:先看反例起点,内部寄存器初始状态是复位后的合法状态,排除初始状态问题;再看输入信号,反例里只拉了写使能,没有读使能,输入序列完全合法;最后回放波形,发现问题是出在控制逻辑在usedw跨越阈值时没有直接组合判断,而是把判断结果寄存了。这个案例说明,断言本身没写错,设计确实存在边沿延后行为,和 spec 里“达到阈值后立刻拉高”的要求不符。
修复方式也简单:把w_almost_full改成组合逻辑,或者把寄存两拍改成组合判断。修完重跑同一组属性,一条报 failure,其他全部证明通过。
5.3 收紧形式化求解空间的技巧:assume、cut、abstraction
如果跑了半天工具一直停在Inconclusive或者超时,你又很确信设计功能没问题,那大概率是约束给得太松。VC Formal 领域有一个口头禅:formal 里 80% 的时间不是写断言,而是在跟约束作斗争。约束给得太宽,求解器会探索大量无意义的状态;约束给得太紧,又会把真实的反例路径剪掉。
三种常用手段我按优先级列出来:
- 约束外部输入合法性的
assume语句优先到位。比如写使能信号来自上游 AXI 主设备,你可以假设写请求有效时,写数据必须稳定:assume property (@(posedge clk) wr_en |=> $stable(wr_data));。 - 对无关紧要的内部逻辑做 abstract 或 cutpoint,把状态空间切掉一块。比如一个 128 位的计数器,如果只关心它是否计数到某个阈值,可以把它替换成一个抽象的 “equivalent class” 模型,求解器状态数会指数级下降。
- 把超时属性拆成 BMC 深度限制下的验证。先跑一个较短的 bound,比如 20 拍,确认在这个深度内没有反例;再逐步加大深度。这样做不是最终证明,但能在压力下快速积累信心,也能寻找反例的蛛丝马迹。
这些手段都不是玄学,背后都在做同一件事:丢掉和当前属性无关的状态自由度,让求解器把算力集中在真正需要证明的空间上。
6. VC Formal 环境与运行期常见的那些坑
6.1 编译检查过,一跑就 Fatal 的典型原因
实际项目中,VC Formal 的报错有一大半不是验证逻辑出问题,而是环境或者工程组织出了问题。最常见的几种:
- 路径错乱:Tcl 脚本里用了相对路径,但在别的目录启动工具导致
read_file找不到源文件。我的经验是脚本入口处先cd到工程根目录,所有路径用变量拼出来,别裸写相对路径。 - 文件重复读:同一个模块或者同一个属性文件被
read_file读了两次,工具直接报重定义错误。属性文件之间互相 include 时特别容易踩。 - bind 模块名冲突:bind 的目标模块和属性模块在多个文件里被重复定义,elaboration 失败。给每个 bind 模块起名时加上前缀区分,比如
fifo_bind、fifo_props,不要用通用名bind_module。 - 没指定
-top或者指定错了 top:工具可能把多个顶层并行编译,状态空间立刻翻倍,性能肉眼可见地崩。
还有一种隐蔽情况:工具启动之后,session 保存和恢复目录不是同一个工作目录,导致日志只写到了临时目录。排错时第一件事就是看 log 文件,而不是盯着 console 输出。console 经常只打一层报错摘要,真正的原因分析都在 log 里。
6.2 从“修复vc环境”这个热搜词说开去:环境异味与运行期状态
网上搜“修复vc环境”的人,绝大多数是 Windows 下 Visual C++ 运行库依赖坏了,程序一启动就弹窗。但 VC Formal 主要是跑在 Linux 服务器上的 EDA 工具,它吃的环境依赖不是 VC 运行库,而是操作系统底层的 library 版本、license 服务、文件锁、共享内存这些。
我自己踩过一个大坑是服务器时间漂移。虽然 VC Formal 本身不强制要求跟 license 服务器保持严格同步,但一些版本在验证许可时会对时钟偏差做宽限检查。如果时间偏差太多,启动直接报 license 失败,或者弹一个莫名其妙的内部错误。那次我排查了很久,最后发现是物理机上的 NTP 服务没起来,时间比真实时间慢了十分钟。所以如果工具突然从能用到不能用的症状,我的第一反而不是翻属性文件,而是检查系统时间和 license 进程状态。
环境干净和设计正确一样重要。做一个 formal 工程时,尽量固定工具版本和服务器镜像,不要随手升级 OS 小版本。EDA 工具链对底层环境非常敏感,一个动态库版本的小变化,可能让一个原本 5 分钟收敛的工程突然跑两个小时还出不来结果。
6.3 哪些“VC”词是来捣乱的
顺手再说几个容易被搜索引擎搅进来的词。Windows 下装 Visual C++ Redistributable 修复的是运行库;VMware vCenter 的“主机和 vc 之间的时间已同步”讲的是管理集群时钟;微信模板消息跳转小程序时设置的miniprogramState的formal表示正式版状态。它们和 Synopsys VC Formal 的关系,就像“苹果”既可以是水果也可以是公司名,语境不同,东西完全不一样。
如果你和我一样是数字验证工程师,下次搜“VC Formal”时,看到这些结果直接略过就好。真正的 VC Formal 内容通常藏在 Synopsys 的 SolvNet 文档、验证工程师博客、行业大会上,而不是搜索引擎第一屏的“运行库修复攻略”里。
7. 把形式化用例沉淀成可以复用的资产
7.1 断言文件模块化:从 FIFO 抽出的通用属性
很多团队做 formal 是一次性的:项目结束,断言文件就丢在角落里吃灰。但如果你稍微花点时间把属性整理成模块化结构,下个项目复用起来的价值会成倍放大。拿我这个 FIFO 来说,p_no_write_when_full、p_no_read_when_empty这两条看似老生常谈,却在几乎每个带 FIFO 的模块里都通用。把它写成一个公共断言文件,用参数区分空满阈值和 FIFO 深度,就能直接复用到下一个 FIFO 实例上。
module fifo_common_assertions #( parameter type fifo_t = logic, parameter int FIFO_DEPTH = 16 ) ( input logic clk, input logic rst_n, input logic wr_en, input logic rd_en, input logic w_full, input logic w_empty ); p_no_write_when_full: assert property (@(posedge clk) disable iff (!rst_n) w_full |-> !wr_en); p_no_read_when_empty: assert property (@(posedge clk) disable iff (!rst_n) w_empty |-> !rd_en); endmodule带小程序的另一个经验是给断言起名带上层级序号。直接用p_xxx的命名方式在上一级综合报告里能清楚地看到每个属性属于哪个模块,而不是几十个assert_1、assert_2挤在一起,报告一长就完全没法看。
7.2 批量回归与报告解析
形式化验证同样需要进回归。每次 RTL 有更新,就把所有属性集合重跑一遍,输出结果和上一次做 diff。VC Formal 的报告中,属性状态通常有Proven、Falsified、Inconclusive、Not_Tried几种。我的回归脚本里加了一个简单的解析逻辑:看到Falsified就立刻把 session 锁住,生成反例路径,然后命令行调用发一封信给相关开发者。这样设计提交一进来,如果触发了 regress 失败,相关负责人几秒钟内就能收到通知。
跑批量任务时还经常遇到并发问题。多个 formal 任务同时开,license 资源的冲突首当其冲。我在工程里强制规定每个任务用独立的工作目录和独立的 session 名,不共享临时目录,否则经常出现 session 锁文件互相覆盖的诡异问题。
7.3 从 property 到覆盖率驱动的 formal sign-off
走到最后一步,你会发现 formal 验证的价值不是给你一个“全部证明通过”的绿点,而是给你一张跟 spec 对应的功能覆盖地图。每一条属性代表一个功能行为,每一个 cover 代表一个行为场景,把这些都整理成一张表,对应到验证计划里,就能清楚地告诉项目组:这项功能的正确性是被数学证明覆盖的,那些行为是被动态仿真覆盖的,两条证据链加在一起,才算真正的验证闭环。
我个人比较推荐的方式是,项目开始时先写一份 formal 验证计划表,把 spec 的每一条 critical requirement 列出来,旁边标上对应的断言文件、覆盖点、模式要求。项目进行中不断维护这张表,等流片前的 formal review 会上,这张表比任何口头汇报都有说服力。这也是为什么我坚持认为,formal 用例不是一次性测试代码,它应该是一个模块长期演进的完整“行为档案”。
最后再分享一个小技巧。每次写新断言之前,先问自己一句:如果这条断言被违反了,电路的实际行为会是什么?答得上来,这条断言才有意义;答不上来,说明你对这个模块的行为边界还没想清楚,写出来的断言大概率不是太宽就是太紧。形式化验证逼着你把设计行为想透彻,这可能是它带给工程师最大的额外收获。