Verilator 和 GTKwave联合仿真
Verilator 和 GTKwave联合仿真
二者都是开源软件,可以免费下载和使用。这在商业软件充斥的IC行业显得额外珍贵。
文章参考:
- Verilator官方文档;
- CSDN文章;
- AI回答生成
文章目录
- Verilator 和 GTKwave联合仿真
- 简介
- Verilator
- Verilator和传统仿真器的区别
- GTKwave
- Verilator入门使用
- Verilator的tb编写
- Verilator的头文件
- #include "Vtop.h"
- #include "verilated.h"
- #include "verilated_vcd_c.h"
- Verilator 初始化
- 仿真环境初始化
- 顶层类初始化
- 波形文件初始化
- Verilator 仿真激励
- 仿真激励的流程框架
- 时钟激励的编写思路
- 非时钟激励编写思路
- 输出检查
- 综合注意事项
- Verilator 收尾工作
- Verilator的命令行参数
- 编译选项
- 调试和波形选项
- 性能优化
- 警告选项
- 预处理选项
- 一个例子
- GTKwave入门使用
- GTKwave的命令行参数
- 核心选项
- 波形文件处理
- 自动化相关
- Makefile的编写
- 一个模板
- 使用方法
简介
Verilator是一个高性能的Verilog/SystemVerilog仿真器(编译器),它将您的硬件设计代码(RTL)转换成一个快速的C++模型;而GTKWave是一个波形查看器,它用于显示Verilator仿真过程中生成的信号波形文件(如VCD或FST),帮助您进行调试。
Verilator
Verilator和传统仿真器的区别
Verilator 是一个编译器,而不是解释器
- 传统仿真器 (如 Modelsim/VCS/Vivado Sim): 它们是“事件驱动”(Event-Driven)的。它们会跟踪仿真时间,并在任何信号发生变化的“事件”点上重新评估电路,支持 Verilog 中的所有语法,包括非综合的延迟(如 #10)。
- Verilator: 它是一个“周期精确”(Cycle-Accurate)的编译器。它读取 Verilog 或 SystemVerilog 代码,并将其编译成一个高度优化的、多线程的 C++ 或 SystemC “模型”(.cpp 和 .h 文件)。
注意:Verilator的设计文件可以是RTL,但**仿真文件一般使用C++或者SystemC实现。**这是我们额外需要学习的点。
核心特点:
- 极高的速度: 由于它将RTL编译成了针对特定设计的 C++ 代码,并省去了事件队列的管理,Verilator 的仿真速度通常比传统的事件驱动仿真器快 10 到 100 倍,尤其适合于大型的、基于时钟的同步设计(如 CPU、SoC)。
- 周期精确,而非时间精确: Verilator 不关心一个时钟周期 内部 发生了什么。它只计算每个时钟边沿(posedge clk)时刻的电路状态。因此,它会忽略所有非综合的延迟(如 #10)和异步逻辑毛刺。(也因此Verilator不支持你在initial块中写#10这种延时,故一般不能用Verilog写tb)
- 专注于可综合代码: 它的设计目标是仿真最终可以被综合成硬件的代码。对于不可综合的 Verilog 语法(如 initial 块中的延迟、fork…join 等),它的支持有限或不支持。
- 需要 C++ Testbench: Verilator 只负责编译您的 DUT (Design Under Test)。您需要自己编写一个 C++ 的“顶层文件”(通常称为 C++ testbench 或 harness),用 C++ 代码来:
- 实例化 Verilator 生成的 C++ 模型。
- 驱动 DUT 的输入信号(例如,模拟时钟、复位)。
- 在每个时钟周期检查 DUT 的输出信号。
- 控制仿真的停止。
GTKwave
GTKWave 是一个功能齐全、完全免费且开源的波形查看工具。它的作用非常纯粹:读取仿真器生成的波形数据文件,并将其以图形化的方式显示出来。它本身不执行任何仿真,它只是一个“查看器”。
核心特点
- 支持多种格式: 它最常用于查看 VCD (Value Change Dump) 文件,这是 Verilog 仿真器(包括 Verilator、Modelsim、VCS、Icarus Verilog 等)都能生成的标准波形格式。
- 高效的 FST 格式: GTKWave 也是 FST (Fast Signal Trace) 格式的主要查看器。FST 是一种高度压缩的波形格式,生成速度快,文件体积小,远优于VCD。(Verilator 可以通过 --trace-fst 选项直接生成 FST 文件。)
- 功能丰富: 具备您期望的所有标准调试功能,例如:
- 添加/删除信号。
- 放大/缩小时间轴。
- 设置标记(Markers)。
- 将信号组合成总线,并更改显示格式(如二进制、十六进制、十进制、ASCII)。
- 搜索信号值。
- 保存信号布局配置(.gtkw 文件),以便下次快速加载。
Verilator 和 GTKWave 协同工作
- 使用 Verilator将Verilog/SV 设计和 C++ Testbench 编译成一个高速的仿真可执行文件。
- 在 verilator 命令中加入 --trace (生成 VCD) 或 --trace-fst (生成 FST) 选项。
- 在 C++ Testbench 中包含必要的代码,以在仿真开始时打开波形文件,并在每个仿真周期转储(dump)信号值。
- 运行编译好的仿真程序,它会输出一个 .vcd 或 .fst 波形文件。
- 使用 GTKWave 打开这个波形文件(例如 gtkwave dump.fst),然后就可以直观地分析和调试设计中所有信号随时间变化的情况了
- 对于需要自动化控制流程的项目还需要编写Makefile文件。
Verilator入门使用
Verilator的tb编写
Verilator的tb可以使用C++或者SystemC编写,由于SystemC相对复杂不适合入门,这里就专注于C++的tb编写。
我们的目标:
- Verilator 的 C++ Testbenc本质上只是一个int main() 函数
- 我们的RTL设计代码在编译完成后会变成一个 C++ 类,然后在 main 函数里实例化这个模块,并手动在 for 循环里启动时钟,给输入端口赋值,并读取输出端口的值。
一个模板:
Verilator仿真平台主要遵循如下的结构:
/* 一:头文件的包含 */
#include "verilated.h" // Verilator 核心库
#include "verilated_vcd_c.h" // 波形生成库
#include "Vtop.h" // 包含由 Verilator 从 top.v 生成的类#include int main(int argc,int **argv){/* 二:初始化配置,如verilator初始化、实例化设计顶层、波形文件初始化等等 */Verilated::commandArgs(argc, argv); // Verilator 初始化Vcounter* top = new Vtop; // 这行代码实例化了我们的 "top" 模块// 初始化波形跟踪 (VCD)Verilated::traceEverOn(true); // 启用波形跟踪VerilatedVcdC* tfp = new VerilatedVcdC; // 创建 VCD 跟踪器对象top->trace(tfp, 99); // 将 "top" 模块的所有信号添加到跟踪器tfp->open("dump.vcd"); // 打开一个 VCD 文件/* 三.仿真激励编写,主要通过循环实现 */// 初始化信号top->clk = 0;top->rst = 1; // 初始状态保持复位// 仿真主循环 (20个时钟周期)for (int i = 0; i < 20; i++) {// --- 模拟时钟下降沿 ---top->clk = 0;top->eval(); // 评估电路状态tfp->dump(main_time); // 将当前时间点的信号值写入 VCDmain_time++; // 推进仿真时间}/* 四.善后工作 */tfp->close(); // 关闭波形文件// 清理top仿真模型,并销毁相关指针,并将指针变为空指针top->final();delete top;top = nullptr;delete contextp;contextp = nullptr;return 0;
}
注意:上面的模板只是示意,不能执行。接下来我分模块说明。
Verilator的头文件
在编写Verilator头文件中,除了C/C++标准库中的头文件,最重要的就是如下三个:
#include "verilated.h" // Verilator 核心库
#include "verilated_vcd_c.h" // 波形生成库
#include "Vtop.h" // RTL顶层头文件,其中包含着顶层的类
注意:"Vtop.h"的名称和RTL顶层module名称直接相关,如果顶层叫做counter.v,那么这个头文件也需要改名为Vcounter.h
#include “Vtop.h”
这是最重要的一个文件,是您的 C++ 仿真平台与 Verilog 设计(DUT)之间的桥梁。这个文件是 Verilator 自动生成的,Vtop 这个名字取决于您 Verilog 的顶层模块名(module top (…))。它定义一个 C++ 类(例如 Vtop),它代表了您 Verilog 模块的一个实例。您在 C++ 中对这个类的所有操作,都会被 Verilator 转换为对 Verilog 模型的激励和评估。
主要的类:Vtop
通过 new 关键字在 C++ 中创建这个类的一个“实例”,就像在 Verilog 中例化一个模块一样。
// 在 sim_main.cpp 中
Vtop* top = new Vtop; // "例化" 顶层模块
主要的成员和方法
- 端口访问 (Port Access)
- 语法:top->{port_name}
- 说明:Verilator将Verilog 模块的所有 input 和 output 端口转换为了 C++ 类的公共成员变量。这是您与 DUT 交互的主要方式。
示例:如果 top.v 有 input clk, input rst, output [7:0] data_out:
top->clk = 1; // 写入 input 端口
top->rst = 0; // 写入 input 端口
uint8_t val = top->data_out; // 从 output 端口读取
- 数据类型:Verilator 使用 verilated.h 中定义的特殊类型来匹配 Verilog 的位宽,例如:
- CData (8-bit, uint8_t): 用于 [7:0] 或 1-bit 信号 (如 clk)
- SData (16-bit, uint16_t): 用于 [15:0]
- IData (32-bit, uint32_t): 用于 [31:0]
- QData (64-bit, uint64_t): 用于 [63:0]
- eval()
- 语法:top->eval();
- 说明:这是 Verilator 仿真中最核心的方法。 当调用 eval() 时,Verilator 会根据当前的输入(如 top->clk)和内部寄存器的状态,计算所有组合逻辑,并更新所有内部信号和输出端口的值。
- 重要:它不会自动触发时钟沿。always @(posedge clk) 的行为是通过您在 C++ 中手动将 clk 从 0 变为 1,并在变化后调用 eval() 来模拟的。
- trace(VerilatedVcdC* tfp, int levels)
- 语法:top->trace(tfp, 99);
- 说明:此方法用于将该模块的信号连接到 VCD 波形跟踪器(tfp 对象,来自 verilated_vcd_c.h)。99 是一个通配符,表示跟踪该模块及其所有子模块的信号。
注意:这个方法只有在您运行 verilator 命令时加了 --trace 选项才会被生成。
- final()
- 语法:top->final();
- 说明:在仿真结束、delete top 之前调用。它用于执行 Verilog 中的 $finish 任务和相关的最终清理工作。
注意事项
- eval() 的调用时机:这是初学者最容易犯错的地方。任何输入信号发生变化后,您都应该调用一次 eval() 来让电路“感知”到这个变化并更新组合逻辑。
- 模拟时钟边沿:模拟一个完整的时钟周期(clk 从 0 变到 1 再变回 0)需要至少两次eval() 调用:
/* 模拟posedge */
top->clk = 0;
top->eval();
// (可选:在此处 dump 波形)
top->clk = 1;
top->eval(); // <--- `always @(posedge clk)` 块内的逻辑在这里被计算
// (可选:在此处 dump 波形)
- 内部信号:默认情况下,您只能访问 input/output 端口。您不能访问 Verilog 内部的 reg 或 wire。这是为了实现最快的仿真速度。
#include “verilated.h”
这是 Verilator 的Runtime库。它提供了一组全局函数(C++ 中称为静态方法),用于控制仿真的全局环境,如时间、命令行参数和仿真停止。
核心作用
管理整个仿真过程,提供 Verilator 运行所需的环境和上下文(Context)。
Verilator 提供了两种管理仿真环境的方式,我将它们概括为:
- Verilated 静态/全局模式:这是老式、简单的方法。它依赖于 Verilator 核心库 (verilated.h) 提供的“全局函数”(在 C++ 中称为静态方法)。
- VerilatedContext 对象/上下文模式:这是现代、推荐的方法。您创建一个“上下文”对象,这个对象持有了仿真的所有状态(时间、$finish 标志、参数等)。
为了方便提供示例,我们假设有一个 top.v 模块
// top.v
module top (input clk
);// 为了能看到$finish的效果always @(posedge clk) beginif ($time > 100) begin$display("[%0t] Verilog side requests $finish", $time);$finish;endend
endmodule
- 使用 verilated.h 的传统方法:Verilated
主要的类:Verilated
这是一个特殊的类,您不需要(也不能)new 它。您通过 Verilated:: 作用域来调用它的全局函数。
在这种模式下,仿真的状态(如“是否 $finish”)被存储在 Verilator 库的全局静态变量中。您通过 Verilated:: 来访问它们。
关键点:时间管理。在这种模式下,Verilator 没有一个内置的全局时间变量供您递增。您必须自己在 C++ Testbench 中创建一个变量(如 main_time)来跟踪时间,并手动将其传递给 VCD 写入器。
//sim_main_static.cpp
#include <stdio.h>
#include "Vtop.h" // 自动生成的 DUT 类
#include "verilated.h" // 核心库 (静态方法)
#include "verilated_vcd_c.h" // 波形库// C++ 全局变量,用于手动管理时间
vluint64_t main_time = 0;// C++ 辅助函数,用于推进时间
// 在 C 语言中,这就像一个普通的全局函数
double sc_time_stamp() {return main_time; // 返回全局时间
}int main(int argc, char** argv) {// 1. 【环境配置】使用 Verilated:: 静态方法Verilated::commandArgs(argc, argv);Verilated::traceEverOn(true); // 全局启用波形// 2. 【DUT 实例化】// 注意:构造函数是默认的,不带参数Vtop* top = new Vtop;// 3. 【波形配置】VerilatedVcdC* tfp = new VerilatedVcdC;top->trace(tfp, 99);tfp->open("dump_static.vcd");// 4. 【仿真主循环】// 使用 Verilated::gotFinish() 检查全局 $finish 标志while (!Verilated::gotFinish()) {// 手动管理时钟和时间if (main_time % 10 == 1) { // 模拟时钟周期为 10 个时间单位top->clk = 1;}if (main_time % 10 == 6) {top->clk = 0;}top->eval(); // 评估电路// 【波形转储】手动传入全局时间tfp->dump(main_time);main_time++; // 【时间推进】手动递增全局时间// Testbench 也可以决定何时退出if (main_time > 200) {printf("[%lu] C++ testbench requests exit\n", main_time);break;}}// 5. 【清理】tfp->close();top->final(); // 执行 $finish 相关的清理delete top;return 0;
}
- 使用 VerilatedContext
首先创建一个 VerilatedContext 对象,这个对象封装了所有仿真状态。
**关键点:时间管理。**VerilatedContext 对象内置了时间。您不再需要 main_time 全局变量,而是调用 contextp->timeInc(1) 来推进时间,用 contextp->time() 来获取时间。
// sim_main_context.cpp
#include <stdio.h>
#include "Vtop.h" // 自动生成的 DUT 类
#include "verilated.h" // 核心库 (上下文类)
#include "verilated_vcd_c.h" // 波形库// C++ 中不再需要全局的 main_time 变量!int main(int argc, char** argv) {// 1. 【环境配置】创建 VerilatedContext 对象// C 语言类比: VerilatedContext* contextp = (VerilatedContext*)malloc(sizeof(VerilatedContext));VerilatedContext* contextp = new VerilatedContext;// 2. 【环境配置】所有配置都通过 contextp 对象的方法调用contextp->commandArgs(argc, argv);contextp->traceEverOn(true); // 在此上下文上启用波形// 3. 【DUT 实例化】// 关键区别:将上下文指针传递给 DUT 的构造函数Vtop* top = new Vtop(contextp); // 4. 【波形配置】VerilatedVcdC* tfp = new VerilatedVcdC;top->trace(tfp, 99);tfp->open("dump_context.vcd");// 5. 【仿真主循环】// 使用 contextp->gotFinish() 检查上下文的 $finish 标志while (!contextp->gotFinish()) {// 【时间推进】使用上下文的时间控制器contextp->timeInc(1); // 时间前进 1 个单位// 模拟一个 2 个时间单位的周期 (0->1, 1->0)top->clk = !top->clk; top->eval(); // 评估电路// 【波形转储】从上下文中获取当前时间tfp->dump(contextp->time()); // Testbench 也可以决定何时退出if (contextp->time() > 200) {printf("[%lu] C++ testbench requests exit\n", contextp->time());break;}}// 6. 【清理】tfp->close();top->final();delete top;// 关键区别:必须释放上下文对象// C 语言类比: free(contextp);delete contextp; return 0;
}
#include “verilated_vcd_c.h”
这是一个可选的辅助库。它不属于 Verilator 核心,但与 Verilator 紧密集成,其唯一目的是将仿真数据写入 VCD (Value Change Dump) 文件,以便 GTKWave 等波形查看器使用。
核心作用
提供一个 C++ 类,用于创建、管理和写入 .vcd 波形文件。
主要的类:VerilatedVcdC
需要 new 一个这个类的实例,这个实例就代表了您要写入的那个 .vcd 文件。
// 在 sim_main.cpp 中
#include "verilated_vcd_c.h"
VerilatedVcdC* tfp = new VerilatedVcdC; // 创建一个 VCD 跟踪器对象
主要的方法(函数)
- open(const char* filename)
- 语法:tfp->open(“dump.vcd”);
- 说明:打开一个文件用于写入。如果文件已存在,它将被覆盖。您必须在调用 dump() 之前先调用它。
- dump(vluint64_t timestamp)
- 语法:tfp->dump(contextp->time()); (或 tfp->dump(main_time)😉
- 说明:这是生成波形的核心函数。 它将所有通过 top->trace(tfp, …) 注册的信号的当前值,写入到 VCD 文件中,并关联到您传入的 timestamp(时间戳)。
- 调用时机:您应该在每一次 top->eval() 之后(或者至少在您关心信号变化的时间点)都调用一次 dump()。
- close()
- 语法:tfp->close();
- 说明:在仿真结束时必须调用。 它会将缓冲区中剩余的数据全部写入文件,并正确关闭文件句柄。
注意事项
- 性能开销:写入 VCD 是一个 I/O 密集型操作,非常非常慢(因为 VCD 是文本格式)。它可能会让您的仿真速度下降 10 到 100 倍。
- FST 格式:Verilator 也支持 FST 波形(verilated_fst_c.h),它生成的波形文件更小,速度更快,是 VCD 的优秀替代品。
- 协同工作:这三个头文件必须协同工作:
- 用 verilated.h (或 VerilatedContext) 启用 traceEverOn。
- 用 verilated_vcd_c.h 创建 tfp 对象并 open 文件。
- 用 Vtop.h 中的 top->trace(tfp, …) 方法将 top 模块注册到 tfp 跟踪器。
- 在仿真循环中,每次 eval() 之后,都用 tfp->dump(time) 写入波形。
- 最后,调用 tfp->close()。
Verilator 初始化
Verilator初始化一般放在main函数的开头,用来初始化仿真环境、初始化DUT的类以及波形配置
仿真环境初始化
和上一大节说明类似,这一部分的初始化主要有两种方式
- 利用静态方法
...//头文件// C++ 全局变量,用于手动管理时间
vluint64_t main_time = 0;int main(int argc, char ** argv){Verilated::commandArgs(argc, argv);Verilated::traceEverOn(true); // 全局启用波形...// 仿真激励//其中仿真时间变量就是:main_time
}
- 利用上下文类
...//头文件int main(int argc, char** argv) {VerilatedContext* contextp = new VerilatedContext;contextp->commandArgs(argc, argv);contextp->traceEverOn(true); // 在此上下文上启用波形...// 仿真激励//其中仿真时间变量就是:contextp->time()
}
顶层类初始化
这个没有什么复杂的地方
...//头文件
int main(int argc,char **argv){...Vtop* top = new Vtop;...//激励
}
波形文件初始化
也是公式化的步骤
...//头文件
int main(int argc,char **argv){...VerilatedVcdC* tfp = new VerilatedVcdC;top->trace(tfp, 99);tfp->open("dump_static.vcd");...//激励
}
如果想要使用FST格式的波形,可以使用如下代码
...//其他头文件
#include "verilated_fst_c.h" //波形文件所需的头文件int main(int argc,char **argv){...VerilatedFstC* tfp = new VerilatedFstC;top->trace(tfp, 99);tfp->open("dump_static.fst");...//激励
}
Verilator 仿真激励
这一部分是仿真文件文件的核心所在,这里简单介绍一下。
仿真激励的流程框架
一个标准的 Verilator 仿真循环(while 循环)通常包含以下几个关键部分:
- 推进时间 (Time Inc):让仿真时间前进。
- 施加激励 (Apply Stimulus):改变 DUT 的输入端口值(如 clk, rst, data_in)。
- 评估电路 (Evaluate):调用 top->eval() 来计算电路状态。
- 转储波形 (Dump Waveform):调用 tfp->dump() 将当前状态写入 VCD/FST 文件。
- 检查输出 (Check Outputs):读取 DUT 的输出端口,用 if 或者 assert 语句判断是否符合预期。
- 检查结束 (Check Finish):检查是否应退出循环(例如时间到了,或 Verilog 调用了 $finish)。
// 在 sim_main.cpp 的 main 函数中
VerilatedContext* contextp = new VerilatedContext;
// ... (Verilator 和波形初始化) ...
Vtop* top = new Vtop(contextp);
VerilatedVcdC* tfp = new VerilatedVcdC;
// ... (trace 和 open 波形) ...// 定义时钟周期(例如,周期为 10 个时间单位)
#define CLK_PERIOD 10
#define HALF_CLK_PERIOD (CLK_PERIOD / 2)// 初始化输入
top->clk = 0;
top->rst = 1; // 假设高电平复位// 仿真主循环
while (!contextp->gotFinish()) {// (A) 推进时间contextp->timeInc(1); // 每次前进 1 个时间单位// (B) 施加激励 - 核心逻辑在这里// ...// (C) 评估电路top->eval();// (D) 转储波形tfp->dump(contextp->time());// (E) 检查输出// ...// (F) 检查结束if (contextp->time() > 1000) { // 示例:仿真 1000 个单位后强制退出printf("Simulation Timeout!\n");break;}
}// ... (清理) ...
时钟激励的编写思路
- 基于时间取模(最常用)
这是最简单、最像 C 语言的思维方式。我们利用 contextp->time() 和 C 语言的取模运算符 (%) 来决定何时翻转时钟。
// 放置在循环的 (B) 施加激励 部分// 假设 CLK_PERIOD = 10。
// 我们希望在时间点 5, 15, 25... 时钟变为 1 (上升沿)
// 我们希望在时间点 10, 20, 30... 时钟变为 0 (下降沿)vluint64_t current_time = contextp->time();if (current_time % HALF_CLK_PERIOD == 0) {top->clk = !top->clk; // 每 5 个时间单位翻转一次
}
- 优点:代码简单,一行搞定。
- 缺点:当有其他激励时,您必须确保它们在 top->clk = 1 之前被施加,以满足setup时间。这可能会让if 语句变得混乱。
- 显式半周期控制(最清晰)
这种方法不使用 timeInc(1) 的“滴答”循环,而是以半个时钟周期为单位来推进时间。这使得“上升沿”和“下降沿”的逻辑块完全分开,非常清晰。但缺点就是显得罗嗦。
// 完整的 main 函数中的循环(替换上面的骨架)// 初始化
top->clk = 0;
top->rst = 1;// 仿真主循环
while (!contextp->gotFinish()) {// -----------------------------------// --- 模拟:时钟下降沿 (Half Cycle 1) ---// -----------------------------------contextp->timeInc(HALF_CLK_PERIOD); // 推进 5 个时间单位top->clk = 0;// 在下降沿可以施加一些激励 (如果 DUT 在下降沿采样)// top->some_input = ...; top->eval(); // 评估 clk=0 时的状态tfp->dump(contextp->time()); // 转储 clk=0 时的波形// -----------------------------------// --- 模拟:时钟上升沿 (Half Cycle 2) ---// -----------------------------------contextp->timeInc(HALF_CLK_PERIOD); // 再推进 5 个时间单位// ***** 关键:在上升沿评估(eval)之前施加激励 *****// 这完美地模拟了 Verilog 的“setup time”// (在时钟边沿到来之前,数据必须稳定)if (contextp->time() == 15) { // 例如,在时间点 15 释放复位printf("[%lu] De-asserting Reset\n", contextp->time());top->rst = 0;}if (contextp->time() == 25) { // 在时间点 25 施加新数据printf("[%lu] Applying data_in = 0xAB\n", contextp->time());top->data_in = 0xAB;}// 真正的“上升沿”发生点top->clk = 1; top->eval(); // <--- Verilog 的 `always @(posedge clk)` 在这里被触发!// ***** 关键:在上升沿评估(eval)之后检查输出 *****// 这完美地模拟了 Verilog 的“hold time / prop delay”// (时钟边沿发生后,等待一小段时间,输出才会更新)if (contextp->time() > 30) {printf("[%lu] Checking output: %d\n", contextp->time(), top->data_out);if (top->data_out != 0xCD) {// ... 报告错误 ...}}tfp->dump(contextp->time()); // 转储 clk=1 时的波形 (包含新输出)// 检查 C++ 侧的退出条件if (contextp->time() > 1000) {break;}
}
非时钟激励编写思路
- 基于时间点的 if 语句(最简单)
// 放置在 "施加激励" 部分 (即 posedge eval 之前)
vluint64_t current_time = contextp->time();if (current_time == 15) {top->rst = 0;
} else if (current_time == 25) {top->data_in = 0x01;
} else if (current_time == 35) {top->data_in = 0x02;
}
- C 语言数组 + 循环(适用于测试向量)
如果激励是一组“测试向量”(Test Vectors),这非常有用。
// 在 main 函数循环之前
int test_vectors[][2] = { // {time, data_in}{15, 0x01},{25, 0x02},{35, 0x03},{45, 0xAA}
};
int num_vectors = 4;
int vector_idx = 0;// ... 进入主循环 ...// 放置在 "施加激励" 部分
vluint64_t current_time = contextp->time();// 保持复位直到时间点 10
if (current_time < 10) {top->rst = 1;
} else {top->rst = 0;
}// 检查是否该施加下一个向量
if (vector_idx < num_vectors && current_time == test_vectors[vector_idx][0]) {top->data_in = test_vectors[vector_idx][1];printf("[%lu] Applying vector %d: data_in = 0x%X\n", current_time, vector_idx, top->data_in);vector_idx++;
}
- 随机激励
注意此时需要头文件 stdlib.h
// #include <stdlib.h> (在文件顶部)
// srand(time(NULL)); (在 main 开头)// 放置在 "施加激励" 部分
if (top->rst == 0 && (rand() % 10) == 0) { // 10% 的概率施加新数据top->data_in = rand() % 256; // 0-255top->data_valid = 1;
} else {top->data_valid = 0;
}
输出检查
主要是利用assert语句,检查输出与施加激励相反,在时钟上升沿 eval() 之后进行。
注意此时需要头文件 assert.h
// #include <assert.h> (在文件顶部)// 放置在 "检查输出" 部分 (即 posedge eval 之后)
vluint64_t current_time = contextp->time();if (current_time > 20) { // 等复位结束后再开始检查if (top->data_out_valid) {printf("[%lu] DUT valid output: 0x%X\n", current_time, top->data_out);// 使用 C 语言的 assert// 如果条件为 false,程序将立即停止并报错assert(top->data_out == expected_value);}
}
综合注意事项
Verilator 收尾工作
主要就是写完激励模块,不要忘记收回内存,这是C/C++程序编写的好习惯
...//头文件int main(int argc,char **argv){...//初始化...//激励top->final();delete top;top = nullptr;delete contextp;contextp = nullptr;return 0;
}
Verilator的命令行参数
一个典型的命令结构如下:
verilator [编译模式] [调试选项] [优化选项] [警告选项] \[Verilog 源文件] \--exe [C++ Testbench 源文件] \-o <输出的可执行文件名>
编译选项
这是最重要的参数,它们决定了 Verilator 的主要工作模式。
参数 | 说明 |
---|---|
–cc | (最常用) 指定 Verilator 将 Verilog/SV 代码转换为 C++ 模型。这是生成 C++ 仿真平台的基础。 |
–sc | 指定 Verilator 将 Verilog/SV 代码转换为 SystemC 模型。 SystemC 是一个 C++ 库。 |
–lint-only | (非常重要) 仅执行代码质量检查(Linting),不生成任何 C++ 代码。这是一种极快的方式,用于在不进行仿真的情况下检查 Verilog 代码的语法错误、风格问题和潜在的逻辑缺陷。 |
–exe | (关键) 告诉 Verilator 您想要创建一个可执行的仿真程序。您必须在此参数之后列出所有 C++ testbench 文件(例如 sim_main.cpp)。 |
–build | (强烈推荐) 这是一个“自动”标志。它告诉 Verilator 在生成 C++ 代码后,自动调用 make 和 g++/clang++ 编译器来编译所有 C++ 文件,并链接生成最终的可执行文件。 |
-o | 指定输出的可执行文件的路径和名称。例如:-o ./obj_dir/sim_top。 |
-Mdir <dir> | 指定生成的 C++ 文件和 Makefile 存放的目录。默认是 obj_dir。在有多个测试时,使用它来区分不同的构建目录(例如 -Mdir build_test1)是个好习惯。 |
调试和波形选项
参数 | 说明 |
---|---|
–trace | (关键) 启用波形跟踪功能。这会使 Verilator 在生成的 C++ 模型中加入必要的代码,以便与 verilated_vcd_c.h 或 verilated_fst_c.h 配合使用,来转储(dump)信号。 |
–trace-vcd | 显式指定使用 VCD (Value Change Dump) 格式生成波形。这是默认的跟踪格式。 |
–trace-fst | (推荐) 指定使用 FST (Fast Signal Trace) 格式生成波形。FST 是 GTKWave 支持的一种二进制格式,它生成的波形文件体积更小(通常小 10-100 倍),并且仿真速度更快。 |
–trace-depth | 设置波形跟踪的层次深度。默认跟踪所有层次。 |
–trace-structs | 启用对 struct 和 union 类型的波形跟踪。 |
–debug | 启用调试模式。这会编译一个未经优化且包含额外断言的 Verilator 模型。仿真速度会急剧下降,但有助于定位 Verilator 内部或 C++ 仿真平台与模型交互时的疑难杂症。 |
性能优化
这些参数用于控制 Verilator 生成的 C++ 模型的仿真速度
参数 | 说明 |
---|---|
-O0 | 不进行优化。编译 C++ 会很快,但仿真速度非常慢。仅用于调试 C++ 代码。 |
-O2 | 默认的优化级别。在 C++ 编译时间和仿真速度之间取得了很好的平衡。 |
-O3 | 最高级别的优化。会指示 C++ 编译器(g++)使用 -O3。这会显著增加 C++ 编译时间,但会换来最快的仿真速度。 |
–threads <N> | (高级) 启用多线程仿真,使用 N 个线程。这并不是总能提高速度。它只对设计中存在多个并行的、时钟域不同或独立的模块(always 块)时才有效。 |
–no-assert | 禁用 Verilog assert 语句的检查,可以轻微提升性能。 |
警告选项
参数 | 说明 |
---|---|
-Wall(强烈推荐) | 启用所有 Verilator 推荐的代码质量警告(“Warnings-All”)。就像 gcc -Wall 一样,这应该成为您的标配。 |
-Wno-<WARNING>(常用) | 禁用某一个特定的警告。例如,如果您的设计中有未使用的信号,Verilator 会报 UNUSED 警告,您可以使用 -Wno-UNUSED 来屏蔽它。 |
-Werror-<WARNING> | 将某一个特定的警告视为致命错误,导致 Verilator 停止。这在 CI/CD(持续集成)中很有用,用于确保团队成员修复了某些关键警告。 |
–fatal-warnings | 将所有警告都视为致命错误。 |
预处理选项
参数 | 说明 |
---|---|
–language <STD> | 指定 Verilog/SV 语言标准,例如 --language 1800-2017 (SystemVerilog 2017)。 |
+incdir+<dir> | (常用) 添加一个 Verilog include 文件的搜索路径。用于 include “filename.vh”。注意 + 号是 Verilog 标准语法的一部分。 |
-I<dir> | 功能同上,这是 C 语言风格的 include 路径参数。推荐使用 +incdir+。 |
+define+<VAR>=<VAL> | (常用) 在 Verilog 代码编译前定义一个宏(macro)。等同于 C 语言的 gcc -DVAR=VAL。例如 +define+SIMULATION=1。 |
-D<VAR>=<VAL> | C 语言风格的宏定义,功能同上。 |
-y <dir> | 添加一个模块搜索目录。当 Verilog 实例化一个模块(如 my_mod u_my_mod (…))时,Verilator 会去这些目录中查找 my_mod.v 文件。 |
一个例子
假设我们有:
- Verilog 顶层:top.v
- Verilog 子模块:./rtl/core.v
- Verilog 头文件:./include/defines.vh
- C++ Testbench:sim_main.cpp
- C++ 辅助文件:utils.cpp
我们希望:
- 使用 SystemVerilog 2017 标准。
- 开启所有警告。
- 定义一个宏 SIM。
- 包含 include 目录。
- 启用 FST 波形跟踪。
- 使用 -O3 优化。
- 自动编译并生成一个名为 sim_run 的可执行文件。
命令如下:
verilator \-Wall `# 开启所有警告` \--cc `# 编译为 C++` \--trace-fst `# 启用 FST 波形跟踪` \-O3 `# 使用 O3 优化` \--language 1800-2017 `# 设置 SV 语言标准` \+incdir+./include `# 添加 include 路径` \+define+SIM=1 `# 定义 SIM 宏` \-y ./rtl `# 添加模块搜索路径` \\--exe `# 指明我们要链接可执行文件` \sim_main.cpp utils.cpp `# 列出所有的 C++ testbench 文件` \\top.v `# 列出 Verilog 顶层文件` \\--build `# 自动调用 make 和 g++` \-o ./sim_run `# 指定最终可执行文件的名称`
GTKwave入门使用
GTKwave可以使用鼠标双击打卡,但实际使用中还是以命令行为主,以下进行相关介绍。
GTKwave的命令行参数
最基本的命令结构如下:
gtkwave [选项] [波形文件] [配置文件]
- [波形文件]: 您的仿真输出,例如 dump.vcd 或 dump.fst。
- [配置文件]: 一个 .gtkw 文件,它保存了您上一次查看波形时的所有设置(添加了哪些信号、信号的颜色、格式是十六进制还是二进制等)。
最实用的命令示例:
# 场景:您正在调试一个项目,并且已经保存了一个信号配置文件
gtkwave dump.fst my_signals.gtkw
核心选项
参数 | 说明 |
---|---|
-f, --file <filename> | 显式指定要打开的波形文件。gtkwave -f dump.vcd 等同于 gtkwave dump.vcd。 |
-a, --save <filename>S | 显式指定要加载的 .gtkw 配置文件。gtkwave -a my_signals.gtkw 等同于 gtkwave my_signals.gtkw。 |
-A, --autosavename (非常实用) | 自动加载配置文件。它会查找与波形文件同名,但扩展名为 .gtkw 的文件。 示例:gtkwave -A dump.fst GTKWave 会自动在同一目录下寻找并加载 dump.gtkw 文件(如果存在)。 |
波形文件处理
这些参数在处理 Verilator 生成的大型波形文件时特别有用。
参数 (长/短) | 说明 |
---|---|
-o, --optimize | (VCD 用户必用) 针对 VCD 文件的优化。当您打开一个巨大的 VCD 文件时,GTKWave 会先将其转换为内部的 FST 格式(dump.vcd.fst),然后再加载。这会使后续的加载速度极快,并大大减少内存占用。 |
-v, --vcd | 从标准输入 (stdin) 读取 VCD 数据。这在高级脚本中很有用,例如: `cat test.vcd |
-c, --cpu <numcpus> | 在加载波形文件(特别是 FST)时,指定使用多少个 CPU 核心。在大型波形上,gtkwave -c 4 dump.fst 可以显著加快加载速度。 |
-s, --start <time> | 仅加载指定时间点之后的波形数据。 |
-e, --end <time> | 仅加载指定时间点之前的波形数据。 |
自动化相关
GTKWave 内部集成了一个 Tcl 解释器,允许您通过脚本自动执行 GUI 操作(例如添加信号、设置颜色、跳转到特定时间)。
参数 (长/短) | 说明 |
---|---|
-S, --script <filename> | 在加载波形之后,执行一个 Tcl 脚本。这是实现“完全自动化”的关键。您可以在这个脚本里自动添加信号、设置格式、缩放波形。 |
-T, --tcl_init <filename> | 在启动时、加载波形之前,执行一个 Tcl 脚本。 |
-W, --wish | 在启动 GTKWave 的终端中启用 Tcl 命令行。您可以在终端中直接输入 Tcl 命令来控制 GUI。 |
-r, --rcfile <filename> | 指定一个自定义的 .gtkwaverc 配置文件,用于覆盖 GTKWave 的默认设置(如字体、颜色主题等)。 |
说明
- 作为初学者,最应该养成的习惯就是:第一次打开波形,手动添加好 clk, rst 和所有关键信号,然后立即 Ctrl+S 将其保存为 my_project.gtkw。从此以后,您重新编译并运行 Verilator 仿真后,只需在命令行执行 gtkwave dump.fst my_project.gtkw,所有信号都会被完美加载。
- 自动刷新:GTKWave 没有像 ModelSim 那样的“实时仿真”模式。但可以实现类似的效果:当 Verilator 仿真正在运行时(例如一个需要 10 分钟的仿真),GTKWave 已经打开了它正在生成的 dump.fst 文件。只需在 GTKWave 窗口中点击 File -> Reload Waveform (快捷键 Shift+Ctrl+R),GTKWave 就会重新加载文件,显示出到目前为止已经仿真的所有波形。
Makefile的编写
verilator和GTKwave都是命令行控制的,非常适合使用Makefile自动控制流程。
此章节由AI生成,仅供参考
一个模板
# =============================================================================
#
# Verilator 仿真 Makefile
#
# =============================================================================
#
# 使用方法:
# make sim - 构建并运行 优化版 仿真
# make sim-debug - 构建并运行 调试版 仿真
# make gdb - 在 GDB 中运行 调试版 仿真
# make view - 在 GTKWave 中打开波形
# make clean - 清理所有生成的文件
#
# 可选参数:
# make sim USER_ARGS="+my_arg=value" - 向仿真程序传递运行时参数
# make TOP_MODULE=my_top ... - 覆盖顶层模块名称
#
# =============================================================================.PHONY: all build build-debug sim sim-debug gdb view clean help# --- 1. 项目配置 (可在此处修改) ---# Verilog 顶层模块名
TOP_MODULE ?= top# 源代码目录
RTL_SRC_DIRS ?= rtl
SIM_SRC_DIRS ?= sim# Verilog Include 路径 (例如 +incdir+<path>)
RTL_INC_DIRS ?= $(RTL_SRC_DIRS)# Verilog 宏定义 (例如 +define+SIMULATION=1)
RTL_DEFINES ?= # --- 2. 工具和文件名 (通常不需要修改) ---# 工具
VERILATOR = verilator
VIEWER = gtkwave# 构建目录 (分离优化版和调试版)
BUILD_DIR_RELEASE = obj_dir_release
BUILD_DIR_DEBUG = obj_dir_debug# 目标可执行文件
# Verilator 总是生成 V<TOP_MODULE> 作为可执行文件
EXEC_RELEASE = $(BUILD_DIR_RELEASE)/V$(TOP_MODULE)
EXEC_DEBUG = $(BUILD_DIR_DEBUG)/V$(TOP_MODULE)# 波形文件 (确保您的 sim_main.cpp 生成这个文件)
WAVE_FILE = dump.fst
GTKW_FILE = waves.gtkw# --- 3. 自动源文件发现 ---# 自动查找所有 Verilog/SystemVerilog 和 C++ 源文件
RTL_FILES = $(foreach d, $(RTL_SRC_DIRS), $(wildcard $(d)/*.v) $(wildcard $(d)/*.sv))
SIM_FILES = $(foreach d, $(SIM_SRC_DIRS), $(wildcard $(d)/*.cpp))# 格式化 Verilog 包含路径
VERILOG_INC_FLAGS = $(foreach d, $(RTL_INC_DIRS), +incdir+$(d))# --- 4. Verilator 编译标志 ---# 基础标志: 启用警告, 编译为 C++, 启用 FST 波形跟踪
VERILATOR_FLAGS_BASE = -Wall --cc --trace-fst \$(VERILOG_INC_FLAGS) \$(RTL_DEFINES)# 优化版 (Release) 标志: O3 优化, 更快的 Verilator 内部优化
VERILATOR_FLAGS_OPT = -O3 --x-assign fast --x-initial fast --noassert# 调试版 (Debug) 标志: 禁用 C++ 优化 (-O0), 启用 GDB 调试 (-g),
# 启用 Verilator 内部调试 (--debug), 启用覆盖率
VERILATOR_FLAGS_DEBUG = -O0 -g --debug --coverage# 传递给仿真程序的运行时参数 (例如 +trace)
USER_ARGS ?=# =============================================================================
#
# Makefile 目标 (Targets)
#
# =============================================================================# --- 默认目标 ---
all: build# --- 构建目标 ---# 构建优化版
build: $(EXEC_RELEASE)$(EXEC_RELEASE): $(RTL_FILES) $(SIM_FILES)@echo "[\033[1;32mBUILD\033[0m] Building Release: $(EXEC_RELEASE)"$(VERILATOR) $(VERILATOR_FLAGS_BASE) $(VERILATOR_FLAGS_OPT) \--build -Mdir $(BUILD_DIR_RELEASE) \$(RTL_FILES) \--exe $(SIM_FILES)# 构建调试版
build-debug: $(EXEC_DEBUG)$(EXEC_DEBUG): $(RTL_FILES) $(SIM_FILES)@echo "[\033[1;34mBUILD\033[0m] Building Debug: $(EXEC_DEBUG)"$(VERILATOR) $(VERILATOR_FLAGS_BASE) $(VERILATOR_FLAGS_DEBUG) \--build -Mdir $(BUILD_DIR_DEBUG) \$(RTL_FILES) \--exe $(SIM_FILES)# --- 仿真目标 ---# 运行优化版
sim: build@echo "[\033[1;32mRUN\033[0m] Running Simulation (Release)..."@./$(EXEC_RELEASE) $(USER_ARGS)# 运行调试版
sim-debug: build-debug@echo "[\033[1;34mRUN\033[0m] Running Simulation (Debug)..."@./$(EXEC_DEBUG) $(USER_ARGS)# 使用 GDB 运行调试版
gdb: build-debug@echo "[\033[1;31mDEBUG\033[0m] Starting GDB session..."gdb ./$(EXEC_DEBUG)# --- 查看目标 ---# 在 GTKWave 中打开波形
view:@echo "[\033[1;36mVIEW\033[0m] Opening waveform: $(WAVE_FILE)"@$(VIEWER) $(WAVE_FILE) $(GTKW_FILE) &# --- 清理目标 ---clean:@echo "[\033[1;33mCLEAN\033[0m] Cleaning up build directories and log files..."@rm -rf $(BUILD_DIR_RELEASE) $(BUILD_DIR_DEBUG)@rm -f $(WAVE_FILE) *.log verilator*.log# --- 帮助目标 ---help:@echo ""@echo "Verilator 仿真 Makefile"@echo "-------------------------"@echo "用法: make [TARGET] [OPTIONS]"@echo ""@echo "主要目标:"@echo " build - 构建 优化版 仿真程序 (默认)"@echo " sim - 构建并运行 优化版 仿真"@echo " build-debug - 构建 调试版 仿真程序 (带 -g 和 --debug 标志)"@echo " sim-debug - 构建并运行 调试版 仿真"@echo " gdb - 在 GDB 调试器中启动 调试版 仿真"@echo " view - 在 GTKWave 中打开波形文件 ($(WAVE_FILE))"@echo " clean - 删除所有构建目录和波形文件"@echo ""@echo "可覆盖的参数:"@echo " TOP_MODULE=name - 指定 Verilog 顶层模块 (默认: $(TOP_MODULE))"@echo " USER_ARGS=\"args\" - 传递给仿真程序的运行时参数 (例如 '+trace')"@echo " RTL_SRC_DIRS=dir1 [dir2 ...] - RTL 源文件目录 (默认: $(RTL_SRC_DIRS))"@echo " SIM_SRC_DIRS=dir1 [dir2 ...] - C++ 源文件目录 (默认: $(SIM_SRC_DIRS))"@echo ""
使用方法
- 创建目录和文件:
- 创建 rtl/ 目录, 放入您的 top.v 和其他 Verilog/SV 文件。
- 创建 sim/ 目录, 放入您的 sim_main.cpp 和其他 C++ 辅助文件。
- 重要:确保您的 sim_main.cpp 被配置为生成名为 dump.fst 的波形文件(即使用了 --trace-fst)。
- 运行命令
- 构建和运行 (快速, 优化版):
make sim
- 构建和运行 (调试版):
make sim-debug
- 使用 GDB 调试 C++ Testbench:
make gdb
(这会自动构建调试版,然后在 GDB 中启动它。您可以在 sim_main.cpp 中设置断点)。
4. 查看波形:
make view
(这会启动 gtkwave dump.fst waves.gtkw &。waves.gtkw 是可选的 GTKWave 配置文件)。
5. 清理:
make clean
- 传递运行时参数: 如果您的 C++ testbench 解析 argc, argv (使用了 Verilated::commandArgs),您可以这样做:
# 假设您的 C++ 代码能识别 +my_custom_flag
make sim USER_ARGS="+my_custom_flag=10"