
1. 为什么选择FSDB波形格式对比与dump前置检查1.1 VCD、VPD、FSDB到底有什么区别很多初学者上来就搜“怎么dump波形”结果搜出来的答案五花八门有的用$dumpfile生成VCD有的用VPD有的用FSDB。这三个能干什么、什么场景用什么其实需要先搞清楚。VCD是IEEE标准几乎所有仿真器都支持通用性最强但文件膨胀得非常厉害仿真时间稍长就是几个GB甚至几十GB而且载入Verdi后看信号层级、查驱动关系都慢。VPD是VCS自带的波形格式用$vcdpluson这类任务生成Verdi也能读部分版本但跨工具兼容性一般尤其当你用irun跑仿真、想统一用Verdi看波形时VPD帮不上忙。FSDB是Verdi生态的专有格式特点是压缩率高、加载快配合Verdi的nWave看波形、追RTL代码、看状态机跳转体验是最好的。所以在公司里主流做法就是“VCS/irun跑仿真dump FSDBVerdi里debug”。有些需求确实用VCD就够了比如做功耗分析或者给第三方工具喂数据但如果你只是想快速定位一个功能bugFSDB是首选。同样一段仿真FSDB通常比VCD小一个数量级打开、缩放、搜索信号都顺滑得多。1.2 dump前先确认这四件事实际项目里dump不出FSDB八成不是代码写错而是环境问题。我建议每次动手前按这个清单过一遍第一确认Verdi安装好了并且$VERDI_HOME环境变量指向正确。FSDB不是仿真器自带的是Verdi提供的PLI能力仿真器必须能加载到Verdi的库。第二确认仿真器版本和PLI库位宽、路径匹配。比如64位的仿真器必须加载64位的novas_pli_boot.so路径一般在$VERDI_HOME/share/PLI下面按仿真器厂商分目录。第三确认testbench的模块层次名写对了。$fsdbDumpvars(0, tb_top)这里的tb_top必须是你顶层模块的实例名写错虽然不报错但生成的文件可能是空的。第四确认运行目录可写。有些回归平台把仿真目录放到只读路径最后fsdb文件写不进去仿真日志又没提示排查起来非常费劲。这四件事看起来琐碎但每一条我都见过真实案例。尤其是PLI路径和层次名这两个最容易让人怀疑人生。1.3 工具链的基本使用流程先把完整流程捋一遍写testbench和DUT用VCS或irun编译编译时打开debug权限并链接Verdi的PLI运行仿真时testbench里的系统任务就会把信号写入fsdb文件仿真结束后用Verdi打开fsdb再加载RTL源码进入调试界面。这个流程里dump方法的选择只是其中一环但决定了你能不能顺利用上后端的整个调试体验。下面三种方法不是互斥的而是对应不同场景。我把它们分别称为“裸写系统任务”“宏开关包裹”“脚本层控制”接下来逐个拆解。2. 方法一testbench里直接调用fsdb系统任务最快跑通2.1 两个最常用的系统任务fsdbDumpfile与fsdbDumpvars第一种方法最简单直接在testbench的initial块里写两行initial begin $fsdbDumpfile(tb_counter.fsdb); $fsdbDumpvars(0, tb_counter, all); end$fsdbDumpfile用来指定输出文件名不写后缀也会自动补.fsdb$fsdbDumpvars是核心三个参数从左到右分别是层级深度、起始模块实例、附加选项。层级深度为0表示从指定模块往下全部dump1表示只dump指定模块这一层。这里有个小细节如果文件路径带目录比如想把波形统一放到wave目录记得先mkdir wave然后写$fsdbDumpfile(wave/tb_counter.fsdb)PLI不会帮你自动建目录目录不存在时波形文件会静默失败仿真终端也不会有任何报错。2.2 不同层级参数控制dump全部还是只dump一层对于一个小的testbench直接(0, tb_counter, all)没问题。但真实项目里顶层下面挂着一大堆子模块、SRAM模型、模拟接口、第三方VIP全dump的结果就是文件巨大、仿真变慢。这时候就需要控制层级。例如// 只dump顶层下一层 $fsdbDumpvars(1, tb_top); // 只dump某一个子模块及其下层 $fsdbDumpvars(0, tb_top.u_core.u_alu); // 只追踪指定信号的驱动关系 $fsdbDumpvars(0, tb_top, signal_name);另外all这个选项会额外把一些默认不记录的信号也记下来比如wire类型里某些延迟变化。想要精确控制可以不用它默认行为已经够用。我个人的习惯是先不加all等发现某个信号在波形里看不到、需要追驱动关系时再加上它重新跑一遍这样能避免每次都生成超大文件。2.3 VCS编译运行命令与irun的PLI加载差异VCS下如果Verdi装好了编译命令里加一个-fsdb参数就行vcs -sverilog -debug_accessall -fsdb -f filelist.f -o simv ./simv-debug_accessall是为了保留所有信号的可访问性-fsdb则告诉VCS在链接时把Verdi的FSDB PLI带进来。没有这个参数仿真时你会看到类似$fsdbDumpvars is not a system task的报错或者编译阶段就挂在系统任务上。irun/xrun这边不太一样它不会自动去找Verdi的PLI需要你显式加载irun -sverilog -access r \ -loadpli1 $VERDI_HOME/share/PLI/NCSIM/LINUX64/novas_pli_boot.so:novas_pli_boot \ -f filelist.f其中-access r相当于打开读权限-loadpli1是加载PLI库并指定启动函数。这里有个坑不同版本的Cadence仿真器和Verdi路径可能不同有的叫NCSIM有的叫XLM建议先执行ls $VERDI_HOME/share/PLI确认目录再填。2.4 验证结果Verdi里能看到什么仿真结束后当前目录会出现tb_counter.fsdb。打开Verdiverdi -f filelist.f -ssf tb_counter.fsdb 界面左侧是源码中间或右侧是波形窗口。能正常看到信号波形旋转、缩放、光标测量说明第一条路已经通了。到这一步很多人会以为大功告成但真正上回归平台时很快会遇到新问题这个testbench是公共的别人跑回归也被迫生成了fsdb。所以就有了方法二。3. 方法二用宏开关把dump包起来让回归脚本说了算3.1 工程上为什么很少直接裸写dump代码直接调用系统任务确实最快但有现实问题很多项目里testbench是所有人共用的你在里面直接写了dump代码别人跑回归时也会跟着生成fsdb文件。几分钟的用例还好几个小时的用例直接写满磁盘甚至影响整个回归任务的调度结果。所以在工程上dump代码几乎不会裸写。通用的做法是提前在testbench里放好一个“开关”平时默认关闭需要调试时在命令行传一个宏把它打开。这样既能保留随时dump的能力又不会污染其他人的回归结果。3.2 ifdef DUMP_FSDB的标准写法宏开关的写法其实很简单就是在原来的dump代码外面加一个条件编译ifdef DUMP_FSDB initial begin $fsdbDumpfile(tb_counter.fsdb); $fsdbDumpvars(0, tb_counter, all); end endif需要注意反引号不能少这是Verilog的编译宏不是C语言。用的时候宏名可以随便起但建议在团队的验证环境规范里统一比如都用DUMP_FSDB这样不同人写的testbench放在同一个回归平台上行为一致不会出现一个开一个关的混乱局面。3.3 VCS和irun下用define控制开关编译时加defineDUMP_FSDB就能打开不加就关闭VCSvcs -sverilog -debug_accessall -fsdb defineDUMP_FSDB -f filelist.f -o simv ./simvirunirun -sverilog -access r defineDUMP_FSDB \ -loadpli1 $VERDI_HOME/share/PLI/NCSIM/LINUX64/novas_pli_boot.so:novas_pli_boot \ -f filelist.f对于回归脚本来说这就是一个参数的事。需要追bug时单独跑一条带defineDUMP_FSDB的用例不需要时整片回归不加这个宏磁盘压力和仿真时间都能接受。我在实际项目里通常还会把回归脚本的日志目录和fsdb目录分开比如在Makefile里定义一个变量sim: vcs -sverilog -debug_accessall -fsdb defineDUMP_FSDB \ -f filelist.f -o simv ./simv -l sim.log这样调试完一个bug波形文件和仿真日志都能快速找到不会混在一堆中间文件里。3.4 让宏更灵活的进阶玩法文件名、层级、时间窗宏开关只是第一步实际用的时候还可以结合其他宏把控制粒度做得更细。比如用宏传递文件名ifdef DUMP_FSDB initial begin ifdef FSDB_FILE_NAME $fsdbDumpfile(FSDB_FILE_NAME); else $fsdbDumpfile(default.fsdb); endif $fsdbDumpvars(0, tb_top); end endif编译时指定文件名vcs -sverilog -debug_accessall -fsdb defineDUMP_FSDB defineFSDB_FILE_NAME\case1.fsdb\注意引号在shell里要转义这是很多人都踩过的小坑。层级控制也可以用宏做比如defineDUMP_LEVEL2配合DUMP_LEVEL使用。时间窗控制则可以在代码里加延迟让dump在仿真开始一段时间后再打开或者先dump一段、关掉再在关键阶段重新打开ifdef DUMP_FSDB initial begin $fsdbDumpfile(tb_counter.fsdb); $fsdbDumpvars(0, tb_counter); // 仿真前1000ns不记录1000ns后再开始 #1000; $fsdbDumpvars(0, tb_counter); end endif不过这里要说明$fsdbDumpvars重复调用时如果参数一致是幂等的如果想中途停止dump再重新开始可以用$fsdbDumpoff和$fsdbDumpon这一对任务。这类高阶用法不常用但在长时间仿真时能救命。4. 方法三irun的-input脚本方式不改代码也能dump4.1 什么场景需要不改代码实际项目里还有一种很头痛的情况testbench是IP厂商或第三方团队给的不方便改动或者是已经回归了一轮的代码你不想为了一次调试去动公共文件。这时候最好能通过仿真器启动脚本把FSDB打开。irun/xrun正好支持这种方式。4.2 database与probe命令的用法新建一个dump.tcl文件内容如下database -open test.fsdb -fsdb probe -create -database test.fsdb -all -depth all run exit然后启动仿真时通过-input把这个脚本喂进去irun -sverilog -access r -input dump.tcl -f filelist.f这里database -open创建一个fsdb数据库文件probe -create指定要记录哪些信号-depth all表示记录下来全部层级。run和exit是让仿真器在读完脚本后继续运行、结束后退出。这个方法的好处是testbench文件一行不用改特别适合在回归平台上给指定用例额外开波形。不过要注意probe命令的-depth all写法在不同版本的irun里可能有细微差异。如果你用的是xrun可以在xrun的help文档里搜一下probe -create的完整参数说明或者直接先跑一个小例子确认语法。4.3 VCS中的替代思路VCS本质上没有完全对等的-input方式但思路类似你也可以在编译仿真命令里做控制。最常用的是方法二里的宏开关把dump代码预留好回归脚本通过defineDUMP_FSDB控制。另一个思路是把dump代码放在一个单独的systemverilog文件里比如fsdb_dump.sv然后用文件列表的取舍来决定要不要把这段代码编进去。无论是宏还是文件列表目的都是把“是否dump”变成命令行参数而不是靠人工改代码。// fsdb_dump.sv module fsdb_dump; initial begin $fsdbDumpfile(top.fsdb); $fsdbDumpvars(0, tb_top); end endmodule需要时就把它加进filelist.f不需要就从文件列表里去掉。这个方式在纯VCS项目里很实用因为不用去改别人维护的testbench主体。4.4 三种方法对比与选型建议为了让大家选的时候更清楚我把三种方法放在一起对比方法是否需要改testbench适合场景VCS用法irun用法方法一直接调用系统任务需要单次调试尽快看波形-fsdb-loadpli1加载PLI方法二宏开关包裹需要但只需一次日常回归按需开关defineDUMP_FSDBdefineDUMP_FSDB方法三启动脚本控制不需要第三方代码、批量回归用宏或文件列表替代-input dump.tcl我的建议很简单个人临时调试用方法一项目回归里统一用方法二遇到不能改代码的场景再上方法三。三种方法不是互斥的很多团队实际上是方法二预留宏、方法三在脚本层兜底两套并行。5. 一个能直接跑的完整testbench示例5.1 示例代码8位计数器DUT与tb下面给出一套完整、可直接跑的示例DUT是一个8位计数器// counter.v module counter #( parameter WIDTH 8 ) ( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count 0; else if (en) count count 1b1; end endmoduletestbench用SystemVerilog写把宏开关也放进去这样方法一和方法二都可以直接体验// tb_counter.sv timescale 1ns/1ps module tb_counter; parameter WIDTH 8; logic clk; logic rst_n; logic en; logic [WIDTH-1:0] count; initial clk 0; always #5 clk ~clk; counter #(.WIDTH(WIDTH)) u_counter ( .clk (clk), .rst_n(rst_n), .en (en), .count(count) ); initial begin rst_n 0; en 0; repeat (5) (posedge clk); rst_n 1; en 1; repeat (20) (posedge clk); en 0; repeat (5) (posedge clk); $finish; end ifdef DUMP_FSDB initial begin $fsdbDumpfile(tb_counter.fsdb); $fsdbDumpvars(0, tb_counter, all); end endif endmodule这个testbench本身是完整可跑的时钟10ns一个周期先复位5拍再使能计数20拍最后停止5拍结束仿真。仿真时间不长生成的fsdb也不大适合新手用来验证环境是否真的通了。5.2 完整编译运行步骤文件列表filelist.f里写上两个文件路径counter.v tb_counter.svVCS完整命令export VERDI_HOME/path/to/your/verdi vcs -sverilog -debug_accessall -fsdb defineDUMP_FSDB -f filelist.f -o simv ./simvirun完整命令先通过ls $VERDI_HOME/share/PLI确认路径irun -sverilog -access r defineDUMP_FSDB \ -loadpli1 $VERDI_HOME/share/PLI/NCSIM/LINUX64/novas_pli_boot.so:novas_pli_boot \ -f filelist.f跑完之后检查一下有没有tb_counter.fsdb生成有就说明环境通了。想试方法三就新建dump.tcl把testbench里的宏拿掉或者不传defineDUMP_FSDB用-input dump.tcl启动也能看到同样的文件。5.3 打开Verdi查看FSDB的步骤波形文件生成后用Verdi打开verdi -f filelist.f -ssf tb_counter.fsdb 打开后先把RTL源码加载进来再在波形窗口里添加顶层信号比如clk、rst_n、en、count。如果一切正常你会看到count从0计数到20然后停止。看到这个波形说明从编译到dump再到波形查看整条链路都是通的。还有一个实用技巧Verdi里按n键可以快速切换到下一组信号按z放大波形、Z缩小波形。如果信号太多看不清可以直接在波形窗口里输入信号名过滤。这些操作看起来基础但很多人在突发调试时反而想不起来浪费不少时间。5.4 常见坑与排查PLI加载失败、空白波形、深度不够最后把我在实际中踩过的坑集中列一下对照排查能省很多时间。报错$fsdbDumpvars is not a system task说明PLI没加载。VCS检查有没有加-fsdbirun检查-loadpli1路径和启动函数名。编译通过但仿真一运行就崩溃多半是PLI库的架构和仿真器不匹配比如64位仿真器加载了32位库或者Verdi版本和仿真器版本差距太大。fsdb文件生成但打开是空的先查testbench里的模块名和$fsdbDumpvars里写的实例名是否一致。比如顶层模块叫tb_counter但顶层实例名可能叫u_tb写错就是空文件。fsdb文件生成但信号不全检查$fsdbDumpvars的层级参数如果写(1, tb_counter)子模块内部信号不会记录。想全部记录第一参数写0。仿真极其缓慢、文件暴大缩小dump范围不要动不动就all加depth all只在需要的层级开波形。irun下读不到PLI路径先跑一下ls $VERDI_HOME/share/PLI看看实际目录结构不同版本差异比想象中大。第一个坑和第四个坑出现频率最高建议先把这两点记下来遇到了不用慌。还有一个容易忽略的点如果testbench里用了$finish结束仿真但fsdb文件还没来得及flush到磁盘也会出现文件不完整的情况。稳妥的做法是在$finish前加一句$fsdbDumpflush;强制把缓冲区内容刷到磁盘。虽然大多数情况下PLI会在仿真结束时自动flush但我在长时间仿真时遇到过几次偶发丢失后来统一在testbench模板里加上这一句再没出过问题。另外probe -create和database脚本我建议你专门用一个txt保存方便复用如果团队有多个仿真器版本脚本路径写绝对路径还是环境变量最好在交付前跟维护环境的同事确认。我自己现在的习惯是所有新建的testbench模板里都提前把ifdef DUMP_FSDB那段放进去这样后面所有用例都能直接加宏开波形不用回头补代码。调试一个bug通常就是先跑一遍不带宏的回归确认环境正常再单独加defineDUMP_FSDB跑一遍拉波形定位完之后把宏去掉再回归。这套流程看起来简单但确实能帮你省掉大量“满世界找代码”的时间。