
做FPGA开发这几年有个现象我一直印象很深很多刚接触易灵思FPGA的工程师在RTL写好后都会卡在同一个环节——仿真。明明代码在脑子里过了一遍觉得没问题一到Modelsim联仿就各种报错要么找不到库要么波形一片红。Efinity作为易灵思的官方IDE功能确实够用自带波形查看器和基础仿真流程但真要跑起严肃的验证来大家还是习惯回到Modelsim的怀抱。这篇文章就把我在项目里把Efinity和Modelsim联合起来跑仿真的完整套路整理出来包括环境搭建、库编译、门级仿真、SDF反标还有一套可以直接抄的Tcl脚本。如果你正在被易灵思的仿真流程折腾这篇应该能帮你省下好几个晚上的时间。先说明一下我用的版本是Efinity 2023.1 ModelSim SE 2022.2Windows 10环境下跑通的。不同版本界面会有些差异但核心原理和脚本流程是一样的只要把路径改对直接套用就行。1. 为什么要折腾Efinity和Modelsim联仿1.1 Efinity自带流程的天然局限Efinity自带的仿真流程不是不能用但对稍微复杂一点的工程来说用起来确实憋屈。一个典型的场景是你在Efinity里写好了Testbench点一下Run Simulation工具会调起一个简化版的波形窗口能看信号也能跑波形。但等你需要做条件断点、批量跑回归、用脚本控制仿真流程、或者对门级网表做时序仿真时Efinity自带的工具就很难受了。另一个更现实的痛点是效率。我用过Efinity的GUI界面跑过一个中等规模工程的仿真编译和加载的时间比Modelsim慢了不少而且波形查看器在信号多了之后卡顿比较明显。做FPGA的都知道仿真验证在开发周期里占的时间比重相当大工具不好用直接影响心情和进度。所以我在项目里干脆自己搭了一套EfinityModelsim的联仿流程把RTL仿真和门级仿真全部放到Modelsim里跑。Efinity只负责综合、布局布线、生成网表和SDF文件。这个分工很清晰Efinity干它擅长干的Modelsim干我擅长用的。事实证明这套流程跑起来之后仿真效率提升非常明显而且排查问题比原来直观得多。1.2 功能仿真和门级仿真必须分清这里我特别想强调一个新手很容易混淆的点联仿不是只有一种。Modelsim联仿至少包含两个层次功能仿真和门级仿真。功能仿真也叫RTL仿真是拿你写的Verilog或者VHDL源码直接跑不包含任何时序信息。它对综合布线结果是否正确不管只管逻辑行为对不对。这种仿真速度快调试方便适合前期功能验证。功能仿真其实不需要易灵思的库你直接用Modelsim把源码和Testbench编译进去就能跑。门级仿真也叫GLSGate Level Simulation是把Efinity综合布局布线之后产生的网表文件拿来仿真同时通过SDF文件把门延迟和布线延迟反标进去。这种仿真更接近真实硬件行为能发现时序问题、竞争冒险问题也能验证你写的时序约束到底有没有生效。门级仿真必须要用易灵思的仿真库。很多新手卡在联仿这一步多半就是没搞清楚这个区别直接把门级网表当普通Verilog编译结果报一堆找不到模块的错误。在我这套流程里功能仿真和门级仿真共用一个Testbench切换只需要改一下脚本参数非常方便。所以搞清楚这两个层次的区别是迈出联仿第一步的关键。2. 环境准备与仿真库编译2.1 版本选型与安装里面的坑先说结论Efinity 2022.1以上版本配ModelSim SE 2022.2这套组合我跑得比较稳。不建议用太老的Efinity版本因为新版本对TTLTitanium和Trion系列的模型支持更完善SDF文件格式也更标准。Modelsim这边SE版是功能最全的PE版阉割掉了一些特性联仿基本够用但不推荐因为处理大工程时性能和稳定性都有差距。安装Modelsim时有个常见的坑是许可证配置。ModelSim SE是需要license文件或者license server的装好之后环境变量LM_LICENSE_FILE必须指向正确的lic文件或端口地址。很多新手装完启动时报Cannot checkout a license十有八九就是这个问题。另外Modelsim的安装路径千万不要带中文和空格我见过不少人在这一步栽跟头后面编译各种诡异报错。Efinity的安装倒是相对省心一路默认就行。但有一点要注意Efinity安装路径也尽量别带空格后面脚本里处理路径会省很多麻烦。我的安装目录是C:/Efinity/2023.1Modelsim在C:/modeltech64/2022.2两个目录都干净利落。还有一个环境变量需要提前配好就是EFINITY_HOME指向你的Efinity根目录。后面编译仿真库的脚本会用到这个变量。Windows下在系统环境变量里新增就行。2.2 为什么必须先编译易灵思仿真库门级仿真的本质是把网表中的逻辑单元实例化之后跑仿真。但网表里实例化的那些单元比如EFX_PLL、EFX_OSC、EFX_IO等等在Modelsim里是没有对应模型的。如果直接编译网表文件然后vsimModelsim会报一堆Instantiation of xxx failed的错误因为它根本不认识这些模块。所以需要先把易灵思提供的仿真模型编译到Modelsim的库里面。这个库包括基本的逻辑门单元模型、BRAM模型、DSP模型、PLL模型、IO单元模型。编译之后在仿真的库搜索路径里指定这个库Modelsim在遇到网表里的易灵思原语时就能在库里找到对应的模型来例化。Efinity安装目录下自带这些模型文件通常位于C:/Efinity/2023.1/ip/prim/model路径下里面有一堆.v文件。不同版本文件名可能略有差异但核心文件基本是这几个EfxPrim.v基础原语模型比如LUT、FF、MUXEfxOsc.v振荡器原语模型EfxPll.vPLL原语模型EfxDsp.vDSP原语模型EfxRam.vBRAM原语模型不同Efinity版本会提供不同数量的仿真模型文件比如新版本还会加入针对Titanium器件的模型。最稳的做法是直接把model目录下面的.v文件全部编译进一个库不用纠结具体是哪个。2.3 编译仿真库的完整步骤我习惯把易灵思的仿真库编译到一个单独名字的库里不叫work叫efx_lib。这样设计文件和库文件分开清洗的时候不会误删。在Modelsim的Tcl命令行里敲下面几条命令或者写成一个脚本文件执行set MODEL_DIR $env(EFINITY_HOME)/ip/prim/model if {[file exists efx_lib]} { vdel -lib efx_lib -all } vlib efx_lib vmap efx_lib efx_lib foreach file [glob -nocomplain $MODEL_DIR/*.v] { vlog -work efx_lib $file }这里用到了glob命令可以自动把目录下所有.v文件都搜出来省得一个个手写。如果你的工程里用到了IP比如FIFO、RAM、PLL这些由EFX生成的IP核还需要把IP生成的仿真模型也编译进去。这些模型通常在工程的ip目录下或者Efinity的ip目录里具体路径看IP生成时的设置。编译完之后可以在Modelsim的Library面板里看到efx_lib库和下面几百个编译好的模块。这一步一次性做完后面所有仿真都复用这套库不需要重复编译。3. Efinity工程侧的关键配置生成门级网表和SDF3.1 工程设置里必须检查的仿真选项很多人做完布局布线就直接把工程关了跑到Modelsim那边开搞结果发现根本没有可仿真的文件这就是漏了关键一步。Efinity默认不会自动生成仿真文件和SDF需要你在工程设置里手动指定。在Efinity的界面里打开Project Settings Synthesis/Implementation需要检查以下几个关键项Generate Synthesis Netlist综合完成后生成网表文件这个选项建议开启Generate Simulation Files在布局布线后生成仿真用的文件.vo或者.v这个必须开启Generate SDF File生成SDF标准延迟文件这是门级时序仿真的核心必须开启另外有一个比较隐蔽的设置项是标题里没提但很容易踩的网表格式选择。有些版本会默认生成.vo或者.v如果是.vo它是VHDL输出格式还是Verilog输出格式需要看你工程的HDL语言选型。建议统一用Verilog格式的网表因为Modelsim对Verilog网表的兼容性更好脚本处理也简单。这三个选项都确认开启后跑一次完整的Implementation流程。等Fit和Layout都跑完之后你会在Efinity工程的outflow目录下看到生成的文件通常是这样的top_module.voVerilog格式的门级网表top_module.sdfSDF标准延迟文件top_module.fdc约束文件这个是输入不是输出不用管有些版本的文件名会带版本后缀比如fit_top_module.vo这都正常。找到最新的那个就行。3.2 定位仿真产物并梳理模块层次实际项目里顶层模块往往是嵌套的门级网表里只包含顶层模块及其逻辑实例。要跑门级仿真你的Testbench里例化顶层模块时端口连接关系必须和RTL仿真完全一致。建议在开始之前先从Efinity的Hierarchy面板里确认一下顶层模块名、端口名和位宽。这里有个小技巧直接打开生成的.vo网表在文件头部能看到module开头的那一行把所有端口名列出来对照自己Testbench里的例化端口确认没有遗漏。另外要特别注意的是Testbench内部信号名字。在RTL仿真里你例化的顶层模块叫uut在Testbench里用uut.reg_a这种层次路径来引用内部信号这在RTL仿真里没问题。但到了门级仿真网表内部信号的名字经过综合布局布线之后可能会被优化、重命名甚至合并层次路径就完全对不上了。所以在Testbench里尽量少用内部层次引用如果一定要检测内部信号建议在RTL里把这些信号引到顶层输出端口或者用(* keep true *)约束避免综合优化掉。不过反标SDF文件的路径是例外。SDF文件里记录的延迟信息路径是布局布线后的物理层次路径vsim命令里通过-sdftyp选项指定路径时需要把Testbench里例化顶层模块的路径写对。比如你的Testbench模块叫tb_top里面例化了顶层模块叫uut那么SDF反标路径就是/tb_top/uut。这个路径如果写错SDF会反标失败但仿真不会报错只是波形根本不含延迟信息效果等于白做。4. Modelsim联仿完整Tcl脚本与逐行解读4.1 一键式联仿脚本可直接复制下面是我整理好的完整脚本功能包括清理旧库、建立新库、编译仿真库、编译设计文件和Testbench、启动门级仿真、反标SDF、添加波形并运行。把它保存为run_sim.tcl放到仿真目录下然后在Modelsim的命令行里执行do run_sim.tcl即可。# # run_sim.tcl # Efinity ModelSim 联仿脚本 # 用法在 ModelSim 中执行 do run_sim.tcl # # 0. 变量配置区根据实际工程修改 set DESIGN top_module set TB_NAME tb_top set EFX_LIB efx_lib set SRC_DIR ../rtl ;# RTL 源码目录 set TB_DIR ../tb ;# Testbench 目录 set NETLIST_DIR ../outflow ;# Efinity 生成的网表目录 set SDF_FILE $NETLIST_DIR/fit_${DESIGN}.sdf set DUT_INST /${TB_NAME}/uut # 1. 清理旧库保证仿真环境干净 if {[file exists work]} { vdel -lib work -all } if {[file exists $EFX_LIB]} { vdel -lib $EFX_LIB -all } vlib work vlib $EFX_LIB vmap work work vmap $EFX_LIB $EFX_LIB # 2. 编译易灵思仿真库 set MODEL_DIR $env(EFINITY_HOME)/ip/prim/model foreach file [glob -nocomplain $MODEL_DIR/*.v] { vlog -work $EFX_LIB $file } # 3. 编译设计源码RTL源码只在功能仿真时需要 if {$GLS_MODE ! 1} { foreach file [glob -nocomplain $SRC_DIR/*.v] { vlog -work work $file } vlog -work work ${TB_DIR}/${TB_NAME}.v vsim -L $EFX_LIB work.${TB_NAME} } else { # 4. 门级仿真编译网表和Testbench vlog -work work $NETLIST_DIR/fit_${DESIGN}.vo vlog -work work ${TB_DIR}/${TB_NAME}.v # 5. 启动仿真并反标SDF vsim -L $EFX_LIB -sdftyp $DUT_INST$SDF_FILE work.${TB_NAME} } # 6. 添加常用波形信号 add wave -hex /$TB_NAME/uut/* add wave -hex /$TB_NAME/clk add wave -hex /$TB_NAME/rst_n # 7. 运行仿真 run -all这个脚本里用了一个$GLS_MODE变量来控制仿真模式在Modelsim命令行里可以通过set GLS_MODE 1开启门级仿真模式不设的话默认走RTL功能仿真。切换非常方便。4.2 脚本里几个关键设计的真实用意脚本看起来不复杂但每一步背后都有讲究。先看库清理这一步。很多人在同一个仿真目录里反复跑经常遇到改完代码重新编译后ModelSim还在用旧编译结果的情况尤其是库里的对象已经过期但没被覆盖的时候报错非常难查。所以我在每次仿真前都无脑把work和efx_lib清理重建保证环境从头到尾是干净的。代价是编译时间多几秒但排查问题的省心程度远超过这点时间成本。编译易灵思库用了foreach加glob的形式这是为了适配不同版本Efinity提供的模型文件数量不一致的问题。如果你只编译固定的几个文件换一个新版本Efinity可能就会因为新增了模型文件导致网表里某些单元找不到库模型报错后你还得回来补编译。用通配符一次性全编译从根上避免这个问题。vsim命令里门级仿真和功能仿真的区别很关键。功能仿真时-L $EFX_LIB用来指定库搜索路径因为你的Testbench和RTL源码可能会例化一些IP核这些IP核的模型也需要从库中找到。门级仿真时除了-L $EFX_LIB之外还加了-sdftyp $DUT_INST$SDF_FILE这行就是SDF反标。-sdftyp表示典型延迟条件这是最常用的因为大多数验证场景关心的是典型情况。如果你的设计对时序非常敏感可以考虑再加一组-sdfmin和-sdfmax做极限情况分析不过那种情况一般会用专门的STA工具而不是在Modelsim里做。add wave这里加的是/tb_top/uut/*的所有信号实际工程中如果层次很深信号很多全部加进来会拖慢波形刷新速度。我的习惯是先全部加上看看全局如果发现问题再手动删掉没用的信号。4.3 功能仿真到门级仿真的切换操作用上面这套脚本从功能仿真切到门级仿真只需要三条命令set GLS_MODE 1 do run_sim.tcl就这么简单。不过切过去之前有几点必须确认第一Testbench里的时钟频率要和约束里的一致。门级仿真的时钟周期影响延迟裕量的计算如果你在Testbench里用的是10ns时钟但约束里写的是20nsSDF反标出来的时序检查和你的激励对不上波形上就会出现莫名其妙的时序违例。第二Testbench里尽量用同步复位不要用异步复位。门级网表里异步复位路径的时序分析更复杂异步复位的释放时刻如果没有处理好仿真容易出现亚稳态或者复位不彻底的问题波形看起来就像逻辑错乱一样。第三门级仿真的速度比功能仿真慢一个数量级。功能仿真一秒能跑几百微秒的仿真时间门级仿真可能只能跑几十微秒。所以跑门级仿真之前确认Testbench里面不要有太长的延时等待把仿真时间尽量压缩到必要的长度。我之前调试一个模块功能仿真跑完需要5秒门级仿真要跑3分钟完全是正常的。5. 常见问题排查与经验台账5.1 Modelsim报错案例速查表整理了一份我在实际联仿中遇到最多的报错和处理方式放在表格里面遇到问题直接对着查。报错信息原因解决办法Instantiation of EFX_PLL failed没有编译易灵思仿真库或者-L参数没指定库确认efx_lib编译完整vsim加-L efx_lib(vsim-3193) Port aaa not foundTestbench例化顶层模块的端口名和网表端口名不一致打开.vo看真实端口名修改Testbench例化$sdf_annotate: Failed to find timing label写Testbench时直接调用$sdf_annotate但路径写错删除Testbench里的$sdf_annotate改用vsim -sdftyp波形全部是红线/X态Testbench没有正确产生复位信号或者复位时间太短检查rst_n信号增加复位持续周期波形有值但和预期不符可能是时序约束没生效SDF反标路径写错用vsim -sdftyp的方式确认反标成功查看transcript打印# ** Fatal: (vsim-3719) Region /tb_top/uut not foundTestbench层次路径名写错顶层模块例化名不叫uut确认Testbench里uut的例化名修改DUT_INST变量Library work not found在非仿真目录下执行脚本cd到仿真脚本所在目录再执行do run_sim.tcl这些报错里最坑的是SDF反标失败但不报错。Modelsim默认情况下反标失败只会打印一行提示不会中断仿真如果你没盯着transcript看很容易忽略。所以跑完仿真第一件事就是在transcript里搜一下SDF关键词确认没有报错。5.2 波形全红的排查方法论波形一片红可以说是新手联仿遇到最多的问题。我的排查顺序固定如下先看Testbench本身有没有问题。最简单的验证方法是把Testbench里的时钟生成和复位初始化逻辑单独放到一个临时工程里不例化任何被测模块直接看时钟和复位波形。如果时钟和复位正常再看Testbench与顶层模块之间的连接。常见问题是位宽不匹配或者信号方向接反这里可以在add wave时把/tb_top/uut/*全部加进来看输入端口上有没有正常的激励。如果输入端口有值但内部信号全红多半是复位没有正常释放或者某些输入使能信号没置位。如果以上都没问题再考虑是不是综合优化把某个信号吞掉了。在门级仿真中用户逻辑里没有扇出的信号会被优化掉你在RTL里加(* keep true *)能避免这个问题但如果你已经跑完布局布线再回头加需要重新跑一遍Implementation才能生效。最后才考虑时序问题。如果波形不是全红而是部分信号在某个时刻突然变成红X并且出现的位置和时间点都相对稳定那往往是时序违例。比如组合逻辑路径太长导致某个信号在时钟沿附近变化产生了建立时间违例Modelsim会在这时候把信号标记为X态。5.3 Modelsim环境变量与常用批处理命令除了Tcl脚本之外Windows下用批处理配合操作也能省不少事。我把常用的启动命令整理成一个start_sim.bat双击就能进入仿真环境echo off set EFINITY_HOMEC:\Efinity\2023.1 set LM_LICENSE_FILEC:\modeltech64\license.dat C:\modeltech64\2022.2\win64\vsim.exe -do run_sim.tcl注意LM_LICENSE_FILE的路径要根据你自己的许可证实际位置来改。如果你的Modelsim用的是license server那这个变量改成portserver_ip的格式。另外一个实用脚本是批量编译全部RTL源码的批处理。我有时候会在仿真目录下写一个compile_all.tcl把整个RTL目录下的文件按依赖关系顺序编译。Modelsim对编译顺序没有强制要求只要在vsim之前所有模块都编译完就行所以直接用通配符编译是最省事的。5.4 增量编译和仿真速度优化技巧工程变大之后每次全量编译的时间越来越不可忽略。我个人的经验是RTL源码改了一两个文件时不需要每次都跑完整的run_sim.tcl。Modelsim支持增量编译只要在之前编译的基础上单独重新编译修改过的文件然后重新vsim就行。vlog -work work ../rtl/uart_tx.v vsim -L efx_lib work.tb_top这样能省掉库重建和全量编译的时间对迭代调试特别友好。这也是为什么我建议把库清理放在最前面、并且用变量控制而不是写到每一步——正常迭代调试时我只做增量编译等到最终回归或者切门级仿真前再跑一次干净的全量流程。至于仿真速度除了前面说的尽量缩短Testbench时间外还有好几个技巧。一个是vsim里加-novopt参数这个参数在旧版本Modelsim里很重要因为旧版本默认优化会把很多信号优化掉导致你添加波形时找不到信号。新版本Modelsim改用-voptargsacc来保留信号可见性。如果发现添加波形时找不到内部信号基本就是优化引起的加上这个参数就能解决。另一个技巧是仿真过程中用run -all让仿真一口气跑完不要用断点单步单步调试门级仿真会让你等到怀疑人生。还有一个常见的提速方式是关闭波形记录。如果某个回归仿真你只需要确认最终结果不关心中间波形可以在vsim时加-wlf参数但不打开wave窗口仿真结束再查看WF文件。比全开着波形窗口实时刷新快不少。6. 项目实战中的经验补充6.1 IP核场景下的联仿特别处理如果你的设计里用到了EFX生成的IP比如PLL、FIFO、RAM等联仿时除了编译基础的prim库之外还需要编译IP的仿真模型。EFX IP在生成时会生成一个仿真模型目录里面包含该IP的时序模型。编译方式很简单在脚本里把IP仿真模型目录也加进去set IP_MODEL_DIR ../ip/pll_model vlog -work $EFX_LIB $IP_MODEL_DIR/*.v有一个容易忽略点很多IP仿真模型在编译时可能需要额外的宏定义比如defineSIMULATION或者defineEFX_GLS具体要看IP生成的readme文件。我见过有些IP不加上对应的宏定义仿真模型编译虽然通过但跑起来行为完全不对白白浪费了几个小时。6.2 从Efinity Tcl Console直接启动Modelsim最后分享一个我目前用的很顺手的流程直接从Efinity的Tcl Console调用Modelsim跑仿真不用来回切窗口。在Efinity的Tcl Console里把do run_sim.tcl改成用exec调用外部命令exec C:/modeltech64/2022.2/win64/vsim.exe -do do run_sim.tcl 这个操作的本质是用Modelsim的批处理模式跑完整个仿真流程然后把生成的波形文件用Modelsim界面打开。相当于把Efinity和Modelsim的联动自动化了。不过要注意这样调用时环境变量需要提前在系统层面配置好否则子进程里读不到路径变量。其实做FPGA仿真这件事工具链顺不顺手直接影响到愿不愿意写Testbench、愿不愿意做充分验证。把Efinity和Modelsim打通之后整个验证流程顺畅了很多。后面如果你们有兴趣我还可以写一篇关于易灵思FPGA时序约束的实操文章那个东西坑更多。有问题欢迎在评论区交流我看到都会回。