ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ModelSim命令行仿真四步走:自动化脚本与常见坑排查

ModelSim命令行仿真四步走:自动化脚本与常见坑排查 为什么还在一个个点波形界面跑仿真如果手头有几十个测试用例、跑完还要把结果汇总上报GUI操作足够让人崩溃。ModelSim完整的命令行流程其实非常轻量vlib建库、vlog编译、vmap映射、vsim仿真四步走完就能把仿真从“手工点鼠标”变成“一条命令行跑完”。这篇内容就是把这四条命令掰开揉碎再附上可以直接拿去改的自动化脚本帮大家把仿真流程真正跑成自动化。适合刚开始接触ModelSim命令行的同学也适合想把手头重复劳动脚本化的工程师参考。1. 内容整体设计与思路拆解1.1 为什么非要命令行GUI不好吗ModelSim的GUI做得不差波形查看、信号追踪、断点调试鼠标点着挺顺手。但一旦涉及到大量回归测试、持续集成、批量跑不同参数组合的场景GUI就明显不够用了。举个实际例子项目里有六十多个testcase如果每个都靠手工添加文件、编译、设置仿真时间、查看波形一遍回归跑下来大半天就没了。命令行模式解决的核心问题有两个可重复和可批量。同样的命令脚本在任何人电脑上执行结果都一样不会因为“上次多勾了个选项”导致结果对不上循环跑case的时候只需要改参数就能一键执行完整个流程。另外命令行也有个隐藏优势资源开销比GUI小很多。GUI需要渲染波形窗口、维护图形界面状态在服务器上跑仿真的时候没有图形环境反而更顺。很多公司的FPGA仿真回归都是在服务器上跑的连GUI都起不了命令行模式是唯一选择。1.2 四条核心命令的职能划分一次完整的ModelSim命令行仿真本质上就是四个阶段命令作用类比vlib创建库目录给项目建一个“仓库”里面专门放编译产物vmap建立逻辑库名与物理目录的映射给仓库挂一个“门牌号”后续命令通过门牌号找到仓库vlog将 Verilog/VHDL 源文件编译进指定库把原料加工成半成品放进仓库vsim加载设计并执行仿真从仓库里提取半成品跑出最终结果这四个命令有严格的先后顺序。vlib 先行因为后续所有命令都要往库里写东西vmap 在最前面建立好映射后vlog 才知道把编译结果放哪vsim 则从库里找设计单元执行。你可能会遇到一种情况只敲vlog不敲vmap也能编译通过。那是因为ModelSim默认有一个work库vlib 默认创建的也是work库两者碰巧对上了。但一旦你需要把不同模块编译到不同库里比如把IP核单独放一个库vmap 就必不可少了。2. 准备工作安装、环境变量与基本路径设定2.1 安装环节的几个常见坑热词里“ModelSim安装教程”“ModelSim破解”“ModelSim激活”出现频率特别高可见安装环节劝退了不少人。我实际操作中比较稳的方案是用Intel FPGA官网提供的ModelSim Intel Starter Edition这个版本跟着Quartus安装包一起分发授权方面省心很多不需要另外折腾破解。如果用的是独立安装的ModelSim SE版本需要注意以下几点安装路径不要带空格或中文核心原则比如D:\Modelsim\win64这种就很好D:\Program Files\ModelSim容易在后续脚本解析路径时出问题。环境变量PATH里要加ModelSim的bin目录路径否则命令行敲vsim系统会提示“不是内部或外部命令”。部分版本需要设置环境变量LM_LICENSE_FILE指向license文件位置具体看安装说明。2.2 确认环境是否就绪在开始所有之前先打开终端敲一句确认vsim -version如果显示类似ModelSim SE-64 2020.4的版本信息环境就没问题。如果提示找不到命令检查一下环境变量PATH是否包含了ModelSim的bin目录。Windows下还可以用where vsim来定位可执行文件的实际位置确认你调用的到底是安装目录下的ModelSim还是其他工具自带的同名工具。顺带提一句Vivado联合ModelSim仿真的情况实际上就是让Vivado调用ModelSim作为仿真器本质仍是通过命令行方式在后台调用这四条核心命令。所以不管你是单独使用ModelSim还是通过Vivado调用命令行基础都适用。3. 核心命令逐个拆解vlib、vmap、vlog、vsim3.1 vlib创建物理库目录vlib的作用很单纯在工作目录下创建一个存放编译产物的文件夹。执行vlib work之后目录下会出现一个work文件夹里面存放ModelSim内部格式的数据库文件后续编译的模块都会写进这里。vlib work有人会问这个文件夹老早就存在了执行时会不会覆盖已有文件实测下来vlib对已存在的库会给出提示不会直接强制清空重建。如果确实需要从零开始要先手动删除旧的work目录再执行vlib# Linux/macOS rm -rf work vlib work # Windows rmdir /s /q work vlib work除了默认的work库你还可以创建多个库用于隔离不同来源的设计代码。比如IP核编译到ip_lib自己写的RTL编译到rtl_lib测试平台编译到work库这样管理起来非常整齐vlib work vlib ip_lib vlib rtl_lib清理旧库这步非常重要。跑过多次仿真后work目录里的编译产物会残留旧版本的模块如果不清理有时候会“莫名其妙”仿真结果和代码对不上多半就是库里存了旧的编译结果。3.2 vmap逻辑库名到物理目录的映射vmap是很多人忽略但其实很好用的命令。它的作用是把一个逻辑名称映射到一个物理目录比如vmap work ./work vmap ip_lib ./ip_lib执行之后ModelSim会在工作目录下生成一个modelsim.ini文件如果不存在里面记录了这些映射关系。后续vlog、vsim通过逻辑名称引用库时ModelSim会从 ini 文件里去查这个逻辑名对应的实际路径。理解映射机制很关键vlog和vsim的工作目录可能跟库的实际位置不在同一处通过先vmap敲一遍后面所有命令都不需要关心路径怎么拼了。对于已有modelsim.ini的场景如果临时想把某个库指到新路径可以这样vmap -del work # 删除现有映射 vmap work ./work_new # 重新映射如果库文件被移动到其他位置但映射没更新vsim会报Cannot find work library之类的错误这个时候先检查一下映射是否正确。再扩展一点如果你从别人那里拿到一个项目往往也会拿到一份modelsim.ini。直接用这份 ini 文件有可能把库路径指向别人机器上的绝对路径导致你的机器上报错。这种时候最简单的方式是删掉项目里的 modelsim.ini从零开始 vlib vmap 重建自己的映射文件。3.3 vlog把源代码编译进库vlog是编译Verilog代码的命令。基本用法vlog -work work ./rtl/uart_rx.v ./tb/tb_uart_rx.v这里的-work参数指定编译到哪个库默认是work库。如果不加-work默认就编译进work。编译完成后work目录里会多出对应的编译产物模块名由源文件里的module名决定而不是文件名。实战中比较常用的参数有几个-sv启用SystemVerilog支持。如果代码里有logic、interface、always_ff等语法不加这个参数会编译报错。-O0/-O5优化级别。调试期间建议用-O0关闭优化避免信号被优化掉导致波形里看不到跑长时间仿真时用-O5速度明显更快。-incr增量编译模式只重新编译有改动的文件大型项目里能省很多时间。-timescale 1ns/1ps显式指定时间单位和精度。如果源文件内部没有 timescale 指令编译选项里指定一个默认值是保险做法。defineXXX在编译时传递宏定义这个在控制条件编译、设置参数时非常常用。一个实际的编译命令示例vlog -sv -work work \ defineSIMULATION \ -timescale 1ns/1ps \ ./rtl/uart_rx.v \ ./rtl/fifo.v \ ./tb/tb_uart_rx.vvlog编译几个文件的顺序其实也有讲究。如果A模块例化了B模块但编译时先编译A再编译BModelSim处理这种跨文件依赖时可能报错也可能不报错取决于具体场景。稳妥做法是先编译被依赖的底层模块最后编译顶层。不过在SystemVerilog模式下ModelSim的编译器会做整体分析跨文件依赖有时能自动处理但不要依赖这种不确定性还是主动按依赖顺序排列编译列表更可靠。vlog编译通过之后你会看到类似这样的输出# ** Note: (vlog-2005) C:/project/rtl/uart_rx.v(12): \ # Module uart_rx declared注意行号信息如果后面仿真行为异常可以回来对照编译输出的行号定位。3.4 vsim运行仿真vsim是最核心的命令涉及到的选项多坑也多。最基本的形式vsim -c work.tb_uart_rx注意这里的模块名格式是库名.顶层模块名如work.tb_uart_rx。如果库就是work直接写tb_uart_rx其实也行但规范起见建议写全。-c是命令行模式command line mode这是自动化脚本的关键选项。不加-c会启动GUI界面。在服务器或纯命令行环境必须加。还常用到的选项选项作用说明-t 1ps设置仿真时间精度默认可能是1ns如果你的时钟沿很密集精度不够会丢失时间细节-voptargsacc保留所有信号的访问能力调试时推荐不加有些内部信号在波形里看不到-L指定额外搜索的库如果你在设计里例化了别的库里的IP-wlf 波形文件名.wlf指定波形记录文件名称默认是 vsim.wlf复用时记得改名字-do 命令串启动时直接执行批处理命令等价于打开时自动跑一堆指令比如-do run -all; quit-G参数名数值修改顶层generic/parameter在做多参数批量仿真时非常好用仿真启动后需要执行run命令来推进仿真时间。常见方式# 运行所有时间直到遇到 $finish run -all # 运行指定时间 run 10us # 运行指定时钟周期 run 1000 ns跑完仿真之后需要在命令行里退出ModelSimquit -sim如果是命令行模式跑回归通常会直接quit -f强制退出不弹任何确认框。典型的完整仿真命令就是vsim -c -voptargsacc -t 1ps -wlf uart_rx.wlf \ -do run -all; quit -f work.tb_uart_rx这样一行执行完仿真跑完自动退出命令行文件波形也留存下来了。后面想看波形再用GUI模式打开vsim -view uart_rx.wlf3.5 结合在一起的完整手工流程不讲脚本先看手工敲命令的顺序你就明白全流程了# 1. 清理旧库 rm -rf work # 2. 建库 vlib work # 3. 映射 vmap work ./work # 4. 编译RTL和TB vlog -sv -work work ./rtl/uart_rx.v ./tb/tb_uart_rx.v # 5. 跑仿真运行结束自动退出 vsim -c -voptargsacc -do run -all; quit -f work.tb_uart_rx # 6. 查看波形可选 vsim -view vsim.wlf如果你经常做这块工作就会发现第5步里的-do选项往往是整个自动化流程能否顺利运转的关键因为几乎所有的自定义行为都是通过-do塞进批处理命令串里的。4. 自动化脚本实践4.1 用环境区分平台的批处理脚本命令行流程理顺之后脚本化是水到渠成的事。这里给一个完整可用的Windows批处理脚本版本核心流程完整、参数可调echo off setlocal enabledelayedexpansion set RTL_DIR./rtl set TB_DIR./tb set TESTCASEtb_uart_rx set TOP_MODULEtb_uart_rx set RTL_FILES%RTL_DIR%/uart_rx.v %RTL_DIR%/uart_tx.v %RTL_DIR%/fifo.v set TB_FILES%TB_DIR%/tb_uart_rx.sv set WAVE_FILEwave_uart_rx.wlf echo [INFO] Clean old library if exist work rmdir /s /q work if exist modelsim.ini del /q modelsim.ini echo [INFO] Create library and mapping vlib work if errorlevel 1 goto :error vmap work ./work if errorlevel 1 goto :error echo [INFO] Compile RTL and TB vlog -sv -work work %RTL_FILES% %TB_FILES% if errorlevel 1 goto :error echo [INFO] Run simulation vsim -c -voptargsacc -t 1ps -wlf %WAVE_FILE% ^ -do run -all; quit -f work.%TOP_MODULE% if errorlevel 1 goto :error echo [INFO] Simulation completed successfully goto :eof :error echo [ERROR] Something went wrong, check above output exit /b 1这段脚本有几个设计点值得说明错误检查每一步命令后用if errorlevel 1做判断任何一步失败就退出并返回非零状态码。这在自动化回归流程里很重要——CI系统可以通过退出码判断构建是否成功。参数集中定义RTL文件列表、TB文件、顶层模块全部集中在脚本头部换测试目标时只需要改头部几行不需要动主体逻辑。名称变量区分TESTCASE与TOP_MODULE分别维护因为很多场景下case名字和顶层模块名不一样。留两个名称可以适应“一个TB文件里多个module”的情况同时也方便后面扩展跑批量参数。Linux/macOS下的shell脚本结构类似只是清理命令和换行方式不一样#!/bin/bash set -e RTL_DIR./rtl TB_DIR./tb TOP_MODULEtb_uart_rx RTL_FILES$RTL_DIR/uart_rx.v $RTL_DIR/uart_tx.v $RTL_DIR/fifo.v TB_FILES$TB_DIR/tb_uart_rx.sv WAVE_FILEwave_uart_rx.wlf echo [INFO] Clean old library rm -rf work modelsim.ini echo [INFO] Create library and mapping vlib work vmap work ./work echo [INFO] Compile RTL and TB vlog -sv -work work $RTL_FILES $TB_FILES echo [INFO] Run simulation vsim -c -voptargsacc -t 1ps -wlf $WAVE_FILE \ -do run -all; quit -f work.$TOP_MODULE echo [INFO] Simulation completed successfully注意shell脚本里的set -e这句话的意思是任何一条命令返回非零状态立即终止脚本。这比在每条命令后手动加if [ $? -ne 0 ]更简洁功能等价。但如果你需要在出错后做清理工作就不能依赖set -e得改成显式判断。4.2 批量参数仿真脚本实际项目中经常要跑多组参数比如FIFO深度分别配成16、32、64或者分频系数分别配成50、100、200。手工一次一次改参数再跑太累写一个批量脚本就能解决。核心思路利用vsim的-G参数在仿真时动态修改高层模块的parameter。先看如何用-G修改parametervsim -c -Gfifo_depth32 -Gdiv_factor100 work.tb_fifo这里-Gfifo_depth32会把顶层中名为fifo_depth的parameter覆盖为32。注意多个-G选项可以连用。但需要明确一点-G只能修改当前加载的顶层模块的parameter如果是子模块内部的parameter需要把参数通过端口或层次化引用的方式传递到顶层或者修改脚本重新编译。批量跑参数循环的批处理脚本Windows版echo off setlocal enabledelayedexpansion for %%D in (16 32 64 128) do ( echo [INFO] Running simulation with FIFO depth%%D vsim -c -voptargsacc -Gfifo_depth%%D ^ -do run -all; quit -f work.tb_fifo echo [INFO] Done depth%%D )或者用shell脚本#!/bin/bash for depth in 16 32 64 128; do echo [INFO] Running simulation with FIFO depth$depth vsim -c -voptargsacc -Gfifo_depth$depth \ -do run -all; quit -f work.tb_fifo echo [INFO] Done depth$depth done如果想给每组参数生成独立的波形文件可以在-do里用变量拼接文件名但注意-do里的字符串是直接传给ModelSim执行的要把脚本变量组合进命令字符串需要处理好引号转义。例如depth64 vsim -c -wlf fifo_depth_${depth}.wlf \ -Gfifo_depth$depth \ -do run -all; quit -f work.tb_fifo批量跑完后所有波形文件都会按名字区分开后续可以统一合并分析。注意这个.wlf文件路径不受库映射影响直接写在当前目录。4.3 想把波形查得明明白白用WLF文件与-do命令串命令行模式不是只能闷头跑到结束。你可以在-do里追加很多波形控制命令把需要的信号记录下来方便后查。例如vsim -c -voptargsacc -wlf uart_rx.wlf \ -do log -r /*; run -all; quit -f work.tb_uart_rxlog -r /*表示记录设计中所有层次下的所有信号。这在调试初期很管用缺点就是波形文件会很大仿真速度也会变慢。跑大规模回归时要谨慎使用全量记录建议换成具体信号列表vsim -c -voptargsacc -wlf uart_rx.wlf \ -do log -r /tb_uart_rx/dut/*; run -all; quit -f work.tb_uart_rx这样只记录DUT下的信号TB顶层的一些控制信号不算进去。另外如果想要两个case共用同一个do文件把波形记录、run命令放到一个.do文件里处理也是常用做法log -r /tb_uart_rx/dut/* run -all quit -f然后在vsim里指定vsim -c -do run_script.do work.tb_uart_rx这里ModelSim会逐行执行run_script.do里的命令语法是Tcl格式。用do文件的好处是仿真控制和编译逻辑分离调试时改仿真行为不用重新编译。4.4 让仿真快速结束或超时控制如果你的testbench没有主动$finishrun -all会一直跑下去。在自动化脚本里这种“跑飞”会让整个CI任务挂死。解决方法是设置上限时间vsim -c -do run -all; quit -f -do run 10ms work.tb_uart_rx等等这样写不对。-do选项如果想连续执行多条命令正确写法是一条-do里用分号隔开vsim -c -do run -all; quit -f -do run 10ms work.tb_uart_rx上面这种连续两个-do的写法ModelSim会依次执行第一个和第二个所以先无限跑再强制跑10ms顺序上不对。正确做法是vsim -c -do onfinish exit; run -all; quit -f work.tb_uart_rx如果担心run -all真的永远跑不完可以在testbench里加一个看门狗风格的超时控制initial begin #10ms; $display([ERROR] Simulation timeout after 10ms); $fatal; end或者在vsim的-do里直接用run 10ms代替run -all保证一定会返回vsim -c -do run 10ms; quit -f work.tb_uart_rx采用哪种方式取决于你对testbench的掌控程度。工程实践中我习惯在TB里放超时断言这不仅能控制仿真长度还能在超时时明确报错。4.5 结合覆盖率命令做回归热词里出现了“ModelSim覆盖率txt怎么合并”这里一并讲清楚。ModelSim覆盖率数据通常存在.ucdb文件里多个case的覆盖率合并用vcover命令vcover merge merged.ucdb case1.ucdb case2.ucdb case3.ucdbvsim生成覆盖率数据的选项是-coveragevsim -c -coverage -do run -all; quit -f work.tb_uart_rx跑完会在当前目录生成vcover_data文件夹或指定文件。想要txt文本报告可以在仿真结束后用vcover report生成vcover report -details -output coverage_report.txt merged.ucdb覆盖率合并的关键在于多个case之间要有共同的代码基础通常是在同一份编译结果上跑不同case。如果不同case用了不同的编译库ucdb合并起来意义不大因为代码行号对应不上。所以正确姿势是先编译一次再用不同参数或不同约束跑多个case最后合并覆盖率。这整套流程同样非常依赖于命令行模式GUI里点覆盖率分析效率很低。5. 常见问题与排查技巧实录5.1 仿真波形全是红线/蓝线/X态热词里“modelsim仿真波形是红线”出现频率很高。波形显示为红线高阻Z或蓝线未初始化X本质是信号没有被正确驱动。排查思路复位是否释放很多TB里复位信号需要拉高再拉低或者反过来检查复位逻辑是否真的执行了。时钟是否生成查看时钟信号是否在跳变。如果时钟一直是0或X所有时序逻辑都不会工作输出自然是X。常见原因是testbench里always #10 clk ~clk;中的clk类型错误——如果clk被声明成wire而不是reg这行根本驱动不了。信号是否被优化掉了vsim默认会对设计做优化未观测的中间信号可能被优化掉导致波形显示红线。加-voptargsacc可以解决这个问题。跨时钟域信号未初始化异步信号进来没有打拍或没有初始值仿真开始时就处于X态。快速验证方法在testbench的initial块里把所有输入信号都给一个确定值再看波形红线是否消失。如果消失了就是驱动逻辑的问题如果还在可能是信号被优化或者层次引用路径不对。5.2 vlog编译报错怎么办vlog报错时先看错误描述。最多见的几类语法错误代码里缺分号、endmodule漏写、端口声明不对。vlog-13067之类的编译选项问题比如使用了SystemVerilog语法但没加-sv。找不到依赖的模块A模块例化了B模块但B没有编译进同一个库。报错通常长这样Module B not found.。解决方法是检查编译顺序或把B的源文件路径加入编译列表。宏未定义代码里用了ifdef SIMULATION但编译时没加defineSIMULATION导致代码块被跳过。检查是否漏了宏定义。编译报错时有个实用技巧先看最顶层的** Error行通常会在最后显示。往下翻几百行找到第一条error往往才是根因后面的报错经常是“雪崩式”的连环报错即使第一个error修复了后面也会自动消失。5.3 vsim说找不到work库或设计单元常见报错有两类** Error: (vsim-19) Failed to access library work at work.这个属于库路径没对上。可能原因是当前工作目录和编译时候的工作目录不一致或者 modelsim.ini 被替换过。解决方法是检查当前目录下是否有modelsim.ini确认里面的work映射路径。如果找不到重新执行vmap work ./work。另一类** Error: (vsim-3170) Could not find work.tb_uart_rx.这个说明设计单元在库里不存在。要么是编译没成功要么是模块名写错。可以用vdir work列出work库里所有编译单元核对实际模块名。还有一个比较隐蔽的场景vlog编译时如果源文件内容发生变化但没重新编译vsim会加载旧版本。这也是为什么回归脚本第一件事永远是rm -rf workvlib work就是为了杜绝这种“编译了但没生效”的诡异问题。5.4 全仿真过程中Testbench卡死或运行时间异常常见两个原因一是testbench里存在无限循环。比如always begin #1; end这种写法仿真时间推进极慢因为每个#1都会触发一次事件仿真器忙不完。在调试阶段可以用run 100us限定运行时间观察一下时间推进是否正常。另一种办法是打开-debug调试信息看仿真器事件循环在哪一步消耗大量时间。二是testbench里用了wait或等待着永远不会到来的事件。比如等一个握手信号但DUT状态机跑死了等不到。这种情况通过波形或$display日志定位卡点即可看哪一行$display打完之后就没了。5.5 波形文件占用空间大、生成慢波形文件动辄几个GB打开和保存都很难受。建议只记录必要的信号层次不要动不动log -r /*。修改采样精度-t 1ns比-t 1ps记录的数据量小很多做功能验证1ns一般够用。用-wlf指定独立波形文件名避免覆盖上一次记录。如果只看几个关键信号可以直接用add waverun的方式只记录GUI能看到的信号但这依赖GUI模式不适合纯命令行回归。5.6 do文件无法自动退出-do run -all; quit -f里如果testbench执行了$stop而不是$finish仿真会在断点处停下run -all不会返回后面的quit -f也就不会执行。脚本就会一直挂在那里。解决方法是修改TB代码把$stop换成$finish或者使用onfinish exit命令让ModelSim在任何仿真结束原因下都能退出vsim -c -do onfinish exit; run -all; quit -f work.tb_uart_rxonfinish exit的意思是当仿真结束无论是因为什么原因时ModelSim直接退出进程。这个命令在自动化回归里非常有用比单纯依赖$finish稳得多。6. 一份最小可跑的完整示例工程前面讲得再多不如给一个你直接能跑的示例工程。这里以常见的UART接收模块为例展示完整的命令行仿真流程。工程结构uart_project/ ├── rtl/ │ └── uart_rx.v ├── tb/ │ └── tb_uart_rx.sv ├── sim/ │ ├── run_sim.bat (Windows) │ └── run_sim.sh (Linux/macOS)uart_rx.v做简化处理只保留可仿真的核心逻辑module uart_rx ( input wire clk, input wire rst_n, input wire rx, output reg [7:0] data, output reg data_valid ); localparam IDLE 2b00; localparam START_BIT 2b01; localparam DATA_BITS 2b10; reg [1:0] state; reg [2:0] bit_cnt; reg [7:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 0; shift_reg 8b0; data 8b0; data_valid 1b0; end else begin case (state) IDLE: begin if (rx 1b0) begin state START_BIT; end end START_BIT: begin bit_cnt 3d0; state DATA_BITS; end DATA_BITS: begin shift_reg[bit_cnt] rx; if (bit_cnt 3d7) begin state IDLE; data shift_reg; data_valid 1b1; end else begin bit_cnt bit_cnt 1b1; end end default: state IDLE; endcase end end endmoduletb_uart_rx.sv简单发送一个起始位加8bit数据模拟UART输入timescale 1ns/1ps module tb_uart_rx; reg clk; reg rst_n; reg rx; wire [7:0] data; wire data_valid; // 50MHz clock, period 20ns localparam CLK_PERIOD 20; // UART 波特率 115200 - bit_time约8680ns localparam BIT_TIME 8680; // DUT instance uart_rx dut ( .clk (clk), .rst_n (rst_n), .rx (rx), .data (data), .data_valid(data_valid) ); // clock generation initial clk 0; always #(CLK_PERIOD/2) clk ~clk; // UART发送任务起始位低 8bit数据LSB first 停止位高 task send_byte(input [7:0] byte_data); integer i; begin rx 1b1; #BIT_TIME; rx 1b0; // start bit #BIT_TIME; for (i 0; i 8; i i 1) begin rx byte_data[i]; #BIT_TIME; end rx 1b1; // stop bit #BIT_TIME; end endtask // 超时看门狗 initial begin #1ms; $display([ERROR] Simulation timeout); $fatal; end // 测试主流程 initial begin rst_n 1b0; rx 1b1; #100; rst_n 1b1; #200; send_byte(8hA5); #200; if (data 8hA5 data_valid 1b1) $display([PASS] Received correct data 0xA5); else $display([FAIL] Expected 0xA5, got 0x%02h, valid%b, data, data_valid); #100; $finish; end endmodule跑仿真的命令就是上面讲过的四步vlib work vmap work ./work vlog -sv -work work ./rtl/uart_rx.v ./tb/tb_uart_rx.sv vsim -c -voptargsacc -do run -all; quit -f work.tb_uart_rx由于Testbench里有$display输出仿真结束后的终端就能看到PASS或FAIL标记。再把这几行写进脚本就是一个完整可复用的自动化回归单元。7. 几个从实践里总结的小经验命令行仿真看起来只是几个命令的组合但真正把流程理顺后对整个开发效率的提升很明显。我个人体会最深的几点库目录不要随意跨越项目复用。每个工程最好都有自己的独立work目录宁可多占点磁盘空间也不要图方便共用。不同工程版本、不同编译选项产生的编译产物混在一起会出现极其难排查的加载错模块问题。编译和仿真尽量用同一个工作目录。在脚本里用cd切换到统一目录再执行命令避免因为启动位置不同导致vmap映射错乱。用到modelsim.ini的时候尤其注意它的查找规则是“当前目录优先”。所有回归脚本都加上退出码透传。任何一步失败脚本马上以非零状态退出。这样才能被CI系统正确识别。很多初学者的脚本从头到尾不管失败直接冲到底结果一天下来发现所有case仿真结果都不可信。用$display在TB里打关键信息。命令行模式没有波形界面调试主要靠log。关键信号变化、断言结果、错误信息都往终端打。比仿真结束后再开波形慢慢翻快得多。最后还有一个小建议如果你的testbench比较重每次编译都要花上不少时间可以考虑把RTL编译和TB编译分开建库RTL库长期保留TB库每次重建。这样迭代TB时只需要重新编译lib一次能省出不少时间。这个做法在大型工程里收益尤其明显我把这个结构调整到日常流程之后每次编译等候的时间几乎少了一半。
返回列表