上个月在调一块数模混合芯片的验证环境,数字主控、SPI寄存器、状态机这些逻辑仿真早就跑通了,可模拟前端一换成真实网表,各种对不上。数字侧觉得模拟输出该在某个沿之前稳定,模拟侧觉得数字控制时序根本没留够,两边各说各话。最后把 ncverilog(老环境里都这么叫,新版其实是 xrun/irun)和 finesim 串成了一条混合仿真流程,再用 Makefile 把编译、仿真、波形导出这些环节全部管理起来,折腾了两周终于理顺。这篇文章就把这套从 Makefile 到 fsdb 的完整链路讲清楚:Makefile 怎么组织、ncverilog 怎么编数字侧、finesim 怎么跑模拟侧、两边波形怎么统一成 fsdb 在 Verdi 里对拍。对正在搭数模混合验证环境的数字验证工程师,或者刚接触混合仿真想知道全流程长什么样的模拟工程师,应该都值得参考。
1. 先想清楚:这条流程到底在解决什么问题
1.1 为什么单跑数字仿真不够
很多数模混合芯片项目,一开始数字验证用行为模型代替模拟模块,仿真跑得飞快,算法和状态机逻辑基本能覆盖。但行为模型毕竟是理想化的,它没有模拟前端的建立时间、失调、非线性、电源上下电的中间态。到了项目后期,流片风险最大的往往是数模接口部分。
举例来说,一个 ADC 的控制接口,行为模型可能一个周期就给出转换结果,真实网表却需要几十个微秒的采样建立时间;一个 LDO 的上电时序,行为模型只需要一个电平翻转,真实网表却有缓慢的斜坡上升过程。如果数字验证环境里看不到这些,等顶层集成时才发现,返工成本非常高。
把真实模拟网表拉进仿真,问题就转化为性能问题——SPICE 跑全芯片级仿真,时间成本完全不可接受。这就是 ncverilog 加 finesim 组合的价值所在:数字侧用事件驱动的 RTL 仿真,模拟侧用快仿引擎,各自发挥优势,再把结果放到一起做联合检查。
1.2 为什么选 ncverilog 加 finesim 这套组合
Cadence 的 ncverilog(或新一代 xrun)是数字仿真里的老面孔,兼容性好,几乎项目里所有 RTL 和验证环境都能直接编译,filelist、宏定义、PLI 加载这些用法大家都熟悉。finesim 属于 FastSPICE 类型工具,对于常见的模拟模块,只要网表和模型库齐备,跑出来的瞬态波形精度和全 SPICE 足够接近,速度却快一个量级以上。
这两个工具的组合非常适合“数字 RTL + 真实模拟网表”的联合验证流程。比起直接上 Virtuoso AMS 那套完整数模混合环境,命令行的方式更轻量,适合在服务器上批量回归,也方便接进现有的 CI 流程。
有一点需要提前说明:这里说的混合仿真,严格讲是数字和模拟各自并行仿真、最后做波形级联的验证方式。数字功能、控制时序、状态机逻辑由 ncverilog 负责,关键的模拟模块行为由 finesim 跑真实网表得出,两边通过相同的激励、上电时序和仿真时间范围对齐,再在波形上做接口级交叉检查。如果项目需要真正意义上的实时双向反馈仿真,比如锁相环的自适应调节、ADC 与数字校准逻辑闭环,那就需要 AMS 环境,让 finesim 作为模拟求解器直接被 ncsim/xrun 动态调用,那是另一套配置思路,本文就不展开了。
1.3 工程目录怎么摆:让 Makefile 好写又好看
搭这套流程之前,先把目录结构定下来,尤其是模拟 netlist 和数字 RTL 的边界。整理清楚之后,Makefile 里就不会出现一长串手拼路径,后续加文件也方便。
我常用的目录结构长这样:
sim/ ├── Makefile ├── filelist/ │ ├── dig.f │ └── ana.f ├── rtl/ ├── tb/ ├── netlist/ │ └── top.spi ├── log/ └── fsdb/filelist 目录存放数字侧和模拟侧各自的文件列表,rtl 下面放数字代码,netlist 下面放模拟网表和模型库引用,log 和 fsdb 目录放运行日志和波形。Makefile 里所有路径都从SIM_DIR变量展开,这样整个工程随便拷贝到哪个服务器都能直接跑,不会因为绝对路径写死而出问题。
2. Makefile 骨架搭建:从目标到依赖
2.1 工具链变量定义与环境准备
Makefile 的核心是“目标-依赖-规则”。目标说明要干什么,依赖说明干这件事之前需要哪些东西已经就绪,规则告诉 make 具体执行什么命令。我在这个项目里把变量分了三类:工具路径、文件路径、仿真选项。
工具路径用?=赋值,这样既允许在命令行里覆盖,也保留默认值:
NCVLOG ?= ncverilog FINESIM ?= finesim VERDI ?= verdi SIM_DIR := $(shell pwd) DIG_FL := $(SIM_DIR)/filelist/dig.f ANA_FL := $(SIM_DIR)/filelist/ana.f LOG_DIR := $(SIM_DIR)/log FSDB_DIR := $(SIM_DIR)/fsdb环境变量方面,最容易被忽略的是 license 和动态库路径。ncverilog 需要LM_LICENSE_FILE或CDS_LIC_FILE指向 license,finesim 同样有自己的 license 配置;Verdi 的 PLI 库需要LD_LIBRARY_PATH包含安装目录下的库路径。这些最好写在工程的 env.sh 里,由用户先 source,或者直接在 Makefile 里 export。我一般单独写一个环境脚本,再在 Makefile 开头 include,这样工具版本升级时只需要改一处。
2.2 filelist 的组织方式
数字侧 filelist 通常包含 RTL 文件、验证文件、宏定义文件,以及库文件的引用路径。ncverilog 读取 filelist 时,-f后面接的就是这个列表。数字侧 filelist 的基本写法:
# filelist/dig.f +define+FSDB +incdir+../rtl ../rtl/top_digital.v ../rtl/spi_ctrl.v ../tb/tb_top.v+incdir+指定 include 目录,+define+预设宏。把宏定义写进 filelist 而不是 Makefile,好处是同一份 filelist 可以同时被 ncverilog、xrun、verdi 共用,宏的作用域也一目了然。模拟侧 filelist 则直接对应 SPICE 网表和模型库:
# filelist/ana.f ../netlist/model/xx.lib ../netlist/ldo_top.spi ../netlist/adc_top.spi模拟网表里不要写死绝对路径,模型库路径统一用相对路径或者环境变量,这样整个工程在不同机器之间搬运时,不需要逐条改文件。
2.3 核心目标:all、sim、wave、clean
Makefile 的目标设计会直接影响使用体验。我把它拆成几条清晰的命令链:
all: run_dig run_ana wave run_dig: $(NCVLOG) -q -f $(DIG_FL) +access+r -l $(LOG_DIR)/dig.log \ -P $(VERDI_HOME)/share/PLI/LINUX64/novas_veriuser.tab \ $(VERDI_HOME)/share/PLI/LINUX64/veriuserfsdb.so run_ana: $(FINESIM) -f $(ANA_FL) -s ../netlist/top.spi -m tr -o ../sim_data wave: run_dig run_ana $(VERDI) -f $(DIG_FL) -ssf $(FSDB_DIR)/merged.fsdb &这条链路里,run_dig和run_ana如果平台支持并行,可以用make -j2并行启动,数字和模拟仿真同时跑,大幅缩短整个流程的时间。wave依赖两个仿真目标,确保两边都完成后再开 Verdi 看波形。clean目标则负责清理临时目录:
clean: rm -rf $(SIM_DIR)/INCA_libs $(SIM_DIR)/sim_data $(LOG_DIR)/* $(FSDB_DIR)/*这里要特别提醒:ncverilog/xrun 生成的INCA_libs目录在编译两次后会产生版本冲突,不删干净容易出莫名其妙的问题。所以 clean 一定要把 INCA_libs 加进去。
3. ncverilog 数字侧实操:编译选项与 fsdb 波形生成
3.1 编译选项怎么选
ncverilog 的命令行选项很多,但混合仿真流程里常用的就几个,把它们的含义搞清楚就能应付绝大多数场景。
-f指定 filelist,这是最常用的方式;+access+r给仿真对象增加读取权限,这个选项直接决定 fsdb 里能不能看内部信号,不加的话,很多信号在 dump 时会是 x 或者直接被优化掉;-q是 quiet 模式,减少终端输出,但 log 里实际上还是会记录必要信息;-l指定日志文件。我的习惯是所有运行日志都统一输出到 log 目录,文件命名按日期或目标名区分,方便回归时排查。
还有一个容易踩坑的选项是-P。它是用来加载 Verdi 的 PLI 库的,让 ncverilog 在仿真过程中能调用$fsdbDumpfile和$fsdbDumpvars这些系统任务。不同版本 Verdi 的 PLI 文件名不一样,常见的有veriuserfsdb.so、novas_pli.so等,tab 文件也是配套的。最稳妥的做法是到$(VERDI_HOME)/share/PLI目录下 ls 一下,看实际存在哪个文件,再写进 Makefile。路径写错基本就是启动即报错,提示找不到 PLI 模块。
3.2 testbench 里怎么 dump fsdb
数字侧 fsdb 波形通常由 testbench 里的系统任务控制。标准写法:
`ifdef FSDB initial begin $fsdbDumpfile("../fsdb/dig_top.fsdb"); $fsdbDumpvars(0, tb_top); end `endif$fsdbDumpfile指定输出文件路径,第一参数 0 表示 dump 整个层次,也可以换成 1 或指定某个模块实例,只 dump 局部信号,减小文件体积。tb_top通常传顶层例化名。平时跑快速仿真时不需要波形,用宏 FSDB 控制这段代码是否生效,配合 filelist 里的+define+FSDB,Makefile 里就无需再改动代码。
testbench 里还有一个细节:复位结束和模拟输出稳定之间需要等待足够时间,或者使用$fsdbDumpvars的开关模式。否则 fsdb 里会录到大量上电和复位过程中的毛刺,文件大小急剧膨胀,真正关心的调试区间反而被淹没。
3.3 数字侧怎么给模拟模块留接口
数字 RTL 里,模拟模块通常以黑盒或行为模型形式出现。黑盒方式就是只声明端口、不定义内部逻辑,这样可以保证编译通过,把内部实现留给 finesim 那侧去验证。端口类型要特别小心,wreal、real、logic 之间的连接关系如果不一致,ncverilog 编译时会报接口不匹配,或者产生隐式转换导致精度损失。
我常用的做法是在 RTL 里给模拟模块写一个可替换的 wrapper,比如:
module ldo_wrapper ( input logic en, input logic vin, output wire real vout ); endmodule这里用wire real表示模拟域的连续值,数字逻辑通过 real 类型读取模拟电压,后续如果需要,可以换成一个行为模型或由 AMS 环境自动生成接口单元。实际联调时可以先用简单的理想行为模型让数字侧 flow 先走起来,再在 finesim 里跑真实网表做对比,这样两边问题的边界更清楚。
4. finesim 模拟侧实操:SPICE 网表怎么跑起来
4.1 命令和文件准备
finesim 的命令行与常见 SPICE 仿真器类似,release 版本之间参数略有出入,我用的基本形式是:
finesim -f filelist/ana.f -s ../netlist/top.spi -m tr -o ../sim_data-f指定文件列表,里面包含模型库和子电路网表;-s指定最高层 SPICE 网表;-m tr表示瞬态仿真模式;-o指定输出目录,仿真中间结果、日志、波形都会在这里集中生成。不同版本对 fsdb 输出的控制方式可能不同,有的通过.option控制,有的在命令行指定,我习惯在 SPICE 网表顶层加入波形输出选项:
.option post fsdb .save vout vref control_signal.save语句作用很像探针,指定你真正关心的节点,限制输出数量。FastSPICE 仿真引擎会做矩阵化简和电路分群,不主动保存的节点很可能在结果文件里看不到,这一点和 hspice 那种全保留模式不一样。
4.2 步长、精度与仿真时长怎么权衡
模拟侧仿真跑得慢,很多时候是步长设置太激进。finesim 默认的 hmax(最大时间步长)可能为了精确而取得很小,对混合仿真这种场景来说,往往没这个必要。数字侧关心的是几个关键沿的时序对应关系,模拟侧的精度只需要保证波形包络和接口电平正确。
我会把 hmax 设置成数字侧时钟周期的几分之一,比如数字主时钟是 10MHz,hmax 取 2ns 或 5ns,这样波形抖动明显可见,速度又能接受。如果还嫌慢,可以进一步加大输出间隔,通过只存探针节点来减小 fsdb 文件体积。电源模块上电瞬态这种大信号变化场景,hmax 反而要适当调小,因为斜坡过程里的过冲和振铃通常就在几十纳秒内发生,步长太大就直接错过了。
仿真时长也要控制节奏。混合仿真不是把整个数字回归跑完,而是重点覆盖复位上电、模式切换、接口控制的临界时序。先跑 1ms 的快速冒烟,看波形基本正确后再扩展仿真周期。
4.3 finesim 结果的 fsdb 导出
finesim 直接输出 fsdb 文件是最省事的方式,这样数字和模拟波形可以同时被 Verdi 读取,不需要额外格式转换。仿真结束后,输出目录里会生成对应的 fsdb 文件,Makefile 里可以把它拷贝到统一的 fsdb 目录,并按照工程约定命名:
run_ana: $(FINESIM) -f $(ANA_FL) -s ../netlist/top.spi -m tr -o ../sim_data cp ../sim_data/top.fsdb $(FSDB_DIR)/ana_top.fsdb这里还有一个小技巧:模拟模块非常多的情况下,建议每个模块单独跑一个 finesim 工程,但输出文件命名上加上模块名。这样某个模块波形改动时,只需要重跑那一个模块,其他模块的 fsdb 可以继续复用,整个流程的迭代效率明显提升。
5. 两边结果怎么“对齐”:从波形到 fsdb 的联合查看
5.1 时间轴与单位的统一
数字仿真和模拟仿真相对于各自的时间原点都会有一个启动偏移,合并前一定要先校准。最常见的做法是让两边使用相同的虚拟上电时刻,并保证复位信号和模拟模块的使能时序一致。
来自不同工具的时间精度也要注意。ncverilog 里的timescale 1ns/1ps精度是皮秒级,finesim 内部步长则可能已经到飞秒量级。在 fsdb 里看波形时,时间轴拉到微秒级基本不影响,但一旦缩放到纳秒级,数字侧的时钟沿和模拟侧的电压变化起点就需要精确对齐。我的经验是,把模拟信号和数字信号放到同一个 Verdi 波形窗口里,利用一个公共的参考信号,比如系统复位释放或 SPI 片选下降沿,手动对齐时间起点,比单纯依赖工具自动对齐更可靠。
5.2 Verdi 里同时打开多个 fsdb
Verdi 的 nWave 支持同时加载多个 fsdb 文件,这是波形级联检查最直接的方式。启动时把数字和模拟的 fsdb 都带上:
verdi -f filelist/dig.f -ssf fsdb/dig_top.fsdb -ssf fsdb/ana_top.fsdb &命令行里多个-ssf会依次打开,波形窗口里数字信号和模拟信号会分开展示。如果希望信号在同一个视图中叠加对比,可以手动把模拟信号拖到数字信号旁边,或者根据公共总线分组。对拍时我最常用的手势是:先找接口信号,比如 ADC 的转换开始/结束、LDO 的上电完成标志,对比这些信号在数字侧和模拟侧的时间差,判断是谁在等谁。
如果环境里有合并工具,也可以把多个 fsdb 合并成单一文件,方便长期归档。合并后的文件命名建议带日期和版本号,否则多个实验波形堆在一起,过几天自己都分不清哪个是哪个。
5.3 实战心得:先跑小 case 再上全芯片
整套流程最忌讳一上来就跑全芯片。我自己的习惯是先挑一个功能粒度最小、但覆盖典型接口的场景,比如“SPI 配置寄存器 -> LDO 使能 -> ADC 采样转换完成”这一条链路。这个小 case 跑通后,数字侧能确认接口时序没有 bug,模拟侧能确认网表模型库没有编译遗漏,Makefile 的依赖关系也没有问题。
随后再逐步增加功能,比如加入时钟切换、低功耗模式、上电复位序列,每加一次功能就做一次分层验证。这样出现问题时,范围一定在最近一次改动附近,定位成本会低很多。
6. 问题排查与避坑速查表
6.1 Makefile 运行报错的常见情况
Makefile 报错有一半是环境变量或者路径问题。最常见的是“ncverilog: command not found”,这通常是 PATH 没配好。用?=定义工具名,但没在命令行覆盖,默认又找不到,就会走系统 PATH 查找。建议先执行which ncverilog确认工具路径。
另一种典型报错是 Makefile 里用了 Tab 开头没问题、但有人用空格代替 Tab,make 直接报缺失分隔符。这种情况在复制别人项目时经常出现,解决方法是把对应行全部替换成真实 Tab 字符。
还有 license 报错,提示找不到 license 或者 feature 不存在。遇到这种情况,先检查LM_LICENSE_FILE,再用工具自带的命令验证是否能正常启动。
6.2 ncverilog 编译数字代码时的典型错误
数字侧编译错误无非几类:语法错误、模块未定义、端口连接不匹配。语法错误看 log 定位很快,模块未定义要检查 filelist 是否包含了所有 RTL 文件,或者模块名有没有拼写错误。端口连接不匹配在引入 wreal 或 real 端口时特别常见,因为普通 logic 和 real 属于不同数据类型,不能直接连接。
如果是 fsdb 波形里信号全是 x 或者看不到内部信号,十有八九是编译时没有加+access+r。还有一个容易忽略的问题是 dump 层次太多导致 fsdb 文件巨大,Verdi 打开时卡顿,这时候建议收敛$fsdbDumpvars的层次参数,有选择地 dump。
6.3 finesim 运行崩溃或者收敛问题
finesim 报收敛失败,大多数情况和模型库、网表连接质量有关。检查模型库有没有对应工艺角文件,连线上有没有悬空节点,电源地有没有统一。瞬态不收敛的另一个方向是步长控制,把 hmax 调小两到三倍往往就能过,代价是仿真时间增加;如果时间无法接受,再考虑把电路分群或只抽取关键路径。
finesim 跑完没有生成 fsdb,要看网表里有没有包含.save探针,以及输出格式是不是设成了 fsdb。有的项目在网表里沿用了 hspice 时代的.option post,生成的是 tr0 格式,Verdi 直接看不了。finesim 的结果文件最好一上来就明确输出 fsdb 格式,避免后期转换的麻烦。
6.4 fsdb 波形显示异常
波形里模拟信号长时间为平线,先看是不是仿真根本没跑远。输出步长太大、输出点数太少,会把瞬态变化完全淹没。把输出 interval 调小再跑一次,波形就能体现细节。
还有一种情况是数字和模拟波形的时间轴偏移很大,看起来像是模拟信号滞后或超前很多。这不是电路问题,而是两边仿真启动时间没对齐,复位释放前的时间基准不同。解决方法是固定一个统一的事件点作为时间零参考,或者在上电序列描述里直接指定两边从同一时刻开始建模仿真。
我自己的体会是,这套流程最花时间的其实在“对齐”这两个字,工具本身命令都不复杂,难的是时间起点、接口数据类型、波形输出粒度这几个维度要一致。搭好之后,改版回归的效率提升非常明显,改一处 RTL 或者换一个模拟模块版本,make 一下波形就出来了。刚开始的时候别急着全芯片,先拿出一条典型链路,把 Makefile 里每个目标单独跑通,再make all一把过。后面就是纯套路了,希望这篇文章能帮你少走点弯路。