做逻辑等价性验证这些年,我最常被问的一句话就是:功能仿真都过了,为什么还要跑 LEC?说实话,早期我自己也觉得 LEC 是“流程里的形式主义”,直到有一次后端反馈说网表里某个 mux 选错了时钟,仿真波形跑了几百万 cycle 都没暴露,LEC 十分钟就把它揪出来了。从那以后,我再也没敢跳过这门“玄学”,也开始认真啃 Conformal LEC 这套工具。这篇东西不是照着用户手册翻译,是我把从入门到能独立负责 block 级 LEC 的完整路径、踩过的坑、以及 Debug 时真正管用的套路整理出来,希望能帮那些正在被 LEC 虐的兄弟们少走点弯路。
不管你是在做 RTL 到门级网表的等价性验证,还是做 ECO 前后的网表比对、时钟树综合后的 formality 复核,Conformal LEC 都是目前工业界用得最广泛的工具之一。它解决的问题一句话就能说清:给定两个设计,一个是功能 golden(通常是 RTL),一个是实现 netlist(通常是综合、CTS、ECO 后的网表),验证它们在所有可能的输入序列下是否功能一致。比仿真快、比仿真全,不用造激励,不用等 regression,跑完就看 Pass 还是 Fail。适合同步数字逻辑的 block 级验证、SoC 集成后的网表验证,以及 ECO 场景下的快速确认。
1. 逻辑等价性验证的核心思路
1.1 为什么要用形式化方式验证等价性
先聊点概念,因为后面所有脚本、Debug 技巧都建立在这个理解上。逻辑等价性验证的本质是数学证明,不是“抽样测试”。仿真是拿有限条激励去跑,验证的是“这些激励下行为一致”;LEC 则是把设计变成布尔函数和状态机,然后证明任意输入下两个设计的行为都一致,相当于把输入空间全部覆盖了。对于组合逻辑来说,理论上是完备的;对于时序逻辑,工具会做状态匹配、重定时分析等处理,在匹配成功的范围内同样是完备证明。
这带来的实际好处特别明显:不需要 testbench、不需要覆盖率、不需要等待仿真时间。一个中型 IP 的 LEC 跑完通常在几分钟到几十分钟量级,比跑完上百万 cycle 的回归仿真快太多。当然它也有局限,比如它只能验证“等价性”,无法验证“功能对不对”。如果你的 RTL 本身就是错的,那 LEC 证明的只是“网表和 RTL 一样错”,所以它不能替代仿真,它是仿真之后的重要补充。
LEC 在流程里承担的角色是“守门员”。综合工具做逻辑优化、DSL 插入、时钟门控、扫描链插入,每一步都可能引入功能变化。DC 或 Genus 输出的网表默认应该和 RTL 等价,但工具也有 bug、约束可能写错、don‘t care 可能处理不当,所以 RTL 到网表这一步必须跑 LEC。后端 ECO 时直接改金属层连线、换单元、调整驱动强度,同样需要 LEC 确认改动没有引入功能差异。可以说,越是后端改动频繁的项目,LEC 的价值越明显。
1.2 Conformal LEC 在整个验证流程中的位置
数字前端流程里,大家习惯把验证分成几个层次:单元级仿真、子系统级验证、芯片级验证,再往后是等价性检查、静态时序分析、物理验证。LEC 的典型插入点是三个:综合后、CTS 后、ECO 后。
综合后跑 LEC,是历史最悠久也最标准的用法。综合工具读入 RTL、约束、库,输出 mapped netlist,理论上功能一致,但实际因为状态机重编码、常量传播、未初始化寄存器处理等原因,经常需要配合 setup 文件做必要的 non-equivalence 排除。CTS 后再跑一次,是为了确认插入时钟树缓冲器、改变时钟 skew 之后,功能上没有意外变化。这时候时钟结构变了,但功能等价性理论上不受影响,跑 LEC 是保险。
ECO 后的 LEC 是重头戏。ECO 往往发生在物理设计后期,前端 RTL 改了几行,但不想重新跑一遍完整综合和布局布线,于是后端基于现有网表做局部修改。这种手工或脚本化的网表修改最危险,因为改的时候可能只关注了时序,忽视了功能。LEC 在这种场景下几乎是唯一能快速证明功能不变的途径。你 compare 的是 ECO 后网表和一个新的、经过综合的 golden 网表,或者直接拿 RTL 做 golden,都能验证。
从团队协作角度看,LEC 的 setup 文件往往比脚本本身更值钱。它包含了对设计所有例外情况的声明:哪些点允许不匹配、哪些寄存器需要映射、哪些常量可以忽略。这套文件沉淀下来,就是团队对设计“哪些地方可以变、哪些地方不能变”的共识。后面我会详细讲 setup 文件怎么写才不容易踩坑。
2. 环境准备与设计读入
2.1 工具启动方式和常用命令交互
Conformal LEC 的启动方式很传统,就是命令行。装好工具、配好 license 后,在 shell 里输入lec或者conformal_lec就能进入交互式环境。交互模式下你可以逐条敲命令,也可以直接执行脚本文件。实际项目里没人会一条条敲命令,基本都是写 dofile(也叫 script 文件),用dofile xxx.lec批量执行。
刚上手的人最容易忽略的一件事是:先确认工具版本、库文件版本和项目其他流程用到的版本是否一致。不同版本的 LEC 在匹配引擎和约束处理上有差异,同一个 setup 文件在版本间迁移后,结果可能不一样。我见过一次项目里综合用 Genus 20.x,LEC 却用 17.x,结果原本 pass 的 block 突然报出几千个非等价点,排查了半天才意识到是工具版本差异导致的内建类型处理不同。
读入设计前,还需要把库准备好。这里说的库不只是 liberty,还有 LEC 需要的lvc库、dofile里引用的db路径。Conformal 对工艺库的读取通常通过read design命令指定,也可以使用search_path变量统一管理。强烈建议在脚本开头就设置好search_path和library,避免每个 block 都重复填一堆路径。
2.2 参考设计与实现设计读入的正确姿势
LEC 里有两个核心容器:一个是 golden 设计,也就是参考设计,通常是 RTL 综合前的源码或者旧的 netlist;另一个是 revised 设计,也就是实现设计,通常是综合后的门级网表、ECO 后的网表。命名上不同版本可能叫 golden/revised,也可能是 reference/implementation,但含义一致。
读入 RTL 作为 golden 时,关键点是read design -verilog -golden -root xxx这些参数要写对,特别是-root指定顶层模块,工具需要知道验证的边界。有些人喜欢把整个 SoC 读进去,然后靠 setup 文件里的add compare或change design来限定比较范围,但这样会让匹配时间变长、内存占用变大,没必要。正确做法是 block 级验证就只读 block 相关的 RTL 和库,设好黑盒,把边界条件交给 LEC 处理。
读入门级网表作为 revised 时,要注意网表里可能包含dont_touch单元、Tie High/Tie Low 单元、以及 DFT 相关单元。Tie 单元会在 LEC 的常量传播阶段自动处理,但有些特殊库单元需要额外的 map 文件。如果网表里有很多未映射的*-__之类内部命名,运行前最好用change design或set mapping先做名称规范化,不然匹配阶段会比较吃力。
脚本里还建议显式做一次set root操作,把当前设计的顶层设清楚。做完read design和read netlist后,用report design info看一眼读入状态,确认没有严重 warning,比如端口不匹配、缺少约束等。这些看起来琐碎,但能省掉后面 Debug 时的大量迷惑。
2.3 黑盒与常量约束的声明方法
实际设计中总有些模块不适合放进 LEC 做全功能比较。典型的有三类:模拟宏、PLL、高速 SerDes PHY,这些混合信号模块没有数字等价模型;SRAM、ROM 等 memory 编译器生成的模块,网表和 RTL 可能在行为级定义上差异很大;还有第三方 IP,只有 behavioral 模型或加密网表,没法直接读入。
这时候要把它们设成黑盒。Conformal 里通常用add black box或set black box命令,指定模块名后工具会把该模块当作不可展开的单元处理,只比较边界行为。设黑盒不影响其余逻辑的比较,但在最终报告里黑盒内部的非等价点不会报出来。如果你希望把某个子模块也纳入验证,就不能设成黑盒,得把它设成map点或者直接展开。
常量约束的典型工具是add constant和add pin constraint。比如某个测试模式下才拉高的信号,在正常功能模式下应该永远为 0,就可以把它设成常量 0,这样工具就不会因为不同路径上的 mux 选择而产生假 fail。设置常量必须有依据,不能为了把 LEC 跑 pass 而乱设。否则就是把 bug 故意掩盖了。我见过有人为了让 block 通过 LEC,把一组比较器输出全部设成常量,结果 ECO 后真出问题,查了一个礼拜才找到原因。
提示:黑盒和常量约束都属于“例外声明”,需要写注释说明原因,并尽可能把审查者需要的信息都放在 setup 文件里。一次规范的例外声明比跑 pass 更值得保存。
3. 核心流程拆解与匹配原理
3.1 从 Elaboration 到 Mapping 的关键阶段
Conformal LEC 的运行流程可以拆成四个大阶段:Elaboration、Setup、Mapping、Compare。不把这四个阶段理清楚,遇到问题会连日志都不知道去哪看。
Elaboration 阶段,工具读入 RTL 和网表,把它们展开成内部统一的数据结构。RTL 里的 always 块、assign 语句、function 调用,在这里都会变成布尔逻辑表达式;网表里的 cell instance、net 连接,也会被转换成同样的内部表示。这个阶段最常见的报错是语法错误、跨模块引用找不到、库单元缺失。如果 elaboration 没过,后面什么都不用谈。
Setup 阶段做的事是“告诉工具怎么比”。比如声明黑盒、设置常量、指定比较点(compare point)、关闭某些逻辑的比较。一些进阶操作也在这个阶段,比如add compare point、change name、set mapping等。setup 文件的质量几乎决定了 LEC Debug 的工作量。
Mapping 阶段是重点中的重点。工具要把 golden 里的寄存器、输出端口和 revised 里的对应点配对。寄存器映射的方法主要有三种:基于名字匹配、基于逻辑锥分析、基于常量/等价类分析。名字匹配最直观,要求 golden 和 revised 的寄存器名一致或存在可识别的前后缀后缀;逻辑锥分析是在名字不匹配时,通过分析信号逻辑锥的特征来配对;常量类映射则用于特殊情况。
Compare 阶段就是真正做布尔比较。工具把每对匹配点的逻辑锥展开,用 BDD、SAT、或者两者混合引擎来证明等价性。这一阶段最怕的是“匹配点太少导致比较范围过大”,或者“匹配点过多但错误映射导致假 pass”。所以 compare 报告不能只看 Pass 数,还要看 Unmatched、Unverified、Aborted 这些指标。
3.2 寄存器匹配机制和常见映射失败原因
寄存器映射是 LEC 稳定性的灵魂。很多看起来莫名其妙的 Fail,根源都是映射阶段出了问题,而不是逻辑真的不等价。
名字匹配是默认也最简单的策略。RTL 里的reg_a,综合后网表里可能叫reg_a_reg,也可能叫n1234,取决于综合工具是否开启了retime、是否改名。Conformal 支持基于前缀后缀的自动匹配,比如在 setup 里配置set mapping -type name -prefix reg_,工具会自动把 golden 里reg_a对应到 revised 里reg_a_reg。但遇到综合工具做了寄存器重定时(retiming)的模块,名字基本对不上,就得靠逻辑锥分析。
逻辑锥分析是基于功能特征来匹配的,理论上能处理网表中寄存器被重命名的情况。但它的计算复杂度高,而且对扇入扇出结构敏感。如果 golden 和 revised 之间插入了大量缓冲器、反相器对,逻辑锥的形态变化大,匹配率可能下降。这时候可以适当提高set mapping effort等级,但也要接受运行时间变长的代价。
经常导致映射失败的情况还有几类:一是 RTL 里用了reg类型但综合时被优化掉,二是网表里因 DFT 插入产生了额外的 shadow register,三是时钟门控单元使寄存器的时钟不再是统一的理想时钟。这些情况需要你在 setup 里给出额外引导。比如 DFT 寄存器可以设定set design -dft模式,工具会自动忽略扫描相关的额外逻辑。
3.3 Setup 文件编写的三大纪律
写 setup 文件要立几条规矩,是我多年实践下来的血泪经验。
第一条,先能跑通再优化。不要一上来就写几百行的黑盒和例外,先把设计读入、底层映射跑一遍,看默认结果是什么。很多 block 其实默认就能 pass,不需要任何例外。只有在默认跑不过的模块里,才需要逐项检查为什么 fail,针对性写例外。
第二条,例外必须可追溯。每一条add black box、add constant、add compare point后面都要加注释,写清楚是谁在什么时候、基于什么原因加的。项目周期长的话,三个月后你自己都会忘记当初为什么加这条。我见过一个项目里的 setup 文件积累了五六十条例外,没人能说清每条的作用,最后出问题时根本没法定位。后来我们强制规定:例外修改必须走 review,注释必须有 JIRA 单号。
第三条,尽可能让设计自己证明自己。能通过设置change name或set mapping就解决的匹配问题,不要用黑盒和常量去掩盖。比如后端改了一个时钟信号的名称,你可以在 setup 里做端口/网络重命名映射,而不要把这个信号设成 constant。因为 constant 会直接消除该信号后续所有逻辑的验证覆盖,改名只是让工具正确找到对应的点,验证本身没有放松。
下面是一个典型 setup 文件开头的样子:
set search_path “./rtl ./netlist ./lib” set library “typical_1v2_25c.db” read design -verilog -golden -root tb_rf -file ./rtl/tb_rf_top.v read design -verilog -revised -root tb_rf -file ./netlist/tb_rf_top.vg set root tb_rf add black box -module async_fifo add constant -pin async_fifo/scan_mode 0 set mapping -type name add compare point -all这段脚本很短,但完整表达了四件事:定义库和路径、读入 golden/revised、声明黑盒和常量、启动映射比较。实际项目里在此基础上扩展,尽量不要把一堆不相关的命令堆在一起,每个部分用注释分隔,后续维护会轻松很多。
4. 手把手跑一次完整 LEC 流程
4.1 典型的 RTL vs Netlist 比较流程示例
我从一个真实的 block 出发,完整走一遍 LEC 比较流程。假设有一个 128 点 FFT 加速器模块,RTL 写在rtl/fft128_top.v,综合后网表生成在netlist/fft128_top.vg。它的内部模块包括蝶形运算单元、RAM 存储、控制状态机和一些跨时钟域同步器。
第一步,写一个干净的 dofile,先不管任何例外,只做基本读入和比较:
set search_path “./rtl ./netlist ./lib” set library “fast_1v08_125c.db typical_1v2_25c.db slow_1v32_0c.db” read design -verilog -golden -root fft128_top -file ./rtl/fft128_top.v read design -verilog -revised -root fft128_top -file ./netlist/fft128_top.vg set root fft128_top set mapping -type name add compare point -all compare report compare data -class noneqv -compare 100 -file lec_fail.rpt这里我读入了三个库,fast/typical/slow 都放进去,LEC 会自动选择合适的工作条件。-class noneqv表示只报告非等价点,避免输出太多无效信息。
第二步,跑完看结果。如果全部 pass,那恭喜,block 的 RTL 到网表转换是干净的。但多数情况下会有几个 fail 或者 unmatched 点。常见的第一次 fail 原因包括:异步复位被综合成了同步复位、常量输出被优化掉了、未初始化寄存器导致的 don’t care 差异、还有 FT 引脚导致的额外逻辑锥。
第三步,针对 fail 点逐项排查。基本套路是:用report compare data看详细报告,找到 fail 的 compare point;用analyze fail或者report analysis看逻辑锥;然后在 setup 里决定是做映射微调、设例外,还是直接认为这里是真 bug 需要转回前端。这里千万不能为了 pass 而 pass,否则 LEC 就失去意义了。
4.2 ECO 前后 Netlist vs Netlist 的比对要点
ECO 场景下 LEC 的用法和 RTL vs Netlist 略有不同。假设你有两个网表,一个是 ECO 前的原始网表pre_eco.vg,一个是 ECO 后网表post_eco.vg,两者应该在大部分逻辑上一致,但某些模块可能需要功能升级。
这种比对的第一个好处是工具能把绝大多数点直接通过名字映射匹配上,运行速度很快,因为两个网表来源于同一个基准,结构和命名几乎一致。第二个好处是差异点非常聚焦,报出来的 fail 点几乎就是 ECO 实际改动影响的逻辑锥。
这类比对的坑主要在后端手工修改。比如后端在post_eco.vg里直接替换了一个 INV 为 BUF,这种不改功能的单元类型变化,LEC 是可以自动 pass 的,因为它做的是布尔比较,不关注具体 cell 种类。但如果后端调整了时钟树,插入了一堆 CLKBUF,而黄金网表还是旧的时钟结构,那么涉及时钟门的逻辑锥可能报异常。
一个实用的经验是:做 Netlist vs Netlist 比对时,如果 fail 点集中在某些特定逻辑锥,并且这些逻辑锥正好覆盖了 ECO 的改动区域,基本可以认为 ECO 是符合预期的;如果 fail 点出现在完全不相关的区域,那就要高度警惕,很可能是 ECO 过程中出现了意外短路、开路、或者脚本改错了层。我会把所有 unexpected fail 点逐条报给后端,让后端对比 layout 走线和逻辑连接,大多数时候都能发现真实问题。
4.3 如何解读 Pass / Fail / Unmatched / Aborted 报告
LEC 的最终报告里,除了绿色 PASS,还有几个指标必须盯紧。Pass 点占比高不代表 LEC 完全通过,如果 Unmatched 和 Aborted 数量大,结论仍然是“不可信”。
Unmatched 表示 golden 和 revised 中有一些点找不到对应的比较对象。可能是名字映射失败,也可能是真正缺失的点。处理方式首先是看报告里列出的 unmatched 点具体情况,如果两边都有,逐个分析;如果只有 golden 侧有而 revised 侧没有,说明网表里少了某个功能逻辑,这可是严重问题。Aborted 表示工具尝试比较但无法在资源或时间限制内给出确定结论。这种情况在大规模设计或者逻辑锥过长时会出现,通常需要调整策略,比如拆分比较、增加 effort、或者设置 cutpoint。
Compare 报告里还应关注Key Points数量,它代表了真正被比较的逻辑锥数量。如果 key points 数量只有几百个,而设计本身有明显更多的寄存器,那可能是映射覆盖率太低,结论没有参考价值。正确做法是保证大部分寄存器都成为了 compare point,再去看 pass/fail。
查看报告有个小技巧:不只看文本摘要,还要打开 GUI 或者导出到图形界面,看具体的逻辑锥路径。Conformal LEC 提供的 GUI 可以把 fail 点的 cone 以原理图形式展示出来,这对分析“为什么 fail”非常有帮助。很多人只跑命令行,但从没打开过 GUI,其实 GUI 在 Debug 时比文本报告高效得多。
5. Debug 实战:把 Fail 点逐个击破
5.1 从 GUI 和命令行定位不一致路径
拿到 fail 报告后,第一步不是改脚本,而是把 fail 点看明白。我发现很多人习惯直接搜报错关键词,然后开始试各种例外命令,这样效率极低。正确方法是定位到具体的不一致路径。
命令行里最常用的两个命令是report compare data和analyze fail。前者给出每个 compare point 的详细状态,后者会把 fail 点的逻辑锥展开,并标出两个实现之间的差别点。analyze fail的输出有时候很长,毕竟一个输出端口的逻辑锥可能包含几十级逻辑,但关键信息集中在几个位置:两个设计的 primary input 是否一致、中间信号是否存在 known constant 差异、寄存器可否匹配。
GUI 方面,在交互式环境里输入gui_start或者执行start_gui可以打开图形界面。在 GUI 里打开 compare report 后,双击 fail 点就能看到逻辑锥图。这张图会把 golden 和 revised 的路径并排显示,红色表示不一致的地方,黄色表示不匹配的点,蓝色表示常量。视觉上定位差异点非常直观,尤其适合有几十个 fail 点、想快速找共性问题的情况。
定位之后要把问题归类。我在实践中总结过 fail 的六大来源,分别是:综合优化造成的不等价、DFT 逻辑造成的差异、常量传播差异、未初始化寄存器 / don’t care 差异、跨时钟域逻辑的伪 fail、后端物理改动导致的真实差异。分完类再决定方案,比逐点去猜高效得多。
5.2 六个最常见的 Fail 来源和对应解法
第一类,综合优化造成的不等价,常见于综合工具做自动 retiming、register merging、或对常量传播后的电路做化简。这类问题的特征是 fail 点逻辑锥很小,通常只有几个逻辑门。解法的核心是调整 setup 里的set mapping选项、允许工具做retime处理,或者在综合约束里避免过度优化。如果 RTL 本身没问题,大部分这种 fail 都可以通过工具选项解决。
第二类,DFT 逻辑造成的差异。网表里一旦插入 scan chain,就会在每个寄存器旁边增加额外的 mux 和 scan_enable 信号。默认情况下 LEC 应该能自动处理,但有时 scan_enable 的置位逻辑过于复杂,或者 clock gating 和 scan 交互,会引入 fail。解法是给工具声明 DFT 模式,比如set design -dft,或者把 scan_enable 设成常量 0,从而把测试逻辑排除在功能比较之外。
第三类,常量传播差异。综合工具会把if (1’b0) ...这种代码直接优化掉,而 RTL 里可能还有残留逻辑。LEC 在做常量传播后,通常也能 pass。但如果门级网表里额外插入了 tie cell,而 gold RTL 没有对应的 tie cell 概念,可能出现 unmatched。解法是正确配置 tie cell 库信息,让 LEC 识别TIEHI和TIELO单元。
第四类,未初始化寄存器导致的 don’t care 差异。很多 RTL 的寄存器没有复位值,上电后是 X;门级网表里复位后可能被设置成某个固定值,也可能仍然是 X。如果这些寄存器的输出后续参与了逻辑决策,可能导致 fail。解法是使用add don’t care声明,告诉工具哪些点的初始值不影响功能等价性。需要注意的是 don’t care 声明要谨慎,别把真实差异也声明掉了。
第五类,CDR 跨时钟域逻辑的伪 fail。设计中存在跨时钟域信号时,golden 中通常用两级同步器打拍,网表中同步器可能被综合成不同的结构。LEC 默认的时钟分析框架下,这些跨时钟域路径可能产生伪 fail。解法是把这类逻辑设成 false path 或 pseudo multi-cycle path,或者使用add clock domain让工具按 CDC 语义处理。
第六类,后端物理改动导致的真实差异。这类最危险,因为你不能随便用例外去掩盖。定位到之后应当立即通知后端确认网表连接、走线、单元相位等问题。如果确实是 ECO 引入的功能错误,那就需要新的 ECO 修复,再重新跑 LEC 验证。
为了让排查更有条理,我用表格整理了六类问题的一站式参考:
| 问题分类 | 典型特征 | 排查命令 | 常用解法 |
|---|---|---|---|
| 综合优化 | 小锥形 fail,多与 retime 有关 | report mappinganalyze fail | 调整 mapping/retime 选项 |
| DFT 逻辑 | fail 与 scan_enable 相关 | report design -dft | set design -dft或约束常量 |
| 常量传播 | tie cell 相关或常量分支差异 | report constant | 配置 tie cell 库 |
| 未初始化寄存器 | 复位值 X 导致 | report dont care | add don’t care |
| CDC 跨时钟 | 同步器路径 fail | report clock domain | 设 false path / CDC 模式 |
| 后端物理改动 | fail 区域与 ECO 区域重叠 | GUI logic cone | 反馈后端,重新 ECO |
5.3 一次真实 ECO Debug 复盘
我印象很深的一次 ECO Debug,发生在某 AI 加速芯片的验证阶段。前端在 block freeze 后改了几行代码,想在不重新综合的情况下让后端直接做 metal ECO。后端在网表里手动改了大约二十个单元,加了几个 mux,跑时序没问题,然后交给我做 LEC 确认。
第一次比较,报出 17 个 fail 点。我当时没有直接写例外,而是逐个打开逻辑锥看。发现其中 14 个 fail 都集中在同一个数据通路上,这个通路正好是前端代码改动涉及的区域,所以这些 fail 是“预期内差异”,因为 ECO 的目的就是改这部分功能。只要我重新拿到一个综合过的新网表做 golden,再和 ECO 网表比较,这些 fail 就会消失。于是我和前端确认,让他们出一版新的 Gtech 网表,作为 LEC golden 重新比较。
剩下的 3 个 fail 点在完全无关的控制逻辑区域,这立刻引起警惕。通过 GUI 打开逻辑锥后,我发现其中一个点是一个时钟选择 mux,网表里 SEL 端被改成了反相连接,导致时钟源选反了。这种情况在功能仿真中非常难发现,因为只有在某些特定时钟配置组合下才会暴露,而形式化等价性检查一把就抓到了。后端检查 ECO 脚本后,发现是某个脚本变量替换错误,导致 mux 的 SEL 连接反了。修复后重跑 LEC,全部 pass。
那次 Debug 让我彻底理解了 LEC 的价值。它不是形式主义,也不是后端流程的负担,它是整个验证链条里最能兜底的一环。总线协议错了可以由测试平台抓,时钟选错这类“潜伏型”问题,如果没有 LEC,可能流片回来才暴露。
6. 进阶技巧与常见问题速查
6.1 大规模设计如何提升 LEC 运行效率
随着设计规模增大,LEC 的运行时和内存占用会急剧上升。遇到上万级寄存器的大 block,跑一天跑不完也不奇怪。提升效率的手段有很多,但前提是不能牺牲验证完备性。
第一个技巧是按层次拆分。把大型 block 拆成子模块分别做 LEC,每个子模块结果 pass 后,再在顶层设置黑盒,只比较子模块之间的互联逻辑。这个策略能把运行时间和内存占用降一个数量级。子模块的划分要跟着 RTL 的 hierarchy 走,如果边界条件复杂,宁可多花时间设计边界约束,也不要硬跑一个大 full chip。
第二个技巧是合理使用 cutpoint。工具在读入设计后会尝试建立比较点,如果某个逻辑锥太大,可以把它切成几个小锥。add cutpoint命令可以把比较点设置在某个中间信号上,这样工具只需验证局部等价性。cutpoint 使用得当能显著加速比较,但设置太多也可能掩盖问题,所以 cutpoint 的设置要基于对设计的理解,最好是配合 team review。
第三个技巧是 engine 选项调整。Conformal 支持灵活选择验证引擎,比如 BDD 和 SAT 的切换、默认 最高 effort 还是 balanced。在大规模设计里,BDD 容易内存爆炸,SAT 则可能在超长逻辑锥上耗时较长。实际做法是先跑一遍默认,看哪里 Aborted,再针对特定逻辑锥选择 SAT 或者 hybrid 引擎。盲目把 effort 调到最大并不可取。
6.2 处理 CDC 和异步逻辑的常用选项
异步设计或存在大量跨时钟域的设计,LEC 默认处理方式容易产生伪 fail。一个重要原因是 LEC 在构建时序逻辑时,会假定所有时钟都是理想、同步的,遇到异步交互就会不知所措。所以设计里有 CDC 时,必须在 setup 里明确告诉工具这些信号的行为。
常用的命令有set design -cdc或者add clock domain,让工具把指定的时钟域分开处理。在比较 RTL 和网表时,两级同步器的实现方式不同也经常造成 fail。通常可以把同步器寄存器设为 black box 或add dont verify,因为同步器的功能本身不是 LEC 的重点,重点是验证它下游的功能逻辑。当然这么做的前提是 CDC 验证已经由专门的 CDC 工具或约束保证过。
异步复位也是常见问题。RTL 里写always @(posedge clk or negedge rst_n),综合后网表里可能是同步复位,可能是异步复位,也可能是复位同步释放结构。LEC 对复位结构的差异通常会用 setup 里的add reset声明来处理。你可以在 setup 里把某个信号声明为异步复位,工具就会把这个信号作为特殊输入而非普通数据路径处理。
从经验看,CDC 相关 setup 的质量很大程度决定了 LEC 在 SoC 级是否可用。你越早把时钟域信息、同步器清单、复位策略整理清楚,LEC 跑起来就越顺。反之,等全部后端做完再来补这些约束,往往要花好几倍的时间清理 fail 点。
6.3 团队协作中的 LEC 规范建议
LEC 不是一个人在战斗,它需要前端、中端、后端、验证多方的输入。我建议团队在项目启动时就定下几条规则,否则后期会非常痛苦。
一是 setup 文件必须电子化管理并纳入版本控制。任何例外修改都要有记录、有注释、可审查。这一步同时保证了可追溯性和团队一致性。我们团队现在每个 block 有独立的 LEC dofile 和 setup 文件,放在项目的verify/lec目录下,固定目录结构。
二是 LEC 结果必须留档。跑完之后的 conf 文件、报告、日志,压缩后归档到项目服务器或者数据管理系统。不要觉得跑 pass 了就没用,回过头排查问题时,这些历史报告比什么文档都好使。
三是接口负责人要明确。前端改动 RTL 后,要在限定时间内重新跑 LEC;后端 ECO 后,也要有人专门负责跑 LEC 确认。没有明确 owner 的验证环节,最后往往会变成“谁有空谁跑”,这种状态离漏检 bug 就不远了。
6.4 新手最常犯的五个错误和避坑清单
新手跑 LEC 最容易翻车的几个点,我列成清单,每一条都是见过真实事故的。
一是把 don’t care 和 constant 混用。don’t care是告诉工具“这个点的值不重要”,constant是强行把一个点固定到某个值,语义完全不同。设错会让验证失去意义。
二是忘记处理 tie cell。一些工艺库里 tie cell 是特殊单元,需要在 library 里显式声明,否则 LEC 报一堆莫名其妙的不匹配。
三是不看 warning 直接跑 compare。读入阶段有 warning,比如端口不匹配、约束缺失,没处理直接 compare,结果往往不可信。这个习惯要改。
四是对 Unmatched 点视而不见。只要 pass 率高就觉得没问题,完全忽略 unmatched 点。实际上 unmatched 点太多说明映射覆盖率不足,报告不可信。
五是随意添加黑盒。黑盒用多了,设计的大部分逻辑都不在验证范围内,LEC 就变成走形式了。应该尽量收敛黑盒数量,黑盒内部确实不验证,也要明确谁承担了对应的验证责任。
注意:每次新增黑盒或例外,都要问自己一句:如果这里真的有问题,LEC 还能测出来吗?如果答案是“不能”,那你要么接受这个风险,要么补一层别的验证手段。
6.5 自动回归脚本与报告解析
最后聊下如何把 LEC 融入日常回归。很多团队只在流片前象征性跑一次 LEC,这是不对的。理想的做法是每次综合或者 ECO 后,LEC 都作为 CI 流水线里的一个环节自动触发。
自动回归脚本一般由三块组成:环境初始化、执行 LEC dofile、解析结果并归档。我提供一个简单的 Makefile 风格思路:
LEC_TOP = fft128_top LEC_SCRIPT = ./lec/${LEC_TOP}.lec LEC_LOG = ./logs/${LEC_TOP}_lec.log LEC_RPT = ./logs/${LEC_TOP}_lec.rpt all: lec lec: lec -nogui -dofile ${LEC_SCRIPT} -log ${LEC_LOG} @grep -E “PASS|FAIL|UNMATCHED|ABORTED” ${LEC_LOG} | tee ${LEC_RPT}这段脚本的核心工作是跑完之后抓取报告中的关键状态。看起来简单,实际运行时需要加很多细节:比如失败时退出码非 0、需要发送邮件通知相关人、结果归档到指定目录等。我们团队的做法是每个 block 的 LEC 结果都要生成一份摘要页面,供验证经理随时查看。
报告解析也有技巧。纯文本日志岁月久了不好查,建议用脚本把compare data中 Pass/Fail/Unmatched/Aborted 的计数抓出来,写到 CSV 或 JSON 文件,这样后续可以画趋势图。如果某个 block 的 fail 数量在两次 run 之间突然从 0 变到 50,那很可能不是设计问题,而是脚本或者库环境变了。这类比较适合用表格记录,项目复盘时非常有价值。
7. 写在最后:从跑通到跑懂
如果你现在刚开始接触 Conformal LEC,我的建议很简单:先拿一个小 block,老老实实从读 RTL、读网表、跑 compare 开始,把流程完整跑通。遇到 fail 不要急着找例外命令,先打开 GUI,把逻辑锥看得清清楚楚,再决定怎么处理。这个阶段最重要的目标是建立“LEC 报出的结果到底意味着什么”的手感。
当你已经能独立处理常见的 fail 点之后,建议花时间把项目里所有例外的来源、背景、依据整理成文档。这个过程会让你从一个“会跑 LEC 的人”变成一个“懂 LEC 的人”。我在实际项目里最深的体会是:能通过加一堆例外把 LEC 跑 pass 不叫本事,能在最少例外的前提下让 LEC 可信地通过,才叫真正吃透了这项技术。
最后再分享一个小技巧。LEC 脚本里我希望你把所有例外命令集中放在文件末尾,用注释清楚分隔。这样每次比较结果有变动时,你可以快速知道是不是某条例外在“发力”。我踩过好几次坑,都是因为例外散落在脚本各处,出了 fail 根本不知道是哪条命令把它消除了。集中管理,再配合版本记录,LEC 出问题时的定位速度会快很多。往后这个技能会成为你在数字验证流程里的又一张安全网。