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。

最实用的命令示例:

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"
相关推荐
知识充实人生5 小时前
时序收敛方法一:控制集优化
stm32·单片机·fpga开发
kkkkk0211065 小时前
软考高级-系统架构设计师案例专题三:系统开发基础
笔记·系统架构
心灵宝贝7 小时前
申威架构ky10安装php-7.2.10.rpm详细步骤(国产麒麟系统64位)
开发语言·php
lly2024067 小时前
PHP 字符串操作详解
开发语言
像是套了虚弱散9 小时前
DevEco Studio与Web联合开发:打造鸿蒙混合应用的全景指南
开发语言·前端·华为·harmonyos·鸿蒙
旭意9 小时前
C++蓝桥杯之结构体10.15
开发语言·c++
颜颜yan_9 小时前
UU远程——让工作、学习、娱乐跨设备无缝衔接,“远程”更像“身边”
学习·娱乐·远程工作
新子y9 小时前
【小白笔记】区分类方法/实例方法和静态函数/命名空间函数
笔记·分类
深思慎考10 小时前
调用百度云语音识别服务——实现c++接口识别语音
c++·语音识别·百度云