上个月在调一块数模混合芯片的验证环境数字主控、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指向 licensefinesim 同样有自己的 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 defineFSDB incdir../rtl ../rtl/top_digital.v ../rtl/spi_ctrl.v ../tb/tb_top.vincdir指定 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、cleanMakefile 的目标设计会直接影响使用体验。我把它拆成几条清晰的命令链all: run_dig run_ana wave run_dig: $(NCVLOG) -q -f $(DIG_FL) accessr -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这是最常用的方式accessr给仿真对象增加读取权限这个选项直接决定 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 里的defineFSDBMakefile 里就无需再改动代码。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 设置成数字侧时钟周期的几分之一比如数字主时钟是 10MHzhmax 取 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 里同时打开多个 fsdbVerdi 的 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 开头没问题、但有人用空格代替 Tabmake 直接报缺失分隔符。这种情况在复制别人项目时经常出现解决方法是把对应行全部替换成真实 Tab 字符。还有 license 报错提示找不到 license 或者 feature 不存在。遇到这种情况先检查LM_LICENSE_FILE再用工具自带的命令验证是否能正常启动。6.2 ncverilog 编译数字代码时的典型错误数字侧编译错误无非几类语法错误、模块未定义、端口连接不匹配。语法错误看 log 定位很快模块未定义要检查 filelist 是否包含了所有 RTL 文件或者模块名有没有拼写错误。端口连接不匹配在引入 wreal 或 real 端口时特别常见因为普通 logic 和 real 属于不同数据类型不能直接连接。如果是 fsdb 波形里信号全是 x 或者看不到内部信号十有八九是编译时没有加accessr。还有一个容易忽略的问题是 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一把过。后面就是纯套路了希望这篇文章能帮你少走点弯路。