符号testbench实战:从SVA到形式化验证的完备性之路
最近和做验证的同事聊到一个高频问题验证工程师表达验证意图是不是只会写SVA就够了聊到最后我们发现SVA当然绕不开但它只是表达方式之一。真正能把“意图”讲全的还有另外一条路——符号testbench。这个概念在形式化验证圈子里不算新但很多跑仿真多年的工程师对它挺陌生一听到“符号”两个字就觉得高深其实它不像想象中那么玄乎。把testbench里的具体输入换成符号变量把“我要生成哪些波形”换成“输入可以是什么范围”剩下的事情交给形式化引擎去展开和判定。这篇文章想把符号testbench这个方向讲透它到底是什么和SVA有什么区别和配合实际项目里怎么落地以及我自己用下来踩过哪些坑。如果你在做UVM验证、被覆盖率盲区折磨过或者刚接触形式化验证想找一个容易上手的切入点这篇内容应该对你有用。我也会用一个可直接运行的例子带你从零搭一个符号testbench工程跑通prove流程并读懂报告。1. 先理清核心概念SVA和符号testbench到底差在哪1.1 我们平时怎么用SVA表达验证意图在绝大多数验证团队里SVASystemVerilog Assertions是表达验证意图的默认工具。我们写一条property绑到时钟上描述“当某个条件发生时后续某个事件必须发生”。比如经典的请求应答关系property p_req_ack; (posedge clk) req |- ##[1:2] ack; endproperty ap_req_ack: assert property(p_req_ack);这段话说的是时钟上升沿看到req为高那么后面1到2拍内ack必须拉高。这种表达非常直观时序关系清清楚楚而且语言结构本身经过多年验证几乎所有工具都支持。仿真回归里SVA就像一个个哨兵每拍检查一次属性是否被违反一旦出现反例马上报错给调试者。但SVA有一个容易被忽略的前提它只在你给出的激励下做判断。也就是说你要先在testbench里用driver产生一组组request序列属性才有机会被触发和检查。如果某条路径没有被激励覆盖到断言可能一整轮回归都不会被执行或者执行了也不代表“所有可能情况都正确”。SVA描述的是“在这个波形片段下设计行为应该是什么样”而不是“在所有合法输入空间里设计行为都应该是什么样”。这不是SVA的问题而是动态仿真的天然局限。我们用SVA表达意图却常常寄希望于随机约束和覆盖率工具帮我们把意图空间填满。诚实一点说覆盖率只能做到采样点的充分性做不到数学意义上的完备性。1.2 SVA的边界激励与枚举的隐性成本我举一个实际场景。假设一个同步FIFO深度16写端口和读端口连续输入随机数据。你想验证的核心意图是“在任意合法读写序列下FIFO不会上溢也不会下溢”。这句话听起来简单但在SVA加动态仿真的框架下你很难拍胸脯说验证完整了。你可以在testbench里写约束随机产生写使能、读使能、数据约束读写不能同时非法然后跑几百万拍。SVA属性也会正常工作比如“当fifo满时若写使能拉高则full信号必须保持”这类检查。跑完以后覆盖率报告可能显示100%分支覆盖率也满足收敛标准。但问题来了随机序列真的覆盖了所有“临界状态”吗比如某一拍fifo处于“几乎满”的状态下一拍又同时来写、不来读这条路径是否被随机踩中过覆盖率工具可能会告诉你这一行代码被执行过但它不会告诉你“在所有合法输入组合下这条路径上的状态转换都正确”。这正是SVA动态仿真框架的边界它擅长检测已知场景下的行为是否符合属性却不擅长证明“在给定抽象模型下所有可能输入都满足属性”。想要弥补这个缺口传统做法是不断增加仿真时间、增加seed、加定向用例、压覆盖率阈值。这条路能提升信心但投入产出比会越来越差而且边际收益递减得厉害。符号testbench就是冲着这个问题来的。它不再追求“产生足够多的波形”而是把输入信号建模成符号变量交给形式化工具去做穷尽性分析。验证意图的表达也从“依赖激励序列”变成了“依赖环境约束和属性自身的数学关系”。1.3 符号testbench是什么验证意图的“声明式环境”符号testbench并不是一种新的断言语言而是一种testbench的构建方式。传统的testbench有driver、monitor、scoreboard通过过程代码不断往DUT引脚上灌激励。符号testbench则完全换了一套思路不写“时钟沿后把req拉高等两个周期后拉低”而是写“req在任意时刻可以取值0或1但必须满足某些协议约束”。这些约束和属性本身一起交给模型检查工具工具会去展开状态空间判断断言是否成立。在具体工具里符号testbench通常体现为两部分。一部分是环境假设用assume property描述输入的合法范围比如“地址总线的值只能落在某个区间”“读写使能不能同时为1”另一部分是验证意图用assert property描述任何合法环境下都必须成立的属性。工具把这两部分转换成布尔约束交给SAT/SMT求解器去做推理。如果找到一组输入可以让assert不成立那就是一个反例对应一条真实的bug路径如果在给定抽象和深度内找不到反例就说明属性对这段输入空间是成立的。所以符号testbench并不是要取代SVA的语法而是在表达层次上做了升级。同样一条“请求应答”意图SVA负责告诉工具“协议长什么样”符号testbench负责告诉工具“请求从哪来、可以被约束成什么范围”。两者不矛盾反而经常配合使用。后面我会用一个可运行的例子把这层关系拆开给你看。2. 符号testbench擅长的事从“验证波形”到“验证所有输入”2.1 最典型的差异一个加法器属性的两种证明路径先看一个极端简化但很能说明问题的例子。假设有一个同步加法器输入a和b都是8bit输出sum是9bit寄存器在时钟上升沿锁存abmodule adder ( input logic clk, input logic [7:0] a, input logic [7:0] b, output logic [8:0] sum ); always_ff (posedge clk) begin sum a b; end endmodule如果用SVA动态仿真来验证“sum应该等于上一拍的ab”你会写这样一个属性property p_sum; (posedge clk) sum $past(a) $past(b); endproperty然后呢你得在testbench里给a和b产生大量随机值跑足够多拍让随机数覆盖各种组合。8bit的输入空间是256乘256等于65536种组合跑起来不算多但如果输入变宽到32bit甚至64bit穷举就不可能了。随机仿真的本质是在这个巨大的空间里撒点撒到的点越多信心越强但永远不能说“全查过了”。换成符号testbench就完全不同。你不需要产生任何具体的a和b值工具会把这两个信号当作符号变量处理。只要写清楚环境范围比如限制高四位为0、低四位自由取值然后让引擎去证明“对于所有合法赋值sum都等于上一拍的ab”。工具内部会把加法器的门级逻辑转换成约束用SMT求解器去搜索有没有违反属性的一组赋值。搜不到就说明在这个抽象层级下属性对所有合法输入都成立。两种路径最本质的差异可以从下面这个表格看得很清楚维度SVA 动态仿真符号testbench激励来源testbench产生波形工具自动符号化遍历判定范围采样/回归的有限集给定抽象下的全空间证明意图表达断言本身断言 环境约束共同构成主要风险覆盖率不足边界遗漏过约束、状态爆炸适用场景大规模SoC全片级验证单元级数据通路、关键协议模块2.2 越过单周期属性符号变量驱动下的状态机验证加法器例子还不算最体现符号优势的场景因为随机仿真也容易逼近。真正让符号testbench脱胎换骨的是它处理跨周期状态机的能力。假设一个控制状态机收到start信号后必须在5拍内进入done状态。用SVA写属性只是几句话但动态仿真验证这条路径时你要保证testbench产生的start时序恰好覆盖状态机各个分支比如某些仲裁冲突、中间状态异常、数据通路还没就绪等场景。如果状态机里有几十个状态、多个并发条件随机序列很可能在某几个状态边缘反复徘徊冷门分支始终没有被触发。符号testbench处理这类问题时把状态机的寄存器初始值、输入信号、甚至总线上的数据都当成符号变量。工具不是从某个固定复位序列跑起而是从所有可达状态出发做分析。这样有一个动态仿真很难替代的好处它能证明“从任意一个合法状态出发只要满足start条件状态机都保证在5拍内进入done”。针对某些安全关键的模块这种“任意状态出发”的验证强度比从单一复位状态出发跑几千条用例高出一个量级。在SymbiYosys这类工具里你可以写类似这样的短期属性assert property ((posedge clk) start |- ##[1:5] done);工具会把它和状态机的转换关系、以及你在符号testbench里声明的输入约束放在一起求解。它不需要你给一个初始波形也不需要你规定start在哪个周期拉高。它自己会把start可以出现的所有周期、所有输入组合都尝试一遍。这就是符号testbench最有趣的地方验证意图不再绑定在“某一条具体时间线上的行为”而是绑定在“所有可能时间线上的普适约束”。2.3 完善性和收敛性SVA管断言符号testbench管环境很多刚接触形式化验证的人容易有一个误解既然有符号testbench是不是以后可以不写SVA了不是的。在实际项目里SVA仍然是表达断言的首选语法因为这个语法足够成熟工具支持面广仿真和形式化都能用。符号testbench真正改变的是“环境”的表达方式。在传统UVM环境里环境是driver、sequencer、virtual interface和一堆约束类。在符号testbench里环境被压缩成一组assume property和输入声明。这些assume本质上就是原来那些约束类的数学化表达。比如原来你写req dist {0:50, 1:50};在符号testbench里你可能写assume property (req |- !busy);。前者告诉随机器“req出现频率大概一半”后者告诉求解器“req为高时busy必须为低”。后者对环境边界的刻画更精确也更适合证明。这里还涉及一个重要概念收敛性。符号testbench的证明能力并不是无限的。状态空间一大或者约束写得不够紧缩求解器可能会长时间不返回结果。这时候需要调整深度、做抽象、或者把一个大模块拆成多个小模块单独验证。经验上控制逻辑复杂、状态多的设计适合用符号testbench聚焦到几个关键属性上数据通路宽、运算密集的设计适合用SMT后端引擎。这些内容我在第三章的实操里会具体展开。3. 实操用SymbiYosys搭建一个符号testbench3.1 工具链与文件结构开源的SymbiYosys是目前最容易上手的符号testbench工具链。它基于Yosys做综合和前端解析后端接多个模型检查引擎。安装方式很简单我习惯直接用oss-cad-suite的预编译包。下载解压后source一下环境脚本确认工具可用就行source ~/oss-cad-suite/environment sby --version然后建一个工程目录文件分成三块设计文件、形式化wrapper文件、sby配置文件。整体结构像这样formal-demo/ ├── rtl/ │ └── adder.sv ├── formal/ │ └── adder_formal.sv └── adder.sby设计文件放DUTwrapper文件放环境约束和断言sby文件是验证任务的配置入口。这种分工和仿真验证里的testbench分层是一个道理DUT保持干净环境单独管理验证意图集中在一处后面换DUT版本或者加属性都会方便很多。3.2 设计一个待测模块并写约束继续用上一章的加法器作为示例。DUT代码保持不变重点是wrapper文件怎么写。在adder_formal.sv里我先例化DUT然后声明输入范围再写验证意图module adder_formal ( input logic clk, input logic [7:0] a, input logic [7:0] b, output logic[8:0] sum ); adder dut (.*); // 环境约束只关注低4位数据高4位输入固定为0 assume property (a[7:4] 4h0); assume property (b[7:4] 4h0); // 验证意图输出是上一个周期的输入之和 assert property ((posedge clk) sum $past(a) $past(b)); endmodule这里有一个关键点我没有给a和b赋任何具体值它们从顶层进来在形式化引擎眼里就是符号变量。assume语句把输入空间限制在低4位这相当于原来UVM环境里的约束类把随机空间压低帮助求解器更快收敛。如果你去除这两条assume工具会尝试证明8bit全空间下的属性理论上也能跑但求解时间会增加不少。$past(a)在这里表示上一个时钟沿的a值。因为sum是寄存器锁存的是上一拍a和b的值所以属性成立的条件是sum等于$past(a) $past(b)。写属性时要注意窗口和深度的关系$past需要至少跨越一个周期所以验证时的展开深度必须留够。后面在sby配置文件里会把深度设成比窗口大。3.3 运行验证并解读结果接下来写sby配置文件。SymbiYosys的配置语法比较直观我用的是mode prove加smtbmc引擎[tasks] prove [options] mode prove depth 4 [engines] smtbmc [script] read -formal rtl/adder.sv read -formal formal/adder_formal.sv prep -top adder_formal [files] rtl/adder.sv formal/adder_formal.sv逐行说下怎么理解。mode prove表示这个任务要做形式化证明如果只想做覆盖分析可以换成mode cover。depth 4表示展开4个时钟周期对于单周期过去值属性来说足够留了两拍余量。smtbmc是SMT-based bounded model checking引擎适合数据通路类设计。script段告诉Yosys如何读入和顶层化处理files段列出工程需要包含的所有文件。在工程目录下执行sby -f adder.sby正常情况下工具经过综合、属性抽取、求解三个阶段后会在终端打印PASS或者FAIL。PASS表示在给定深度和约束下属性对所有合法输入都成立。FAIL则表示找到了一条反例路径工具会把反例波形导出成trace文件你可以用波形工具打开精确定位是哪一拍、哪一组输入值触发了违反。为了验证这个流程确实能抓bug我故意把DUT代码改成sum a - b重新跑一遍。工具很快报了FAIL反例里能看到具体的那组a和b值。这样你就确认了符号testbench不是摆设它是真的有穷尽分析能力。我在实际项目里经常用这种做法来检查约束和属性是否写到位故意埋一个错误看验证环境能不能检测出来。4. 常见问题与避坑实录4.1 过约束证明通过不代表设计正确符号testbench最常见的坑不是工具不会用而是assume写得过强导致工具证明了一条在真实场景中永远不会发生的空属性。我在一个总线仲裁模块上就踩过这个坑当时想限制“两个master不能同时请求”于是直接assume了两路req互斥。属性全部PASS功能仿真也通过代码上了FPGA之后偶尔出现总线冲突。问题就出在assume把真实设计可能遇到的场景给约束掉了。实际场景里两个master完全可以同时请求仲裁器正是需要处理这种情况。我把“真实世界的合法输入”写窄了aggre属性自然变得容易成立。这种“空洞通过”比验证不通过更危险因为报告看起来一切正常信心反而被误导。排查方法有两个。一种是审查每条assume问自己“这条约束在真实协议里真的成立吗”如果是“我们假设它成立”就要警惕。另一种是在符号testbench里故意埋一个关联的bug比如把仲裁优先级写反然后看验证环境能否报错。如果连反例都找不到大概率是环境约束过强把bug路径也屏蔽了。这个习惯我建议每个做形式化验证的人都养成。4.2 状态爆炸与抽象分析状态爆炸是形式化验证绕不开的话题。符号testbench把输入空间符号化之后求解器要做的是在一组约束里搜索可满足解复杂度往往随状态寄存器和输入位宽指数增长。我遇到过最典型的情况是一个32bit计数器加上一个多级状态机属性很简单但引擎跑了一晚上都没有结果内存也一直在涨。应对状态爆炸第一刀永远是缩小范围。比如把计数器从32bit截断到8bit做抽象分析或者把“所有输入合法”收窄成“关键路径上的输入受约束”。第二刀是拆属性。一条大属性拆成几条小属性每条只关心一个独立行为证明难度会明显下降。第三刀是换引擎。我通常先试smtbmc因为它对数据通路友好如果遇到控制逻辑密集的状态机abc或者superprove的表现反而更好。记不住的话就直接在[engines]里多列几个后端让工具轮着尝试。还要提醒一点depth不是越大越好。depth越深状态空间越大求解时间越长。写属性之前先想清楚最远的时序窗口是几拍比如##[1:5]就需要至少6拍深度太少会影响结果可靠性太多会让求解变慢。这个参数和仿真里的时间尺度一样需要根据具体属性精确裁剪。4.3 从报告结果看pass、fail和undetermined拿到报告先别急着高兴PASS、FAIL、UNKNOWN三个状态的含义差别很大。PASS说明属性在给定抽象下成立但它的置信度取决于约束和抽象是否合理FAIL说明找到反例这个反例可能源于真实bug也可能源于环境约束不足导致求解器探索了实际不存在的路径UNKNOWN通常意味着资源耗尽或引擎无法收敛需要调整参数再来。我整理了一份排查速查表平时调试时可以对照参考现象可能原因排查/解决方向属性全部PASS但仿真中发现bugassume过约束环境被写窄逐条审查assume检查是否有不具备协议依据的限制长时间不返回UNKNOWN状态空间过大引擎选型不合适缩小输入位宽、拆分属性、尝试其他引擎反例路径在实际协议中不存在环境约束不足符号变量过于自由补充握手关系等合理assume收敛输入空间属性PASS但没有实际检查到行为断言写成了空属性vacuously true检查预条件是否太苛刻用cover模式确认属性可被触发UNKNOWN的情况最考验耐心。我个人的习惯是先降低一点深度看能不能快速出结果如果降depth后能PASS再逐步加回来定位瓶颈如果始终UNKNOWN就说明这个模块的抽象层级不合适需要从验证策略层面调整而不是继续盲目等待。4.4 符号testbench真正落地的三个建议第一从数据通路模块切入不要一上来就对整个SoC做符号testbench。数据通路的输入输出关系清晰属性容易表达求解器也更容易收敛。先跑通一个加法器、一个FIFO、一个简单仲裁器建立起对符号验证的直觉再往控制逻辑密集的模块扩展。第二属性和约束分开管理。把断言和assume放在独立的wrapper文件里让仿真和形式化验证共享同一份“意图清单”。这样同一个属性既能在UVM回归里跑又能在符号testbench里做穷尽性证明两边互相印证覆盖面更完整。我团队里现在维护一份excel表格每条属性都登记了SVA源码、适用场景、约束范围和评估结果查起来非常方便。第三不要把符号testbench当成动态仿真的替代品而是当成它的补充。仿真负责验证大系统的互联、软件栈和中断时序符号testbench负责在关键单元上给出更强有力的完备性证据。两者并行推进验证团队对设计的信心才会真正叠加起来。我自己用得最多的场景就是那些“仿真跑了很多轮都通过、但还是觉得心里没底”的模块。每次看到符号testbench在几分钟内报出一个仿真里从未出现的反例时我都会意识到验证意图这件事光靠写断言是不够的还要为断言搭一个足够精确的符号化环境。SVA仍然是验证语言里的主角但符号testbench让我多了一双看得更远的眼睛也让“验证意图”这四个字在项目中有了更扎实的落点。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →