3. VCS编译与仿真流程自动化

VCS 的日常使用看似只是两条命令:vcs 编译、simv 仿真。但在真实项目中,编译选项往往多达几十个,涉及语言版本、宏定义、库路径、覆盖率、调试、时序检查、性能优化等多个维度;仿真阶段又要区分不同 test、不同 seed、不同回归标签。如果每次都手敲命令,既容易出错,也无法在回归平台中稳定复现。本节的目标是把这两条命令工程化:用封装层隔离选项细节,用脚本层组织编译与仿真,用参数化支持批量回归,再用增量编译与缓存策略把编译时间压下来。

一、命令行选项封装:把散落的开关收进配置层

VCS 选项可以按用途分成几类,理解分类是封装的前提:

  • 语言与解析类:-sverilog、-full64、-ntb_opts uvm-1.2、-timescale=1ns/1ps,决定源码如何被解析。
  • 宏与包含类:+define+SIM_TOP、+incdir+./tb、-f filelist.f,决定编译哪些文件、走哪条条件编译分支。
  • 库与绑定类:-y ./rtl +libext+.v、-v ./lib/tech.v、-lca,决定模块解析来源。
  • 调试与波形类:-debug_access+all、-kdb、-lca,影响 Verdi 可回溯能力。
  • 覆盖率类:-cm line+cond+fsm+tgl+branch、-cm_dir ./cov,决定覆盖率采集范围。
  • 性能与增量类:-j 8、-Mupdate、-Mdir ./csrc,决定并行度与增量行为。

封装的核心思路是:把选项按“项目公共 + 环境差异 + 单次运行差异”三层拆分。项目公共层放语言版本、UVM 库、覆盖率开关;环境差异层放 RTL 路径、宏定义、filelist;单次运行差异层放 test 名、seed、波形开关。这样回归平台只需替换最外层,不必关心底层细节。

一个实用的封装方式是使用 Makefile 变量加 shell 脚本。例如定义 VCS_OPTS 作为公共选项,ENV_OPTS 作为环境选项,RUN_OPTS 作为运行选项,最终拼接成完整命令。封装层还应提供“dry-run”能力,即只打印最终命令而不执行,便于排查选项拼接错误。

二、编译脚本设计:从 filelist 到可执行 simv

编译脚本的职责不是简单调用 vcs,而是完成“准备输入 → 组装命令 → 执行编译 → 检查产物”的闭环。一个典型的编译脚本包含以下环节:

  1. 环境检查:确认 VCS 版本、license 可用、工作目录可写。
  2. 生成 filelist:按 RTL、TB、UVM、VIP 分组,支持 -f 嵌套引用,避免路径硬编码。
  3. 组装选项:按前述三层拼接,并记录到 compile.cmd 文件。
  4. 执行编译:调用 vcs,捕获 stdout/stderr 到日志。
  5. 产物校验:检查 simv 是否生成、csrc 目录是否完整、编译日志中是否有 error。

下面是一个简化的编译脚本示例,展示选项拼接与日志记录方式:

#!/bin/bash
# compile.sh - VCS 编译封装示例
set -e
WORK_DIR=$(pwd)
LOG_DIR=$WORK_DIR/logs
mkdir -p $LOG_DIR

# 三层选项
VCS_COMMON="-full64 -sverilog -timescale=1ns/1ps -ntb_opts uvm-1.2"
VCS_ENV="+define+SIM_TOP +incdir+./tb -f ./flist/rtl.f -f ./flist/tb.f"
VCS_DBG="-debug_access+all -kdb -lca"
VCS_COV="-cm line+cond+fsm+tgl+branch -cm_dir ./cov"

# 组装
CMD="vcs $VCS_COMMON $VCS_ENV $VCS_DBG $VCS_COV -o simv -l $LOG_DIR/compile.log"

echo "[compile] $CMD" | tee $LOG_DIR/compile.cmd
eval $CMD

# 产物校验
if [ -x ./simv ]; then
  echo "[compile] simv generated OK"
else
  echo "[compile] ERROR: simv not found" >&2
  exit 1
fi

脚本设计的关键点是可复现:每次编译都把完整命令写入 compile.cmd,回归失败时可以直接查看当时用了哪些选项。另一个关键点是失败即停,用 set -e 或显式检查返回值,避免编译失败后继续跑仿真产生误导结果。

三、仿真参数化:test、seed、波形与回归标签

仿真阶段的参数化主要围绕四个维度:test 名、seed、波形开关、回归标签。VCS 仿真通过 +plusarg 传递参数,例如 +UVM_TESTNAME=my_test、+ntb_random_seed=12345、+wave_on。脚本层应把这些参数统一成变量,避免在命令行里散落。

一个常见的做法是定义 run.sh,接受 TEST、SEED、WAVE 三个环境变量,并生成对应的仿真命令:

#!/bin/bash
# run.sh - 仿真参数化示例
TEST=${TEST:-smoke_test}
SEED=${SEED:-1}
WAVE=${WAVE:-0}

PLUSARGS="+UVM_TESTNAME=$TEST +ntb_random_seed=$SEED"
if [ "$WAVE" = "1" ]; then
  PLUSARGS="$PLUSARGS +wave_on"
fi

./simv $PLUSARGS -l ./logs/run_${TEST}_${SEED}.log

参数化的价值在于批量回归:回归平台可以循环生成 TEST × SEED 组合,每个组合独立日志、独立覆盖率目录,失败用例可单独复现。需要注意的是,seed 必须显式记录,否则失败用例无法复现;波形开关应默认关闭,只在调试时打开,否则大量回归会因写波形而拖慢速度。

四、增量编译与缓存策略:把编译时间从小时压到分钟

大型 SoC 项目全量编译可能耗时数十分钟到数小时。VCS 提供了增量编译机制,核心是 -Mupdate 与 -Mdir:首次编译生成 csrc 目录,后续只重新编译发生变化的模块,未变模块复用已有对象文件。使用方式是在编译命令中加入:

vcs ... -Mupdate -Mdir ./csrc -o simv

增量编译生效的前提是编译选项保持一致。如果两次编译的宏定义、include 路径、覆盖率开关不同,VCS 会判定为不兼容而触发全量重编。因此工程上应把编译选项固化到配置文件中,增量编译只针对源码变化。

缓存策略则更进一步:把 csrc 目录、simv、覆盖率数据库按“编译配置指纹”做键值缓存。指纹可以由选项字符串加 RTL 版本号计算得到。当同一配置再次编译时,直接复用缓存,跳过编译。常见做法包括:

  • 本地缓存:在 ~/.vcs_cache 下按指纹存放 csrc,编译前先查缓存。
  • 共享缓存:在 NFS 或制品库中存放常用配置的编译产物,团队共享。
  • 清理策略:按时间或容量淘汰旧缓存,避免磁盘膨胀。
增量编译与缓存不是万能的:当 RTL 顶层接口、UVM 库版本、编译宏发生变更时,必须全量重编。建议在脚本中提供 --clean 选项,强制清理 csrc 后重新编译,用于排除缓存污染导致的诡异问题。

五、整体流程与关键判断

把上述四部分串起来,VCS 自动化流程可以概括为:配置层提供选项,编译脚本组装并执行 vcs,仿真脚本参数化运行 simv,增量与缓存层负责加速。下图展示了这一流程中选项、脚本、产物与缓存之间的数据流关系,以及增量编译的命中判断路径。

配置层 VCS_COMMON VCS_ENV VCS_DBG / VCS_COV 语言·宏·库·调试·覆盖率 编译脚本 compile.sh 环境检查 filelist 生成 选项拼接 → compile.cmd 执行 vcs + 产物校验 编译产物 simv csrc/ compile.log compile.cmd 仿真脚本 run.sh TEST SEED WAVE +plusargs 增量编译命中判断 选项指纹 是否一致? 是 复用 csrc 缓存 仅重编变化模块 否 清理 csrc 全量重编 缓存策略 本地 ~/.vcs_cache 共享 NFS / 制品库 按指纹存取 · 定期淘汰 仿真运行与结果观察 simv + plusargs TEST / SEED / WAVE run_${TEST}_${SEED}.log UVM 报告 / 错误计数 覆盖率 cov/ line/cond/fsm/tgl 失败用例复现 固定 TEST + SEED

图中左侧是配置层与编译脚本,中间是编译产物与增量判断,右侧是仿真脚本与缓存策略,底部是仿真运行后的日志、覆盖率与复现路径。实际工程中,这套结构可以进一步接入 CI:每次提交触发编译脚本,回归平台按 test 列表调用仿真脚本,覆盖率与日志统一归档。判断一次自动化是否成功,关键看三点:编译命令是否可复现、失败用例是否可单独复现、增量编译是否真正命中缓存。只要这三点成立,VCS 流程就从“手工操作”变成了“可回归、可缓存、可追溯”的工程能力。