Verilator:把 Verilog“编译成程序”的高速数字电路仿真器

在 FPGA、ASIC 和数字芯片设计中,写完 Verilog 或 SystemVerilog 代码之后,并不能直接认为设计是正确的。在真正进行综合、布局布线甚至流片之前,工程师通常需要先回答一个最基本的问题:

这段硬件逻辑,行为到底对不对?

这就是数字电路仿真的任务。

在众多 HDL 仿真工具中,Verilator 是一个非常特殊的存在。它与 ModelSim、Questa、VCS 等传统仿真器最大的不同,是它并不主要依靠一个通用的事件驱动仿真引擎去“解释执行” HDL,而是采用一种更加接近编译器的技术路线:

把 Verilog/SystemVerilog 转换成 C++ 或 SystemC,再编译成本机可执行程序。

正是这一机制,使 Verilator 成为目前数字逻辑高速仿真、自动化测试和大规模回归测试中非常有代表性的工具。

Verilator仿真原理

一、Verilator 本质上做了什么?

假设有这样一段简单的 SystemVerilog:

always_ff @(posedge clk) begin
    if (!rst_n)
        count <= 0;
    else
        count <= count + 1;
end

从硬件角度看,它描述的是一个触发器组成的计数器:

时钟每出现一次上升沿,计数值增加一次。

传统 HDL 仿真器通常会建立完整的仿真时间和事件系统,例如:

10 ns:clk 变化
10 ns:检测到上升沿
10 ns:执行 always_ff
10 ns:登记非阻塞赋值
10 ns:更新 count

也就是说,仿真器不断维护时间、信号变化以及事件调度。

Verilator 的思路不同。

它首先分析 RTL,然后把这些逻辑转换成类似 C++ 程序的计算过程。概念上,可以把前面的逻辑理解成:

if (clock_rising_edge) {
    if (!rst_n)
        count = 0;
    else
        count = count + 1;
}

真正生成的代码当然要复杂得多,因为还需要正确处理组合逻辑、时序逻辑、非阻塞赋值、信号依赖、模块层次以及 SystemVerilog 调度语义。

但核心没有改变:

Verilog / SystemVerilog
          ↓
      Verilator
          ↓
       C++模型
          ↓
     GCC / Clang
          ↓
      本机机器码
          ↓
        执行

因此,Verilator 可以理解为一种编译型 HDL 仿真技术

二、为什么这种方法会非常快?

传统事件驱动仿真器需要不断回答:

哪个信号发生变化?

哪个 always 块需要执行?

当前仿真时间是多少?

某个赋值应该在哪个调度区域生效?

后面还有没有新的事件被触发?

这种机制非常灵活,也能够表达复杂的仿真语义,但代价是运行开销较大。

Verilator 会在编译阶段提前分析逻辑之间的依赖关系。

例如有:

A → B → C → D

传统仿真器可能把 A、B、C、D 的变化看成多个连续事件。

Verilator 则可以把相关计算组织成高效的 C++:

B = !A;
C = B & enable;
D = C ^ mode;

随后 GCC 或 Clang 还会继续进行常量传播、函数内联、死代码删除、寄存器分配等编译优化。

最终真正运行的是高度优化后的 CPU 机器码。

所以 Verilator 的性能优势,本质上并不是因为它“少算了电路逻辑”,而是因为:

大量原本需要仿真器在运行期间动态完成的工作,被提前放到了编译阶段。

这和解释型语言与编译型语言之间的差异有些相似。

三、Verilator 仿真由哪些部分组成?

一个典型仿真系统至少包括两个核心部分:

Testbench
    ↓
输入激励
    ↓
DUT
    ↓
输出结果
    ↓
检查与验证

其中 DUT 是 Device Under Test,即“被测设计”。

DUT 可以是:

一个计数器;

一个 FIFO;

一个 UART;

一个 CPU 内核;

一个 DMA 控制器;

也可以是一个完整 SoC。

Testbench 则负责制造输入条件,例如产生时钟、复位、总线访问、随机数据,并检查 DUT 输出是否符合预期。

Verilator 很经典的一种使用方式,是使用 C++ 编写 Testbench。

例如:

Vcounter dut;

dut.rst_n = 0;
dut.clk = 0;
dut.eval();

dut.clk = 1;
dut.eval();

这里最关键的是:

dut.eval();

它可以粗略理解为:

让数字电路根据当前输入状态重新计算一次。

假设:

clk:0 → 1

调用 eval() 后,Verilator 会识别出时钟上升沿,并执行与:

@(posedge clk)

有关的时序逻辑。

因此,软件中的一次函数调用,就对应了硬件模型的一次状态更新。

四、没有真实晶振,时钟从哪里来?

真实 FPGA 或 ASIC 中,时钟通常来自晶振、PLL 或其他时钟源。

软件仿真环境中不存在真实电信号,因此 Testbench 需要主动制造时钟。

最简单的方式就是:

clk = 0;
eval();

clk = 1;
eval();

clk = 0;
eval();

clk = 1;
eval();

于是形成:

      ┌─────┐     ┌─────┐
──────┘     └─────┘     └──────
      ↑           ↑
   上升沿       上升沿

每出现一次上升沿,DUT 中相应的时序电路便会执行。

因此,从某种意义上说,Testbench 不仅是“测试代码”,它还承担了整个虚拟硬件世界的外部环境。

五、一次完整的 Verilator 仿真过程

典型流程可以概括为:

编写 RTL
    ↓
编写 Testbench
    ↓
Verilator 解析 HDL
    ↓
语法与静态检查
    ↓
生成 C++
    ↓
C++ 编译
    ↓
链接生成可执行文件
    ↓
运行仿真
    ↓
检查结果
    ↓
生成并查看波形

例如已有:

counter.sv
sim_main.cpp

可以执行:

verilator --cc counter.sv --exe sim_main.cpp --build

其中:

--cc

表示将 RTL 转换成 C++ 模型。

Verilator 通常会生成一个 obj_dir 目录,其中包含诸如:

Vcounter.cpp
Vcounter.h
Vcounter.mk

等文件。

其中 Vcounter 可以理解为 RTL 顶层模块对应的 C++ 类。

完成编译以后,就可以直接运行生成的仿真程序。

此时已经不再是 Verilator 在逐条“解释 HDL”,而是在执行真正的本机程序。

六、波形仍然非常重要

自动化测试能够判断 PASS 或 FAIL,但 FPGA 和数字芯片开发中,波形仍然是定位问题的重要工具。

Verilator 可以生成 VCD、FST 等波形文件,例如:

simulation.fst

再通过 GTKWave 等工具查看:

clk
rst_n
data_valid
state
address
data

这些内部信号。

于是整个过程变成:

Verilator
    ↓
VCD / FST
    ↓
GTKWave
    ↓
查看数字波形

当出现状态机跳转错误、握手时序异常、计数器偏差或者数据错位时,波形通常能够直观暴露原因。

七、Verilator 真正重要的价值:把硬件测试“软件工程化”

如果仅仅把 Verilator 当成“另一个波形仿真器”,实际上低估了它。

它最大的价值之一,是能够让 RTL 开发采用很多成熟的软件工程方法。

例如测试一个 FIFO,可以编写自动测试:

向 FIFO 写入一万个随机数据;

随机插入读操作;

建立一个软件参考队列;

实时比较 RTL 输出与软件模型;

任何一个数据不一致立即报错。

最终测试结果可能只有:

100000 tests passed
0 tests failed

而不是工程师盯着数十万周期的波形逐个检查。

更进一步,还可以把它接入:

Git
↓
CI
↓
Verilator
↓
自动仿真
↓
PASS / FAIL

每次提交 RTL 以后自动运行几百甚至几千个测试用例。

这样一来,数字硬件开发就拥有了与现代软件开发非常类似的:

单元测试、自动化测试、持续集成和回归测试能力。

八、与“黄金模型”结合尤其强大

复杂数字算法通常可以先建立一个参考实现,例如使用:

Python、C++ 或 MATLAB。

这个软件参考模型通常被称为 Golden Model,即“黄金模型”。

整个验证过程可以变成:

                相同输入
                   │
          ┌────────┴────────┐
          ↓                 ↓
     Python/C++模型       RTL设计
          ↓                 ↓
      参考结果           Verilator
          ↓                 ↓
          └────────┬────────┘
                   ↓
                 比较
                   ↓
              PASS / FAIL

这对于:

数字信号处理;

视频处理;

通信算法;

AI 加速器;

CPU 指令执行;

密码算法;

数据压缩;

网络协议处理

等设计特别有价值。

工程师不需要人工判断“这个结果看起来是不是差不多”,而可以进行严格的逐样本、逐周期甚至逐比特比较。

九、Verilator 与综合工具完全不是一回事

这是初学者非常容易混淆的一点。

Verilator 做的是:

SystemVerilog
    ↓
C++模型
    ↓
软件仿真

而 Vivado、Quartus 等 FPGA 工具做的是:

SystemVerilog
    ↓
综合
    ↓
逻辑网表
    ↓
布局布线
    ↓
配置文件
    ↓
FPGA

所以 Verilator 不负责把设计真正变成 FPGA 内部的 LUT、寄存器、RAM 和布线资源。

它主要回答:

逻辑功能是否正确?

综合和实现工具则进一步回答:

这段逻辑如何映射成真实硬件?

以及:

它能否在目标时钟频率下正确运行?

二者是互补关系,而不是替代关系。

十、Verilator 也不是万能的

Verilator 的优势集中在:

RTL 功能验证、高速仿真、大规模回归、软硬件协同验证以及自动化测试。

但如果问题变成:

布线延迟是多少?

某条路径是否违反建立时间?

实际 FPGA 内部的时钟偏斜是多少?

特定工艺库中的门延迟是多少?

那么这些已经进入综合后时序分析或者门级仿真的范畴。

这类问题通常需要 FPGA 厂商工具、静态时序分析工具或者专业 EDA 仿真器完成。

因此,更合理的工程流程不是“Verilator 还是传统仿真器二选一”,而是:

RTL设计
   ↓
Lint
   ↓
Verilator功能验证
   ↓
自动化回归测试
   ↓
综合
   ↓
静态时序分析
   ↓
布局布线
   ↓
必要时进行更精细仿真
   ↓
真实硬件验证

不同工具分别解决不同层面的问题。

十一、现代芯片开发为什么越来越重视 Verilator

数字设计规模不断扩大以后,传统的“写 RTL—跑一下仿真—人工看波形”已经越来越难以支撑复杂系统开发。

一个现代数字系统可能包含:

数百个模块;

数万乃至数百万行代码;

大量总线接口;

多个处理器;

复杂状态机;

大量并发逻辑。

此时真正重要的已经不是“能不能仿真”,而是:

能不能自动、快速、重复地验证。

Verilator 的价值正体现在这里。

它把 RTL 转换成高速软件模型,使硬件设计可以借用现代软件行业已经成熟几十年的工程体系,包括自动化构建、单元测试、持续集成、随机测试和大规模回归测试。

从这个角度来看,Verilator 并不仅仅是一款 HDL 仿真器。

它实际上代表了一种越来越重要的数字硬件开发理念:

把硬件描述语言当成可以持续编译、持续测试和持续验证的工程代码。

当数字系统规模越来越大,这种方法的重要性也会越来越高。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Are you human? Please solve:Captcha