ARTICLE DETAIL

资讯详情

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

Vivado行为仿真入门:用与非门走通FPGA波形验证全流程

Vivado行为仿真入门:用与非门走通FPGA波形验证全流程 带过几届新人做 FPGA 之后我发现一个挺普遍的现象很多人装完 Vivado第一件事是照着教程点一颗 LED 灯灯亮了就觉得自己上手了可真当综合报错、或者板子上的行为跟预期对不上时连问题出在哪一层都说不清。根子往往在于跳过了行为仿真这一步——他们从来没有在波形窗口里一条一条地把逻辑关系亲自对过。这篇我就拿最朴素的与非门当样本把 Vivado 行为仿真这条路从建工程、写代码、写激励、跑仿真到看波形完整走一遍。与非门的逻辑功能简单到一句话就能说完但它能覆盖的工具操作、代码结构、波形判读方法和跑一个几千行的复杂模块是完全一致的。所以这个例子不是玩具而是把整条流程压缩到最小规模让你把注意力放在流程本身而不是被逻辑复杂度分散掉。适合刚接触 FPGA 的学生、从软件转过来的工程师以及写了不少代码但一直靠上板试错来验证的同行。1. 先从为什么要仿真说起行为仿真的位置和价值1.1 行为仿真在 FPGA 开发链路里卡在哪一步一个完整的 FPGA 开发过程大致是写 RTL 代码 → 行为仿真也叫功能仿真、前仿真→ 综合 → 实现布局布线→ 时序仿真后仿真→ 生成比特流 → 下载到器件。行为仿真排在综合之前意味着这时候工具还没有把你的代码翻译成查找表、触发器和连线它只是在按 Verilog 的语义解释你写的行为。这个位置很关键你验证的是逻辑对不对而不是时序能不能收敛。为什么把功能验证提前到这一步因为越往后排错的代价越高。纯逻辑错误在行为仿真阶段可能十分钟就定位了如果拖到综合后看门级网表信号名被工具改得面目全非拖到板子上你手里只有几个 LED 和一台示波器排查手段极其有限。我见过不止一个项目上板跑不通回头跑一次行为仿真波形里一眼就看出是复位极性写反了——这十分钟如果早花能省掉一整天的折腾。对刚入门的人来说行为仿真还有一个隐藏价值它逼着你把我以为的逻辑和实际写出来的逻辑对齐。你脑子里想的与非门是一个真值表但代码里一个~的位置、一个写成|都会在波形里暴露出来。波形是最诚实的老师。1.2 拿与非门当样本到底练的是什么有人会问 与非门这么简单写一行assign y ~(a b);就完了有什么可练的我的答案是练的从来不是这个逻辑而是三件事。第一是代码结构意识。被测模块常写作 DUTDesign Under Test和测试平台testbench是两类完全不同的代码前者要可综合、要能映射到硬件后者只服务于仿真、爱怎么写怎么写。很多新手把激励代码直接写进被测模块里结果综合时报一堆错或者综合出来了但硬件里根本没有那些#10延时。这一层区分必须在最简单的例子里就建立起来。第二是工具流的肌肉记忆。Add Sources 加文件、Set as Top 设顶层、Run Simulation 跑仿真、波形窗口加信号改进制、Restart 重跑——这套动作在 Vivado 里是固定的。用一个逻辑最简单、几乎不可能出错的设计去熟悉工具出错时你能确定是操作问题而不是逻辑问题排查效率高得多。第三是真值表与波形的一一对应能力。与非门四个输入组合对应四个输出值你要能在波形窗口里逐段指出这一段 a0、b0y 应该是 1波形确实是 1通过。这个动作看起来幼稚但它是所有验证工作的原型。等以后面对一个状态机你靠的还是同一种能力把预期行为和实际波形逐段比对。1.3 行为仿真做不到什么别把它当万能这里必须泼一盆冷水免得你后面踩更深的坑。行为仿真基于理想模型默认认为所有门的翻转都是瞬时完成的、所有连线都是零延时的除非你显式写了#延时。这意味着下面这些问题在行为仿真里根本看不出来问题类型行为仿真能否发现说明逻辑功能写错能这是它的主场组合逻辑竞争毛刺一般不能零延时模型下看不到真实毛刺建立/保持时间违例不能需要时序仿真或静态时序分析时钟树偏斜、跨时钟域问题不能需要时序约束加上后仿真验证未初始化寄存器上电状态部分能仿真里是 X实际器件可能上电为 0复位逻辑时序不当能如果激励写得对前提是你的 testbench 里给了复位所以正确的理解是行为仿真是第一道筛子它筛掉大部分低级错误但筛不掉的必须靠后面的环节去补。把功能仿真跑通就宣布设计完成是新手最容易犯的认知错误之一。2. 建工程阶段就被卡住几个设置项的真实影响2.1 工程名和工程路径是两个独立字段Vivado 新建工程向导第一步会让你填 Project Name 和 Project Location。这里有个设计得挺反直觉的地方Project Name 是工程名Project Location 是工程的父目录工具会在父目录下再创建一个以工程名命名的文件夹。也就是说如果你填 Location 为D:/fpga、Name 为nand_demo最终工程文件在D:/fpga/nand_demo/下面而不是直接躺在D:/fpga/里。新手常常在 Location 里手动写上D:/fpga/nand_demo结果得到D:/fpga/nand_demo/nand_demo/这种嵌套目录之后找文件、写脚本都别扭。另一个必须提的点是路径里不要出现中文和空格。Vivado 的底层工具链包括综合器和仿真器大量依赖命令行调用路径里的中文和空格在传参时容易出问题——有些版本会直接报找不到文件有些会生成一堆乱码目录名。工程放在D:/work/fpga/这种纯英文无空格路径下是最省心的做法。这一点不是危言耸听我带的人里有三四个都是被中文路径坑过一次才改习惯的。2.2 器件Part选择跟仿真无关但别乱选向导里会让你选目标器件或者开发板。纯做行为仿真的话器件选择其实不影响结果——因为你根本没进综合那一步。但有两个理由让我建议你认真选一是后续连贯性。同一个工程后面大概率要综合、要实现、要上板器件选错了后面得重建工程或者手动改很麻烦。如果你手里有具体的开发板直接在 Boards 标签里选对应型号Vivado 会自动带上管脚约束文件如果只想练仿真随便选一个同系列的常见器件就行但要知道这只是临时方案。二是仿真模型依赖。如果你后面用到了厂商提供的 IP 核比如时钟管理单元、存储器控制器这些 IP 的仿真模型是跟器件系列绑定的。器件系列选得不对例化 IP 时会报器件不匹配的错误。所以从一开始就养成习惯选你实际要用的那个器件。2.3 添加源文件三种入口的区别Vivado 左侧 Flow Navigator 里的 Add Sources 点了之后有三个选项很多人看都不看就随手点第一个其实它们用途不同。Add or create design sources添加设计源文件也就是你的 RTL 代码会被纳入综合范围。Add or create simulation sources添加仿真源文件也就是 testbench不会被综合只用于仿真。Add or create constraints添加约束文件XDC用于管脚分配和时序约束。这个区分的意义在于testbench 里的initial、#10、$display这些东西是不可综合的如果你把它们放到 design sources 里跑综合时会直接报错。所以从与非门这个练习开始就要把两类文件分开放。我通常的做法是在工程里额外建两个目录rtl/放设计文件、tb/放仿真文件加文件时就按目录分类添加工程结构一眼就能看清后面文件多了也不会乱。3. 与非门代码和 testbench两种代码两套写法3.1 被测模块三种描述风格你都得认识与非门在 Verilog 里有三种写法它们仿真结果完全一样但体现的抽象层次不同。第一种是数据流描述也是最常用的timescale 1ns / 1ps module nand_gate ( input wire a, input wire b, output wire y ); assign y ~(a b); endmodule这里的assign是连续赋值右边的表达式只要有一个操作数变化左边就立即重新计算。wire类型必须由assign或者模块端口驱动不能像变量那样保存值这一点和后面 testbench 里用reg是根本区别。第二种是门级描述直接例化一个原始与非门module nand_gate ( input wire a, input wire b, output wire y ); nand u1 (y, a, b); endmodule这种写法在教科书里常见实际项目中很少手写因为可读性差、优化空间小。第三种是行为描述module nand_gate ( input wire a, input wire b, output reg y ); always (*) begin y ~(a b); end endmodule注意这里y必须声明成reg因为always块里被赋值的信号在 Verilog-2001 的语义里必须是 reg 类型SystemVerilog 的logic统一了这个限制实际工程里更推荐用logic。三种写法的波形完全一致你可以把三种都跑一遍亲眼确认工具对它们的处理结果相同——这对建立抽象层次不影响功能的直觉很有帮助。3.2 testbench 的结构它是个没有端口的模块testbench 的本质是一个空的顶层模块里面做两件事把被测模块例化进来然后给它喂激励。它没有输入输出端口因为它不需要跟外界打交道。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 $finish; end endmodule几个细节值得抠一下。连接到被测模块输入端的信号必须声明成 reg因为它们要被initial块赋值连接到输出端的信号声明成 wire因为它由被测模块驱动。这是新手最容易写错的地方把a声明成 wire然后initial块里给它赋值仿真会报不能给 wire 赋值之类的错误或者干脆静默地什么都不发生。端口例化建议始终用按名连接.a (a)这种写法不要用按位置连接nand_gate u1 (y, a, b)。按位置连接一旦端口顺序调整就全错而且出错时很难看出来。按名连接虽然多打几个字但它自解释、抗重构是工程里的标准做法。还有模块名和文件名的关系。Verilog 语言本身没有强制要求文件名和模块名一致但 Vivado 在添加文件时默认会用文件名推断模块名而且综合和仿真阶段查找模块是按名字找的。所以实践中一定要保持一致nand_gate.v里就是module nand_gatetb_nand_gate.v里就是module tb_nand_gate。不一致时工具经常会给出找不到模块或重复定义的提示让人一头雾水。3.3 激励向量四个组合一个都不能少与非门的真值表只有四行我强烈建议在入门阶段把四行全部测到一个不落aby预期001011101110为什么强调全都测因为新手写的激励经常差一个。比如上面那段代码如果我没有显式地在第一个时间点设置a0, b0仿真从 0 时刻起两个信号都是 X未初始化波形一开始就是一段 X跑完再看会以为设计有问题。再比如有人只测了00和11恰好与非门这两种情况的输出是 1 和 0看着挺对但01、10这两个有一边为低的情况其实覆盖了与逻辑的一个分支——如果代码里误写成|只测00/11是发现不了的~(0|0)1、~(1|1)0恰好也对得上。所以激励向量的设计原则是覆盖所有输入组合尤其是能让不同逻辑分支产生不同结果的那些组合。给激励的方式还有更紧凑的写法用循环配合数组适合输入位宽较大的情况integer i; initial begin for (i 0; i 4; i i 1) begin {a, b} i[1:0]; #10; end $finish; end这种写法在大位宽时优势明显但入门阶段我建议先手写四种情况把每一行都看一遍建立我在控制每一个时刻的信号这种感觉再过渡到循环写法。3.4timescale与#10的真实语义timescale 1ns / 1ps这行看起来不起眼但它决定了波形窗口里时间的刻度含义。斜杠前面是时间单位后面是时间精度。1ns / 1ps的意思是代码里写#10代表 10ns仿真器内部最小的分辨能力是 1ps。也就是说如果你写#0.00050.5ps会被舍入到 1ps 精度上可能产生你意料之外的波形。#10本身是仿真里的延时语句只能出现在 testbench 或者用initial/always描述的仿真模型里它不可综合。综合器看到#会直接忽略掉延时或者报错。这也是为什么 testbench 必须和设计代码分开管的根本原因。另一个常见问题是多个文件里的timescale不一致。如果你在nand_gate.v里写了1ns/1ps在tb_nand_gate.v里写了10ns/100ps那么两个文件里的#1含义完全不同仿真时间轴会变得很诡异波形看着卡住不动实际上是时间单位差了十倍。我自己的习惯是每个文件都在第一行写timescale而且整个工程统一用1ns/1ps这样看到#10就知道是 10ns不需要回忆上下文。4. 跑仿真从点下按钮到波形窗口的完整动作4.1 点下 Run Behavioral Simulation 之后发生了三件事按钮点下去的一瞬间Vivado 在后台做了三件事。第一件是编译把工程里参与仿真的 Verilog 文件翻译成仿真器能识别的中间形式语法错误在这一步暴露。第二件是elaboration展开/细化确定模块层次、解析参数、连接端口、选定顶层模块这一步能发现找不到模块端口宽度不匹配顶层重复定义这类问题。第三件才是真正调用仿真内核运行启动 xsim把时钟推进起来按initial块里的语句顺序执行。理解这三步的意义在于报错的类型能直接告诉你问题出在哪一步。编译阶段的报错基本都是语法问题比如少了endmodule、信号用了没声明、begin/end不配对elaboration 阶段的报错基本都是结构问题比如例化的模块名写错、端口名对不上、有两个文件定义了同名模块只有前两步都过了才会进入真正的运行阶段此时的问题才需要用波形去看。顺带说一句这些动作在 Tcl Console 里都有对应命令。你在图形界面里点的每一步控制台都会打印出对应的 Tcl 命令。比如launch_simulation、run all、restart、run 100ns、close_sim。刚开始不用刻意去学 Tcl但养成看一眼控制台输出的习惯将来要写脚本批量跑仿真时这些记录就是现成的脚本素材。4.2 Simulation Settings 里值得动手改的几个默认值工程设置里的 Simulation 选项默认值不总是合适有几个我每次都会检查。第一个是Simulation top module name。默认情况下 Vivado 会自己猜顶层如果工程里只有nand_gate和tb_nand_gate两个模块它有时会把nand_gate当成顶层——因为没有任何模块例化它。一旦选错仿真跑起来也是成功的但波形里只有静止的输入端口你会以为激励没生效。跑仿真前先确认顶层是 testbench这是最高频的坑之一。第二个是xsim.simulate.runtime运行时长。默认可能是一段有限时间也可能直接是all。设成all的含义是一直跑直到 testbench 里遇到$finish。这个设置很依赖你 testbench 的严谨性如果 testbench 里忘了写$finish仿真会一直跑下去看起来像卡死。我的习惯是 testbench 里一定写$finish同时把 runtime 设成all让仿真自然结束如果只是想快速看一眼就直接设成1000ns这种具体值。第三个是log_all_signals相关的属性。默认情况下仿真器为了省内存只记录顶层模块的端口和部分信号。如果你发现波形窗口里找不到被测模块的内部信号就是这个原因。解决办法有两个一是打开 log all signals 选项代价是仿真变慢、波形文件变大二是只对你想看的信号右键 Add to Wave Window按需记录。后者更精细是我更推荐的做法。4.3 波形窗口的判读游标、进制、分组仿真跑完或跑到一半按暂停波形窗口会自动弹出。默认布局下横轴是时间纵轴是信号列表每个信号一行波形。第一次看的时候先把三件事做了。第一件是加信号和分组。左侧 Scopes 面板里列出了整个模块层次点开tb_nand_gate能看到下面的u_nand它内部的信号都能拖进波形窗口。信号多了之后建议用CtrlG分组把相关的信号放在一个组里判读时视线不会被无关信号干扰。第二件是改进制Radix。默认可能是十六进制对于单比特信号看着别扭。选中信号右键把 Radix 改成 Binary波形的取值就变成 0 和 1和真值表一一对应。多位宽的总线需要看十进制或者无符号数时再对应切换。这个操作看似琐碎但多位的十六进制显示经常让人把10二进制 1_0误读成一个十进制十。第三件是用游标量时间。把光标移到波形上会显示对应时刻的信号值。想精确测量可以按两次快捷键放置两个游标Marker界面上会直接显示两个游标之间的时间差和信号变化。这在验证我的#10是不是真的产生了 10ns 的间隔时非常直观——如果波形上两段激励之间差了 100ns那就是你的timescale出问题了。判读与非门的四条激励时我建议按时间顺序念一遍0 到 10nsa0、b0y1符合10 到 20nsa0、b1y1符合20 到 30nsa1、b0y1符合30 到 40nsa1、b1y0符合。这个过程做上几遍你就再也不会觉得看波形是件玄学的事。4.4 把波形配置存下来.wcfg文件和 Tcl 复用一个不太起眼但很省时间的技巧波形窗口的配置加了哪些信号、什么分组、什么进制是可以保存的。Vivado 会在仿真目录下维护一个配置文件也可以在波形窗口工具栏里手动 Save Waveform Configuration 存成.wcfg文件。下次跑仿真时加载它之前摆好的信号布局原样回来不用重新拖信号。这件事在模块复杂之后价值极大。想象一个模块有八十个信号你花十分钟才挑出需要看的二十个、分好组、设好进制结果改一行代码重启仿真全部重置——这种重复劳动完全可以避免。更进阶的做法是把加信号的操作写成 Tcl 脚本比如add_wave -r /tb_nand_gate/u_nand/*跑仿真时在 Tcl Console 里执行一行所有关心的信号一次性加完。入门阶段不必强求但知道这条路存在后面需要时会想起来。5. 仿真跑不通时的排查顺序5.1 第一道关编译期和 elaboration 期的报错这类报错的特点是仿真根本没跑起来波形窗口要么不弹要么弹出来是空的控制台里有一片红字。按下面的顺序看。先看报错信息的第一个错误不要看最后一个。工具报错往往会连锁少了一个endmodule后面会附带上十条语法错误实际上根源只有一个。定位到第一条往往是真正的病因。再看报错里的模块名和文件行号。常见的几类module xxx not found或类似的 cannot find module你例化的模块名拼错了或者对应的文件没有加入仿真源文件集或者文件被 exclude 了。去 Sources 面板确认文件图标不是灰色的。端口相关的报错比如 named port connection does not exist按名连接时端口名写错了注意大小写和拼写a和A是两个信号。重复定义比如同一个模块名在两个文件里都有检查是不是复制粘贴时忘了改模块名这种错误在你从例子里抄代码时特别容易出现。位宽不匹配的警告仿真器一般只warning不error但位宽对不上会导致高位被截断或者补零逻辑结果自然不
返回列表