我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Vivado行为仿真入门:与非门Testbench自检全流程

Vivado行为仿真入门:与非门Testbench自检全流程 带过几届新人做 FPGA 入门我发现一个挺普遍的现象第一次打开 Vivado大多数人的操作路径都高度一致——新建工程、写两行代码、直接点综合和实现然后一脸期待地等着比特流。至于行为仿真这一步通常是等到板子上的灯不亮、串口没回包、状态机跑飞了才想起来回头补。其实在 Vivado 里做行为仿真Behavioral Simulation的成本低得离谱不用等综合不用等布局布线几秒钟就能拿到结果。而拿与非门NAND Gate来当第一个练手对象是我认为最划算的选择它的逻辑功能足够简单简单到可以把它当成一把尺子去丈量整条仿真链路——RTL 源码怎么写、Testbench 怎么搭、激励向量怎么给、波形窗口怎么读、验证结果怎么自动判定。这套流程跑通一遍后面不管你仿的是 FSM、FIFO 还是 DDR 控制器骨架都是同一个。这篇内容偏入门但不会只停留在点哪个按钮的层面。我会把每个操作背后的原因讲清楚比如为什么入门建议选数据流描述而不是行为描述、为什么 Testbench 里要用自检而不是靠肉眼看波形、为什么 timescale 写错了会让你对时间的判断全盘偏移。适合刚接触 Vivado、能看懂基础 Verilog 语法、但还没完整跑通过一次仿真的读者如果你已经能熟练用 ModelSim 写自检 Testbench这里面的排查思路应该也能帮你省点时间。1. 入门为什么先拿与非门开刀1.1 与非门的功能完备性值得你花时间吃透数字逻辑里有个经典结论与非门是功能完备的也就是说只用二输入与非门就能搭出非门、与门、或门、或非门、异或门理论上可以组合出任何组合逻辑电路。这一点很关键因为它意味着你验证与非门的那一套方法是可迁移的。非门怎么接把两个输入短接到一起y ~(a a) ~a。与门呢在与非门后面再挂一个与非门当反相器用。这种用一个基本器件去拼装其他逻辑的思路恰恰是 FPGA 里 LUT 的工作方式——一个 6 输入查找表本质上就是一张可配置的真值表你写什么组合逻辑它都能映射进去。所以第一个仿真对象选择与非门不是因为简单而是因为它处在整个数字逻辑的地基位置。跑通它你对输入激励怎么施加、输出怎么观测、延迟怎么建模这几个问题的理解后面可以直接复制到任何组合逻辑模块上。先把真值表摆出来后面所有的验证都围绕它展开aby ~(a b)001011101110这张表只有四行肉眼扫一遍就记住了。但要提醒一句真值表是静态的只告诉你稳定之后输入输出该是什么关系。真实的电路在输入翻转的瞬间还有一段不稳定的过渡过程波形上会看到毛刺。仿真如果不加延迟输出是瞬间跳变的看起来完美但和真实器件行为有差别。这个差别什么时候要管、什么时候可以不管我在第 4 节会具体说。从硬件实现角度看CMOS 与非门是两个 PMOS 并联 两个 NMOS 串联的结构。A、B 全为 1 时两个 NMOS 同时导通把输出拉到地其余三种情况串联链至少有一个管子截止同时上拉的并联结构至少有一个 PMOS 导通输出被拉到高电平。理解这个结构你在波形里看到延迟不对称、上升下降沿时间不一样就不会觉得是仿真器出问题了。1.2 行为仿真到底在验什么和另外两种仿真差在哪Vivado 的仿真链路里其实有三个层次很多新手把它们混为一谈结果拿时序仿真的标准去要求行为仿真白折腾半天。行为仿真Behavioral Simulation也叫 RTL 功能仿真、Post-Synthesis 之前的仿真只针对你的 RTL 源码和 Testbench不涉及任何工艺库、不涉及布局布线信息。它的核心目的是验证逻辑功能对不对——真值表对不对、状态机跳转对不对、握手协议对不对。因为它跑的是行为模型速度最快调试也最方便仿真器里所有信号都能看到。综合后功能仿真Post-Synthesis Functional Simulation会把 RTL 综合成门级网表再仿真逻辑结构已经映射到 LUT、触发器等具体资源上但不含布线延迟。用来确认综合有没有把你的逻辑优化掉、有没有产生锁存器之类的问题。时序仿真Post-Implementation Timing Simulation则是在网表基础上加上 SDFStandard Delay Format延迟文件把所有走线延迟和单元延迟都算进去。这一步跑得慢、波形难读但它是判断在目标频率下能不能稳定工作的关键。仿真类型输入文件是否含工艺延迟主要用途相对耗时行为仿真RTL Testbench否功能正确性验证最低综合后功能仿真综合网表 Testbench否确认综合结果与 RTL 一致中等时序仿真实现网表 SDF Testbench是建立/保持时间、时序收敛验证最高入门阶段把精力全部放在行为仿真上是完全正确的。功能都没跑对谈时序没有任何意义。而且说句实话即便是做了很多年的人日常开发中跑得最多的也是行为仿真时序仿真往往只在关键路径或者高速接口调试时才启用。注意行为仿真通过不代表上板一定成功。它只排除逻辑错误无法暴露跨时钟域、时序违例、管脚约束错误等问题。这个认知要在入门阶段就建立起来。1.3 开工前的环境确认别让工具问题打断节奏在动手之前花两分钟确认几件事能避免后面 80% 的无效折腾。Vivado 的版本选择上2018.3、2020.2、2022.2、2023.2 这些都是比较常用的版本。入门不必追求最新选一个稳定、资料多的版本就行因为仿真流程在各版本之间基本一致XSim 仿真器的核心用法没变过。安装时记得把 XSim 组件勾上如果只装了 Vivado 的精简版或者漏选了仿真组件后面点 Run Simulation 会直接报错找不到仿真器。器件型号方面如果你手上没有具体的板子选一款常见的就行比如 Artix-7 系列的 xc7a35t 或者 Zynq-7000 系列的 xc7z020。器件型号对行为仿真几乎没有影响因为行为仿真根本不涉及具体器件资源它只影响后面的综合实现。但工程里必须选一个Vivado 不允许器件留空。磁盘空间和内存也提一句。Vivado 本身比较吃资源仿真时波形数据会额外占内存。如果仿真波形里信号加得太多、仿真时间又拉得很长可能会看到内存占用一路飙高。入门阶段信号少、时间短不会遇到这个问题但心里要有数。2. 工程搭建与 RTL 源码落地2.1 新建工程的几个选项选错了后期难受打开 Vivado在起始页点 Create Project接下来是一个向导流程。这里有几个选项值得细说。第一项是工程类型选 RTL Project并且勾上 Do not specify sources at this time。为什么建议先建空工程再手动加文件因为向导里加源文件的时候很多人分不清 Add Sources 里 Design Sources 和 Simulation Sources 的区别把 Testbench 加到了设计文件里头结果综合的时候把 Testbench 也一起综合了报一堆莫名其妙的错误。先建空工程等进了主界面再分别添加边界清晰。第二项是器件选择。Boards 标签页里如果恰好有你手上板子的预设可以直接选板卡没有的话切到 Parts 标签页按系列、封装、速度等级筛选。入门建议直接选一个通用型号别在器件上纠结太久。第三项工程路径和工程名。这里有个小坑路径里尽量不要出现中文和空格。Vivado 对非 ASCII 路径的支持一直不太理想尤其是在 Windows 上路径里带中文有时会导致综合或者仿真阶段报文件找不到。养成用纯英文短路径的习惯比如D:/fpga_lab/nand_sim。工程建好之后建议立刻做一件事在 Settings 里确认一下目标语言Target language是 Verilog 还是 VHDL。默认通常是 Verilog如果你更熟悉 VHDL 就切过去两者在仿真流程上完全一样只是文件后缀和语法不同。后面所有代码示例我都用 Verilog 写。2.2 同一个与非门数据流描述和行为描述怎么选给与非门写 RTL至少要掌握两种写法它们分别代表了不同的抽象层次。第一种是数据流描述用连续赋值语句assignmodule nand_gate ( input wire a, input wire b, output wire y ); assign y ~(a b); endmodule这种写法的特点是简洁、直观直接对应布尔表达式。综合出来的结果和你写的意思几乎一一对应仿真速度快波形可读性好非常适合组合逻辑。第二种是行为描述用always块配合if-elsemodule nand_gate_behav ( input wire a, input wire b, output reg y ); always (*) begin if (a 1b1 b 1b1) y 1b0; else y 1b1; end endmodule注意这里的输出端口必须声明成reg类型因为它在always块里被赋值。这是很多新手第一次写的时候必然踩的坑——写成output wire y然后报语法错误或者干脆不加类型Vivado 默认给你当 wire 处理又是一堆报错。那入门为什么推荐先写第一种因为assign写法没有敏感列表这个概念也就不存在always (*)漏写信号导致的仿真与综合不一致问题。行为描述在某些边界情况下——比如敏感列表不完整、用了阻塞赋值又在时序逻辑里混用——会产生仿真结果和综合结果对不上的情况这种问题对新手来说极难定位。等你对仿真流程完全熟悉了再去写行为描述心里有底。2.3 源码里几个看起来没事、实际会咬人的细节端口方向别写错。input是输入output是输出inout是双向。与非门只有单向简单但养成习惯写之前先想清楚数据流向。位宽要对齐。a和b是 1 位y也是 1 位。如果你不小心把a定义成了[1:0]那a b就是两位按位与y也得是两位语法不报错但结果完全不是你想要的。位宽不匹配是组合逻辑里最常见的隐性错误之一仿真波形上会表现为信号值莫名其妙实际是位宽在捣鬼。运算符优先级。~的优先级高于但你写~a b的含义是(~a) b不是~(a b)。所以与非门一定要老老实实写~(a b)括号不能省。少写一个括号逻辑就从与非门变成了非 A 与 B真值表直接错一半。模块名和文件名保持一致。Vivado 虽然不强制要求但自动推导顶层模块的时候是按模块名来的。模块叫nand_gate文件叫nand_gate.v这是最容易维护的状态。乱起名字后期文件一多找都找不到。加完源文件后Vivado 会自动做语法检查有问题会在窗口下方的 Messages 里报出来。养成看一眼 Messages 的习惯别不管不顾直接往下走。3. Testbench 怎么写才算规范3.1 最小可用骨架长什么样Testbench 本质上也是一个 Verilog 模块只是它没有端口。它的工作是产生激励、例化被测模块DUT、观测输出。timescale 1ns / 1ps module tb_nand_gate; reg a; reg b; wire y; nand_gate u_nand ( .a (a), .b (b), .y (y) ); initial begin a 1b0; b 1b0; #10; a 1b0; b 1b1; #10; a 1b1; b 1b0; #10; a 1b1; b 1b1; #10; $display(simulation finished at %0t, $time); $finish; end endmodule这里面有几个点必须讲清楚。为什么a和b是reg而y是wire因为在initial块里给信号赋值被赋值的信号必须是reg或者logicSystemVerilog 里统一了。而被测模块的输出是它驱动的Testbench 只负责观测所以用wire。这个规则记牢写 Testbench 一半的错误都是这里来的。u_nand是例化名可以随便起但建议加上前缀比如u_方便区分。端口连接推荐用.port_name(signal_name)的命名关联方式而不是位置关联。位置关联容易顺序搞错尤其是模块端口多了以后改一个端口顺序就全乱了。命名关联虽然多敲几个字但可靠性高得多。3.2 激励向量的设计穷举是最省心的起点与非门只有两个输入所有组合也就四种。对这种情况直接穷举是最省心也最保险的做法——把真值表的每一行都跑一遍。但穷举也有讲究每施加一组激励之后要给足够的时间让输出稳定这个足够由#10这样的延迟语句实现。为什么要加延迟因为 Verilog 仿真器是事件驱动的如果你连着写a 0; b 0; a 1; b 1;中间不加延迟仿真器会在同一个时间点time 0处理完所有赋值输出的中间状态可能根本来不及反映到波形上。那#10这个 10 是不是随意定的在timescale 1ns / 1ps的前提下#10就是 10 纳秒。对于行为仿真来说具体的数值意义不大只要保证每个激励之间有明确的时间间隔、波形上能清清楚楚地看出分段就行。但在带延迟建模的仿真里这个数值就要认真选了——它得比器件延迟大才能保证采样到的是稳定值。真实的测试激励当然不会像与非门这么简单。如果是状态机或者接口协议通常会采用随机激励 定向激励结合的方式随机激励负责覆盖面定向激励负责打边界条件。但对入门来说先把穷举这条路走通。3.3 自检测试让人眼从波形里解放出来只给激励不做判定意味着每次跑完仿真你都得盯着波形图一条一条核对。信号少还能忍信号一多就是纯粹的时间消耗。更好的做法是在 Testbench 里直接把期望值写进去让仿真器自己判定。timescale 1ns / 1ps module tb_nand_auto; reg [1:0] vec; reg expected; wire y; nand_gate u_nand ( .a (vec[1]), .b (vec[0]), .y (y) ); integer i; initial begin $display( start ); for (i 0; i 4; i i 1) begin vec i[1:0]; expected ~(vec[1] vec[0]); #10; if (y ! expected) $display(FAIL: a%b b%b y%b expect%b, vec[1], vec[0], y, expected); else $display(PASS: a%b b%b y%b, vec[1], vec[0], y); end $display( done ); $finish; end endmodule这里有几个细节值得单独说。i[1:0]把循环变量当成向量用四次循环刚好覆盖 00、01、10、11 四种输入组合。这是写小规模组合逻辑穷举测试的常用技巧。!是不等的全等比较它会把 X 和 Z 也当作可比较的值。如果你用普通的!当y是 X 态时比较结果也是 Xif会走 else 分支报出 PASS——假的通过。用!才能把 X 态抓出来。这是自检测试里最容易犯的错误之一。$display的输出会打印在 Vivado 下方的 Tcl Console 里不需要看波形就能判断结果。跑完看到四行 PASS 和两行分隔线说明逻辑功能没问题。这种一行输出定生死的验证方式效率比看波形高一个数量级。3.4 timescale 和仿真时间的关系写错了会全盘偏移timescale 1ns / 1ps这行指令由两部分组成前一个数字是时间单位time unit后一个数字是时间精度time precision。单位决定了#10代表多长精度决定了仿真器能分辨的最小时间步长。如果 Testbench 里没写这行Vivado 会在编译时给出类似 time unit used without timescale 的警告。有些工程里验证代码全都没写 timescale靠默认值跑短时间内看不出问题一旦涉及多模块协同或者需要精确计时就会出大乱子。这张表能帮你快速对照timescale 写法时间单位#10实际延迟适用场景1ns / 1ps1 纳秒10 ns通用推荐入门使用1ns / 1ns1 纳秒10 ns精度要求不高时仿真稍快10ns / 1ns10 纳秒100 ns慢速逻辑、秒级计时1ps / 1ps1 皮秒10 ps高速接口波形数据量大建议整个工程的 Testbench 统一用1ns / 1ps不要一个文件一个样。混用 timescale 时仿真器会按取最小精度的规则统一处理日志里会给出提示但很多新手不看日志结果时间轴上全是错的推断。4. 跑仿真从启动到读懂波形4.1 启动行为仿真的两条路径与运行时长设置工程里 RTL 和 Testbench 都加好之后启动仿真有两条路径。第一条是走 Flow Navigator。在左侧面板找到 SIMULATION 分组点 Run Simulation然后选 Run Behavioral Simulation。Vivado 会自动编译所有仿真源文件把 Testbench 设为顶层打开仿真界面和波形窗口。第二条是在 Sources 面板里右键 Testbench 文件选 Set as Top然后同样从 Flow Navigator 启动。这两条路殊途同归区别只在于你更习惯从哪边操作。启动仿真前有一处设置值得改。在 Settings 里找到 Simulation 标签页里面有一项xsim.simulate.runtime默认值可能是1000ns之类。这个参数决定仿真默认跑多久。如果你的 Testbench 里有$finish把它设成all仿真会一直跑到$finish语句才停最省事。如果没有$finish那就得手动指定时间比如10000ns否则仿真要么秒停要么跑到天荒地老。另外一个常被忽略的设置是xsim.simulate.log_all_signals。这个选项控制是否记录所有信号的波形。默认打开的时候仿真器会把每一个信号的变化都记下来方便你事后加信号看图代价是内存占用和仿真速度都会受影响。信号少无所谓工程一大就得权衡。这个选项在排查仿真慢的问题时很有用后面第 5 节还会提到。4.2 波形窗口的操作用熟了效率翻倍仿真跑起来之后波形窗口是你待得最久的地方几个操作必须熟练。加信号到波形。在仿真界面左侧的信号树里找到 Testbench 里的a、b、y右键 Add to Wave Window。也可以直接把整个 DUT 例化的层级拖进去一次加完。信号加多了之后右键可以 Group 分组给组起个名字视图会清爽很多。调整信号进制。默认显示的是二进制a、b这种单比特信号不用改。如果后面验证的是多位宽数据比如计数器、状态寄存器右键信号选 Radix可以切成十六进制、无符号十进制、有符号十进制。状态机信号建议用一个自定义枚举列表来显示可读性会好很多。控制仿真推进。工具栏上有几个按钮Restart 把仿真时间重置到 0 并重新开始Run All 一直跑到结束Run For 可以指定跑多少时间比如跑 10ns 就停。调试的时候 Restart Run For 的组合最常用能达到分段观察的效果。光标测量。在波形上双击可以打一个标记或者用两条光标测两个事件之间的时间间隔。做时序相关的调试时这个功能是刚需。4.3 用真值表对照波形逐条核验跑完仿真波形上应该是这样一幅图a和b每 10ns 变化一次按 00、01、10、11 的顺序走y在前三段保持高电平最后一段a1, b1变成低电平。时间段ab期望 y观察 y判定0 ~ 10ns0011通过10 ~ 20ns0111通过20 ~ 30ns1011通过30 ~ 40ns1100通过波形是瞬变的——a和b在同一时刻跳变y也在同一时刻跟着翻转没有过渡带。这是行为仿真的典型特征因为没有延迟信息。真实的器件里输出会在输入变化之后过一段时间才响应这段时间就是传播延迟。4.4 给门加上延迟让波形更接近真实器件想让波形看起来更真一点可以在 RTL 里加一个延迟assign #5 y ~(a b);加上之后a或b变化后 5nsy才会有反应。如果a和b是错开变化的——比如a先变、5ns 后b再变——那y会在中间出现一个短暂的跳变这就是毛刺glitch。毛刺在组合逻辑里是绕不开的问题它是输入信号到达时间不一致造成的。做实际项目时遇到组合逻辑驱动的时钟、复位、使能信号就要特别警惕毛刺造成的误触发。注意在综合流程里assign #5这种延迟是不可综合的综合工具会直接忽略它。所以在 RTL 里加延迟只影响仿真不影响实际硬件。真正的延迟建模应该放在综合后或实现后的仿真里由工具提供的延迟信息决定。如果你的 Testbench 里用了#10的间隔而 RTL 里加了#5的延迟那每个采样点都在输出稳定之后波形会是干净的阶梯状。反过来如果延迟大于采样间隔就会在采样点上看到还没稳定的值——这是很多仿真结果随机变化问题的根源一定要让延迟和采样时间匹配。5. 仿真结果不对时的排查手册5.1 常见报错与现象速查表跑仿真遇到问题是必然的为了让你少走弯路我把新手最容易撞上的几类情况整理如下。现象可能原因排查方向波形里所有信号都是 XTestbench 里reg变量没赋初值或 DUT 未被正确例化检查initial块是否给所有输入赋了初值检查端口连接输出信号是 Z蓝线wire型输出没有驱动源检查 DUT 例化是否成功端口拼写是否一致波形全红高阻信号被多个驱动源同时驱动或驱动冲突检查是否有重复例化、多个assign驱动同一wire报 module not found源文件没加入仿真源集或模块名拼写错在 Sources 里确认文件归在 Simulation Sources 下仿真跑不动 / 秒退没有$finish或 runtime 设置为all但代码无终止条件加$finish或手动设置合理运行时长仿真结果与预期不符但语法无错位宽不匹配、运算符优先级问题、逻辑写反逐行对照 RTL打印中间信号辅助定位修改代码后仿真结果没变仿真没有重新编译关闭仿真界面重新 Run Simulation或先 Restart时间轴数值异常timescale 缺失或与其他文件不一致统一在 Testbench 顶部声明timescale这张表建议收藏。实际上 90% 的入门级仿真问题都落在前四行里而且原因高度集中在信号没驱动和赋值没生效这两类。5.2 X 态和 Z 态两种看起来差不多、诊断完全相反的现象X 态在波形上显示为红线或者菱形含义是不确定值。它通常来自两种情况一是reg变量声明后没有赋初值Verilog 里reg的默认初始值是 X二是多个驱动源产生冲突仿真器无法决定该用哪个值。排查 X 态先用$display把相关信号的值和时间戳打出来定位到是哪一级开始变成 X 的。Z 态显示为蓝色的中间线含义是高阻也就是没有驱动。在 Testbench 里如果被测模块的输出端口没接上、或者端口名拼错导致悬空波形就是 Z。这种情况在例化端口较多的时候很容易发生我的习惯是把例化语句里的端口名和 RTL 里的声明逐个对照或者干脆用命名关联来避免。一个容易被忽略的点wire类型的输出如果没有任何驱动综合阶段会报警告仿真阶段直接显示 Z。所以看到 Z先别怀疑逻辑先查连接。5.3 仿真速度慢下来的时候该砍哪里仿真几万纳秒还没跑完是很多人都会经历的一步。速度慢的原因通常有几个按收益排序。第一关掉全信号记录。在 Settings 里的xsim.simulate.log_all_signals设为false只在需要的时候手动把关心的信号加到波形窗口。这个改动对速度的提升往往是最明显的因为波形记录的 I/O 开销很大。第二缩减$display的打印频率。放在大循环里的$display是性能杀手尤其是打印格式化字符串时。调试期临时加可以验证稳定之后要记得删掉或者加条件控制。第三缩短仿真时长。用$finish明确终止条件别让仿真空跑到时间上限。第四检查 Testbench 里有没有超长的延迟语句。有人在 Testbench 里写了#1000000来模拟毫秒级事件结果每次跑仿真都得等很久。如果确实需要长延时考虑用参数化或者分阶段仿真。第五避免在always块里做无意义的反复计算。虽然组合逻辑的评估是事件驱动的但如果敏感列表写得过宽会导致大量无谓的重新计算。5.4 几条只有踩过才知道的经验第一条工程目录别放在云同步盘里。某些同步客户端会持续扫描文件变化导致 Vivado 的临时文件被反复触碰仿真的编译环节可能失败或者异常缓慢。把工程放在本地固定盘上是省心的做法。第二条仿真跑之前先看一遍 Messages 窗口。Vivado 会把所有的警告都列在那里比如输出端口未连接位宽不匹配。这些警告在综合阶段可能不影响结果但在仿真阶段往往是问题源头。养成扫一遍警告的习惯能提前拦掉很多问题。第三条Testbench 也要进版本管理。有人觉得 Testbench 是临时文件改完就丢。实际上好的 Testbench 是资产——它能复现你曾经修过的每一个 bug。我自己的习惯是每个 RTL 模块配一个对应的 Testbench命名用tb_模块名跟着 RTL 一起提交。第四条先写测试再写 RTL。听起来有点反直觉但操作性很强拿到需求先把真值表或者时序图写成自检测试跑起来肯定是 FAIL然后再去写 RTL一直改到全部 PASS。这种测试驱动的顺序能让你的注意力始终集中在功能对不对上而不是波形好不好看上。第五条波形只用来解释现象不用来做判定。前面反复提到自检测试核心原因就在这。波形适合在你已经知道有问题之后去找问题出在哪一步而判定功能是否通过应该交给$display和自动比较。两件事分开做效率高得多。最后分享一个延伸的方向。与非门跑通之后可以试着用它拼出更复杂的结构两个与非门交叉耦合就是 SR 锁存器再加时钟控制和门控就是 D 触发器把它们连起来就是寄存器。每一步都配上自检测试你会发现数字逻辑的验证思路其实是高度一致的——给激励、看响应、比期望。这套方法打磨熟练之后不管是仿一个带 AXI 接口的模块还是仿一个完整的 SoC 子系统你都知道从哪下手、卡住的时候往哪查。我自己带新人时这一步通常安排在两周之内完成之后就可以放手让他们去啃真实的项目模块了。
返回列表