资讯动态

从SVA到符号testbench:验证意图的另一种表达方式

发布时间:2026/9/9 7:29:13 来源:尧图企业网站定制
1. 从SVA到符号仿真验证意图的两种表达做芯片验证这行的人对SVASystemVerilog Assertion应该都不陌生。断言就像给设计贴的一张张“医嘱”这个信号拉高后三拍内那个信号必须为高、那个请求发出后不能连续两次被拒……我们把验证意图写在时序逻辑里然后跑仿真跑几万个周期看这些断言会不会在某一个随机种子下爆红。但做了几年验证之后我越来越觉得SVA这条路有种说不出的别扭。它本质上是个“采样器”——采到什么算什么靠的是海量随机激励去碰运气。如果一个问题需要在特定深度、特定组合下才能暴露传统仿真就得先花大力气把这些场景“凑”出来否则断言写得再严谨也白搭。后来我开始在真实项目中接触符号testbenchSymbolic Testbench思路一下被打开了。符号testbench不用你给具体的数据激励而是把输入管脚绑成“符号变量”让工具自己去遍历合法的取值空间去找一条能触发断言失败的路。这跟SVA的关系不是替代而是互补SVA是“大数定律”思路符号testbench是“解方程”思路。这篇文章我想把符号testbench这套东西掰开揉碎讲清楚。适合对验证方法学有一定基础、正在被复杂场景覆盖率和断言调试折磨的工程师也适合刚入行想拓宽验证视野的同学。看完你会知道这种“另类”的表达方式到底在解决什么问题、它有什么先天短板、以及你怎么在自己的流程里先跑起来。2. 符号testbench到底在验证什么从“给数据”到“给约束”2.1 SVA强大的地方和它看不到的盲区SVA确实是表达验证意图的好工具。它最大的优势是贴近时序a ##1 b就是把“这个周期a成立、下个周期b成立”的时序关系钉死工具会把这个断言映射成仿真器里的一个监控器每拍都在检查。但SVA有一个绕不过去的盲区它只站在“观测者”的角度不会主动去制造场景。你把断言写好了仿真器每周期帮你查一次可要是激励永远走不到那个状态断言查一万次都是白查。验证完备性被迫押注在约束求解器的随机质量上而随机是件很玄的事——同样的种子池上一版跑得很好下一版加了两个约束某个角落场景可能就再也出不来了。传统仿真是往前推的给定初态和输入序列推未来。这决定了SVA这类断言只能回答“当前这条路径上意图是否被违反了”。如果你关心的是“是否存在某条路径能违反意图”——这是截然不同的一个问题。2.2 符号testbench的思路让设计“自己找出路”符号testbench换了个提问方式。它不把一个输入管脚固定成0或1而是把整个输入空间用一个符号值“X”来表示。仿真器遇到这个X时不再做二选一的分支推进而是把两条路径都保留下来各自带着一个路径条件。这样走完几拍之后工具手里握着的不是一条执行轨迹而是一棵“可能性树”。这棵树的规模会指数膨胀所以符号仿真必须借助SAT求解器或BDD来剪枝。它的巅峰形态是模型检验Model Checking把所有状态和输入都用符号编码一次性判定一个性质在整个状态空间中是否成立。卡内基梅隆的SMV、Cadence的JasperGold、以及后来英特尔的IFVIndustrial Formal Verification流程核心都是这个思想。我们常见的“符号testbench”就是这种思想在测试平台层面的落地保留测试平台的层次结构但把刺激源替换成符号。你可以理解为传统testbench是“一个一个喂数据”符号testbench是把所有可能的数据用一个数学表达式打包然后让求解器告诉我们“哪些数据能让被测设计出问题”。验证意图不变但提问的层级从“仿真路径级别”上升到了“状态空间级别”。2.3 验证意图的两种表达方式对比放下抽象概念我把两类做法摆在表格里做个对比维度SVA 传统仿真符号testbench 形式验证意图表达位置写在断言里以时序逻辑为主写在约束和属性里以状态关系为主探索方式随机/定向激励逐周期推进符号编码 求解器一次性枚举路径规模瓶颈仿真速度快但场景有限状态空间指数级需抽象和剪枝典型覆盖目标功能覆盖率、代码覆盖率性质覆盖率、可达性分析适用场景全芯片回归、大数据流验证协议模块、仲裁逻辑、控制通路发现问题的时效可能在验证后期才暴露通常在早期就能收敛到反例这张表容易误导人好像两者是二选一。真实工程里成熟团队通常是“SVA写好监控 约束写好环境 符号引擎做深挖”。SVA负责盯住每条仿真路径上的实时行为符号testbench负责回答“还有没有我没走到过的路径会出问题”。2.4 一个直观的类比查库存和查账本想快速理解这种差异可以把它想成两种查账方式。传统仿真是“流水账查法”把这个月每一天的出入库记录全部翻一遍看看有没有哪一天库存变成负数了。记录越多、翻得越细越可能发现问题但要是某一天压根没记进去那永远翻不出来。符号testbench更像是“账目逻辑查法”我不看每一天的具体数字而是把“入库量 出库量 初始库存”这个约束扔给求解器让它反推出存在哪一组数字会让库存为负。如果这组数字真的存在工具还会把具体的那一天“示范”给你看——这就是反例轨迹。SVA是这个月翻账本的记录员符号引擎是那个拿着一堆数学公式问“有没有可能”的审计师。两者都是查账但一个靠遍历记录一个靠逻辑演绎。3. 拆解符号testbench的核心组成约束、属性与引擎3.1 约束验证意图的“边界条件”符号testbench里的约束Constraint相当于传统testbench里随机激励的constraint block但它们有本质差异传统约束是“生成器”告诉随机器“按这个范围撒数据”符号约束是“筛选器”告诉求解器“我只要满足这些条件的解”。比如要验证一个FIFO的读端口逻辑传统写法是class fifo_read_seq extends uvm_sequence #(fifo_trans); constraint c_read_when_not_empty { trans.rden 1b1; } endclass这是“确保发一个读命令”。符号testbench里同样的意图会写成对符号变量的限定// 伪代码风格的符号约束描述 symbolic_input logic read_enable; symbolic_input logic [7:0] data_in; assume property (read_enable 1b1); assume property (data_in inside {[0:255]});关键区别是传统约束直接产生一个具体数值符号约束不给数值只描述“这一组变量的取值集合”。求解器在这个集合内做全域搜索如果存在一组值能破坏读逻辑的性质它就能把这组值找出来。实际做项目的时候我建议把约束分成两类维护一类叫“环境约束”模拟接口协议时序的一类叫“场景约束”指定这次要验证的场景的这样复用性会好很多。3.2 断言和属性验证意图的“裁判标准”符号testbench里表达验证意图的载体有两种一种是assert property另一种是assume property。assert定义的是“必须成立”的性质相当于裁判说“这条规则谁也不能违反”。比如assert property ((posedge clk) read_enable |- !(fifo_full) |- $past(fifo_count) 0);这句的意思读使能且FIFO处于未满状态时上一个周期的FIFO计数必须大于0——不能从空FIFO里读数据。assume则是对环境输入的限制相当于赛场的边界线“我不会让你跑到场外去”。在符号引擎里assume和assert还有一个微妙的互动——过多的assume会把解空间圈得太小导致本来能发现的问题被圈没了太少的assume又会让求解器花大量时间探索根本不可能出现的输入组合。在SVA里我们很少纠结assume和assert的区分因为仿真是单向的随机器不会因为assume“拉扯”方向。但符号仿真里求解器真的会把assume当方程的一部分你用错了结果会差很多。3.3 引擎和算法背后的SAT、BDD与有界模型检验符号testbench能跑起来底层是靠两个东西SAT求解器和BDD二叉决策图以及它们的组合架构。SAT求解器是主角。它解决的是“布尔可满足性问题”给定一堆布尔变量和约束是否存在一组赋值让所有约束同时成立。现代SAT求解器比如MiniSat、CaDiCaL做了大量工程优化能在秒级处理几十万变量的公式。符号仿真把每一拍的行为编码成布尔约束求解器在约束空间中寻找一条从初态到违反断言状态的路径找得到就返回一个反例找不到就增大展开深度再试——这被称为“有界模型检验”Bounded Model CheckingBMC。BDD擅长表达状态集合之间的关系。它会构建设计的一个规范化符号状态图通过固定点运算求出所有可达状态然后直接判定性质在所有可达状态上是否成立。这种“无界”能力是BMC做不到的但BDD对变量顺序极度敏感很容易出现内存爆炸。商用形式验证工具JasperGold、VC Formal以及开源工具SymbiYosys Yosys Z3/SuperProve基本都是BMC和BDD/证明引擎混合使用先用BMC快速逼近反例再用非BMC引擎做无界性质判定。工程层面的关键决策是展开多少拍、抽象到什么粒度、要不要用假设保证模式。3.4 实战组件选择商业工具与开源工具怎么选在真实项目里选符号验证工具通常绕不开三套方案。Cadence JasperGold是目前工业界覆盖能力最强的一档它把SVA直接当成属性输入语言配合先进的抽象引擎能处理几百万门级的模块。适合在大型IP验证中做重点突破。缺点是license贵而且学习曲线陡——熟练的SVA工程师上手JasperGold的约束建模通常也要两三周。Synopsys VC Formal在回归流程的集成度上做得不错能和VCS的仿真结果联动适合已经有Synopsys流程的团队。如果项目预算紧张或者只是想做技术预研我推荐开源这一支Yosyssymbiyosysz3用Verilog描述设计用.sby文件描述验证任务用SVA子集或自定义属性描述意图。这套组合这几年成熟度提升明显对付中小规模控制逻辑比如FIFO仲裁器、状态机、寄存器接口译码绰绰有余。匹配度上我个人的建议是核心复杂模块总线协议控制器、访存调度器上商业工具外围简单模块和教学验证用开源工具。符号testbench的意图表达与工具无关你把约束和属性写好迁移成本其实不高。4. 从头搭一个可运行的符号testbench示例4.1 选一个足够小、但能说明问题的被测设计为了不让示例变成空谈我用一个经典的细节设计同步FIFO的控制逻辑重点验证它的空/满/读/写行为。这类模块场景清晰、状态量少但又包含真正的时序逻辑判断逻辑正好适合落地符号testbench。FIFO的主体代码简化版module sync_fifo #( parameter DEPTH 4, parameter WIDTH 8 )( input logic clk, input logic rst_n, input logic wr_en, input logic rd_en, input logic [WIDTH-1:0] din, output logic [WIDTH-1:0] dout, output logic full, output logic empty ); logic [WIDTH-1:0] mem [DEPTH]; logic [$clog2(DEPTH):0] count; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) count 0; else begin case ({wr_en !full, rd_en !empty}) 2b10: count count 1b1; 2b01: count count - 1b1; default: count count; endcase end end always_ff (posedge clk) begin if (wr_en !full) mem[count[$clog2(DEPTH)-1:0]] din; if (rd_en !empty) dout mem[count[$clog2(DEPTH)-1:0]]; end assign full (count DEPTH); assign empty (count 0); endmodule这个设计有个典型的简化细节用一个counter做剩余深度判断逻辑上等价于读写指针比较。它便于符号验证做路径展开状态量只有counter和mem非常适合作为入门实验对象。4.2 用SystemVerilog描述一条符号testbench我们不用UVM因为符号testbench往往不需要那么重的组件层级。核心是把输入做成符号把要验证的性质写成assert property。module tb_symbolic_fifo; logic clk; logic rst_n; logic wr_en, rd_en; logic [7:0] din; logic [7:0] dout; logic full, empty; sync_fifo #(.DEPTH(4), .WIDTH(8)) dut ( .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en), .din(din), .dout(dout), .full(full), .empty(empty) ); // 符号变量不给具体值让求解器自行决定 symbolic logic wr_symbol; symbolic logic rd_symbol; symbolic logic [7:0] din_symbol; assign wr_en wr_symbol; assign rd_en rd_symbol; assign din din_symbol; // 模拟时钟 initial begin clk 0; forever #5 clk ~clk; end // 复位 initial begin rst_n 0; repeat (2) (posedge clk); rst_n 1; end // 环境假设不能同时读写同一个入口特殊情况 assume property ((posedge clk) !(wr_en rd_en full empty)); // 性质1FIFO为空时读使能不应改变数据 assert property ((posedge clk) empty rd_en | $stable(dout) |- !empty); // 性质2只要不同时读写count的变化必须吻合 assert property ((posedge clk) !(wr_en rd_en) !full wr_en | !empty); endmodule看到这里你可能已经感觉到了符号testbench的代码量比传统testbench精简得多因为它省略了“随机种子管理”、“sequence/item发送”、“scoreboard比对数据”这些环节。数据比对这一步在哪答案是——属性本身就是裁判求解器去找反例。有一点经验值得说顶层用assign wr_en wr_symbol这种“符号直通”写法在商用工具里可以直接用但在开源工具链比如SymbiYosys里通常需要用专门的命令把管脚声明成符号输入不能靠SystemVerilog的symbolic关键字。我的建议是在学习阶段先在工具原生的建模文件里练手跑通后再回到SystemVerilog语法层面。4.3 在开源工具链SymbiYosys中落地并跑出反例SymbiYosyssby是目前最接地气的开源形式化验证工具。它的工作流分三步Yosys负责把Verilog综合成逻辑网表sby负责编排求解引擎和属性绑定Z3/SuperProve/Boolector负责做SAT求解。我们写个.sby描述文件把前面FIFO的验证意图绑定进去[options] mode bmc depth 20 skip 2 [engines] smtbmc z3 [script] read -formal sync_fifo.v prep -top sync_fifo [files] sync_fifo.v这个配置的关键参数mode bmc有界模型检验模式从初始状态开始逐步展开。depth 20展开20拍。对这个4深度FIFO来说足够覆盖写满、读空、同时读写所有正常场景。skip 2跳过前两个周期等复位完成后再开始检查。engines smtbmc z3用Z3做SMT求解。对8位数据、4深度FIFO这种小规模任务Z3完全够用。跑起来之后如果某个性质有问题工具会打印一条反例轨迹并用VCD波形文件告诉你第几拍、什么条件下、哪个属性违例了。我第一次跑的时候故意在空FIFO时允许读使能结果Z3在深度4的位置生成了一个完整反例我把VCD拉进GTKWave里一帧一帧看那种“工具自己找到了路”的体感特别强烈。跑反例的正确心态是反例不是在“挑刺”而是在帮我们补充验证盲区。传统仿真里要撞几个百万周期才能偶遇一两次的空读符号引擎几秒内就把最短路径给你摆出来了。4.4 手工做一个“最小符号化”实验来理解化解法如果你手头暂时没有商业工具也不方便装Linux下的开源链还有一个非常“裸”的办法可以理解符号testbench的内部逻辑——自己写一个枚举式符号步进器。思路是这样的把FIFO的状态变量count、mem内容全部展开成布尔向量把每个周期的状态转移函数写成布尔表达式然后从初态出发把所有可能的wr_en/rd_en/din组合枚举一遍看有没有哪个状态违反了性质。理论上这就是BMC的暴力版。我用Python做过类似实验from itertools import product def next_state(state, wr, rd, din): count state[count] full count 4 empty count 0 ncount count if wr and not full: ncount 1 if rd and not empty: ncount - 1 # 省略mem更新逻辑 return {count: ncount} init {count: 0} all_wr_rd product([0,1], repeat2) for depth in range(10): frontier {frozenset(state.items())} # 展开所有可能的输入组合 for wr, rd in all_wr_rd: din 0 # 数据值对手工实验不重要 ns next_state(init, wr, rd, din) if ns[count] 0: print(fAt depth {depth}: underflow possible) break这个实验虽然粗糙但它做了一件事把“遍历所有可能输入”变成可操作的过程让你直观看到状态空间是怎么扩张、又是怎么被约束剪掉的。我建议读完这篇文章的同学花半天时间做一遍这个实验比看十篇PPT都有用。5. 我踩过的最典型的五个坑以及排查方法5.1 约束过度导致虚假证明这是符号验证里最常见的翻车点。有次我给一个AXI-Lite从机接口写属性为了让环境贴近真实使用场景我加了一条“地址必须按32位对齐”的约束。结果跑出来全pass但我拿手工仿真用未对齐地址打过去设计其实会出错。问题就出在我把地址限制得太死把能暴露问题的输入组合全部圈出局了。排查方法其实不复杂跑一轮“无约束模式”看看求解器能不能释放出未对齐地址场景。如果无约束模式下反例出来了说明你的assume加错了方向。约束应该描述“外部环境不会提供的输入”而不是“我认为设计应该能处理的输入”——前者是环境边界后者是你要验证的预期。5.2 属性写得太强或太弱边界难把握属性分三种角色安全的规则invariant、期望的响应eventually、防止故障发生的禁忌never。我见过有人把所有属性都写成invariant结果要求每拍dout都必须为0设计一跑就挂也有人的属性弱到“只要不清空就是对的”什么bug都抓不出来。我的习惯是写属性之前先给每个关键需求列一张表这个信号必须始终保持什么、这个条件发生后多少拍内必须发生什么、这个状态组合绝对不能出现什么。然后一条属性对应一行需求属性强弱跟着需求走而不是跟着感觉走。强弱边界模糊时可以先跑一个“最小实现Dummy逻辑”的负例确保属性能在负例上Fail。5.3 求解器超时怎么办缩小状态空间和有界深度遇到超时八成是状态空间太大。这时先别急着换更强的机器检查三件事第一接口位宽是不是能抽象。数据总线的某些位如果对验证目标没有影响可以折叠成更少的符号变量。比如一个24位计数器只参与加1操作前20位完全可以抽象掉。第二复位周期后的初始状态是不是过大。很多设计有可编程配置寄存器如果把配置组合全保留着求解器会非常痛苦。第三BMC深度是否过大。有界深度从10加到20求解时间常常指数增长如果问题能在12拍内暴露没必要设到20。JasperGold里我惯用的做法是先设一个小的depth跑一遍看反例分布在哪一拍再逐步加深找到“让每一个反例都有机会暴露”的临界深度。5.4 与传统回归的配合符号引擎挂了仿真还有意义吗符号testbench不是银弹。全芯片级验证中存储阵列、低速模拟接口这些行为如果用符号建模要么抽象失真、要么爆炸根本跑不下去。所以成熟项目都是“符号先跑模块级仿真再跑系统级”。我的实践经验是每轮新代码合入之前先跑一遍符号testbench作为“预检岗”。它会把所有属性在未来N拍内是否有反例一次性排查一遍——这个动作传统仿真通常要几天回归才能等价覆盖。预检通过后再灌入大批量随机回归让仿真去覆盖那些符号引擎不擅长处理的数据通路和数据变换逻辑。5.5 开源工具链里最常见的崩溃和安装坑很多入坑SymbiYosys的人第一步就摔在安装上。直接pip install或apt install得到的Yosys版本经常和SymbiYosys不兼容或者缺少smtbmc引擎依赖。我推荐用预编译的二进制包# 在Ubuntu/Debian系统上 wget https://github.com/YosysHQ/oss-cad-suite-build/releases/latest/download/oss-cad-suite-linux-x64.tgz tar -xzf oss-cad-suite-linux-x64.tgz export PATH$PWD/oss-cad-suite/bin:$PATH这个工具包自带Yosys、SymbiYosys、Z3以及GTKWave版本之间匹配关系是官方维护好的省去至少半天编译时间。跑sby -f fifo.sby如果还报错第一反应查yosys -V和sby --version的版本匹配性而不是去改代码。6. 把符号testbench纳入你的验证流程我的落地建议6.1 三步走模块选择、团队准备、流程集成我建议任何团队引入符号testbench都按这三步走。第一步是模块选择。挑一个状态空间可控、控制逻辑密集、SVA仿真覆盖率难以打满的模块当“试验田”。典型候选FIFO仲裁器、总线协议控制器、配置寄存器接口译码、复位状态机。不要一上来就挑战全芯片或带模拟接口的大模块。第二步是团队准备。符号验证对工程师的思维要求不同传统验证是“构造场景”符号验证是“构造反例”。培训方式可以让团队成员先用开源工具链复现几个经典反例比如无符号加法溢出检测再回到自家设计上去做“找茬练习”。第三步是流程集成。符号testbench不要跑在孤立环境里最好是挂在CD/CI流程的早期阶段每晚编译跑一轮BMC深度50以内的符号回归把结果自动生成给验证负责人看。等团队形成习惯后再把覆盖面扩大。6.2 一个可参考的工程模板每天“符号预检仿真回归”这里我给一个自己在项目中实际用过的模板算是“抄作业”级别的参考每个发布候选RC版本合入时按顺序自动执行lint / 编译检查符号预检BMC深度80只跑核心控制模块的断言常见反例自动归类与负责人分发大规模随机回归跑满性能指标;覆盖率收集若低于阈值提示“是否需要在下一轮扩展符号抽象深度”。执行下来最明显的变化是过去需要等三天回归才能发现的grund-level协议问题现在编译后两小时就能被符号引擎抓到。团队里年轻的验证工程师也很快接受了这种“先证明、再仿真”的节奏因为符号预检能帮他们提前规避掉很多无谓的仿真debug时间。6.3 什么情况下别硬上符号testbench我也遇到过某些项目真的不适合符号验证。存储密集型的模块比如带大数组Cache的数据通路符号引擎一碰到大容量memory就很容易出现内存爆炸因为它会把memory的每个bit都编码进SAT公式里纯数据运算密集型的模块比如FP乘法器、CNN加速器的卷积核符号引擎通常不会比定向仿真更高效因为这类模块没有“控制状态”给求解器剪枝模拟和数字混合接口符号引擎对模拟信号的抽象能力很弱基本无能为力。遇到这些情况踏踏实实回到SVAUVM覆盖率驱动反而效率更高。符号testbench存在的意义不是为了取代传统流程而是在它擅长的领域里大幅度压缩验证盲区。不硬上本身就是一种工程判断力。回到标题那句话“SVA之外表达验证意图的另一种方式”——符号testbench不是要替代你手里那套习惯了的SVA断言而是让验证意图的表达多了一个维度、多了一种提问方式。SVA负责描述预期约束负责框定场景求解器负责回答“有没有可能”这三者叠在一起你会突然发现很多过去靠海量回归才磨出来的bug现在用数学的办法直接就能“算”出来。我个人在实际项目里最深的一个体会是验证工程师的核心竞争力不在于会写多少种约束而在于理解每一种工具“擅长回答什么问题”。符号testbench让我重新理解了这句话。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价