1. 项目概述:在Batch Mode下捕获仿真波形
在数字电路设计,尤其是ASIC和FPGA验证流程中,仿真波形是我们调试和分析设计行为的“眼睛”。对于使用Cadence NC-Verilog/Xcelium的工程师来说,交互模式(Interactive Mode)下通过GUI添加信号、运行仿真、查看波形是常规操作。然而,当我们需要进行大规模的回归测试、长时间的后仿,或者在无图形界面的服务器/计算节点上运行任务时,Batch Mode(批处理模式)就成了唯一的选择。在这种模式下,仿真器从头到尾自动执行,没有人工干预的环节,那么如何确保关键的波形数据被自动保存下来,以供后续离线分析呢?这就是我们今天要解决的核心问题:配置NC-Verilog在Batch Mode下自动保存指定信号的仿真波形到数据库文件。
很多工程师初次接触Batch Mode仿真时,会遇到一个尴尬的局面:仿真成功跑完了,日志也没有报错,但就是找不到波形文件,无法进行时序和逻辑的深入分析。手动编写一个TCL脚本,在仿真启动时自动执行“探针(Probe)”命令,是解决这个问题的标准且高效的方法。本文将从一个资深验证工程师的角度,详细拆解如何通过一个精心编写的startup.tcl脚本,实现对仿真波形的灵活、自动化捕获。无论你是正在搭建自动化验证环境,还是需要为某个特定测试保存深度调试信息,这套方法都能直接拿来复用。
2. 核心思路与脚本设计解析
在Batch Mode下让NC-Verilog保存波形,其核心思路是利用仿真器提供的初始化和预加载机制。NC-Verilog支持在启动时通过+ncinput+选项加载一个TCL脚本,这个脚本会在仿真内核初始化之后、开始运行(run)之前自动执行。我们可以在这个脚本里,放置所有需要在仿真前配置好的命令,其中最关键的就是probe命令。
2.1probe命令的角色与工作原理
probe命令是Cadence仿真器中用于创建信号数据库的瑞士军刀。你可以把它理解为一个“示波器探头”的自动化配置工具。在交互模式下,你在波形查看器(SimVision)里用鼠标点选信号,后台其实就是生成了对应的probe命令。在Batch Mode下,我们通过TCL脚本提前写好这些“探头”的配置,告诉仿真器:“请你在运行过程中,持续记录这些信号的变化,并把数据存到指定的文件里。”
这个命令的强大之处在于它的高度可配置性。它不仅仅能决定记录哪些信号(-all或指定层次),还能决定记录数据的格式(-shm,-vcd,-evcd)、数据库的名称(-database)、以及记录的层次深度(-depth)。理解每个参数的含义和适用场景,是写出高效、实用脚本的前提。
2.2 脚本设计的关键考量点
设计startup.tcl脚本时,我们需要权衡几个关键因素:
- 信号范围与深度:是记录整个测试平台(TB)的所有信号,还是只记录核心设计模块(DUT)的信号?记录的层次结构要深入到什么程度?过深的记录会导致波形文件(SHM数据库)体积爆炸式增长,严重影响仿真性能和磁盘I/O;过浅则可能丢失关键的子模块内部状态,不利于调试。
- 数据格式选择:SHM、VCD、EVCD各有优劣。SHM是Cadence原生的高性能二进制格式,文件小、读写快,但只能用SimVision或Cadence工具查看。VCD是IEEE标准的文本格式,通用性强,任何支持VCD的工具都能读,但文件巨大,生成和加载都很慢。EVCD是VCD的扩展,支持更多的数据类型。
- 运行控制:脚本的结尾必须是
run命令吗?不一定。probe命令配置的是数据记录器,而run是启动仿真运行的命令。在更复杂的脚本中,我们可以在probe之后进行其他设置,比如设置断点(stop)、或执行一段自定义初始化TCL代码,最后再调用run。对于最简单的“保存波形并跑完”这个需求,probe后直接接run是最直接的。
3. 脚本编写与参数详解
让我们从一个最基础、最常用的脚本实例开始,然后逐步分解每个参数,并探讨如何根据实际项目需求进行定制。
3.1 基础脚本实例
创建一个纯文本文件,命名为startup.tcl,内容如下:
# startup.tcl - 自动保存仿真波形 probe -create -shm -database my_waveform -all -depth 2 run这个脚本做了两件事:
- 创建(
-create)一个波形探针,使用SHM格式(-shm),将数据保存到名为my_waveform.shm的数据库(-database my_waveform),记录所有(-all)信号的波形,记录层次深度为2(-depth 2)。 - 执行仿真运行(
run),直到遇到$finish系统任务或达到指定的时间。
3.2 核心参数深度解析
3.2.1 信号选择参数:-allvs. 指定路径
-all:这是一个“省心但可能昂贵”的选项。它会让仿真器记录当前仿真环境中所有可见的信号。在大型设计中,这会产生海量数据。适用场景:小型模块验证、或初期调试时不确定问题出在哪里,需要全局视野。- 指定信号路径:这是生产环境推荐的做法。你可以使用通配符来精确定位需要关注的信号集,极大地减少数据量。
实操心得:我通常会为不同的测试场景准备不同的# 只记录测试平台顶层信号 probe -create -shm -database my_waveform tb.* -depth 0 # 记录DUT顶层及其下一级子模块的所有信号 probe -create -shm -database my_waveform dut.* -depth 1 # 混合记录,同时添加特定关键信号 probe -create -shm -database my_waveform dut.* sys_clk sys_rst_n tb.monitor.err_count -depth 1startup.tcl脚本。例如,wave_full.tcl用于记录所有信号进行深度调试,wave_dut_only.tcl用于回归测试时只记录DUT接口和内部关键状态,wave_cover.tcl则专门记录与功能覆盖点相关的信号。通过Makefile或脚本根据仿真目标动态选择加载,实现灵活性和效率的平衡。
3.2.2 数据库格式参数:-shm,-vcd,-evcd
-shm(Shared Memory):默认且最常用的格式。仿真器将波形数据写入一个二进制的SHM数据库文件(.shm目录)。它的优势是速度快、文件相对紧凑,并且与SimVision工具链无缝集成,支持反向标注(back-annotation)、信号值查询等高级调试功能。绝大多数情况下,你应该首选-shm。-vcd(Value Change Dump):生成一个文本格式的.vcd文件。它的唯一优势是通用性,可以被GTKWave、Synopsys DVE等其他厂商的波形查看器打开。致命缺点是性能:文本解析和写入速度慢,文件体积通常是SHM格式的10倍甚至100倍以上。仅在需要与其他非Cadence工具链交换波形数据时使用。-evcd(Extended VCD):VCD的增强版,支持reg和wire以外的更多数据类型。优缺点与VCD类似。注意:
-shm、-vcd、-evcd是互斥的选项,一次probe只能选择一种格式。如果你需要同时生成SHM和VCD,必须写两条probe命令。
3.2.3 层次深度参数:-depth N
这个参数控制信号记录的“纵向”深度。
-depth 0:只记录你指定的路径本身的信号。例如probe -create tb.dut.* -depth 0,只会记录tb.dut这一层下的所有信号(如tb.dut.clk,tb.dut.data),但不会记录tb.dut.u_submodule子模块内部的任何信号。-depth 1:记录指定路径及其下一级子模块的所有信号。这是最常用的设置,因为它既能抓住模块接口和关键内部寄存器,又不会让数据量失控。例如,对于tb.dut,深度1会记录tb.dut.*以及tb.dut.u_ram.*,tb.dut.u_ctrl.*等直接子模块的信号。-depth 2或更高:记录更深层次。随着深度增加,信号数量呈指数级增长。务必谨慎使用,通常只在追踪一个非常具体的、深埋于层次结构中的bug时才临时启用。- 省略
-depth:如果不指定,仿真器使用默认深度1。所以在我们最初的例子中,-depth 2是可以省略的,效果等同于-depth 1。但显式地写出来是一个好习惯,可以让脚本的意图更清晰。
3.2.4 数据库名称参数:-database NAME
- 这个参数指定生成的波形数据库文件的基本名。对于SHM格式,最终会生成一个名为
NAME.shm的目录。对于VCD格式,会生成NAME.vcd文件。 - 省略
-database:如果不指定,NC-Verilog会使用一个默认的数据库名,通常是ncsim.shm(SHM格式)或ncsim.vcd(VCD格式)。在自动化环境中,强烈建议显式指定一个有意义的名称,例如包含测试用例名、时间戳或种子号(-database test_alu_randseed12345),这样可以避免多次仿真输出相互覆盖,便于归档和追溯。
4. 完整实操流程与集成方法
掌握了脚本的编写方法后,我们需要将其集成到实际的仿真流程中。下面是一个从环境准备到执行分析的完整闭环流程。
4.1 环境准备与脚本创建
首先,在你的仿真工作目录下,使用任意文本编辑器(如Vim, VSCode, gedit)创建startup.tcl文件。根据你的调试需求,选择上述参数组合编写脚本内容。一个针对中等规模DUT的推荐配置如下:
# 文件名:wave_dut_debug.tcl # 描述:记录DUT及其子模块信号,用于功能调试 probe -create -shm -database waves/${TESTNAME}_${SEED} dut.* -depth 1 probe -create -shm -database waves/${TESTNAME}_${SEED} tb.clk tb.rst_n tb.start tb.done tb.error -depth 0 probe -create -shm -database waves/${TESTNAME}_${SEED} tb.monitor.* -depth 0 run这个脚本做了几件聪明的事:
- 使用了变量
${TESTNAME}和${SEED}来动态生成数据库名,这需要你在调用NC-Verilog前定义这些TCL变量,或者通过环境变量传递。 - 将波形数据库统一输出到
waves/子目录下,保持工作区整洁。 - 分层记录:对DUT记录深度1,对TB顶层的关键控制信号只记录自身(深度0),对特定的监控模块记录其所有信号。
4.2 启动仿真并加载脚本
在终端中,使用ncverilog或xrun(Xcelium的启动命令)命令启动仿真,并通过+ncinput+选项指定你的TCL脚本。
# 基本用法 ncverilog +ncinput+startup.tcl -f filelist.f +define+SIMULATION top_tb.v # 更实际的例子,包含常见选项 xrun \ -f ../rtl/filelist.f \ -top top_tb \ -access +rwc \ -input wave_dut_debug.tcl \ +define+POST_SIM \ +UVM_TESTNAME=my_test \ -svseed random \ -l simulation.log命令解释:
-input wave_dut_debug.tcl: 这是+ncinput+的另一种等效写法,告诉仿真器在初始化后执行该TCL脚本。-access +rwc: 确保对设计文件的读写权限,这对于probe命令正常工作有时是必要的。-l simulation.log: 将仿真器的标准输出和错误重定向到日志文件,便于批处理运行后查看结果。
4.3 仿真执行与输出确认
仿真启动后,你会在终端或日志文件中看到仿真进度。如果脚本加载成功,通常会有类似以下的输出:
ncsim> source wave_dut_debug.tcl Creating shm database waves/test_alu_randseed12345 Probing signals... ... ncsim> run仿真结束后,检查当前目录或waves/目录下,是否生成了test_alu_randseed12345.shm文件夹及其内部的一系列数据文件。
4.4 波形查看与分析
生成SHM数据库后,你可以使用Cadence的SimVision工具进行离线查看和分析。
# 打开SimVision并加载指定的波形数据库 simvision waves/test_alu_randseed12345.shm &在SimVision中,你可以像在交互仿真中一样,添加信号到波形窗口,进行测量、标记、查找等所有调试操作。这就是Batch Mode仿真的价值所在:将计算密集型的仿真过程与图形化的调试分析过程解耦。你可以在高性能服务器上无头(headless)运行大量夜间回归测试,第二天早上再在本地工作站上加载感兴趣的用例波形进行详细分析。
5. 高级技巧与避坑指南
在实际工程应用中,仅仅掌握基础命令是不够的。下面分享一些能显著提升效率和可靠性的高级技巧,以及我踩过的一些“坑”。
5.1 动态脚本生成与集成
在复杂的UVM或基于Makefile的验证环境中,硬编码的startup.tcl可能不够灵活。我常用的方法是使用脚本(如Perl、Python或Shell)动态生成TCL文件。
示例:通过Makefile集成
TEST_NAME := alu_random_test SEED := $(shell echo $$RANDOM) WAVE_DB := waves/$(TEST_NAME)_seed$(SEED).shm sim_wave: mkdir -p waves echo "probe -create -shm -database $(WAVE_DB) dut.* -depth 1" > dynamic_wave.tcl echo "probe -create -shm -database $(WAVE_DB) tb.clock tb.reset tb.scoreboard.* -depth 0" >> dynamic_wave.tcl echo "run" >> dynamic_wave.tcl xrun -f filelist.f -top tb -input dynamic_wave.tcl +TESTNAME=$(TEST_NAME) +SVSEED=$(SEED)这样,每次运行make sim_wave都会生成一个包含随机种子和测试名的唯一波形数据库,完美避免了文件覆盖。
5.2 性能优化与磁盘空间管理
波形记录是仿真性能的主要瓶颈之一。以下是一些优化策略:
- 按需记录,切忌贪婪:这是最重要的原则。永远不要在生产环境的回归测试中使用
-all。仔细定义你的调试信号集合。 - 使用
-depth明智地裁剪:大部分bug通过模块接口和一级子模块的内部状态就能定位。将-depth设为1作为默认值。 - 分时段记录:有时bug只出现在仿真的特定阶段。你可以使用TCL的
when命令,在特定时间点才开始记录。# 在仿真时间100ns后开始记录波形 when {now > 100ns} { probe -create -shm -database late_wave dut.counter -depth 0 } run 200ns - 定期清理:SHM数据库是目录,直接
rm -rf即可。建议在自动化脚本中加入归档和清理逻辑,例如只保留最近一周的波形,或者只保留失败的测试用例波形。
5.3 常见问题排查实录
问题1:仿真运行了,但没有生成任何波形文件。
- 可能原因1:
probe命令语法错误或路径不存在。仿真器在source TCL脚本时遇到错误,可能静默失败或报错在日志中。排查方法:仔细检查仿真日志文件(simulation.log),搜索“Error”、“Warning”以及你的TCL脚本文件名。确保信号路径在仿真环境中存在。 - 可能原因2:脚本中没有
run命令。probe只是设置了记录器,没有run,仿真不会执行。确保脚本最后有run、run 1ms或类似的执行命令。 - 可能原因3:数据库文件路径权限问题。如果指定了像
waves/这样的子目录,确保该目录存在且可写。可以在TCL脚本开头加入file mkdir waves来创建目录。
问题2:波形文件生成了,但SimVision打开后看不到预期的信号。
- 可能原因1:
-depth设置过浅。如果你指定了tb.dut.* -depth 0,那么你只能看到tb.dut这一层的信号,子模块tb.dut.u_core内部的信号不会被记录。尝试增加深度或直接指定子模块路径。 - 可能原因2:信号在仿真期间从未发生变化。对于常量或初始值后一直保持不变的信号,有些视图设置下可能默认不显示。在SimVision中尝试使用“Add All Signals in Region”功能。
- 可能原因3:信号被优化掉了。如果某个信号在代码中未被“使用”(例如,只被驱动,但它的值不影响任何其他逻辑或输出),综合或仿真工具可能会将其优化掉。在RTL代码中为该信号添加
(* keep *)或/* syn_keep */等属性(具体语法取决于工具),防止其被优化。
问题3:仿真速度因为记录波形而变得极慢。
- 立即行动:检查你的
probe命令。你是否使用了-all?-depth是否设置得过大(比如5或99)?信号列表是否包含了像tb.*这样庞大的总线?优化策略:立即将记录范围缩小到最小的可疑模块。使用-depth 1。考虑是否真的需要记录整个测试平台的波形,也许只记录DUT和比对器(checker)的信号就足够了。
问题4:需要生成VCD给第三方工具,但文件太大。
- 妥协方案:VCD文件庞大是无法根本解决的缺点。可以尝试以下方法缓解:
- 只记录最关键、最少的信号。
- 缩短记录的时间范围(例如,只记录出错前后的一段仿真时间)。
- 考虑使用压缩工具对生成的
.vcd文件进行压缩(如gzip),在需要时再解压查看。不过,这增加了操作步骤。 - 评估是否真的必须使用VCD。如果团队内部都使用Cadence工具链,极力说服大家使用SHM格式,可以节省大量时间和存储空间。
掌握在Batch Mode下保存波形的技能,是数字设计验证工程师迈向高效自动化工作流的关键一步。它让你摆脱了对图形界面的依赖,使大规模验证、持续集成和远程计算成为可能。从今天起,为你每一个重要的测试用例配上定制的波形记录脚本吧。