做课程设计那会儿,我第一次用 Quartus 写了一个分频器模块,逻辑上觉得自己全都想明白了,上板子却看到四个 LED 灯乱闪,完全找不出原因。后来被师兄按在工位前,打开 quartus 波形仿真,五分钟就定位到问题——寄存器没有复位,初始状态是红色的 X。从那次之后我就养成了一个习惯:任何数字逻辑设计,在上板之前一定会先跑一遍波形仿真。这篇文章不讨论太高深的东西,就针对“quartus 波形仿真”这个最常用、也最容易被忽略的功能,讲清楚它解决什么问题、完整操作怎么走、以及红线、乱线这类高频问题到底怎么排查。
1. 波形仿真到底解决什么问题
很多初学者会把波形仿真和 testbench 搞混,或者干脆觉得“代码编译过了,功能肯定没问题,直接下载到板子上看效果不就行了”。这种想法在简单实验里可能还能蒙混过关,一旦设计稍微复杂一点——涉及状态机、多时钟域、计数器溢出、毛刺捕捉——上板调试的成本会翻好几倍,因为你能观察到的只有几个 IO 引脚的电平,内部的寄存器、状态转移全靠盲猜。
波形仿真的本质,是用软件模拟数字逻辑的行为,把信号随时间变化的轨迹画出来。你可以看到每一个寄存器在每个时钟沿之前和之后是什么状态,可以看到组合逻辑的传输延迟,可以看到哪些信号出现了毛刺,还可以在极短的时间内验证一组复杂的输入序列。最核心的一点:它不依赖真实硬件,不需要下载器,不需要连续运行几分钟去等一个计数器翻转。仿真跑 1 微秒,在真实时钟下可能只需要几个周期,但在仿真器里你可以把时间无限拉长去观察低频变化。
能解决的问题大概分三类:
- 组合逻辑验证:不考虑延迟的理想情况下,输出是否满足真值表。比如多路选择器、译码器、加法器。
- 时序逻辑验证:给出时钟和复位信号后,寄存器在时钟沿是否正确采样,状态机是否正确跳转。
- 异常现象排查:输出出现高阻、未知态、毛刺、竞争冒险,通过波形一眼看出是在哪一个时刻、哪一个信号引发的。
另一方面,Quartus 自带的波形仿真不需要写 Verilog testbench。这一点对很多刚接触 EDA 工具的人非常友好。你只需要在图形界面里画激励波形,软件就会自动生成一个 VWF(Vector Waveform File)文件,然后调用仿真器进行计算。它的定位是“快速功能验证”,适合课程实验、模块设计早期阶段,以及还没有配置 ModelSim 时应急使用。如果做复杂项目,需要系统级验证、脚本自动化回归,那通常还是会切换到 ModelSim 写 testbench,这部分我会在最后一节展开讲。
2. 折腾环境前先搞懂:Quartus 版本与工程文件的基本盘
2.1 版本选择:不要一上来就追新版
热搜词里有一堆版本号,Quartus II 9.1、13.0、13.1、15.0,还有 Quartus Prime 18.1、20.1、25.1。这里先理清一个概念:Altera(现在是 Intel FPGA)的老产品线叫 Quartus II,后来的产品线叫 Quartus Prime。Prime 又分 Lite、Standard、Pro 三个版本,其中 Lite 是免费授权的,Standard 和 Pro 需要 license。
我的建议是:
- 如果只是做课程实验、学习数字逻辑基础、跑简单的波形仿真,首选 Quartus Prime Lite 版本(比如 18.1、20.1 这些长期维护版本),免费、支持主流 Cyclone IV/V/10 系列器件,界面和 Quartus II 也差不了太多。
- 如果学校机房、老教材用的是 Quartus II 13.0/13.1,那就跟着课程环境走,别自己单独装新版,否则提交工程时可能因为版本不兼容打不开。
- 9.1 这种非常老的版本只在一些特定的老设备场景下才需要,新装的话不建议从它开始。
安装方面没有太多坑,但注意两点:
- 安装路径不要带中文,这一点和很多 EDA 工具一样,工程路径也同样不能有中文,否则编译、仿真都可能出现奇怪的错误。
- 安装时建议把 USB-Blaster 驱动一起装上。驱动通常在 Quartus 安装目录下的
drivers\usb_blaster文件夹里,如果设备管理器里识别到未知设备,手动指定到这个文件夹安装即可。
2.2 工程建立:三步走
打开 Quartus 后,用 New Project Wizard 建立工程。界面虽然因版本而异,但核心选项是固定的:
- 输入工程名和顶层实体名(建议两者一致)。
- 选择目标器件。如果你用的是学校实验板,参考板卡手册选具体型号;如果只是仿真验证,随便选一个支持的器件就行,不影响功能仿真结果。
- EDA Tool Settings 页面,在 Simulation 一栏里如果没有安装 ModelSim,就选 None;如果安装了 ModelSim-Altera 版,可以选 ModelSim-Altera,但注意这只是告诉 Quartus 之后调用哪个仿真器,本文第一遍可以先选 None,直接用内置 VWF。
之后写一个待测试的模块。举个例子,用一个 4 位二进制计数器作为本文的演示对象:
module counter4 ( input wire clk, input wire rst_n, output reg [3:0] q ); always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 4'b0000; else q <= q + 1'b1; end endmodule保存为counter4.v,在工程中添加该文件,然后执行一次完整的编译(Start Compilation,或者至少执行 Analysis & Synthesis)。波形仿真之前必须先保证编译通过。如果语法有问题、例化有问题,仿真是不会给你正确结果。
这里有一个经验:即便只是做功能仿真,我也建议完成全编译而不是只做分析与综合。因为全编译会生成完整的网表文件,后续调用 VWF 仿真时更稳定,也方便你随时切换到时序仿真对比结果。
3. 上手波形仿真:一套从建 VWF 到出波形的完整流程
3.1 新建 VWF 文件并导入信号节点
这一步不同版本菜单略有差异。Quartus II 13.x 和 Prime 的大多数版本路径是:
File -> New -> University Program VWF
文件后缀自动是.vwf。新建后会打开一个波形编辑窗口,左侧是空的信号列表,右侧是时间轴。
接下来把要观察的端口导入进来。右键点击左侧 Name 区域的空白处,选择Insert Node or Bus,或者通过菜单View -> Node Finder打开 Node Finder。Node Finder 是这里比较关键的工具:
- Filter 下拉框里默认是
Pins: all,导入顶层模块的引脚。 - 如果你还想观察内部寄存器的信号,可以把 Filter 改成
Registers: all或Design Entry (all names)。
注意,内部信号能否在功能仿真中观察到,取决于该信号是否被优化掉。对于计数器的 q 这种输出寄存器,必然可以看到。但如果是纯组合逻辑的中间节点,Quartus 可能会在综合时把它优化掉,这种情况用 VWF 看不到,得靠 ModelSim 或把信号设为 output 才能观察。
选中所需节点后,点击>加入右侧列表,再按 OK。此时波形窗口里就有 clk、rst_n、q 这三条信号了。
3.2 设置仿真结束时间与时钟周期
默认仿真结束时间可能是 1us,也就是 1 微秒。这个时间对很多设计来说不够用。比如你想让一个 4 位计数器跑完一个完整循环,如果时钟周期是 20ns,那么 16 个状态只需要 320ns,1us 勉强够;但如果时钟周期是 1us,一个完整循环需要 16us,默认的时间就不够了。
所以第一步先设置总时间:菜单Edit -> Set End Time,输入一个合适的值,我习惯设成 10us 或 100us,方便反复调整激励。接着把时间轴的网格(Grid Size)设置成一个合理的值,比如 20ns,这样鼠标拖拽时更容易对齐时钟沿。
然后双击 clk 信号所在行的 Name 区域,会弹出 Clock 设置窗口,填入时钟周期 Period 为 20ns,占空比 Duty cycle 默认 50%,也就是高电平 10ns、低电平 10ns。确认后这一行的波形会变成规则的方波。
这里有个小细节:如果你希望时钟信号在 0 时刻正好是上升沿,注意设置里可以决定起始相位。默认情况下 0 时刻由低电平开始,第一个上升沿在 10ns 处。对于计数器来说,上升沿采样没问题,不必纠结于从 0 时刻就开始上升沿。
3.3 给复位和输入信号赋值
时钟画好之后,还需要给 rst_n 信号设置一个复位过程。最稳妥的做法是:
- 用鼠标框选 rst_n 整个仿真时间段。
- 右键选择
Forcing High (1),先把复位信号拉高。 - 再框选 0 到 25ns 这一小段,右键选择
Forcing Low (0),让这一段为低电平。
这样做的效果是:前 25ns 复位有效,计数器的 q 被清零;25ns 之后复位释放,计数器开始从 0 递增。在实际工程里,这种“先复位、再释放”是非常标准的测试序列,因为寄存器的上电初始状态是不确定的,没有复位过程,仿真结果几乎必然会出现红色 X(未知态)。
如果你测试的是组合逻辑模块,比如一个与门、加法器,那么需要手动给每个输入信号设置电平。方法类似:框选一段波形区域,右键选择Forcing High (1)或Forcing Low (0),可以通过在时间轴上分段、框选多次,构造出你想要的输入序列。
3.4 分组信号与进制显示
q 是一个 4 位总线信号。导入节点后,它默认会以总线方式显示。右键信号名,选择Radix,可以切换显示格式:
Binary:二进制Hexadecimal:十六进制Decimal:十进制Unsigned Decimal/Signed Decimal:分别按无符号/有符号十进制
对于计数器,我习惯设置成Hexadecimal,这样一眼就能看出 q 从 0 到 F 循环,输出是否正确一目了然。在后续实际项目中,观察地址总线、数据总线时,这个设置更是刚需。
默认 bus 信号在波形上会显示成“多根线叠加”的锯齿状,只有设置好 Radix 之后才会显示数值。这是很多新手第一次仿真时觉得“怎么波形是乱的”的原因之一——其实不是乱,只是你还没让工具告诉你它现在的值是多少。
3.5 启动功能仿真
激励都画好之后,保存 VWF 文件,然后执行仿真。菜单路径通常有两个地方可以进:
Processing -> Start Simulation直接运行当前 VWF。Simulation -> Simulation Tool打开仿真工具界面,列出所有待仿真文件,点 Run 运行。
第一次运行时,Quartus 会先做功能仿真网络表编译,也就是把顶层设计映射到仿真库里的逻辑单元上,生成仿真网表。这个过程一般十几秒到几分钟不等,取决于设计规模和电脑性能。
仿真完成后会自动打开波形报告窗口。展开波形视图,你应该能看到 clk 以 20ns 周期翻转,rst_n 在开头一个低脉冲,q 从 0x0 开始,在每一个上升沿之后递增到 0x1、0x2、0x3……一直到 0xF,再回到 0x0。
如果一切正常,恭喜你,第一次 Quartus 波形仿真已经跑通了。
3.6 修改激励重新仿真的标准操作
在验证过程中,你几乎一定会反复修改激励波形。比如要让计数器的复位发生在第 50ns 而不是 25ns,或者想增加一段“暂停计数”的输入组合。修改方法很简单:
- 关闭当前的仿真波形报告窗口(不关也可以,但容易混淆)。
- 回到 VWF 编辑窗口,用右键菜单重新框选、重新赋值。
- 保存文件,再次运行 Start Simulation。
有一点值得注意:如果你修改了 Verilog 代码,而不仅仅是修改激励波形,仿真器并不会自动加载新的设计网表。你需要先重新编译工程(或至少重新综合),让仿真网表更新,然后再运行仿真。跳过编译直接仿真,结果仍然是老代码,这是很多人调试半天发现“代码改了但波形没变”的根本原因。经验公式:改代码 -> 全编译 -> 再仿真;改波形 -> 直接仿真。
4. 红线不是玄学:仿真波形异常的完整排查链路
热搜词里反复出现“modelsim仿真波形是红线”,说明这是新手遇到最多的问题。其实在 Quartus 自带的 VWF 仿真里,红线同样高频出现。这里我完整走一遍排查链路,把根因和对应处理方式说清楚。
4.1 红线到底代表什么
Quartus VWF 波形窗口中,红色段大多数情况代表信号处于未知状态,也就是逻辑电平既不是 0 也不是 1,而是 X。我给你解释一下这个 X 是从哪来的:
数字电路里,寄存器的存储单元在上电后的初始状态在物理上是随机的。仿真器不知道它到底是 0 还是 1,于是用一个特殊值 X 表示“未知”。如果后续逻辑依赖于这个未知值,计算不出确定结果,X 就会沿着信号链一路传播下去。你在波形上看到一片红,通常就是这条传播链的外在表现。
某条信号线在仿真中可能出现的几个状态:
| 波形颜色 | 逻辑状态 | 含义 |
|---|---|---|
| 红色(部分版本为橙色/黄色) | X | 未知/不确定,可能是 0 也可能是 1 |
| 蓝色(或黑色) | 0 / 1 | 确定的逻辑低/高电平 |
| 高阻状态 | Z | 未驱动,常见于三态门输出无下拉/上拉时 |
| 高低电平快速交替线 | 不定 | 震荡、毛刺或采样间隔过密导致看不清 |
还需要提醒一下:不同 Quartus 版本的颜色编码略有差异,有的版本 X 状态显示为红色,有的显示为橙色。关键不是记颜色,而是看到异常颜色时把鼠标悬停在波形上,工具会显示这一时刻信号的具体数值,比如显示X或Z。
4.2 最常见的三个根因:未复位、未赋值、时钟缺失
结合我带过的学生踩坑经历,90% 的红线是这三个原因导致的。
第一,未复位或复位释放后被理解错误。比如上面的 counter4 模块,如果你在 VWF 里忘记给 rst_n 设置低电平脉冲,那么仿真一开始 q 就是 X,并且永远保持 X,因为所有加法运算都是基于未知初始值。解决办法:给所有异步复位/清零信号添加一段有效的低脉冲(或高脉冲,取决于极性),等复位完成后再释放。这是一个非常好的测试习惯,无论设计有没有同步复位逻辑,我都会给顶层信号加复位序列。
第二,输入信号从未赋值。比如你测试一个二输入与门,只给一个输入画了波形,另一个输入一直是“无驱动”状态。这个没有驱动的引脚在仿真时就可能是 Z 或 X,导致输出同样不可确定。解决办法:逐个检查信号列表,确保每个 input 端口在仿真时间段内都有被框选并赋值过。
第三,时钟缺失或时间设置不合理。时序逻辑电路如果没有时钟沿,寄存器的输出永远不变。有时候你明明设置了时钟,但波形看起来还是平的,多半是仿真结束时间太短。比如时钟周期是 1us,而仿真结束时间默认只到 1us,等于整个仿真过程只经历了一个时钟周期,计数器还没跳变几次,波形自然平淡。把 End Time 改大,比如 20us,再看就会正常。
4.3 结构性问题:信号被优化掉、顶层选错
有时候问题不在激励,而在设计本身。
如果你想去观察某个内部信号,却发现 Node Finder 里找不到它,或者导入后它一直是红线、没有任何跳变,很可能是 Quartus 综合优化把这个节点优化掉了。比如你写了一个中间信号,它只被赋值了一次,综合器可能直接把它替换成线网连接。解决办法是看综合报告里有没有 warning 提示“signal is optimized away”,或者干脆把这个内部信号临时引到模块输出口上用于仿真观察。
另一个结构性问题是顶层模块选错。如果你的工程里有多个 Verilog 文件,而当前设置为顶层实体的模块并不是你想仿真的那个模块,那么 VWF 导入的端口就是另一个模块的端口,仿真结果自然对不上。检查方法是查看工程设置里的 Top-level entity,确保与设计意图一致。
4.4 仿真结果不更新的排查
还有一种常见情况:仿真能跑,波形也不全红,但你觉得“这个波形不对,我明明改了激励/代码,重新仿真还是老样子”。按下面的顺序排查:
- 确认是否修改代码后忘了重新编译。仿真的前提是基于最新的网表,Quartus 不会自动替你编译。
- 旧的仿真报告窗口没有关闭,你看到的其实是上一次结果。建议重新点击 Start Simulation,强制生成新报告。
- 检查保存 VWF 的路径是否在工程目录下,如果文件在外部路径,有时 Quartus 会读取到旧的默认 VWF。我的习惯是 VWF 文件与工程文件放在同一目录,文件名与顶层实体尽量一致,避免混乱。
- 如果以上都不行,在工程目录下手动删除
db、incremental_db、simulation文件夹里的旧仿真缓存(Quartus 的仿真临时文件),然后重新全编译、重新仿真。这个操作能解决很多“莫名其妙没生效”的问题。
这一套排查链路走下来,不敢说 100%,但至少能解决绝大多数 VWF 仿真红线问题。ModelSim 环境下的红线排查思路类似,但还要多检查一步库文件和 testbench 是否正确编译加载,这个问题留到下一节的进阶内容里顺带提一下。
5. 进阶用法:把波形仿真用到日常设计的几个习惯
5.1 测试向量要设计边界条件
很多人做波形仿真,只会把输入信号从高到低随便拖几个段,看到输出有变化就觉得通过。这样远远不够。设计测试向量的核心是覆盖边界条件:
- 组合逻辑:遍历所有输入组合。比如 4 位加法器,至少测 0+0、最大值+1、溢出情况、进位传递。
- 时序逻辑:覆盖复位状态、正常运行、计数满回绕、使能信号拉低停止、异步事件发生等。
- 多输入条件:不要只测一个输入为变量、其他输入固定,因为组合逻辑的传播路径可能因某个输入固定而隐藏 bug。
在 VWF 中实现边界条件测试,靠手动框选确实繁琐,但也不是没有技巧。比如对于输入总线,你可以把一组信号右键组合成 Bus 类型,然后用Arbitrary Value命令直接输入十六进制值,这样比一位一位拖要快得多。比如我想让一个 8 位输入 data 依次等于 0x00、0xFF、0x55、0xAA,可以直接对 bus 赋值。
5.2 用光标和标记做时间测量
波形仿真出来的数据,不仅仅是“对或者错”。如果你做的是时序逻辑验证,通常还需要测量信号延迟、建立时间余量。在波形报告窗口里,有两个工具非常实用:
- 光标(Cursor):鼠标点击或拖动时间轴上的光标线,底部状态栏会显示当前时间。
- 标记(Marker):在波形上右键添加标记点,可以记录信号跳变沿的位置,然后对比两个标记的时间差。
比如你想知道复位释放后第一个上升沿距离多少 ns、输出 q 翻转相对时钟沿延迟了多少 ns,用标记分别点在时钟上升沿和 q 的跳变沿,差值就出来了。这比肉眼数格子靠谱得多。
5.3 VWF 不是万能的:什么时候该切到 ModelSim
虽然 VWF 上手快,但它有几个先天局限:
- 编写激励不够高级,不能像 testbench 那样用循环、条件判断、文件读写来生成复杂激励。
- 无法很方便地做自动化断言检查。
- 功能仿真只验证逻辑正确性,不验证实际布线延迟下的时序收敛。
- 大规模设计中,VWF 的网表加载和仿真速度通常不如 ModelSim 优化的编译库快。
所以一旦设计规模变大,或者你需要做回归测试、批量自动化测试,就应该切换到 ModelSim 写 testbench。这时的流程是:在 Quartus 的 EDA Tool Settings 中把 Simulation 工具选为 ModelSim,然后通过 Test Benches 页面添加 testbench 文件,Quartus 会负责生成仿真模型并调用 ModelSim 运行。
ModelSim 中如果遇到波形红线,除了前面说的复位和激励问题,还要额外检查一条链路:仿真库是否编译成功、顶层模块是否指向正确、testbench 中时钟生成语句是否写了 initial 块却忘了持续赋值。比如用reg clk;声明时钟信号,但只写了initial clk = 0;没写always #10 clk = ~clk;,时钟就只是个平线,时序逻辑全部停在初始状态,这也会导致一片红。
5.4 我个人的工作习惯参考
最后分享几个我平时写代码、做仿真时形成的习惯,供参考:
- 任何时序逻辑模块,端口上我都会保留异步复位或同步复位引脚。哪怕产品需求里明确说不需要复位,我也会先用复位信号把逻辑拉到一个确定态再讨论其他。这个习惯让仿真一开始就有一个稳定的起点。
- VWF 文件中,复位信号释放之后我会特意等到第二个时钟沿再改变输入。为什么?因为第一个时钟沿往往是寄存器才进入正常采样状态,数据的变化如果刚好落在时钟沿附近,容易在波形上产生亚稳态假象,干扰判断。
- 我喜欢把 VWF 的根网格设置成时钟周期的整数倍。这样所有信号都对齐到同一条时间刻度线上,截图做报告、和同事讨论问题,都比乱糟糟的时间轴清晰很多。
- 做模块集成验证时,不要只把顶层模块拉到 VWF 里,要把关键子模块的内部寄存器节点也加入观察列表。这样一旦波形异常,能立刻判断是哪个模块的输出先出了问题,而不是从顶层往下层层猜。
从最初磕磕绊绊调不出来,到后来十分钟内完成一个小模块的全部功能验证,波形仿真已经变成我设计流程里不可跳过的一环。在这条路上确实踩过不少坑,但每一次红色波形的背后,都在逼我重新审视设计的初始状态和激励条件。希望这篇文章能帮你把 Quartus 波形仿真的流程理顺,至少下次看到红线时,你已经有一个清晰的排查链路,而不是抓瞎。