ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

轻量级Verilog仿真环境搭建:VSCode + iverilog + GTKWave 指南

2026/10/6 3:29:44 拓冰建站 浏览量
轻量级Verilog仿真环境搭建:VSCode + iverilog + GTKWave 指南 你刚改完一行状态机的跳转条件想在仿真里看下波形对不对结果Quartus II光启动就要一分钟编译整个工程又是好几分钟好不容易调到ModelSim还时不时弹出一个“failure to obtain a verilog simulation license”之类的窗口整个人血压直接上来。我以前干过太多次这种事后来换成了VSCodeiverilogGTKWave这套轻量组合验证一个小模块从改代码到看到波形基本控制在十几秒内。这篇文章就把这套环境的搭建过程、常用命令和踩过的坑全部写出来给还在Quartus II里等得心慌的Verilog玩家一条快车道。1. 你等的那几分钟到底浪费在哪了——以及为什么换成iverilog会更快1.1 Quartus II 的等待来自哪里Quartus II是一个完整的FPGA开发工具链它做的事情远比“仿真”多得多工程索引、宏单元分析、引脚规划、综合、布局布线、时序分析每一环都要消耗时间。尤其当你打开一个稍微大一点的工程光是初始化IDE和加载上次的编译结果就够你泡一杯茶了。在你只是想验证一个计数器模块是否按预期翻转的时候这种重量级流程完全是杀鸡用牛刀。更难受的是很多人在Quartus里点开ModelSim做仿真还要先解决编译库、仿真库路径、license配置一系列问题。“Quartus II cannot launch ModelSim”这个话题下面隔几天就能看到新同学提问。ModelSim本身是好工具但它和Quartus的联动涉及太多环境变量只要版本对不上、路径带中文或空格、license环境变量没写对它就能给你整出一堆与代码无关的报错。1.2 iverilog 和 GTKWave 各管哪一段活这套轻量方案的核心是两个开源工具iverilog负责把Verilog代码编译并执行仿真GTKWave负责把仿真过程中生成的VCD波形文件可视化。iverilog的全称是Icarus Verilog它帮你完成的是“写代码 - 编译 - 运行testbench”这条主干流程。它不做综合不做布局布线也不管你最终要烧到哪家的FPGA芯片里。它只做一件事按照IEEE 1364标准解析你的Verilog代码运行测试激励把信号变化记录到VCD文件里。GTKWave就是那个看波形的窗口。VCD是“Value Change Dump”的缩写本质就是一个纯文本文件记录每个时刻哪些信号发生了跳变。GTKWave读入这个文本把它画成你熟悉的时序波形图。你可以缩放、拖拽、改进制显示、给信号分组操作手感和ModelSim的Wave窗口没有本质区别。1.3 这套方案适合谁不适合谁如果你是在学Verilog语法、写计数器、UART、SPI、I2C、FIFO、CRC这些基础模块或者你正在做算法预研、想在写大工程之前快速验证一个小IP的行为这套方案简直为你的场景而生。它的启动速度、迭代速度远超重型IDE而且完全不需要license。但它不是万能的。如果你要把设计烧到具体FPGA上需要做引脚分配、调用厂商IP核、跑时序约束、做门级仿真那么你依然要回到Quartus或Vivado这类原厂工具链。iverilog帮不了你这些。所以我通常的建议是功能仿真阶段尽量留在轻量环境里等代码逻辑收敛了再进原厂工具做一次严肃的综合与时序收敛。2. 开工前先把工具备齐VSCode、iverilog、GTKWave 的安装与识别2.1 VSCode 安装与配置编辑器这块别马虎VSCode本身很常规去官网下载安装包即可Windows、Linux、macOS都有对应版本。如果你习惯命令行Windows下也可以用winget install Microsoft.VisualStudioCode一把装好。装完以后第一时间做两件事把命令行code命令打开在VSCode里按CtrlShiftP输入Shell Command: Install code command in PATH这样后面从终端进入工程文件夹就很顺畅然后把工程目录统一用英文命名这一点对GTKWave的稳定性很重要后面我细说。装完VSCode本体真正影响体验的是插件。我用得最多的是Verilog-HDL/SystemVerilog这个插件作者是mshr-h支持语法高亮、自动例化、代码补全、缩进整理还支持调用iverilog做实时lint。装好之后打开设置搜verilog linting把linter切到iverilog再把lint运行时机设为onSave。这样每当你保存一个.v文件VSCode就会自动调iverilog检查语法错误问题面板里直接标红报错比你在终端里看到错误再回去改代码快太多。另一个推荐的是Verilog Format用来做代码格式化。Verilog代码风格各家有各家的习惯这个插件默认的缩进和空格风格基本符合常规写testbench的时候尤其省心。2.2 Windows 下安装 iverilog 以及PATH配置iverilog在Windows上安装很简单去官网下载集成安装包。安装的时候注意一个点安装路径尽量不要带空格和中文比如装到C:\iverilog不要用C:\Program Files\iverilog因为后面你在Makefile或者VSCode task里拼命令行时带空格的路径要额外处理引号麻烦不说还容易引入诡异的转义问题。安装完成后需要把iverilog的bin目录加进系统PATH。以C:\iverilog\bin为例右键“此电脑”-属性-高级系统设置-环境变量在Path里新建一条。然后开一个新的终端窗口输入iverilog -v能打印出版本号说明安装成功。iverilog安装包里还带了一个vvp命令这个才是真正执行仿真的程序。iverilog相当于把.v文件编译成一个.vvp仿真可执行文件vvp负责运行它。二者配合使用就像gcc和生成的可执行文件的关系。2.3 GTKWave 安装与中文路径的坑GTKWave单独安装去官网下载对应平台的安装包。Windows下安装后同样把可执行文件目录加入PATH。装好之后在终端里敲gtkwave --version验证部分版本不支持--version那就直接敲gtkwave能弹出窗口就算成功。这里必须说一个我踩过无数次的坑GTKWave对中文路径和特殊字符非常不友好。如果VCD波形文件放在D:\学习\仿真\这种目录下轻则打开后信号树空白重则直接闪退。所以从一开始建工程目录就养成全英文、无空格、无括号的习惯。我一般这样组织D:\projects\ counter\ src\ tb\ sim\src放设计代码tb放测试平台sim放编译输出和VCD文件。所有工作都在sim目录里执行干净隔离。工具定位安装方式命令行验证VSCode代码编辑官网安装包或wingetcode --versioniverilog编译Verilog代码官网安装包iverilog -vvvp执行仿真随iverilog安装vvp -hGTKWave波形查看官网安装包gtkwave --version3. 用计数器跑通第一个仿真从代码到波形的最小闭环3.1 计数器模块与测试平台代码理论说再多不如直接跑一个最小例子。先建一个counter.v写一个带参数的二进制计数器timescale 1ns/1ps module counter #( parameter WIDTH 8 )( input wire clk, input wire rst_n, output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count 0; else count count 1; end endmodule再写一个counter_tb.v测试平台。测试平台里除了例化模块最核心的是把波形dump出来timescale 1ns/1ps module counter_tb; reg clk 0; reg rst_n 0; wire [7:0] count; always #10 clk ~clk; // 50MHz时钟 initial begin $dumpfile(counter_tb.vcd); $dumpvars(0, counter_tb); repeat(5) (posedge clk); rst_n 1; repeat(300) (posedge clk); $finish; end counter #( .WIDTH(8) ) dut ( .clk (clk), .rst_n (rst_n), .count (count) ); endmodule$dumpfile指定波形文件名$dumpvars(0, counter_tb)表示把counter_tb这个模块内部以及它下面所有层次例化的信号全部记录下来。层级参数如果写错可能什么都dump不到记住这个结构就好。3.2 编译运行iverilog、vvp 和 VCD 文件是啥关系在counter工程根目录下执行iverilog -o sim/counter_tb.vvp tb/counter_tb.v src/counter.v这条命令把testbench和设计文件一起编译成一个可执行的仿真文件counter_tb.vvp。注意顺序没有强制要求但建议把包含模块定义的依赖文件一并列出忘了写谁就一定会报“模块未定义”的错误。然后执行vvp sim/counter_tb.vvp运行结束后在sim目录下会生成counter_tb.vcd。这个VCD文件就是波形记录的原始档案。最后打开它gtkwave sim/counter_tb.vcdGTKWave打开后左侧是信号树找到dut下面的count双击或拖到右侧波形窗格你就能看到一条从0一直数到255然后回绕的计数线。第一次跑通这个流程你的轻量验证环境就算正式上岗了。3.3 我常用的Makefile一条命令完成编译和仿真每次敲三行命令还是太慢我习惯在每个工程里放一个MakefileTB counter_tb VSRC tb/$(TB).v src/counter.v SIM_DIR sim all: wave compile: mkdir -p $(SIM_DIR) iverilog -o $(SIM_DIR)/$(TB).vvp $(VSRC) run: compile vvp $(SIM_DIR)/$(TB).vvp wave: run gtkwave $(SIM_DIR)/$(TB).vcd clean: rm -rf $(SIM_DIR)之后每次改完代码只要执行make wave它就会自动完成编译、运行、打开波形三个步骤。Windows下如果没装make可以用MinGW或安装GnuWin32的make但如果你不想折腾也可以把同样的命令写成一个run.bat脚本效果一样。核心思路是把重复劳动压到一条命令里把注意力留给波形分析。4. 加一点真实项目味道UART发送模块的仿真与波形分析4.1 用 task 封装字节发送让 testbench 像串口助手一样好用计数器太简单咱们上一个更有代表性的模块UART发送。UART的发送本质就是把一个字节拆成起始位、8个数据位、停止位按波特率一个bit一个bit地从txd引脚送出去。这类模块非常经典也非常适合在iverilog环境里调试。UART发送模块代码如下timescale 1ns/1ps module uart_tx ( input wire clk, input wire rst_n, input wire [7:0] tx_data, input wire tx_start, output reg txd ); parameter CLK_FREQ 50_000_000; parameter BAUD 115200; localparam BIT_TICKS CLK_FREQ / BAUD; localparam IDLE 2d0, START 2d1, DATA 2d2, STOP 2d3; reg [1:0] state; reg [7:0] data_buf; reg [15:0] cnt; reg [2:0] bit_idx; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; txd 1b1; cnt 0; bit_idx 0; end else begin case (state) IDLE: begin txd 1b1; if (tx_start) begin data_buf tx_data; state START; cnt 0; end end START: begin txd 1b0; if (cnt BIT_TICKS - 1) begin cnt 0; state DATA; bit_idx 0; end else begin cnt cnt 1; end end DATA: begin txd data_buf[bit_idx]; if (cnt BIT_TICKS - 1) begin cnt 0; if (bit_idx 7) begin state STOP; end else begin bit_idx bit_idx 1; end end else begin cnt cnt 1; end end STOP: begin txd 1b1; if (cnt BIT_TICKS - 1) begin cnt 0; state IDLE; end else begin cnt cnt 1; end end endcase end end endmodule这个设计的状态机清晰用iverilog跑完全没问题。测试平台里最有价值的是用task封装发送一个字节的操作这样后面无论你想连续发多少个字节都只需一行调用timescale 1ns/1ps module uart_tx_tb; reg clk 0; reg rst_n 0; reg [7:0] tx_data; reg tx_start 0; wire txd; always #10 clk ~clk; // 50MHz initial begin $dumpfile(uart_tx_tb.vcd); $dumpvars(0, uart_tx_tb); repeat(5) (posedge clk); rst_n 1; send_byte(8hA5); send_byte(8h3C); repeat(10) (posedge clk); $finish; end task send_byte(input [7:0] byte); begin tx_data byte; (posedge clk); tx_start 1; (posedge clk); tx_start 0; wait (txd 1b1); repeat(2) (posedge clk); end endtask uart_tx dut ( .clk (clk), .rst_n (rst_n), .tx_data (tx_data), .tx_start(tx_start), .txd (txd) ); endmoduletask的好处在于它像一个函数一样封装了“拉高tx_start、等待启动、拉低tx_start、等待字节发送完成”这一串流程。如果你要测一长串数据比如发一个数组的前16个字节只需要写个for循环不断调send_byte而不需要复制粘贴大段时序控制代码。这也是“verilog task调用”在实际testbench里的最典型用法。4.2 把数据写到文件里仿真结果不只在波形中有一类问题光看波形很费劲比如你发100个字节想知道其中第47个字节的发送波形是否正确。与其肉眼扫描不如让testbench自己做日志。用一个整数文件句柄在task里把发送的字节顺手写到文本文件integer fd; initial begin fd $fopen(uart_tx.log, w); end task send_byte(input [7:0] byte); begin tx_data byte; (posedge clk); tx_start 1; (posedge clk); tx_start 0; wait (txd 1b1); repeat(2) (posedge clk); $fwrite(fd, %02x\n, byte); end endtask然后在结束后initial begin ... $fclose(fd); $finish; end生成的uart_tx.log里每一行就是一个发送成功的字节十六进制值。如果你想做更复杂的校验比如自发自收可以在testbench里同时例化一个uart_rx模块把发送数据和解码数据比对。一旦对不上直接用$display或$error在终端里报错if (recv_data ! byte) $error(send %02x but recv %02x, byte, recv_data);这种终端直接输出结果的方式比每次去波形里找故障点高效得多。记住一个原则简单信号的正确性看波形批量数据的一致性看日志。4.3 在GTKWave里如何高效分析串行波形打开UART发送的VCD后把clk、rst_n、txd、state、cnt、bit_idx这几路信号加到波形窗格。需要调整几个显示设置否则看起来会很乱。第一把state信号右键设置成文字显示GTKWave支持把枚举状态的数值映射成文字虽然需要手动配置但看状态机跳变会非常直观。第二把cnt这种计数器信号右键设置成Unsigned或Hexadecimal避免一长串01让你眼花。第三用鼠标滚轮缩放波形找到txd拉低的那一个沿那就是起始位开始的位置。你还可以把信号拖成组比如把txd、state放到一个组里折叠起来看整体时序。GTKWave的界面虽然朴素但该有的功能都有用熟了以后除了界面旧了点效率上并不输商业工具的波形窗口。5. 最容易劝退新手的四个坑license、编码、隐式信号、波形过时5.1 那些年被商业仿真器的license支配的恐惧这套方案里没有很多初学者在Quartus里跑仿真遇到“failure to obtain a verilog simulation license”或者“cannot launch ModelSim”这类问题时会非常沮丧因为问题根源往往不在你的代码而在license环境变量、服务器地址配置、网卡绑定这些和电路设计毫无关系的事情上。我在5.1节里不想教你任何破解或者绕授权的手段纯属违法也不安全。但我想说的是学习场景下你完全可以选择开源方案这就是法律的合理路径。iverilog全程不需要任何license文件不需要MAC地址登记不需要设环境变量装完就能跑。它的语法覆盖范围对你的练习场景已经足够。我之前带新人学Verilog只用这套方案头两周的作业全在里面完成没人被环境问题卡住过。等大家要接触真正的FPGA工程了再进Quartus去理解综合、约束、引脚分配这些概念反而是水到渠成的事情。5.2 VSCode编译输出乱码和GBK/UTF-8问题这个坑在Windows平台尤其常见。VSCode默认使用UTF-8保存文件而Windows下很多历史工程或者编辑器比如老版本的Quartus II自带的文本编辑器用的是GBK编码。如果testbench里的注释含有中文文件编码不统一iverilog编译时遇到意外的字节序列报错信息可能变成乱码或者明明代码没问题就是因为注释的原因给你一个奇怪的警告。解决方案很简单所有Verilog工程文件统一使用UTF-8。在VSCode设置里把files.encoding设为utf8同时开启files.autoGuessEncoding。打开一个旧文件时如果发现底部编码是GBK点一下编码选择“通过编码重新打开”选UTF-8另存覆盖一遍。把编码问题解决掉编译输出面板里的报错信息才能和你的代码文件一一对应。另外在Windows终端里如果iverilog输出的中文乱码那是终端代码页的问题可以执行chcp 65001切到UTF-8代码页或者在VSCode终端设置里把默认编码改成UTF-8。5.3 隐蔽的隐式信号和遗漏的timescaleVerilog有一个“人性化但坑人”的特性如果你在代码里用了一个没有声明的信号它会被自动默认为1位的wire类型。在大型testbench里这种错误极难发现因为你可能只是把一个信号的名字拼错了一位比如data_out写成data_o编译器顶多给一个warning仿真结果却完全对不上。我的经验是编译时加两个参数让警告无处藏身iverilog -Wall -Wimplicit -o sim/tb.vvp tb/tb.v src/design.v-Wall打开更多警告-Wimplicit把隐式信号当成明确警告打出来。调试的时候把编译器输出滚动到最上面看到类似“warning: implicit definition of variable”这样的字样基本就是拼写错误或者漏写了一整行声明。另一个相关问题是timescale。所有设计文件和testbench都应该在第一行写清timescale。如果不写iverilog会用默认的时间单位和精度可能和你期望的#10含义大相径庭。我就见过有人在testbench里写#10以为是一百兆赫兹的半个周期结果没写timescale实际仿真时间完全对不上。统一在第一行加上timescale 1ns/1ps时钟、延时、$dumpvars的时机才可控。5.4 GTKWave里的波形“不更新”问题你修改了RTL代码重新跑了vvp也生成了新的VCD文件但GTKWave窗口里看到的还是老波形。这不是仿真没跑而是GTKWave默认不自动重新加载文件。解决办法有几种。最粗暴的是直接把GTKWave关掉重开重新打开VCD文件。不想关窗口的话用菜单里的File-Reload Waveform它会把文件重新读一遍界面上的波形立即刷新。还有一种情况是你因为担心看漏细节经常把波形放大到某个时间点重载后视图还停留在旧位置这时用工具栏里的Zoom Fit缩放到适应全部信号一下就能跳回完整视图。如果你在终端里执行gtkwave时发现信号树为空优先检查VCD文件是否真的生成了或者$dumpvars(0, 模块名)里的模块名是否写对。Windows下还要确认路径里有没有中文或空格这一点在前面安装小节里专门强调过遇到问题先从路径排查。6. 进阶用法把环境自动化顺便应对远程开发6.1 把常用命令绑进VSCode Task一键完成全流程手动敲iverilog和vvp始终有一点重复劳动的痕迹。VSCode的Task功能可以把这些命令集成到IDE里按一个快捷键就自动编译仿真。在工程根目录建一个.vscode/tasks.json内容大概这样{ version: 2.0.0, tasks: [ { label: sim: run and open wave, type: shell, command: make wave, group: { kind: build, isDefault: true }, problemMatcher: [] }, { label: sim: clean, type: shell, command: make clean, group: build, problemMatcher: [] } ] }之后你在VSCode里按CtrlShiftB选“sim: run and open wave”它就会在终端窗口里执行make wave编译错误直接在问题面板里跳出来一切都在编辑器里完成。如果你需要为一个模块跑多组参数还可以用iverilog的宏定义实现条件编译。比如在testbench里写ifdef FULL_TEST // 跑大量随机数据 else // 跑基本冒烟测试 endif编译时加-DFULL_TEST就能切换测试深度iverilog -DFULL_TEST -o sim/tb.vvp tb/tb.v src/design.v这套做法在验证CRC、异步FIFO这类数据敏感性模块时特别有用。你可以把不同强度的测试编成同一个Makefile里的不同target一键切换。6.2 在远程服务器上跑仿真、本机看波形最后这点是我自己用得最舒服的姿势。有时需要在Linux服务器上做大量仿真或者需要复用服务器的算力跑多组回归测试我不想在服务器上装图形界面也不想每次把文件拷来拷去。方案很简单服务器上安装iverilog然后本机VSCode通过Remote-SSH远程连接服务器。在服务器上创建工程目录写好代码后直接在终端里执行iverilog和vvp生成的VCD文件就存放在服务器上。这时你不需要在远程窗口里打开GTKWave因为远程窗口压根没有图形界面。你只需要在本地终端执行一条SCP或者SFTP命令把VCD文件拉到本地目录再用本机的GTKWave打开。VCD文件对中小规模的仿真来说体积并不大拉取几乎瞬间完成。这个流程比在服务器上重定向X11或者折腾远程桌面稳定得多。VSCode的Remote-SSH插件负责代码编辑和编译命令执行本机GTKWave负责波形可视化两边的工具各司其职体验非常顺滑。6.3 什么时候我要回Quartus II虽然这套环境很香但它解决的是功能仿真层面的问题。当你的设计要落到真实FPGA上时有几件事是iverilog做不到的引脚约束和管脚分配、厂商IP核的例化比如PLL、DDR Controller、真实的时序约束和时序收敛、布线后的门级仿真。这些必须回到Quartus II配合原厂工具链来做。我现在的习惯是RTL设计阶段和模块级验证阶段全程留在VSCodeiverilog里把功能逻辑打磨到尽量准确只有在需要综合、布局布线、上板验证时才把这个相对成熟的代码拿到Quartus II里建工程。这样Quartus里那些耗时操作只会在关键节点出现而不是每次改一行代码都要经历一遍大管道。我自己现在写模块的基本流程是在VSCode里建好src和tb目录写好Makefile用iveilg编译一遍确保无语法错误再用vvp跑仿真、GTKWave看波形功能验证通过后才把代码迁移到Quartus工程里做综合。整套流程里最花时间的往往不是仿真而是思考怎么设计状态机和接口时序这才是工程师该花时间的地方。最后再分享一个小技巧每次新建工程都从旧工程里复制Makefile和目录结构能省下不少重复配置的时间。