Verilator 和 GTKwave联合仿真

Verilator 和 GTKwave联合仿真

二者都是开源软件,可以免费下载和使用。这在商业软件充斥的IC行业显得额外珍贵。

文章参考:

  1. Verilator官方文档;
  2. CSDN文章;
  3. AI回答生成

文章目录

简介

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 协同工作

  1. 使用 Verilator将Verilog/SV 设计和 C++ Testbench 编译成一个高速的仿真可执行文件。
  2. 在 verilator 命令中加入 --trace (生成 VCD) 或 --trace-fst (生成 FST) 选项。
  3. 在 C++ Testbench 中包含必要的代码,以在仿真开始时打开波形文件,并在每个仿真周期转储(dump)信号值。
  4. 运行编译好的仿真程序,它会输出一个 .vcd 或 .fst 波形文件。
  5. 使用 GTKWave 打开这个波形文件(例如 gtkwave dump.fst),然后就可以直观地分析和调试设计中所有信号随时间变化的情况了
  6. 对于需要自动化控制流程的项目还需要编写Makefile文件。

Verilator入门使用

Verilator的tb编写

Verilator的tb可以使用C++或者SystemC编写,由于SystemC相对复杂不适合入门,这里就专注于C++的tb编写。

我们的目标:

  • Verilator 的 C++ Testbenc本质上只是一个int main() 函数
  • 我们的RTL设计代码在编译完成后会变成一个 C++ 类,然后在 main 函数里实例化这个模块,并手动在 for 循环里启动时钟,给输入端口赋值,并读取输出端口的值。

一个模板:

Verilator仿真平台主要遵循如下的结构:

c 复制代码
/* 一:头文件的包含 */
#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); // 将当前时间点的信号值写入 VCD
        main_time++; // 推进仿真时间
    }

    /* 四.善后工作 */

	tfp->close(); // 关闭波形文件

	// 清理top仿真模型,并销毁相关指针,并将指针变为空指针
	top->final();
	delete top;
	top = nullptr;
	delete contextp;
	contextp = nullptr;

	return 0;
}

注意:上面的模板只是示意,不能执行。接下来我分模块说明。

Verilator的头文件

在编写Verilator头文件中,除了C/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 中例化一个模块一样。

C++ 复制代码
// 在 sim_main.cpp 中
Vtop* top = new Vtop; // "例化" 顶层模块

主要的成员和方法

  1. 端口访问 (Port Access)
  • 语法:top->{port_name}
  • 说明:Verilator将Verilog 模块的所有 input 和 output 端口转换为了 C++ 类的公共成员变量。这是您与 DUT 交互的主要方式。

示例:如果 top.v 有 input clk, input rst, output 7:0 data_out:

C++ 复制代码
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
  1. eval()
  • 语法:top->eval();
  • 说明:这是 Verilator 仿真中最核心的方法。 当调用 eval() 时,Verilator 会根据当前的输入(如 top->clk)和内部寄存器的状态,计算所有组合逻辑,并更新所有内部信号和输出端口的值。
  • 重要:它不会自动触发时钟沿。always @(posedge clk) 的行为是通过您在 C++ 中手动将 clk 从 0 变为 1,并在变化后调用 eval() 来模拟的。
  1. trace(VerilatedVcdC* tfp, int levels)
  • 语法:top->trace(tfp, 99);
  • 说明:此方法用于将该模块的信号连接到 VCD 波形跟踪器(tfp 对象,来自 verilated_vcd_c.h)。99 是一个通配符,表示跟踪该模块及其所有子模块的信号。

注意:这个方法只有在您运行 verilator 命令时加了 --trace 选项才会被生成。

  1. final()
  • 语法:top->final();
  • 说明:在仿真结束、delete top 之前调用。它用于执行 Verilog 中的 $finish 任务和相关的最终清理工作。

注意事项

  • eval() 的调用时机:这是初学者最容易犯错的地方。任何输入信号发生变化后,您都应该调用一次 eval() 来让电路"感知"到这个变化并更新组合逻辑。
  • 模拟时钟边沿:模拟一个完整的时钟周期(clk 从 0 变到 1 再变回 0)需要至少两次eval() 调用:
C++ 复制代码
/* 模拟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 模块

verilog 复制代码
// top.v
module top (
  input clk
);
  // 为了能看到$finish的效果
  always @(posedge clk) begin
    if ($time > 100) begin
      $display("[%0t] Verilog side requests $finish", $time);
      $finish;
    end
  end
endmodule
  1. 使用 verilated.h 的传统方法:Verilated

主要的类:Verilated

这是一个特殊的类,您不需要(也不能)new 它。您通过 Verilated:: 作用域来调用它的全局函数。

在这种模式下,仿真的状态(如"是否 $finish")被存储在 Verilator 库的全局静态变量中。您通过 Verilated:: 来访问它们。

关键点:时间管理。在这种模式下,Verilator 没有一个内置的全局时间变量供您递增。您必须自己在 C++ Testbench 中创建一个变量(如 main_time)来跟踪时间,并手动将其传递给 VCD 写入器。

cpp 复制代码
//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;
}
  1. 使用 VerilatedContext

首先创建一个 VerilatedContext 对象,这个对象封装了所有仿真状态。

**关键点:时间管理。**VerilatedContext 对象内置了时间。您不再需要 main_time 全局变量,而是调用 contextp->timeInc(1) 来推进时间,用 contextp->time() 来获取时间。

cpp 复制代码
// 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 文件。

C++ 复制代码
// 在 sim_main.cpp 中
#include "verilated_vcd_c.h"
VerilatedVcdC* tfp = new VerilatedVcdC; // 创建一个 VCD 跟踪器对象

主要的方法(函数)

  1. open(const char* filename)
  • 语法:tfp->open("dump.vcd");
  • 说明:打开一个文件用于写入。如果文件已存在,它将被覆盖。您必须在调用 dump() 之前先调用它。
  1. dump(vluint64_t timestamp)
  • 语法:tfp->dump(contextp->time()); (或 tfp->dump(main_time)😉
  • 说明:这是生成波形的核心函数。 它将所有通过 top->trace(tfp, ...) 注册的信号的当前值,写入到 VCD 文件中,并关联到您传入的 timestamp(时间戳)。
  • 调用时机:您应该在每一次 top->eval() 之后(或者至少在您关心信号变化的时间点)都调用一次 dump()。
  1. 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的类以及波形配置

仿真环境初始化

和上一大节说明类似,这一部分的初始化主要有两种方式

  1. 利用静态方法
cpp 复制代码
...//头文件

// C++ 全局变量,用于手动管理时间
vluint64_t main_time = 0;

int main(int argc, char ** argv){
    Verilated::commandArgs(argc, argv);
    Verilated::traceEverOn(true); // 全局启用波形
    ...// 仿真激励
    //其中仿真时间变量就是:main_time
}
  1. 利用上下文类
cpp 复制代码
...//头文件

int main(int argc, char** argv) {
    
    VerilatedContext* contextp = new VerilatedContext;

    contextp->commandArgs(argc, argv);
    contextp->traceEverOn(true); // 在此上下文上启用波形
    ...// 仿真激励
    //其中仿真时间变量就是:contextp->time()
}
顶层类初始化

这个没有什么复杂的地方

cpp 复制代码
...//头文件
int main(int argc,char **argv){
    ...
    Vtop* top = new Vtop;
    ...//激励
}
波形文件初始化

也是公式化的步骤

cpp 复制代码
...//头文件
int main(int argc,char **argv){
    ...
    VerilatedVcdC* tfp = new VerilatedVcdC;
    top->trace(tfp, 99);
    tfp->open("dump_static.vcd");
    ...//激励
}

如果想要使用FST格式的波形,可以使用如下代码

cpp 复制代码
...//其他头文件
#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)。
cpp 复制代码
// 在 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;
    }
}

// ... (清理) ...
时钟激励的编写思路
  1. 基于时间取模(最常用)

这是最简单、最像 C 语言的思维方式。我们利用 contextp->time() 和 C 语言的取模运算符 (%) 来决定何时翻转时钟。

cpp 复制代码
// 放置在循环的 (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 语句变得混乱。
  1. 显式半周期控制(最清晰)

这种方法不使用 timeInc(1) 的"滴答"循环,而是以半个时钟周期为单位来推进时间。这使得"上升沿"和"下降沿"的逻辑块完全分开,非常清晰。但缺点就是显得罗嗦。

cpp 复制代码
// 完整的 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;
    }
}
非时钟激励编写思路
  1. 基于时间点的 if 语句(最简单)
cpp 复制代码
// 放置在 "施加激励" 部分 (即 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;
}
  1. C 语言数组 + 循环(适用于测试向量)

如果激励是一组"测试向量"(Test Vectors),这非常有用。

cpp 复制代码
// 在 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++;
}
  1. 随机激励

注意此时需要头文件 stdlib.h

cpp 复制代码
// #include <stdlib.h> (在文件顶部)
// srand(time(NULL)); (在 main 开头)

// 放置在 "施加激励" 部分
if (top->rst == 0 && (rand() % 10) == 0) { // 10% 的概率施加新数据
    top->data_in = rand() % 256; // 0-255
    top->data_valid = 1;
} else {
    top->data_valid = 0;
}
输出检查

主要是利用assert语句,检查输出与施加激励相反,在时钟上升沿 eval() 之后进行。

注意此时需要头文件 assert.h

cpp 复制代码
// #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++程序编写的好习惯

cpp 复制代码
...//头文件

int main(int argc,char **argv){
    ...//初始化
    ...//激励

    top->final();
    delete top;
    top = nullptr;
    delete contextp;
    contextp = nullptr;

    return 0;
}

Verilator的命令行参数

一个典型的命令结构如下:

bash 复制代码
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 的可执行文件。
    命令如下:
bash 复制代码
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的命令行参数

最基本的命令结构如下:

Bash 复制代码
gtkwave [选项] [波形文件] [配置文件]
  • 波形文件: 您的仿真输出,例如 dump.vcd 或 dump.fst。
  • 配置文件: 一个 .gtkw 文件,它保存了您上一次查看波形时的所有设置(添加了哪些信号、信号的颜色、格式是十六进制还是二进制等)。

最实用的命令示例:

Bash 复制代码
# 场景:您正在调试一个项目,并且已经保存了一个信号配置文件
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生成,仅供参考

一个模板

Makefile 复制代码
# =============================================================================
#
#                         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)。
  • 运行命令
  1. 构建和运行 (快速, 优化版):
Bash 复制代码
make sim
  1. 构建和运行 (调试版):
Bash 复制代码
make sim-debug
  1. 使用 GDB 调试 C++ Testbench:
Bash 复制代码
make gdb

(这会自动构建调试版,然后在 GDB 中启动它。您可以在 sim_main.cpp 中设置断点)。

  1. 查看波形:
Bash 复制代码
make view

(这会启动 gtkwave dump.fst waves.gtkw &。waves.gtkw 是可选的 GTKWave 配置文件)。

  1. 清理:
Bash 复制代码
make clean
  1. 传递运行时参数: 如果您的 C++ testbench 解析 argc, argv (使用了 Verilated::commandArgs),您可以这样做:
Bash 复制代码
# 假设您的 C++ 代码能识别 +my_custom_flag
make sim USER_ARGS="+my_custom_flag=10"
相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495644 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr4 小时前
2607C++,soui与安卓
c++·soui
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新4 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
千寻xun6 小时前
三、实战篇-NVME SSD控制之FPGA实现(一)
fpga开发·nvme ssd·nvme协议
a1117767 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习