ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Verdi 断言波形调试:从失败日志到正确采样沿

Verdi 断言波形调试:从失败日志到正确采样沿 凌晨一点半回归脚本刷出一行红字tb_top.u_dma.a_hs_done: started at 128450ns failed at 132450ns。我把valid、ready、done三根线拉进 Verdi 的 nWave来回放大到 128450ns 附近盯着看沿是干净的信号也是对的握手看起来完全正常可断言就是红的。那一次我花了将近三个小时最后发现问题根本不在波形上——是我看波形的方式和断言采样数据的方式压根不是同一套时间语义。这篇东西就是想把那次以及后来若干次类似的折腾整理清楚在 Verdi 里观察 assertion 波形做 debug到底该怎么下手。主要讲三件事——断言的数据是怎么被采进去的、为什么它和你肉眼看到的波形经常错开一拍、怎么用 Verdi 的 nTrace 与 nWave 把一条失败的 SystemVerilog 断言从日志里的时间戳一路追到真正的那一拍。不管你是刚接触 SVA 的验证新人还是写了很多年代码但一直靠$display硬扛的 RTL 工程师都能照着这里的流程走一遍。1. 断言波形难读根源在于它和普通信号不是同一套时间语义1.1 采样窗口落在时钟沿之前你看到的沿后值不是它看到的值这是所有困惑的总源头。并发断言在时钟事件上采样采样动作发生在Preponed区域也就是这一拍所有非阻塞赋值生效之前。换句话说(posedge clk)在 T 时刻采到的data是 T 时刻之前那个已经稳定下来的值而你在波形窗口里 T 位置看到的data是 T 时刻刚被触发器更新出来的新值。这两笔数据是相邻两拍。所以会出现一种非常折磨人的现象波形上你按拍读逻辑完全自洽断言那边按采样值读是另一条时间线上的一拍偏移版本。很多人第一次遇到会怀疑工具其实工具没错是自己读错了位置。对抗这个坑我有两个土办法都很有效把参考点整体前移一拍。判断断言结果时不要看失败时刻那一列而是看它左边紧邻的那一列。养成这个习惯之后很多莫名其妙的失败会立刻自己解释清楚。在波形里加对照信号。在 RTL 里临时加一组always_ff (posedge clk) sampled_dbg data;然后观察sampled_dbg而不是data。sampled_dbg在 T 时刻显示的值才和断言在 T 时刻采到的是同一批。这个方法要多编译一次但在纠缠不清的时候它比任何推理都快。注意这个偏移和 Delta 周期无关不是你仿真精度不够。它是语言语义本身决定的改仿真器、改精度都不会变。1.2 空洞成功比失败更危险的一种绿灯第二种让人误判的情况正好相反——断言没红你以为过了其实它压根没被真正检查过。这就是空洞成功vacuous success。举个典型例子(valid) |- ##[1:5] done。如果valid在整个仿真里一次都没拉起来这条断言永远不会失败也永远不会真正验证到任何东西工具默认不会特别提醒你。你看到的是零失败得出的结论是逻辑正确但事实上你什么都没验证。我在项目里遇到过一次更隐蔽的断言挂在某个子模块上而那个子模块因为配置参数的关系在本次回归里根本没被实例化。整轮跑了六个小时断言报告干干净净直到有人手改了一个参数才炸出来。处理办法不复杂编译时打开空洞成功的上报VCS 侧有对应开关选项名各版本略有差别用vcs -h | grep -i vacuous确认一下你手上的版本先让这些假绿灯暴露出来。加配套的覆盖属性确认前置条件真的被触发过比如valid拉高的次数。检查断言所在的模块确实被例化了。这一条听起来傻但真的发生过不止一次。1.3 失败时间戳指的是判负那一拍不是出事那一拍再看开头那行日志started at 128450ns failed at 132450ns。两个时间戳中间差了 4000ns。这 4000ns 就是断言从尝试attempt到最终判负之间走完的时间。如果你的属性里带##[1:5]、throughout、until这类跨周期的算子失败时刻和触发时刻之间就是有距离的。盯着failed那个时间点看波形你在看的是结论不是原因。正确做法永远是先把属性表达式拆成时间轴上的一段序列从failed往回数找到对应的started然后从那一点开始看。一个实用的经验在 nWave 里给时钟加网格或 Marker把采样沿可视化出来然后按##N的 N 值往前数格子。手数周期这件事很蠢但是当属性里嵌套了两层序列、还带局部变量的时候数格子往往比在脑子里推演更快。2. 让 Verdi 真正看懂断言编译和 dump 链路的准备2.1 -kdb 是 Verdi 能点进源码的前提很多人卡在第一步Verdi 能打开 FSDB波形也能看但层次树里是乱的点源码没反应断言更是完全找不到。这种情况九成是编译时漏了-kdb。-kdb会在simv.daidir目录下生成一个kdb子目录把设计的结构信息、源码关联、断言绑定关系全部记录下来。Verdi 通过-dbdir simv.daidir读取它才能做到双击信号跳源码、搜索层次、识别断言语句。几个实际会踩到的点daidir 的名字跟着-o走。如果你写的是-o simv_debug那目录就是simv_debug.daidir不是simv.daidir。我见过同事因为目录名找错以为-kdb没生效重新编译了好几轮。-debug_accessall的代价要评估。它会让编译变慢、simv明显变大回归农场里成千上万个 case 全都带这个选项是浪费。正常做法是只在本地 debug 版本上开回归用轻量配置。某些细分能力需要额外的许可选项比如类调试相关的class之类通常要再补一个-lca。具体支持矩阵看你手上的版本说明。-sverilog别漏否则断言语法直接不认编译期就会报一堆莫名其妙的错。2.2 FSDB 里该 dump 什么以及断言数据从哪来FSDB 里默认只有信号跳变。断言的 attempt/success/failure 属于额外的一类信息需要显式打开。不同版本里这个开关的位置不一样有的在$fsdbDumpvars的选项参数里有的走 plusarg。我不建议背参数名建议这么做先按下面的最小配置跑通一次然后在 Verdi 里打开断言视图看列表是不是空的——空的就是没 dump 上再去查当前版本的$fsdbDumpvars选项帮助。另外两个 dump 层面的细节容易被忽略层深别写小了。$fsdbDumpvars(0, tb_top)里的 0 表示全层深。如果你写成$fsdbDumpvars(3, tb_top)断言所在的那个子模块很可能刚好被切掉波形里就是找不到它的信号。别只 dump 顶层端口。断言内部引用的是叶子模块里的信号这些信号必须在 dump 范围内。2.3 一套可以直接抄的编译、仿真、查看组合下面这套是我本地 debug 时的固定配置改改文件列表就能用。vcs -full64 -sverilog -timescale1ns/1ps \ -kdb -debug_accessall \ -assert enable_diag \ -f rtl.f -f tb.f \ -o simv_debug \ -l comp.log-assert enable_diag打开断言的运行时诊断能力后面用系统任务动态开关断言就靠它。Testbench 里的 dumpinitial begin $fsdbDumpfile(wave.fsdb); $fsdbDumpvars(0, tb_top, all); end跑仿真./simv_debug fsdbautoflush -l sim.logfsdbautoflush值得加上。不加的话波形是攒在内存里定期刷盘的万一仿真中途崩了或者被 kill磁盘上的 FSDB 可能缺一大段——而断言失败往往恰好就发生在那一段里。打开 Verdiverdi -dbdir simv_debug.daidir -ssf wave.fsdb -nologo -dbdir给设计信息-ssf给波形两个都要有。只给波形你能看信号但点不进源码只给设计你能看结构但没有数据。3. 从一行失败日志走到波形上正确的采样沿3.1 先把日志拆成三个要素VCS 的断言失败信息里最有用的是三样东西字段例子怎么用断言实例名tb_top.u_dma.a_hs_done定位层次Verdi 里按这个路径搜信号和源码时间戳对started at 128450ns failed at 132450ns前者是触发点后者是结论点从前者开始看Offending 表达式Offending (!(ready !valid))告诉你具体是哪一段子表达式判负了第三项经常被忽略但它其实是最省事的一条线索。上面这个 offending 表达式说明在判定那一刻ready为高而valid为低。这一句话就把该看哪两根线、该看哪个值组合给定死了比在波形里瞎猜高效得多。如果属性里带了局部变量或者嵌套序列offending 表达式会显示成展开后的形式这时候需要对照源码里的原始写法来读。3.2 在 nTrace 和 nWave 之间来回切定位的动线大致是这样先在 nTrace 里加载源码文件列表编译时用的-f列表可以直接喂给 Verdi用信号搜索框输入断言实例名的层级或者直接打开断言所在的源文件找到assert property那一行。Verdi 对 SVA 有语法识别断言相关的关键字和表达式会被区分显示。找到那一行之后真正要做的不是看这行代码而是把它引用到的东西全部拉进波形。断言里出现的每一个信号都是这条属性成立与否的判据一个都不能少。做法很简单在 nTrace 里选中信号名右键加到波形或者在 nWave 里敲g调出信号选择窗口按层次加快捷键各版本可能不同菜单里找 Get Signals 也一样。这里有个容易漏的点断言里常常还引用了参数和宏。比如DEPTH、MAX_LATENCY这类如果这些值和你脑子里的假设不一致波形看到的东西就完全解释不通。Verdi 的信号选择窗口可以显示参数值值得花两分钟确认一遍。3.3 手工重建采样序列笨办法但最可靠当断言视图里没有数据、或者属性写得非常绕的时候手工重建是最后的兜底手段而且往往是最快的手段。步骤固定把时钟信号拖到波形顶部设为参考。把 antecedent|-左边那一整段涉及的所有信号按顺序排在下面。按##N的 N 值从started那一刻开始逐个采样沿读值在纸上或者注释里写下每一拍的组合。读到failed那一拍看看是哪一个子表达式先不成立。用 Verdi 的 Signal Event Report 能省不少事——选中几个信号指定时间窗口它会把这段时间内所有跳变按时间顺序列成一张表。有这张表你不需要用鼠标一格格挪。配合前面说的时钟网格对齐采样沿一个中等复杂度的属性通常十分钟内能读明白。提示如果读了几遍都自相矛盾先怀疑采样偏移问题回到 1.1 节的办法加一组sampled_dbg对照信号。我自己的经验是读不明白的属性里有一半以上是栽在这个偏移上。3.4 断言面板能省很多事但要先确认它有数据Verdi 有专门面向断言的视图不同版本入口位置不完全一样——常见的是在 nWave 的菜单里或者界面底部的标签页中。它列出的信息通常包括断言实例、类型assert / assume / cover、以及 attempt、success、failure 的统计。点某一条能直接跳到源码位置如果 FSDB 里带了断言事件还能看到它在时间轴上的分布。这个面板的价值在于你不需要再猜这条断言到底被触发了多少次。触发次数为零的断言失败列表当然是空的而你就会误以为它通过了——又回到 1.2 节的空洞成功问题。如果打开之后列表是空的说明 dump 里没有断言数据这时候老老实实退回 3.3 节的手工重建。别在这个界面上耗时间。4. 三类高频假故障的现场排查记录4.1 复位期间没被 disable iff 覆盖现象仿真刚起来几百纳秒一堆断言集中报错时间戳都挤在复位释放前后。原因基本是复位窗口没排除。断言在复位有效期间也在采样而那时候信号全是复位值任何一个正常业务的属性都会判负。标准写法property p_hs; (posedge clk) disable iff (!rst_n) valid ready |- ##[1:4] done; endpropertydisable iff的语义是当这个条件成立时把当前的尝试直接中止中止不算失败。所以正确覆盖复位之后复位区间的报错会全部消失。但这里有个更隐蔽的变体复位释放后的前一两拍仍然报错。这通常不是disable iff写错了而是复位本身是异步释放、同步生效的信号真正稳定下来比你想象中晚。这时候要么把disable iff的条件放宽一点比如用一个rst_sync_n而不是原始复位要么在属性前面加一个初始化完成的前置条件。我倾向于后者因为它不掩盖真实的时序。4.2 $past 在序列起始处返回的是初值现象只在仿真最开始报一次错之后一切正常。$past(sig, 1)在第一个时钟沿上取不到上一拍它会返回信号的初始值而初始值通常是 X。所以任何形如valid |- data $past(data)的属性在首个有效拍上都会判负。处理方式有几种我按推荐顺序排加一个有效窗口前置条件比如chk_en |- ...chk_en在初始化完成之后才拉高。这是最干净的做法因为它顺带把其他初始化阶段的问题一起挡掉了。把断言的起始点往后推一拍用##1跳过第一个采样。用$isunknown做前置过滤把首拍的未知值单独排除。我一般不推荐第三种作为首选因为它会让属性变得难读而且容易在别的地方留下漏洞。顺便说一个相关的坑timescale 不一致也会造成类似的偶发误报。RTL 和 testbench 如果没统一时间单位断言的采样点和波形的时间刻度会对不上表现就是有时候对有时候不对。-timescale编译选项统一加上别指望文件头各自的\timescale 指令能配合好。4.3 X 态参与求值导致的失败现象断言失败但波形上看两根线的值都在合法范围内看不出问题。这种情况要怀疑 X。当表达式里有 X 参与比较时结果是 X而在属性求值里 X 会被当作不成立处理于是失败。但 X 在波形上常常显示成一个不显眼的颜色缩放比例稍微大一点就看不见了。排查动线在 nWave 里用 Verdi 的 X 追踪能力从断言里涉及的那根异常信号往上追驱动源。常见来源是未初始化的寄存器、多驱动冲突、或者某个模块因为配置没例化导致输出悬空。找到源头之后处理方式通常是加复位初值或者修正例化条件而不是去改断言。注意千万不要为了让断言变绿而给断言加$isunknown屏蔽掉 X。X 是有价值的信息屏蔽掉它等于把问题藏起来等它变成更难查的功能性 bug 再回来找你。5. 不重新编译就能控制断言的几种手段5.1 用 disable iff 做条件门控调试阶段最常见的需求是先把噪音关掉专注看一条断言。如果每次都要改代码重编译一轮就是十几分钟效率太低。最稳的做法是在断言里预留一个门控条件property p_hs; (posedge clk) disable iff (!rst_n || !sva_en) valid ready |- ##[1:4] done; endpropertysva_en用一个独立的寄存器或者$test$plusargs控制跑仿真时通过 plusarg 切换。这样不重编译就能筛掉不关心的断言同时被筛掉的尝试会被中止而不是判负日志会干净很多。5.2 运行时开关和它的注意事项VCS 在-assert enable_diag下提供了一组系统任务可以在仿真过程中动态地开启、关闭、中止断言粒度可以细到某个层次。这对定位问题特别有价值——比如你怀疑某条断言干扰了时序可以先只留它一条跑一小段看看。需要注意的是这组系统任务的参数编码是有讲究的控制类型、断言类型、指令类型各自一套取值写错了不报错但行为不符合预期。我不建议凭记忆写用的时候对着手册确认一遍参数。相比之下5.1 节的门控方式虽然要提前埋点但语义直白、不出错日常我更常用它。另外两个减少噪音的编译期开关值得知道一个是限制失败打印数量的选项避免一个错法重复报几千次把日志刷爆一个是前面提到的空洞成功上报。两者的具体选项名随版本变化vcs -h里搜 assert 相关条目能全找出来。5.3 bind 进来的断言怎么定位层次很多团队的断言不是写在 RTL 里的而是通过bind挂上去的放在一个独立的验证文件里。这种情况下你在 Verdi 的层次树里按 RTL 模块名找是找不到断言实例的因为它的实例层次挂在 bind 的目标模块下面。定位方法直接在 nTrace 里打开那个 bind 文件找到assert property那一行然后用 Verdi 的层次跳转能力看它的实例路径或者查编译日志里断言的完整层次名。拿到完整路径之后不管是在波形里搜信号还是在日志里过滤都有了准确的关键词。6. 几个踩过坑之后才养成的习惯第一个习惯新写的断言先故意弄坏一次。改一行 RTL 或者改一个测试激励让它必然违反那条属性确认它真的红了再改回来。不做这一步你永远不知道那条断言是真在检查还是因为前置条件从来没触发过而在空转。我做过统计团队里第一轮加的断言大约有相当一部分在首次反向验证时暴露出了问题——不是没生效就是判据写反了。这个动作花十分钟能省掉后面几天的以为验证过了。第二个习惯失败日志先看 offending 表达式再看波形。前者是工具替你做的一次表达式化简告诉你到底哪个子条件不成立比肉眼在几十根线里找快得多。跳过这一步直接扑到波形上是我早期最常见的效率浪费。第三个习惯怀疑采样偏移的时候直接加对照信号不要继续推理。推理的成本随着属性复杂度指数上升而加一组sampled_dbg只需要一次编译。算下来永远是加信号更便宜。第四个习惯debug 用的编译配置和回归用的分开。-kdb -debug_accessall这套只在你本地需要看波形的时候用回归任务里带它纯属浪费机器时间。写两个编译脚本别偷懒用一个。第五个习惯也是我觉得最值钱的一个把每次读断言的时间戳、采样沿、子表达式结论记下来。我自己有一份 markdown 笔记专门记某条属性在某个场景下报错最后是哪个子表达式的问题。时间一长同一类问题再出现翻笔记比从头推理快十倍。断言 debug 这件事很多时候卡住不是因为难而是因为每次都在从零开始理解同一条属性的时间语义。
返回列表