4、Modelsim/Questa Sim 脚本化:命令行编译、仿真、波形查看

说实话,很多工程师用了好几年 Modelsim,还是习惯点鼠标。点一下编译,点一下仿真,再点一下添加波形。嗯,一次两次还行,但如果你要跑 50 个测试用例呢?手指头都得点抽筋。

我个人习惯是:能用脚本解决的,绝不用鼠标。脚本化仿真,说白了就是把你的操作步骤写成 .do 文件。这样不仅快,还能保证每次跑的流程一模一样。我在项目中遇到过好几次,同事手动操作漏了某个编译选项,结果仿真结果对不上,查了一整天。

4.1 为什么非要脚本化?

你想想看,手动仿真最大的问题是什么?

  • 不可重复:今天点的步骤,明天可能就忘了
  • 效率低:每次改完代码,都要重新点一遍
  • 容易出错:少点一个库,仿真直接崩
  • 没法集成:CI/CD 流程里,谁帮你点鼠标?

脚本化之后,一个命令搞定全部。我团队里现在新来的同事,第一天我就让他们学 .do 文件怎么写。

核心思想:把仿真流程写成脚本,一次编写,反复使用。

4.2 基础:Tcl 脚本与 .do 文件

Modelsim 和 Questa Sim 都支持 Tcl 脚本。.do 文件就是 Tcl 脚本的一种形式。你可以在 GUI 里敲命令,也可以直接写文件批量执行。

最简单的例子:

# 编译设计文件
vlog -work work ../rtl/top.v
vlog -work work ../rtl/sub_module.v

# 编译测试平台
vlog -work work ../tb/tb_top.v

# 启动仿真
vsim -voptargs=+acc work.tb_top

# 添加波形
add wave -r /*

# 运行
run 1us

把这个保存成 run.do,然后在 Modelsim 里执行:

do run.do

搞定。就这么简单。

我的习惯:每个项目都建一个 scripts/ 目录,里面放 compile.do、simulate.do、wave.do。分工明确,好维护。

4.3 命令行编译:vlog 与 vcom

Modelsim 支持两种语言:Verilog 用 vlog,VHDL 用 vcom。我个人更常用 Verilog,所以重点讲 vlog。

常用选项:

选项 说明 示例
-work 指定库名 vlog -work work file.v
-sv 启用 SystemVerilog vlog -sv file.sv
-cover 开启覆盖率收集 vlog -cover bces file.v
-mfcu 多文件编译单元 vlog -mfcu *.v
-lint 检查 lint 警告 vlog -lint file.v

我曾经踩过一个坑:编译顺序。如果你的模块 A 调用了模块 B,必须先编译 B,再编译 A。否则会报找不到模块的错误。后来我学乖了,用文件列表 + 排序脚本来自动处理。

# 推荐做法:用文件列表
vlog -work work -f filelist.f

# filelist.f 内容示例
../rtl/sub_module.v
../rtl/top.v
../tb/tb_top.sv
注意:文件列表的顺序很重要!被调用的模块要放在前面。我建议用脚本自动生成 filelist,避免手写出错。

4.4 命令行仿真:vsim 的进阶用法

编译完就是仿真。vsim 命令的选项比 vlog 还多,但常用的就几个:

# 基本用法
vsim -voptargs=+acc work.tb_top

# 带参数传递
vsim -voptargs=+acc work.tb_top -gTEST_WIDTH=32

# 批量跑测试用例
vsim -c -voptargs=+acc work.tb_top -do "run -all; exit"

这里有个关键点:-voptargs=+acc。这个选项会保留所有信号的可见性,方便调试。但代价是仿真速度会慢一点。如果你只关心最终结果,可以去掉它。

我一般这样用:

  • 调试阶段:加 +acc,方便看波形
  • 回归测试:不加 +acc,跑得快

4.5 波形查看:自动添加与保存

手动添加波形太累了。尤其是几百个信号的设计,一个个点得点到什么时候?

我的做法是:写一个专门的 wave.do 文件。

# wave.do
onerror {resume}

# 清除旧波形
quietly WaveRestoreZoom {0 ps} {1 us}

# 添加顶层信号
add wave -divider "Top Level"
add wave -hex /tb_top/dut/clk
add wave -hex /tb_top/dut/rst_n

# 添加内部总线
add wave -divider "Internal Bus"
add wave -hex /tb_top/dut/bus/addr
add wave -hex /tb_top/dut/bus/data
add wave /tb_top/dut/bus/valid

# 添加状态机
add wave -divider "FSM"
add wave -hex /tb_top/dut/fsm/state
add wave /tb_top/dut/fsm/state_next

# 保存波形配置
wave save wave.do

然后在仿真脚本里调用:

vsim -voptargs=+acc work.tb_top
do wave.do
run 1us
小技巧:用 add wave -r /* 可以递归添加所有信号。但信号太多时仿真会变慢。我建议只添加关键信号,或者分层次添加。

4.6 完整示例:一键仿真脚本

下面是我项目里常用的完整脚本,你可以直接拿去改:

# run_all.do
# 作者:我
# 功能:一键编译+仿真+波形查看

# 清理旧文件
if {[file exists work]} {
    vdel -all
}

# 创建库
vlib work
vmap work work

# 编译 RTL
echo "=== Compiling RTL ==="
vlog -work work -sv ../rtl/*.sv
vlog -work work ../rtl/*.v

# 编译 TB
echo "=== Compiling Testbench ==="
vlog -work work -sv ../tb/tb_top.sv

# 检查编译错误
if {[vcom -work work -check_syntax ../rtl/*.vhd] != 0} {
    echo "ERROR: Compilation failed!"
    exit 1
}

# 启动仿真
echo "=== Starting Simulation ==="
vsim -voptargs=+acc -t 1ps work.tb_top

# 加载波形配置
if {[file exists wave.do]} {
    do wave.do
} else {
    add wave -r /*
}

# 运行
echo "=== Running ==="
run 10us

# 保存波形
wave save result.wlf

echo "=== Done ==="

执行方式:

vsim -c -do run_all.do

或者直接在 GUI 里:

do run_all.do

4.7 避坑指南

我这些年踩过的坑,分享给你:

  • 路径问题:脚本里的路径尽量用相对路径。我见过有人用绝对路径,换台电脑就跑不了。
  • 库冲突:不同版本的 Modelsim 库不兼容。记得每次跑之前 vdel -all 清理一下。
  • 退出码:在 CI 里跑仿真,记得检查退出码。仿真失败要返回非零值,否则 CI 会以为通过了。
  • 波形文件太大:跑长时间仿真时,波形文件会很大。可以用 log -r /* 只记录需要的信号。
我曾经犯过的错:有一次在服务器上跑回归测试,忘了加 -c 选项(命令行模式),结果仿真窗口弹出来卡住了,整个 CI 流程挂了半小时。从那以后,我所有自动化脚本都默认加 -c

4.8 进阶:批量跑测试用例

如果你有多个测试用例,可以这样写:

# batch_run.do
set testcases {test1 test2 test3 test4}

foreach test $testcases {
    echo "=== Running $test ==="
    
    # 重新编译(如果需要)
    vlog -work work ../tb/tb_${test}.sv
    
    # 仿真
    vsim -c -voptargs=+acc work.tb_${test} -do "
        run -all;
        if {[string equal [examine -radix hex /tb_top/dut/result] 32'hDEAD_BEEF]} {
            echo \"PASS: $test\";
        } else {
            echo \"FAIL: $test\";
        }
        exit;
    "
}

这样跑完所有用例,结果一目了然。我团队现在每天跑 200+ 个用例,全靠这套脚本撑着。

总结:脚本化仿真不是炫技,是实实在在提升效率的手段。花 10 分钟写脚本,省下的是未来无数次的重复劳动。

嗯,这一章就到这里。下一章我会讲如何用脚本做自动化回归测试和覆盖率收集,那才是真正的大招。