ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EDA三巨头仿真验证技术路线对比与工程实践指南

EDA三巨头仿真验证技术路线对比与工程实践指南

仿真验证这件事,在芯片设计圈子里有个很形象的比喻:你花三个月画完一张复杂的电路图,流片一次烧掉的钱够在一线城市买套房,而仿真验证就是你在按下"确认下单"之前,唯一能反复检查、反复试错的机会。EDA 三巨头——也就是这个行业里绕不开的那三家头部工具厂商——它们各自的仿真验证技术路线,基本上代表了过去三十年数字电路验证方法论的演进史。我这些年做过几个中小规模的 SoC 项目,从最开始的纯手工写激励,到后面搭 UVM 环境、跑形式验证、做低功耗仿真,几乎每一家的工具都摸过一遍。这篇文章我想把这条"仿真验证之路"拆开来讲清楚:三家各自的技术底子是什么、为什么会长成那个样子、实际项目里怎么选、怎么用、哪些坑我踩过。不管你是刚入行的验证新人,还是做了几年想系统梳理一下方法论的老手,应该都能从里面找到能直接抄作业的东西。

1. 先搞清楚仿真验证到底在验什么

1.1 仿真验证的本质:在虚拟世界里把芯片"跑一遍"

很多人刚接触验证的时候,会把它和"测试"混为一谈。测试是芯片回来之后拿示波器、拿测试机去测,那是物理世界的事;仿真验证是在流片之前,用软件把设计的行为模拟出来,喂各种激励进去,看输出对不对。它的核心价值在于:在还没有硅片的时候,就能发现设计里的功能缺陷。

一个中等规模的 SoC,验证工作量往往占到整个项目人力的 60% 以上,这个比例不是拍脑袋来的。因为芯片一旦流片失败,重来的成本是千万级别,而仿真验证多花的人力成本相比之下几乎可以忽略。所以行业里有个共识:验证不是"做完设计之后补的工序",而是和设计并行的、同等重要的一条主线。

仿真验证要验的东西,粗分下来有这么几类:功能正确性(逻辑对不对)、时序行为(在时钟节拍下行为是否符合预期)、协议一致性(接口是否符合总线协议)、低功耗行为(电源关断、时钟门控下状态是否保持)、以及一些边界和异常场景。不同的验证目标,对应不同的技术手段,这也是为什么三巨头会演化出各自不同的工具矩阵。

1.2 从定向测试到随机约束:方法论的两次跃迁

早期的验证是"定向测试"为主,工程师手写一个个测试用例,针对每个功能点写激励、写期望值。这种方式直观,但覆盖率上不去——你只能测到你想到的场景,想不到的就永远测不到。一个稍微复杂的模块,定向用例写到几百个之后,边际收益急剧下降。

第一次跃迁是随机约束验证的普及。核心思路是:不再手写具体激励,而是描述激励的"合法范围"和"约束条件",让工具自动生成大量随机激励。这样能覆盖到人脑想不到的组合。配合功能覆盖率(covergroup)来度量"到底测到了多少",验证从"我觉得测够了"变成了"数据说话"。

第二次跃迁是形式验证和仿真验证的融合。形式验证不跑具体激励,而是用数学方法穷举证明某个性质在所有情况下都成立。它和仿真是互补的:仿真擅长复杂场景和大规模设计,形式验证擅长把某些关键性质"证死"。现在主流的方法学是把两者结合,用形式验证去啃那些仿真很难覆盖到的角落。

这两次跃迁,恰好对应了三巨头各自的技术积累方向,下面逐个拆。

2. 三巨头的技术底子与路线差异

2.1 第一家:仿真器起家,靠"快"和"生态"筑墙

这家厂商的看家本领是事件驱动仿真器。它的核心优势在于编译型仿真技术——把 Verilog/SystemVerilog 代码先编译成高度优化的中间表示,再执行,速度比传统的解释型仿真器快一个数量级。我实测过一个中等规模的模块,同样的测试平台,编译型仿真跑完一轮大概十几分钟,解释型要一个多小时,差距非常明显。

它的护城河不只是速度,更是验证方法学的完整生态。UVM 这套现在几乎成了行业标准的方法学,最早的成熟实现和工具支持就是围绕它建立的。你写一个 UVM 环境,从激励产生、参考模型、记分板到覆盖率收集,整条链路都有成熟的库和工具支撑。对团队来说,选它意味着"招人好招、资料好找、出了问题有人问",这种生态优势在工程上比单纯的技术指标更重要。

但它的短板也明显:license 贵,而且对硬件仿真加速的依赖度高。大规模 SoC 的回归测试,纯软件仿真跑不动,得上硬件加速器,那又是一笔不小的投入。中小团队用起来会有压力。

2.2 第二家:形式验证的旗手,把"证明"做成了产品

第二家的技术标签非常清晰:形式验证。它起家的工具就是等价性检查(formal equivalence checking),用来证明"综合后的网表和 RTL 在功能上等价"。这个能力在流程里极其关键——综合、插入 DFT、时钟树变换之后,你怎么保证网表还是原来那个逻辑?靠仿真跑用例是跑不全的,等价性检查用数学方法直接证,几小时就能给出确定结论。

后来它把形式验证扩展到了属性检查(property checking)和连通性检查。属性检查的思路是:你用 SVA(SystemVerilog Assertion)描述"这个信号在任何情况下都不应该和那个信号同时为高"这类性质,工具去穷举证明。证明通过,就是真的在所有情况下都成立,比仿真跑一万个用例还有说服力。

这条路线的价值在于补仿真的盲区。仿真再随机,也有覆盖不到的死角;形式验证在它能力范围内是完备的。但形式验证有个硬约束:状态空间爆炸。设计一大,工具就跑不动了,所以它更适合模块级、接口级、以及一些控制逻辑密集的块,不适合拿来做整个 SoC 的顶层验证。实际项目里,我的用法是:仿真做主力覆盖,形式验证挑关键性质去"证死",两者配合。

2.3 第三家:低功耗与混合信号仿真见长

第三家的差异化在低功耗验证和混合信号仿真。随着移动设备和物联网芯片爆发,功耗成了和功能同等重要的指标。低功耗设计的核心手段是电源域划分、电源关断、时钟门控、体偏置等,这些手段会引入大量"正常功能仿真里看不到"的状态——比如某个电源域关掉之后,里面的寄存器值是否保持、隔离单元是否正确钳位、电源开关的时序是否满足。

这家厂商的低功耗验证工具,能把 UPF(统一功耗格式)描述和 RTL 仿真结合起来,在仿真过程中动态地"关电、开电",检查低功耗结构是否正确。我做过一个多电源域的项目,纯功能仿真完全看不出问题,一上低功耗仿真,立刻暴露出某个隔离单元在电源关断时没有正确钳位,导致下游逻辑收到不确定值。这种 bug 如果漏到硅片上,就是灾难性的。

混合信号仿真则是它的另一块阵地,把模拟电路的行为模型和数字仿真在同一个环境里跑,解决数模接口的验证问题。这块相对小众,但做模拟前端、电源管理的团队绕不开。

2.4 三家路线对比:没有全能选手,只有场景匹配

把三家放在一张表里对比,思路会清楚很多:

维度第一家(仿真器+生态)第二家(形式验证)第三家(低功耗+混合信号)
核心强项编译型仿真速度、UVM 生态等价性检查、属性证明低功耗仿真、数模混合
典型场景大规模 SoC 回归、VIP 复用网表等价、关键性质证死多电源域、模拟接口
主要短板license 成本高、依赖硬件加速状态空间受限、大设计跑不动通用仿真生态相对弱
适合团队中大型、方法学成熟有形式验证经验的团队低功耗/混合信号产品

这张表不是让你"选一家站队",而是告诉你:真实项目里往往是三家工具混用。仿真主力用第一家,关键性质用第二家证,低功耗场景用第三家跑,这是很多团队的常态。工具是手段,验证目标是目的,别被"用哪家"这个问题带偏。

3. 搭一个能跑的仿真验证环境:从零到回归

3.1 环境骨架:一个最小可用的 UVM 平台长什么样

不管用哪家工具,UVM 平台的骨架是相通的。我按实际搭环境的顺序讲。一个最小可用的验证平台,包含这几个组件:激励产生器(sequence/sequencer)、驱动器(driver)、监视器(monitor)、参考模型(reference model)、记分板(scoreboard)、覆盖率收集(coverage)。

先建顶层 testbench 模块,例化 DUT(被测设计),把接口信号用 virtual interface 的方式传给 UVM 环境。这一步的关键是接口的抽象层次——不要把 DUT 的每个信号都直接连到 testbench 里到处引用,而是通过 interface 打包,再通过 config_db 传递。我见过新手直接在 testbench 里用绝对路径引用 DUT 内部信号,结果 DUT 一改层次结构,整个平台全崩。用 interface + config_db 是标准做法,虽然多写几行,但后期维护成本低太多。

interface my_if(input logic clk, input logic rst_n); logic valid; logic [31:0] data; logic ready; clocking drv_cb @(posedge clk); default input #1step output #0; output valid, data; input ready; endclocking modport DRV(clocking drv_cb); endinterface

上面这段是个接口模板,clocking block 的作用是把驱动和采样的时序关系固定下来,避免竞争冒险。#1step和#0的取值是有讲究的:输入采样在时钟沿前一个时间步,输出驱动在时钟沿后零延迟,这样能保证驱动和采样不打架。这个细节很多教程不讲,但不设好,仿真里会出现莫名其妙的 X 传播。

3.2 激励与约束:随机不是乱来

随机约束验证的核心是"约束",不是"随机"。你得把激励的合法空间描述清楚,工具才能在这个空间里高效采样。举个实际例子,验证一个带 burst 传输的总线接口,约束大概长这样:

class burst_seq extends uvm_sequence #(bus_item); `uvm_object_utils(burst_seq) rand int unsigned burst_len; constraint c_len { burst_len inside {[1:16]}; burst_len dist {1 := 3, [2:8] :/ 5, [9:16] :/ 2}; } task body(); repeat (burst_len) begin `uvm_do_with(req, {req.kind == WRITE;}) end endtask endclass

这里dist的用法是关键:短 burst 出现概率高(权重 3),中等长度权重 5,长 burst 权重 2。为什么要这样配?因为实际系统里短传输最常见,长 burst 是压力场景,权重低但必须覆盖到。如果你均匀随机,长 burst 会被过度采样,浪费仿真时间;如果完全不采长 burst,压力场景又漏了。权重的设置要基于对真实流量特征的理解,这是约束随机里最考验经验的地方,工具帮不了你。

注意:约束写得太紧,随机空间被压得很小,覆盖率上不去;写得太松,大量非法激励被生成又被丢弃,仿真效率低。这个平衡点要靠覆盖率报告反复调。

3.3 参考模型与记分板:判断对错的裁判

激励进去了,怎么判断 DUT 输出对不对?靠参考模型。参考模型是 DUT 功能的"黄金实现",可以是 SystemVerilog 写的行为级模型,也可以是用 C/C++ 写的、通过 DPI 接进来的模型。DUT 的输出和参考模型的输出都送进记分板比对,不一致就报错。

这里有个实操经验:参考模型不要照抄 RTL 的实现思路。如果参考模型和 RTL 是同一套逻辑写的,那 RTL 里的理解错误会同时出现在两边,比对永远通过,bug 就漏了。参考模型应该基于规格文档独立实现,最好由不同的人写,形成交叉验证。我做过一个项目,参考模型是照着 RTL 翻译的,结果一个边界条件两边都错得一模一样,直到流片前做形式验证才被揪出来,惊出一身冷汗。

记分板的设计要注意乱序比对。很多总线协议支持乱序返回,DUT 的输出顺序和激励顺序不一致。记分板不能简单地按顺序比,得用事务 ID 或者地址做索引,把期望值和实际值配对后再比。这个坑我踩过,一开始按顺序比,报了一堆假错,查了半天才发现是乱序返回。

3.4 覆盖率驱动:什么时候可以停

验证什么时候算做完?靠覆盖率说话。功能覆盖率(covergroup)度量你测到了哪些场景,代码覆盖率(行覆盖、分支覆盖、翻转覆盖)度量 RTL 的哪些部分被执行到。两个都要看,但功能覆盖率是主,代码覆盖率是辅。

代码覆盖率 100% 不代表功能验全了——它只说明每行代码都跑过,不说明每个场景组合都测过。反过来,功能覆盖率达标但代码覆盖率有大量空洞,说明有些 RTL 分支根本没被激励触及,可能是设计冗余,也可能是激励没覆盖到,要逐个分析。

covergroup bus_cg @(posedge clk); cp_len: coverpoint burst_len { bins short_b = {[1:4]}; bins mid_b = {[5:8]}; bins long_b = {[9:16]}; } cp_kind: coverpoint kind; cx_len_kind: cross cp_len, cp_kind; endgroup

cross 交叉覆盖是重点,它度量的是"长度和类型的组合"是否都测到了。很多 bug 就藏在组合场景里,单看每个维度都覆盖了,交叉起来却有空洞。回归跑完看报告,先看 cross 的空洞,再回头补定向激励或者调约束权重,这是标准闭环。

4. 形式验证怎么用才不浪费

4.1 等价性检查:流程里的"守门员"

等价性检查在流程里的位置很固定:综合之后、插入 DFT 之后、时钟树综合之后,每个变换节点都要跑一次,证明变换前后的网表功能等价。这个检查是自动化的,你只需要给出参考网表和实现网表,工具自己找对应关系、自己证。

实操里最容易出问题的是对应关系建立。综合工具会做寄存器复制、逻辑重组、边界优化,导致两边的信号名对不上。工具靠名字匹配失败后,会退化成靠结构匹配,但结构匹配在复杂设计上容易出错,报一堆"非等价"。这时候不要慌,先看报出来的非等价点是不是真的功能差异,很多是匹配错误导致的假阳性。手动加 mapping 约束,把关键寄存器、关键端口的对应关系显式指定,假阳性会大幅减少。

提示:等价性检查跑不动的时候,可以按模块切分,先证子模块,再证顶层。顶层直接证,状态空间太大,工具容易卡死。

4.2 属性检查:把"不可能发生"证死

属性检查的用法是:用 SVA 写性质,工具穷举证明。适合证的性质有几类:互斥性(两个信号不能同时有效)、不变式(某个条件永远成立)、可达性(某个状态一定能到达或一定不能到达)。

写性质有个原则:从规格出发,不从 RTL 出发。比如规格说"读和写不能同时进行",你就写assert property (@(posedge clk) !(rd_en && wr_en)),而不是看着 RTL 里有个互斥逻辑就照着写。前者能抓出 RTL 违反规格的 bug,后者只是把 RTL 重述一遍,没有验证价值。

属性检查跑不动是常态。工具会报"bound reached"或者"inconclusive",意思是它在给定的深度内没证完。这时候的应对手段:加约束缩小输入空间、加 assume 把环境行为约束住、或者把大性质拆成小性质分块证。不要为了让它跑完而乱加 assume,assume 加多了等于把要证的东西假设掉了,证出来的结果是空的。

4.3 形式验证和仿真的分工

我的经验分工是这样的:控制逻辑密集、状态空间不大的模块,优先上形式验证,比如仲裁器、握手协议、状态机;数据通路密集、规模大的模块,以仿真为主,形式验证挑关键性质辅助。接口协议的一致性,可以用形式验证的协议检查工具跑,比仿真写协议检查器省事。

形式验证不是"更高级所以更好",它是另一种手段,有明确的适用边界。把它用在合适的地方,能大幅提升验证完备性;用错地方,就是浪费时间等工具跑不完。

5. 低功耗仿真:那些功能仿真看不见的坑

5.1 UPF 描述与仿真的结合

低功耗验证的前提是有 UPF 描述:电源域怎么划分、每个域的电源开关是什么、隔离单元和电平转换器插在哪、保持寄存器有哪些。UPF 写完之后,仿真工具要能"理解"它,在仿真过程中动态控制电源状态。

具体做法是把 UPF 和 RTL 一起读进仿真器,仿真器会为每个电源域维护一个"电源状态",当激励触发电源关断时,仿真器把该域内的信号强制成关断状态(通常是 X 或者保持值),同时检查隔离单元的输出是否符合预期。这个机制让"关电"变成了仿真里可观测、可检查的事件。

我踩过的一个坑:UPF 里的电源域层次和 RTL 层次对不上。UPF 是按设计意图写的,RTL 综合后层次会变,如果 UPF 没有及时更新,仿真器会找不到对应的域,要么报错,要么静默地把某些信号当成常开,低功耗检查就形同虚设。所以 UPF 和 RTL 的层次同步,要纳入日常检查清单。

5.2 隔离与保持的检查要点

低功耗仿真要重点检查两类结构:隔离单元和保持寄存器。

隔离单元的作用是:当一个电源域关断时,它的输出不能是浮空的 X,要被钳位到一个确定值(通常是 0 或 1),否则下游常开域的逻辑会收到 X,行为不可预测。仿真里要检查:关断发生时,隔离单元的输出是否在规定的时钟周期内被钳位、钳位值是否符合规格。

保持寄存器的作用是:电源关断期间,某些寄存器的值要保住,靠的是备份电源或者保持锁存结构。仿真里要检查:关断再上电之后,这些寄存器的值是否和关断前一致。这个检查用 SVA 写最方便:在关断前采样寄存器值,上电后比对。

property p_retention_check; @(posedge clk) disable iff (!rst_n) ($rose(pwr_off) |-> ##[1:5] $stable(ret_reg_q)); endproperty

上面这个性质的意思是:电源关断信号上升后,保持寄存器的值在若干周期内保持不变。实际写的时候要根据电源开关的时序参数调整延迟窗口,这个窗口设错了会报假错。

5.3 低功耗场景的激励设计

低功耗仿真的激励和功能仿真不一样,重点是构造电源状态切换的场景:正常工作时突然关某个域、关断期间有外部事件进来、上电顺序不同、多个域同时切换等等。这些场景在功能仿真里根本不会出现,因为功能仿真默认所有电源都是开的。

我的做法是单独写一套低功耗场景的 sequence,专门驱动电源控制信号,配合功能激励一起跑。比如先跑一段正常读写,中途触发某个域关断,再触发上电,检查数据是否保持、隔离是否正确。这套 sequence 要覆盖所有电源域的开关组合,组合数多了会爆炸,所以要基于实际使用场景做裁剪,优先覆盖产品真实会遇到的电源状态迁移。

6. 常见问题与排查实录

6.1 仿真跑不通/结果不对的排查顺序

仿真出问题,排查要有顺序,别上来就瞎改。我的固定顺序是:先看编译有没有 warning(尤其是隐式声明、位宽不匹配)、再看复位和初始化、再看时钟和时序、最后看激励和比对逻辑。

位宽不匹配是最常见的隐形杀手。SystemVerilog 里赋值时位宽不一致,高位会被截断或者补零,编译器可能只给个 warning,仿真结果却错了。我习惯在编译选项里把位宽相关的 warning 升级成 error,强制在编译期暴露。

X 传播是另一个高频问题。仿真里出现 X,先追 X 的源头:是未初始化的寄存器、还是多驱动、还是读到了未写的存储器。用仿真器的 X 追踪功能(dump 波形时打开 X 传播记录),能快速定位第一个产生 X 的点,从那里往回查。

6.2 覆盖率上不去的典型原因

覆盖率卡在某个值上不去,原因通常有几类,我整理成速查表:

现象可能原因处理思路
某 cross 空洞约束把组合空间压太小放宽约束或加定向激励
代码分支未覆盖激励没触及该分支条件分析分支条件,构造激励
翻转覆盖率低某些信号长期不变检查是否死逻辑或激励不足
覆盖率虚高covergroup 定义太粗细化 bins,加 cross

覆盖率虚高这个要特别警惕。有些团队为了"达标",把 covergroup 写得很粗,几个大 bin 一盖,数字很好看,实际什么都没验到。覆盖率的定义要能反映真实验证意图,这是设计 covergroup 时最该花心思的地方。

6.3 回归测试的效率优化

回归跑得慢是常态,优化手段我按性价比排序:第一,并行化,把测试用例分发到多台机器或者多个仿真进程并行跑,这是最直接的;第二,编译缓存,把不常变的 VIP 和参考模型预编译成库,只编译变化的 DUT 和 testbench;第三,用例裁剪,把已经稳定覆盖的场景从每日回归里拿掉,放到周回归;第四,仿真器优化选项,比如关闭不必要的波形 dump、调高优化等级。

波形 dump 是隐藏的性能杀手。全量 dump 波形能让仿真慢好几倍,日常回归根本不需要全量波形,只在失败用例上开波形就行。这个习惯能省大量时间。

7. 我个人的几点实操体会

做了这些年验证,有几个体会是文档里不会写、但实际特别管用的。

第一,验证平台本身也是要"验证"的。参考模型、记分板、覆盖率收集逻辑,都可能出错。我习惯在平台搭好后,先故意往 DUT 里注入一个已知 bug,看平台能不能抓到。抓不到,说明平台有问题,先修平台再继续。这个"平台自检"步骤能避免大量假通过。

第二,别迷信工具,要理解工具在干什么。仿真器、形式验证工具都是黑盒,但它们的报错信息、覆盖率报告背后是有逻辑的。花时间读懂工具的输出,比盲目调参数有用得多。比如形式验证报 inconclusive,你得知道是深度不够还是约束太松,才能对症下药。

第三,验证计划比验证平台更重要。平台是骨架,验证计划是灵魂。哪些功能要验、用什么手段验、验到什么程度算够,这些想清楚了,平台怎么搭是水到渠成的事。我见过太多团队一上来就埋头搭 UVM,搭了两个月发现验证计划没理清,返工重来。

第四,三巨头的工具,学一家精通、其余会用即可。方法学是相通的,UVM 的思想、形式验证的思路、低功耗验证的检查点,换一家工具只是 API 和命令不同。把一家的工具用透,建立完整的方法论认知,换工具的成本比想象中低。真正值钱的是方法论,不是某个工具的快捷键。

最后分享一个小技巧:每次回归失败,把失败用例的随机种子(seed)记下来。随机约束验证的失败往往依赖特定种子,记下种子就能稳定复现,排查效率高很多。这个习惯我从入行保持到现在,救过无数次命。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进