从这篇开始,正式步入 NutShell RTL 实现的学习,
我们将以软件模拟硬件实现的形式去验证 RTL(Register Table Layer),借助之前文章所提到的模拟工具 Verilator 即可以达成模拟硬件运行的目的,这样一来学习成本就会低很多,无需一遍遍的烧录 Bit 流到 FPGA 去验证;

软件架构

那我们就先以最简单的模拟架构为起始点开始学习,
先不考虑 Difftest 差分验证,这点后面再看,因为 Difftest 本身并不是 RTL 的一部分,它只是用来监测 RTL 运转的准确性,
我们先从实现的核心点出发,看看 Verilator + RTL 软件实现架构图,

根据以上整体软件框图,提及两个重点,

  • RAM 完全是 Verilator 通过 mmap 系统调用开辟的一段内存空间,随后通过 DPI-C 能力将 C++ 侧的接口直接显露给到 Verilog 侧调用;
  • 所以 RTL 最终实现的 Verilog 代码均会通过 Verilator 翻译为 C++ 数据结构包含进工程并参与编译;

软件流程

相信看了上面的整体架构图,我们在大脑中大致可以构建出硬件模拟的运行流程,
不妨这里再来一起过一遍软件运行流程,

模拟软件入口

基于 Verilator 主要是赋予翻译 Verilog 的能力,而具体软件的编译还需要开发者自己来完成,
NutShell Simulation 的入口位置及具体函数实现如下,

// difftest/src/test/csrc/common/main.cpp

#define DUT_MODEL Emulator

int main(int argc, const char *argv[]) {
  ...
  auto emu = new DUT_MODEL(argc, argv);
  while (!emu->is_finished()) {
    emu->tick();
  }
  ...
}

NutShell 模拟器软件架构以 Emulator 类为核心展开,
在如上 main 函数中,先是 new 了一个 Emulator 类,随后循环调用其 tick 方法;

先来看一下 Emulator 类的构造函数做了哪些事情,

// difftest/src/test/csrc/verilator/emu.cpp

Emulator::Emulator(int argc, const char *argv[]):
  dut_ptr(new VSimTop),
  cycles(0), trapCode(STATE_RUNNING), elapsed_time(uptime())
{
  ...
  args = parse_args(argc, argv);
  ...
  // init flash
  init_flash(args.flash_bin);
  ...
  // init core
  reset_ncycles(10);
  ...
  init_ram(args.image, ram_size);
  ...
}
  • 硬件 RTL 引入dut_ptr(new VSimTop),这里是重中之重,Verilaor 将 RTL 翻译转换成了 VSimTop 类,那么 Verilator 的能力就会在这里有所体现;
  • 参数解析,可执行文件的传递、日志控制、cycles 最大值限制或 difftest 环境的指定等会在这里解析和配置;
  • 初始化 flash,看工程中好像没有实际引用,暂不关注;
  • 复位硬件,这里会拉低 Reset 信号给硬件做个复位;
  • 初始化 ram,通过 mmap 系统调用分配一段内存空间,可执行文件会被写入到内存中,而后将这段内存空间作为 RTL 的 DDR;

构造完成后,步入到上面提到的 tick 方法,

// difftest/src/test/csrc/verilator/emu.cpp

int Emulator::tick() {
  ...
  single_cycle();
  ...
  trapCode = difftest_nstep(step);
  ...
}
  • single_cycle 的作用是驱动整个 RTL,它通过不停地将dut_ptr->clock时钟信号置为高、低电平来达到驱动的目的;
  • 通过 difftest 来做 RTL 执行时的实时差分验证;