简介:这份资源是面向硬件验证工程师、芯片设计师及半导体设计自动化从业者的 Cadence Xcelium(xrun)操作指南,兼顾初学者与有经验的技术人员。内容从 Linux 环境下的安装检查、单步与三阶段分离仿真讲起,系统梳理基础仿真选项,并深入解析 xcelium.d 目录作用、shm/db/fsdb 等波形文件生成、覆盖率收集策略,以及 Gate Level Simulation 中的 SDF 反标与参数调整,还针对复杂工程常见报错与 license 问题给出排查思路。资源包为 1 个 PDF 文件,约 505KB,便于随身查阅与快速检索。目前已有 2050 人学习下载,读者可借此掌握 xrun 从基础到进阶的完整操作脉络,积累调试与排错经验,在项目规划、日常仿真实施和故障排查中提升效率与准确性。
1. 从一次跑不通的回归说起:Xcelium 与 xrun 到底解决什么问题
如果你正在做数字 IC 验证,大概率遇到过这种场景:RTL 改了一行,回归跑了一整夜,第二天打开日志发现仿真在 3 小时前就卡死了,而波形文件只存了最后 10 微秒。更让人头疼的是,同一个 testcase 在同事机器上能过,在你这里就是编译报错,查了半天发现是某个-define宏没传进去。这类问题的根源往往不在设计本身,而在于仿真工具的使用方式——编译选项、运行参数、波形记录策略、多核并行配置,每一项都可能成为黑匣子。
Cadence 的 Xcelium 是目前数字前端验证里主流的仿真器之一,而xrun是它对外统一入口的命令行工具。很多人把它当成irun的替代品,实际上 xrun 把编译、精化、仿真三个阶段串成了一条流水线,用一套参数就能覆盖从单模块 smoke test 到全芯片回归的完整流程。这篇文章面向的是已经写过 SystemVerilog testbench、但还没把 xrun 用透的验证工程师,以及需要搭建回归环境的 CAD 人员。我会从最小可跑通的命令讲起,逐步拆到多核加速、覆盖率收集、波形按需 dump 这些高级用法,最后给出一套我常用的调试习惯。读完之后,你应该能独立搭起一个可复现、可扩展的 xrun 回归脚本,并且知道每个参数改了之后去哪里看效果。
2. xrun 的三段式流程与最小可跑通命令
2.1 编译、精化、仿真:xrun 到底替你做了哪些事
传统上,用xmvlog、xmelab、xmsim三步走是 Xcelium 的标准流程:先把 SystemVerilog 源码编译成中间格式,再精化出仿真可执行体,最后启动仿真内核。xrun的价值在于它把这三步封装成一次调用,根据文件后缀和参数自动判断每个文件该走哪个阶段。你给它.sv文件,它调xmvlog;给它.v文件,它同样处理;给它一个已经精化好的 snapshot,它直接进仿真。
这种封装带来的第一个好处是参数统一。比如-define宏定义,在传统流程里你需要在编译阶段传给xmvlog,在精化阶段可能还要再传一次给xmelab。而 xrun 里只需要写一次-define,它会自动分发到需要的阶段。第二个好处是增量编译。xrun 默认会检查每个源文件的修改时间,只重新编译变动的部分,这在大型回归里能省下大量时间。
但封装也意味着你需要理解它的阶段划分,否则遇到报错时根本不知道是编译问题还是精化问题。一个简单的判断方法:如果错误信息里出现xmvlog或near "xxx"这类语法层面的提示,就是编译阶段;如果出现xmelab或hierarchical name找不到,就是精化阶段;如果仿真跑起来之后才报$fatal或断言失败,那才是运行阶段的问题。
2.2 一个四行命令跑通 counter 仿真
假设你有一个最简单的计数器模块counter.sv和对应的 testbenchtb_counter.sv,下面这条命令就能跑通:
xrun -sv \ -access +rwc \ -timescale 1ns/1ps \ -top tb_counter \ counter.sv tb_counter.sv逐项说明:-sv告诉 xrun 按 SystemVerilog 标准解析源文件,不加这个参数时.sv文件可能被当成 Verilog-2001 处理,导致logic、always_ff这些关键字报错。-access +rwc开启读、写、连接三种访问权限,波形记录和force/release操作都依赖它,不加的话仿真能跑但 dump 波形时会发现信号全是红线。-timescale 1ns/1ps设置全局时间精度,如果源文件里已经写了`timescale,命令行参数会被覆盖,所以更稳妥的做法是在每个源文件里显式声明。-top tb_counter指定精化时的顶层模块名,xrun 会从这个模块开始展开层次,找不到就会报cannot find top module。
跑完之后当前目录下会生成xcelium.d目录,里面包含编译中间文件和精化后的 snapshot。下次再跑同样的命令,如果源文件没改,xrun 会直接复用 snapshot,启动时间从几十秒降到几秒。这个目录也是排查问题的入口:xcelium.d/xmvlog.log是编译日志,xcelium.d/xmelab.log是精化日志,仿真日志默认输出到xrun.log。
2.3 用 -f 文件管理上百个源文件
实际项目里不可能把所有源文件都写在命令行上。常见做法是建一个filelist.f,每行一个路径,支持+incdir+和-define:
// filelist.f +incdir+./include +incdir+./tb/include -define+SIMULATION -define+DEBUG_EN ./rtl/counter.sv ./rtl/alu.sv ./tb/tb_counter.sv ./tb/tb_alu.sv然后 xrun 调用时用-f指定:
xrun -sv -access +rwc -top tb_counter -f filelist.f这里有个容易翻车的点:-f文件里的路径是相对于 xrun 执行时的工作目录,不是相对于 filelist.f 所在目录。如果你在sim/目录下执行 xrun,而 filelist.f 放在sim/scripts/里,里面的./rtl/counter.sv就会解析成sim/rtl/counter.sv。我一般会在脚本里先cd到项目根目录再调 xrun,或者用绝对路径生成 filelist。另一个坑是-define在 filelist 里的写法:必须写成-define+SIMULATION,不能写成-define SIMULATION,后者会被当成两个独立的参数,导致宏定义失败但又不报错。
3. 多核加速、覆盖率与波形 dump 的参数调优
3.1 用 -mce 和 -parallel 把回归时间压下来
Xcelium 的多核引擎通过-mce开启,配合-parallel指定并行度。但并不是所有设计都能线性加速,我实测下来,一个中等规模的 SoC 验证环境在 4 核下大约能到 2.5 倍加速,8 核下到 3.5 倍左右,再往上收益就明显递减了。原因是仿真内核里有些操作是串行的,比如 PLI 回调、覆盖率采样、某些 SystemVerilog 约束求解。
xrun -sv -access +rwc \ -mce \ -parallel 4 \ -top tb_soc \ -f filelist.f-mce后面不加数字时默认使用所有可用核心,但我不建议这么做,因为回归机器上通常同时跑着多个 case,全占满会导致上下文切换开销反而拖慢整体吞吐。-parallel 4是显式指定 4 个线程,这个数字最好和你的 testbench 里fork-join的并发度匹配。如果 testbench 本身只有两个initial块在跑,开 8 个线程也是浪费。
还有一个隐藏参数-mce_license,控制多核 license 的获取策略。默认是-mce_license auto,工具会根据可用 license 数自动决定并行度。如果公司 license 紧张,可以设成-mce_license wait,让仿真排队等 license 而不是直接报错退出。这个参数在夜间回归脚本里特别有用,避免因为 license 争抢导致一半 case 失败。
3.2 覆盖率收集:-covfile 与 -covoverwrite 的配合
覆盖率是验证闭环的关键,但 xrun 默认不收集覆盖率,必须显式开启。最简做法是加-coverage all,但这会收集包括代码行、分支、条件、状态机、翻转在内的所有类型,仿真速度可能下降 30% 到 50%。更精细的做法是用-covfile指定一个覆盖率配置文件:
xrun -sv -access +rwc \ -coverage all \ -covfile cov.cfg \ -covoverwrite \ -top tb_soc \ -f filelist.fcov.cfg的内容可以按模块粒度控制:
select_coverage -module counter -block -expression select_coverage -module alu -fsm -toggle deselect_coverage -module tb_top -all这段配置的意思是:对counter模块收集块覆盖和表达式覆盖,对alu模块收集状态机覆盖和翻转覆盖,对tb_top不收集任何覆盖率。-covoverwrite的作用是每次仿真前清空已有的覆盖率数据库,避免多次运行的覆盖率数据混在一起。如果你做的是增量回归,想累积覆盖率,就去掉这个参数,但要注意不同 seed 下的覆盖率合并需要保证设计版本一致,否则会出现“覆盖率虚高”的假象。
覆盖率数据库默认输出到cov_work/目录,用imc工具打开查看。我一般会在回归脚本最后加一步imc -load cov_work -exec merge把多个 case 的覆盖率合并成一个总库,再用imc -load merged -report生成文本报告,方便集成到 CI 流水线里。
3.3 波形 dump 的三种策略与性能取舍
波形是调试的主要依据,但全量 dump 会让仿真慢到无法接受。Xcelium 支持三种 dump 方式:全量-access +rwc配合$dumpvars、按层次 dump、按时间窗口 dump。
全量 dump 最简单,在 testbench 里加initial $dumpvars(0, tb_soc);就行,但一个中等规模 SoC 跑 1 毫秒仿真就能产生几十 GB 的 FSDB 文件。按层次 dump 是在$dumpvars里指定层次深度,比如$dumpvars(2, tb_soc)只 dump 顶层往下两层。按时间窗口 dump 则是用$dumpoff和$dumpon在关键时间段开启记录:
initial begin $dumpfile("wave.fsdb"); $dumpvars(0, tb_soc); $dumpoff; // 默认关闭 #1000; $dumpon; // 1us 后开启 #5000; $dumpoff; // 6us 后关闭 end更推荐的做法是用 xrun 的-dumpvars参数在命令行控制,这样不用改 testbench:
xrun -sv -access +rwc \ -dumpvars "tb_soc 0" \ -dumpfile wave.fsdb \ -top tb_soc \ -f filelist.f-dumpvars "tb_soc 0"里的0表示无限层次,1表示只 dump 顶层,2表示两层。实际项目中我通常设成2或3,只 dump 关键模块的接口信号,需要深入排查时再单独对某个子模块开全量 dump。FSDB 文件的大小还和信号翻转率有关,一个时钟信号跑 1 毫秒就能产生 100 万个跳变,如果不需要看时钟细节,可以在 testbench 里用$fsdbDumpoff把时钟树排除掉。
4. 避坑与排查:xrun 回归里最常见的五类翻车
4.1 编译通过但精化报 “cannot find module”
现象:xrun 编译阶段没有任何报错,但精化时提示某个模块找不到,比如cannot find module 'alu_pkg'。原因通常是 filelist 里漏了包文件,或者包的编译顺序不对。SystemVerilog 的 package 必须先于使用它的模块编译,而 xrun 默认按 filelist 里的顺序处理文件。如果alu_pkg.sv写在alu.sv后面,精化时就会找不到包里的类型定义。
解决办法是在 filelist 里把包文件放在最前面,或者用-makelib和-endlib显式划分编译库。我一般会在 filelist 开头加一段注释标明“包文件区”,把所有的_pkg.sv和_if.sv集中放在那里。另一个容易忽略的点是+incdir+的顺序:如果两个目录下有同名头文件,xrun 会用第一个找到的,所以要把项目自己的 include 目录放在工具自带目录前面。
4.2 仿真跑着跑着卡死,日志最后一行是 “$finish”
现象:仿真日志停在$finish那一行,但进程没有退出,CPU 占用率 100%。原因通常是 testbench 里有fork-join_none启动的线程没有正确结束,或者$finish被某个final块里的死循环挡住了。Xcelium 在收到$finish后会等待所有final块执行完毕,如果某个final块里有wait或forever,仿真就永远退不出来。
排查方法是在 xrun 命令里加-finish 0,让工具在$finish调用后立即退出,不等待final块。如果加了之后能正常退出,就说明问题出在final块里。另一个方法是加-timeout 3600,设置仿真墙钟超时时间,超过一小时自动杀掉进程并输出当前调用栈。这个参数在夜间回归里是必备的,避免一个卡死的 case 占着机器不放。
4.3 覆盖率数据合并后反而下降
现象:单独跑每个 case 时覆盖率都在涨,但用imc -merge合并后总覆盖率比预期低。原因通常是不同 case 用了不同的设计版本,或者-covoverwrite在某个 case 里被误加了,导致那个 case 的覆盖率数据被清空。还有一种可能是覆盖率模型不一致:某个 case 编译时用了-coverage all,另一个 case 只用了-coverage block,合并时工具会以最少的那个为准。
解决办法是在回归脚本里统一覆盖率参数,并且每次合并前先检查cov_work目录下的时间戳,确保所有 case 用的是同一个 snapshot。我习惯在合并前跑一遍imc -load cov_work -report生成每个 case 的独立报告,确认没有异常后再合并。如果发现某个 case 的覆盖率明显偏低,先单独打开它的报告看是哪些模块没覆盖到,而不是直接合并。
4.4 多核加速后结果和单核不一致
现象:同一个 testcase 用-mce -parallel 4跑出来的结果和单核跑的不一样,比如某个断言在单核下通过,多核下失败。原因通常是 testbench 里有竞争条件,比如两个initial块同时写同一个变量,或者用了$random但没有设置 seed。多核引擎会改变线程调度顺序,把原本隐藏的竞争暴露出来。
解决办法是先用-seed 0固定随机种子,确保每次运行的随机序列一致。然后在 testbench 里检查所有共享变量的访问是否用了semaphore或mailbox做同步。如果确认是竞争问题,不要试图用-parallel 1绕过,因为竞争条件在真实硅片上也会导致问题,早发现比晚发现好。Xcelium 提供了一个-mce_debug参数,可以输出多核调度的详细日志,帮助定位是哪个线程的调度顺序变了。
4.5 FSDB 文件太大导致磁盘写满
现象:仿真跑到一半报disk full,检查发现 FSDB 文件已经几百 GB。原因通常是 dump 了太多信号,或者仿真时间太长但没有分段记录。一个 100 万门的设计,如果全量 dump 所有信号,每毫秒仿真大约产生 10 GB 到 20 GB 的 FSDB 数据。
解决办法是分层 dump 加时间窗口。先用-dumpvars "tb_soc 2"只 dump 顶层两层,跑完一遍看哪些信号需要深入排查,再针对那个子模块单独开全量 dump。另外可以用$fsdbAutoSwitchDumpfile让 FSDB 文件按大小自动切分,比如每 2 GB 切一个文件,避免单个文件过大导致无法用波形查看器打开。还有一个技巧是在 testbench 里用$fsdbDumpflush定期刷写缓冲区,这样即使仿真崩溃,已经记录的数据也不会丢失。
5. 用 xrun 做增量回归与调试的进阶习惯
5.1 增量编译的边界:什么时候必须全量重编
xrun 的增量编译依赖xcelium.d目录里的依赖关系数据库。如果只改了 testbench 里的一个$display,增量编译几秒就能完成。但如果改了 package 里的一个类型定义,所有import这个包的模块都需要重新编译,这时候增量编译可能比全量还慢,因为工具要逐个检查依赖。我的经验是:改 RTL 模块内部逻辑,增量编译;改 package、改宏定义、改+incdir+路径,直接删掉xcelium.d全量重编。判断标准很简单,如果 xrun 启动后超过 30 秒还没进仿真阶段,就说明增量编译在反复检查依赖,不如直接全量。
5.2 用 -input 和 -runargs 做交互式调试
xrun 支持在仿真启动后通过-input传入一个 Tcl 脚本,实现自动化的交互式调试。比如你想在某个信号变化时自动打印调用栈:
# debug.tcl when {/tb_soc/u_dut/state == 3'b101} { echo "FSM entered state 101 at time $now" stack continue } run然后 xrun 调用时加-input debug.tcl。这个用法在排查偶发问题时特别有用,因为你可以让仿真一直跑,只在特定条件触发时才输出信息,避免全量日志淹没关键线索。-runargs则是把参数直接传给仿真内核,比如-runargs "-sv_seed 12345"固定随机种子,或者-runargs "-assert_verbose 1"打开断言的详细输出。
5.3 回归脚本的骨架与日志归档
一个可复现的回归脚本应该包含四个部分:环境准备、编译、仿真、结果收集。我常用的骨架是这样的:
#!/bin/bash # run_regression.sh CASES="test1 test2 test3" SEED_BASE=1000 RESULT_DIR="./results/$(date +%Y%m%d_%H%M%S)" mkdir -p $RESULT_DIR for case in $CASES; do for seed in $(seq $SEED_BASE $((SEED_BASE+9))); do xrun -sv -access +rwc \ -mce -parallel 4 \ -coverage all -covoverwrite \ -seed $seed \ -top tb_$case \ -f filelist.f \ -l $RESULT_DIR/${case}_${seed}.log \ -xmlibdirname ./xcelium_${case}_${seed}.d if [ $? -ne 0 ]; then echo "FAIL: $case seed=$seed" >> $RESULT_DIR/fail.list fi done done # 合并覆盖率 imc -load cov_work -exec merge -report $RESULT_DIR/cov_report关键点是-xmlibdirname为每个 case 和 seed 指定独立的编译库目录,避免并行运行时互相覆盖。-l把日志输出到结果目录,方便事后排查。-seed控制随机种子,每个 case 跑 10 个 seed 是常见的做法,既能覆盖随机场景又不会让回归时间爆炸。最后用imc合并覆盖率并生成报告,整个流程可以挂到 CI 上每天自动跑。
5.4 一个我常犯的错误:忘了清 cov_work
早期做回归时,我经常发现覆盖率报告里有一些“幽灵覆盖”——某个模块的覆盖率突然到了 100%,但那个模块明明还没写 testcase。查了半天发现是上一次回归的cov_work目录没删,新的仿真把数据追加进去了。Xcelium 的覆盖率数据库默认是追加模式,除非加-covoverwrite。所以现在我的回归脚本第一行永远是rm -rf cov_work xcelium.d,确保每次从干净状态开始。这个习惯帮我省下了无数次“为什么覆盖率对不上”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取