☰
数字IC验证必备:LEC与Formal Verification调试实战指南
2026/10/4 5:27:41 网站建设 项目流程

做数字IC验证的都知道,LEC(Logic Equivalence Checking,逻辑等效性检查)和Formal Verification(形式验证)这两样东西,跑通流程不难,真正磨人的是debug。很多时候,工具给的反馈就一句话——“not equivalent”或者“prove failed”,但这句话背后可能是RTL写错、网表约束漏配、时钟处理不一致,甚至纯粹是工具配置没弄对。我在这个领域泡了十几年,可以负责任地说:LEC/Formal debug的本质,不是“看懂工具报错”,而是“建立一套可复现、可收敛的排查方法”,把无数种可能快速归因到唯一根因。

这篇内容算是LEC/FormAL系列的第III部分,前两部分把原理和流程讲透了,这篇专门聊debug。不管你是刚接触形式化验证的验证工程师,还是被领导临时抓去“跑一下LEC”的数字后端开发,又或者是在Formal验证上反复被counterexample折磨的资深选手,这篇文章里都有可以直接抄作业的方法论。我会把常见错误分类、LEC debug的实操路径、Formal debug的波形分析方法、以及我这些年踩过的坑,一整套都整理出来,保证比读工具自带手册有用得多。

1. 为什么要单开一篇讲debug:LEC/FormAL问题定位的本质

1.1 两者面向的问题不同:等价性对比 vs 属性证明

不少刚入行的同事会把LEC和Formal当成同一个东西,甚至会在工具里来回切,遇到fail就慌了。实际上,这两个验证手段的debug思路差异很大,必须先分清楚自己在跟什么问题打交道。

LEC验证的是“两个设计是否等价”。拿Cadence Conformal LEC或者Synopsys Formality来说,输入是RTL和综合后的网表,工具会先把两侧设计映射成一组可对比的关键点(key points),比如寄存器输出、黑盒输出、顶层输出端口,然后比较pair上所有状态下的逻辑是否一致。LEC的debug,本质上是在找“这两个电路在某种场景下为什么不同”,问题的边界很明确,就是两边实现方式的差异。

Formal Verification验证的是“设计是否满足某些性质(property)”。常见工具像JasperGold、VC Formal、Questa Formal,核心做的事是把设计建模成形式化系统,再用求解器去穷举证明property在所有可达状态下都成立。一旦证明不了,工具会返回一个反例(counterexample)——一条具体的时间序列,告诉你设计在哪一刻违反了哪条断言。所以Formal的debug,工作量重心在于“从头走到尾把一个property拆干净”,看它是环境约束没给够,还是RTL本身真的有bug。

LRc和Formal虽然是不同的验证手段,但debug的底层步骤其实是同一套:复现问题、缩小范围、观察内部信号、确认根因、修改后回归。理解了这套底层逻辑,工具换什么版本或者换什么厂家的EDA套件,你都不会慌。

1.2 debug难在定位而不是难在原理

我见过太多人debug进度卡住,根本原因是把精力放在了重新“学工具命令”上,而不是放在“怎么缩小怀疑范围”上。其实无论LEC还是Formal,工具给的信息都是足够的,关键是你有没有一套自己的分析路径。

举一个经验层面的例子:Formal跑一个property不通过,新手第一反应是把波形打开,一条信号一条信号去追。这做法不能说错,但没有效率。反例波形通常是几十拍甚至上千拍,一条一条看信号能把人看瞎。正确做法应该是先把property本身拆开,确认它的前置条件(antecedent)和后置条件(consequent)分别依赖哪些信号,然后重点看“最后一拍”和“翻转时刻”附近的状态。也就是说,你要先让工具告诉你“哪个状态变量在这个时刻决定了成败”,再去看波形验证这个怀疑,而不是一头扎进波形里漫无目的地翻。

LEC的debug也一样。两个design不匹配的时候,report会告诉你哪个key point不匹配、工具跑不出匹配的寄存器对有哪些。但很多人只盯着“不匹配的point”,忘了先检查整体设计环境问题——比如时钟没配对、常量寄存器没有设成constant、某些端口被错误地设成了dont verify。这一类环境层面的问题不解决,后面所有精细分析都是白费功夫。

debug的核心原则就一条:先确认工具的环境假设是否正确,再确认逻辑本身是否有差异,最后才动手改代码。顺序反了,你会在错误的方向上浪费大量时间。

2. 先给错误“分分类”:从fail结果锁定排查方向

2.1 按工具反馈分类:fail、abort、inconclusive、timeout

LEC和Formal工具跑完后,结果不会只有“通过/不通过”两种。我习惯把工具反馈先归类成四类,每一类对应的处理策略完全不同。

第一类:Fail(明确失败)。这是最好处理的一种。LEC里表示两个design在某个或某几个key point上确实不等价;Formal里表示求解器找到了一个反例,证明property不成立。这种结果通常伴随详细的报告,只要按报告定位即可。

第二类:Abort(验证中止)。工具在一个点上因为内部资源限制,比如某个组合锥太大、ALU乘法器结构太复杂、Solver超时等,无法给出确定结论。很多人看到Abort会以为设计有bug,其实不一定,也有可能是这个比较点在结构上差别太大。这时候需要人为干预,比如设置cutpoint、使用simulation-based hint、或者给工具加时间预算。

第三类:Inconclusive(无法确定)。Formality里很常见的“undetermined”,Conformal里也经常出现。表示工具既没能证明等价,也没能证明不等价,某些状态可能是don't care,也可能是工具无法解析。inconclusive往往和X态传播、未初始化寄存器、非确定行为有关。

第四类:Timeout(超时)。证明过程在指定时间内没有跑完。Timeout未必代表错误,更多的是工具算力或约束效率的问题。遇到timeout,我通常第一条建议是先把property拆小,再加assumption,而不是傻傻等工具跑通宵。

为了更直观,把这四种情况整理成一张速查表:

工具反馈含义不一定是bug优先排查方向
Fail明确不匹配/反例找到否逻辑差异、约束遗漏
Abort无法完成证明是组合锥过大、结构复杂度、工具资源
Inconclusive无法确定结果是X态、don't care、未初始化状态
Timeout时间超时是property过大、约束太弱、求解效率

2.2 按设计根因分类:逻辑不匹配、时序处理不当、约束错误

按工具反馈分类是第一步,接下来要按设计根因来分。我在实际debug中见过的问题,几乎都可以归到三个门类下。

第一类:纯逻辑差异。RTL和门级网表在实现上确实不等价。RTL里写的是case语句或算术表达式,综合工具做了优化、资源共享、常数传播后,网表结构和RTL已经面目全非。这种差异通常需要比较点级别的分析才能看出来。

第二类:时序处理差异。LEC比对时,两侧design的寄存器划分不一致,比如RTL端的敏感列表、异步复位写法、时钟门控方式,和网表端插入的clock gating cell对不上。Formal里则表现为property里包含时序延展,或时钟复位序列没被正确建模。这类问题如果环境脚本里没做对应处理,工具会报出大量假fail。

第三类:约束与假设错误。这是最隐蔽、也最坑人的一类。LEC里常见的是把某些关键输入错误地设成了constant,或者把某个寄存器的初始态设成了X;Formal里则是assume写得太强或太弱,太强会漏掉真实错误,太弱会导致反例无效。约束一旦错了,后续看再多波形也是缘木求鱼——因为整个“问题空间”就定义错了。

3. LEC debug实操:从“不匹配”到“定位根因”的五步走

3.1 跑通环境与初始比对:确认工具配置正确

很多人拿到LEC fail报告直接去看fail point,我建议先别急。我遇到过太多次“假fail”了,环境问题不解决,后续debug全是白费。

第一步要做的是回到最基础的setup阶段,逐项确认三个东西。一是顶层模块是否正确指定,二是时钟信号是否完整定义,三是reset信号、blackbox等约束是否设定。用Formality做例子,通常在setup之后用report_clock、report_port等命令看关键信息,用Conformal LEC则用report design data、report clock去检查。

确认完这些,再跑一次matching,看两侧design的寄存器配对情况。如果unmatched points很多,停下来分析原因。寄存器不配对通常是时钟定义不全、异步逻辑处理过当,或者工具没有正确识别某些DFF。匹配阶段就失败的话,硬跑compare不会有任何有意义的结果。

这一阶段的核心目的是把工具的运行环境调整到和真实设计语义一致。环境对了,后面的fail才是“真fail”。

3.2 锁定fail point:组合锥分析和比较点映射

当compare跑完,工具报出某些point不等价,接下来要做的不是直接改RTL,而是先分析fail point所在的组合逻辑锥。以Conformal LEC为例,在GUI里点开fail point,工具能直接画出该点对应的逻辑锥图,显示两侧design逻辑锥的结构差异。我发现一个很好用的习惯:先看fail point输入端的信号列表,然后把两侧的逻辑锥一比,很快就能看出是“结构等价但内部逻辑顺序变了”,还是“连功能都完全对不上”。

Formality里同样可以在GUI里查看schematic,命令行的report_failing_points也支持指定详细pattern。有时工具还会给出“附赠”证明信息:这些input pattern下两侧输出不一样。这个pattern细节很重要——它能帮你快速反推出是哪一种输入组合触发了差异,比如一个加法器进位链中间某一位的carry逻辑不同,在输入组合里通常能看到高位进位被触发的pattern。

拿到这些信息后,再对照RTL代码,手动走一遍相应分支,效率会非常高。比起直接在几万行网表里找差异,看逻辑锥+输入pattern是标准解法。

3.3 利用setup、dont verify、cutpoint等指令缩小范围

修环境不对的时候盲目改代码没有意义,LEC的调试之美在于它支持“假设型验证”——你可以在不修改RTL和网表的情况下,通过指令去验证“如果不是这里,是不是就等价了”。

比如你怀疑某个reg在综合时被优化成常数,但RTL端还保留着,导致两边不等价。这时你可以在RTL端把这个reg设成constant,重新跑compare,如果结果变成等价,就验证了你的推测。Conformal里用add constant,Formality里用set_constant都能做。

又如某个netlist单元缺失库单元,工具找不到功能模型,这个点会一直fail。这时用set_dont_verify将其排除,能让你集中注意力在真正重要的点上。cutpoint也是我从后端同事那里学来的高级技巧:把深度路径在某一点“切断”,在两侧都建立相同的新key point,可以显著降低等价比对的复杂度。

工具提供的这些指令,不是为了让你“掩盖问题”,而是为了帮你做“假设实验”——把大问题拆成一连串小判断,快速定位根因。这就是工程化的debug:不追求一次搞定,而是让每一步都产生信息量。

3.4 和RTL/网表逐行对照:找到“语义差一截”的具体位置

当工具层面的分析都做完了,最后一步往往是回归原始的代码对照。这个环节没什么捷径,纯靠经验和耐心,但有一个技巧能让你少走弯路——先对照环境配置,再对照功能代码。

我踩过的一个真实例子:网表里某寄存器没有复位,RTL里却有异步复位。LEC报fail,我一开始怀疑综合优化把复位吃了,后来仔细看综合脚本发现,reset信号在SDC里被设成了false path,综合工具理所当然地把它优化掉了。问题压根不在RTL功能,而在约束把异步复位给“误伤”了。这种问题在逻辑锥图里也能看到:网表端的寄存器根本没有reset pin。

所以逐行对照时,第一看信号连接关系,第二看常量,第三看时钟和复位接入方式。如果这三类都没问题,再深入到组合逻辑本身。这样分层的对照法可以避免在错误层面卡住。

3.5 修完以后回归验证:LEC也要有“clean reg”

代码改完后的回归同样重要。我见过有人在debug过程中把RTL改好了,结果网表用的还是旧版,跑出来的结果当然还是错的。更细一点的问题在库模型:某些IP厂家的仿真模型和综合网表行为不完全一致,这也会导致LEC无法收敛。

所以我的习惯是:每次修改后,保留一版干净的回归脚本,一次性把environment setup、match、compare全跑完。并且要在回归日志里记录下当前RTL和网表的版本号,确保比对时两边确实是你要验证的那一版。这个习惯救了我很多次——尤其在项目后期,RTL和网表频繁迭代的时候,能省下大量的“人工确认时间”。

4. Formal debug实操:用counterexample撬开问题真相

4.1 Formal验证环境搭建与常用性质写法

Formal debug的前提是先有一套能跑通的环境。环境的搭建和仿真不太一样,仿真直接加载testbench跑waveform即可,Formal需要做的是三件事:读入设计、设置时钟复位、声明property。

读入设计这一步,记得把约束文件一并读进来,比如在JasperGold里可以这样写基础命令脚本:

read_file -format sverilog rtl/top.sv read_file -format sverilog rtl/mem.sv read_file -format sverilog constr/formal_constr.sv set_top top_module elaborate

时钟和复位处理是Formal环境里最容易出问题的地方。我常用的方法是显式创建时钟,并在约束文件里用assume声明异步复位的行为:

create_clock -name clk -period 10 reset_deassertion -sequence { rst_n } -clock clk

property通常用SVA写,基础格式长这样:

property p_req_ack; @(posedge clk) req |=> ack within 1 to 3; endproperty assert property (p_req_ack);

这里要提醒一点:Formal里除了assert,还有assume和cover。assume是给环境加的约束,比如输入信号不能x、数据总线在复位释放后必须稳定;cover则用来确认某些新功能是不是能被穷举到。三者配合使用才能既保证property有意义,又不至于让求解器去探索一大堆无关状态。

4.2 property fail后的第一手材料:波形和反例

Formal工具在property fail时,会给出一个counterexample(CEX),本质是一段波形。JasperGold里能直接打开trace,也能导出成VCD/FSDB到外部波形工具看。

拿到CEX以后,我不看完整波形,只看三段:复位释放初期、property启动那一刻、和property失败的那一拍。因为绝大多数Formal bug都能归结为这三个时间点上的状态不对。比如property要求req后ack在3拍内到达,那重点就看req拉高后的三拍,ack是否真的没来;再看这三拍里,是数据路径delay太长了,还是assert的使能条件根本没满足。

这里有个新手最容易踩的坑:Formal的反例不同仿真波形,它不代表真实输入sequence,可能是一个极端边角情况——比如地址总线在某一拍同时出现了多个请求并且还有乱序返回。不要轻易把这种corner case当成“时钟设计bug”,很多时候它恰恰是好事,帮你提前发现了仿真测不到的边界漏洞。

4.3 用约束排查和solver信息快速定位

如果property本身没问题,但Formal就是报fail,问题大概率出在约束上。过约束会让property无法证明,过约束会让求解器“偷懒”找不到反例导致property误通过。

判断过约束还是欠约束,有一个简单方法:把设计中的关键变量在property路径上的驱动关系拉出来看看。JasperGold里有不少好用的命令,比如检查哪些信号被assume约束了、覆盖范围如何。把约束打印出来逐条看一遍,往往比看波形更快定位问题。

求解器本身也会提供一些信息,虽然这点很多工程师不在意。比如JasperGold在证明过程中显示某个gate一直保持某值,或者在超时时告诉你“证明尝试了状态空间范围的95%但卡在某个区域”,这些都是线索——如果工具长时间反复证明某个子状态,说明那里可能逻辑存在复杂耦合。这些solver hints结合CEX,会让定位效率高出不少。

5. 高频问题速查:把踩过的坑直接列成清单

5.1 五类高频debug问题与解法对照表

为了便于日常工作,我把这些年遇到的高频问题按“现象—根因—解法”整理成了一张速查表,建议收藏起来,debug卡壳时先过一遍这张表:

高频现象常见根因排查/解决建议
LEC大量寄存器unmatched时钟定义不全/异步逻辑处理不当检查clock setting、reset setting
LEC单个point反复fail综合优化了常量化信号用add constant验证假设
LEC report但GUI不显示logical cone库单元missing或模型不一致检查库文件,set dont verify后重跑
Formal property有反例但仿真是过的约束过弱,包含了仿真未覆盖的输入增强assume,再把property拆细
Formal一直timeoutproperty规模过大增加assume限制;换multi-property证明方式
Formal证明pass但仿真failproperty过约束/环境建模错误逐条检查assume,优先怀疑时钟复位约束
CEX波形和RTL预期大相径庭reset/clock建模错误在工具中显式set reset sequence
工具报inconclusive无法判断X态未建模或未初始化reg定义好初始状态,避免X态扩散

这张表里除了问题本身,更重要的是最后一列的行动模式。你会发现这些解法有个共同点:都是回到环境/约束/假设层面做调整,而不是一上来就改RTL或网表。先让工具环境符合设计语义,再看逻辑差异,永远是最稳的路径。

5.2 长期受用的几个debug习惯

这几条经验和具体命令无关,更多是做事方式层面,但我一直觉得这些反而才是真正区分经验丰富和刚入门的地方。

第一,保留现场。每一次fail/output/log/GUI session统统存好。Formal debug经常要来回比对多个版本,没有现场记录,你会面对一堆“我又忘了上次怎么跑出来的”问题。我一般按日期+工具类型+design名建目录,脚本和脚本输出一并入库。

第二,建立“最小可复现case”。无论LEC还是Formal,遇到复杂大设计跑不动或查不清时,把环境代码精简到一个几百行的小模块,把相关信号接成常量,把状态空间尽量剪小,然后单独跑。这个小case既是复现问题的工具,也是你向同事请教时的“最短路径”。

第三,多用GUI但别依赖GUI。GUI能直观展示逻辑锥、波形,很方便;但凡是重要结论,一定要用命令脚本固化下来。一方面GUI操作很难追溯,另一方面等环境换到服务器上无界面运行,你只能靠脚本。所以从第一天起,就把所有关键步骤写成Tcl脚本,GUI只用来做视觉确认。

第四,排查顺序别搞反:先环境,再约束,最后才看逻辑。我好多次看到同事一头扎进RTL里苦读,最后发现是时钟约束写错。让人崩溃的不是复杂设计,而是方向性错误。每次debug启动前,给自己三秒钟,问一句“环境是否干净,约束是否完整”,再决定往哪走。

第五,和综合工具、仿真工程师多沟通。Formal和LEC的debug常常牵扯到综合约束、SDC、IP的交付形态,这些信息在工具报告里看不全。跟写综合脚本的同事确认一下retiming、scan insertion是否打开,往往比自己在工具里瞎试一个小时更管用。

我个人在实际操作中的另一个体会是:debug要分“天窗”时间。证明类问题有个特点,盯着屏幕看半天,不如出门走一圈换个思路。有一次Formal反复fail,我下班路上突然想到可能是reset sequence没建模对,第二天一验果然是这样。所以遇到烧脑的case,别死磕,记录好当前状态,离开一下,大脑后台线程往往已经帮你算起来了。冷静、分步、体系化,这就是LEC/FORMAL debug的全部心法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询