从这篇开始,正式步入 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 执行时的实时差分验证;