NSIM:基于 NEMU 框架搭建 NPC 仿真环境
本文最后更新于 179 天前,其中的信息可能已经有所发展或是发生改变。

一生一芯的学习中,写完 NPC 处理器之后,需要搭建仿真环境。第六期一生一芯讲义首先就提出为 NPC 搭建 sdb 调试器。在此之前呢,学生都已经完成了 PA 1 实验,即 NEMU,在 NEMU 中已经搭建了所需要的 sdb。所以,学生的普遍做法是想移植 NEMU,为 NPC 搭建仿真环境。另外一种情况是,verilator 仿真使用的是 C++ 代码,而 NEMU 使用的是 C 代码,故想直接重新写一个 C++ 版本的仿真环境。以上这两种做法无疑是繁琐的工作,增加出错和调试的概率。本文的做法是,直接在原有的 NEMU 的代码框架基础上,添加对 NPC 仿真的适配代码,以可以在 NEMU 上运行 NPC 的仿真,并且使用 NEMU 已有的调试工具。该做法只需要添加 250+ 行左右的代码,就能完成仿真,而且执行的正确性由原有实现的 NEMU 保证。

1 引入

1.1 动机

初学阶段实现的 NPC,不带内存,不带外设,需要仿真环境来模拟实现这些部分的行为。NEMU 是一个支持多种体系结构的指令模拟,也实现了对内存、外设以及异常的模拟。已有的两种做法:1. 移植 NEMU 代码。2. 基于 NEMU 的实现思路重写仿真环境。

如果采用移植的方式来完成 NPC 的仿真环境的搭建,需要移除 NEMU 中原有支持其他指令集的部分(NPC 只支持一种指令集)。删除代码的过程就有可能引入很多新的错误(删多了缺少标识符的 error,删少了,就是未使用的标识符的 warning);NEMUC 代码和仿真的 C++ 代码如何友好地合在一起编译,需要一些工作来完成脚本的编写;此外,仍然需要考虑,如何友好地引入 DPI-C?如果对 NEMU 的代码没有深刻的理解,后面的调试也会有一定的阻力。

如果选择重构,可以比较自由地编写自己所需要的部分,甚至可以直接使用 C++,这样和 verilator 结合非常的方便。前期可能感觉到,重构才是工作量最小的选择,而且自己也非常清楚的了解自己的仿真是如何工作的,也方便后面的调试。一生一芯的学习,学生们来自各个专业,可能对软件架构不是特别的了解,习惯性地基于过程编程。后期会发现,NEMU 的模块抽象是实现地非常好,也可以根据实际的使用需求,对这些模块的功能的开和关。这些内容,对后面的调试选择性关闭功能提供了便利。当然,前期可能比较难有这样的编程考虑,后期只有对这些部分进行重构了,工作量反而上升了。

1.2 NSIM

我们将基于 NEMU 框架开发搭建的 NPC 仿真环境命名为 NSIMNSIM 提供 NPC 的指令执行接口,供 NEMU 的处理器执行模块使用。此外,提供仿真环境 DPI-C 的接口,供 NEMU 的寄存器堆、内存获取必要的指令执行信息。该方式实现有以下优势:

  • 使用原有的 NEMU 各种功能,包括 difftest,而无需对这些代码进行修改(配置文件需要修改)。
  • 只需添加少量的仿真环境接口,新增代码只有 200+ SLoC。

image-20241103231715024

2 NSIM 架构

先来看看 NEMU 的架构:

image-20241103180656644

既然 NSIM 是基于 NEMU 实现的,那我们讲讲 NEMU 处理器执行这一部分是怎么实现的。

2.1 NEMU 的指令执行

NEMU 的执行模块的架构如下:

nemu

每一个 ISA 都有一套自己指令集相关的寄存器堆,并且实现自己的指令执行引擎(Exec Engine),完成指令集约束的取指和执行操作,会对寄存器或者是内存状态进行修改。执行引擎部分对外提供 isa_exec_once 的接口。Difftest 模块提供架构相关的寄存器对比功能,对外提供 isa_difftest_checkregs 接口。NEMU 的执行模块根据配置需求,选择对应的 ISA 模块的接口。

2.2 NPC ISA

类似的,我们也可以实现一个 NPC ISA,如下图所示:

npc

其中 Difftest 和其他 ISA 相同,不同的事执行引擎。在仿真环境中,NPC 会提供激励执行的函数接口,但是并不表示执行完一次,就表示一次指令执行完成。此时需要 DPI-C 来通知一条指令的执行完成。由于 DPI-C 和仿真强相关,我们不考虑在这个位置实现 DPI-C

这样,我们的 NSIM 的仿真架构图如下:

image-20241103181248804

2.3 NSIM 的思路

NSIM 的实现的构件图如下:

nsim

NEMU 的原有框架中,我们仅提供回调函数供仿真环境调用。这部分仍然由 C 语言实现,参与到 NEMU 的编译中。仿真环境由 C++ 语言实现,当指令执行完成后通过回调函数更新 NEMU 中的执行状态。这部分参与到 verilator 的编译中,最终生成一个归档(archives)供 NEMU 链接使用。

这种做法解耦了仿真环境和 NEMU,两者编译互不影响。

3 NSIM 实现

3.1 初始化

第一次尝试,为了不影响自己原有实现的 NEMU 代码,故复制了一份原有代码的基础上进行修改。目录名命名为 nsim。使用命令行工具,将目录中所有带 nemu 以及 NEMU 的单词全部替换为 nsim 或者 NSIM

这部分做法问 ChatGPT 也非常有用,几行命令就可以解决而且不会改漏。

添加系统 PATH 路径,新增 NSIM_HOME

Kconfig 中要将 ISA_npc 标记为 RV64ISA64;头文件 isa_def.h 中添加 ISA_npc 的识别;monitor.c 添加对 CONFIG_ISAnpc 的情况识别,在这里,直接识别成 riscv64

3.2 NPC ISA 实现

NEMU 的 ISA 中,有以下几个部分需要实现:

  • ISA 相关的头文件:定义 ISA 相关的寄存器信息。
  • init.c:初始化相关寄存器,加载默认程序。
  • inst.c:指令执行相关的实现,按照 2.2 节的描述,这里我们需要实现通知指令执行完成的回调函数。
  • reg.c:寄存器相关的操作。
  • dut.cdifftest的测试设备相关。
  • nmem.c:接受 NPC 的访问内存请求。

3.3 NPC 仿真实现

这部分代码不妨实现在 $(NSIM_HOME)/src/verilator 的目录中。(亦可以其他目录)需要修改 .gitignore 丢弃一些编译中间的代码。

首先需要的是 NPC 的 RTL 代码,可以将 Chisel 生成的 RTL 代码直接连接到 NSIM 目录下。使用 Verilator 将这些 RTL 代码生成 C++ 代码文件,方便后面编程的语言服务。按照仿真需求,实现以下基本的函数:

  • step():执行一个时钟周期的函数。
  • restart():重置 NPC 的函数。NEMU 中的 init.c 调用该函数可以完成对处理器的初始化。
  • exec_once():执行一条指令。
  • 以及其他各个 DPI-C 调用函数的实现。

如何知道 NPC 执行完了一条指令?

在 2.2 节中我们提到,NPC 通过 DPI-C 调用来通知指令的完成。然而,在实际的实现中,DPI-C 函数可能有很多个,而且执行顺序不太能自己确定(跟信号传递的先后顺序有关),所以,通知指令完成的调用不一定是真正意义上的“指令完成”。

为了解决这个问题,我们设置一个全局变量,表示一条指令的结束。当发生指令完成的 DPI-C 调用时,修改这个变量为 true,表示已经完成。然后结束这个时钟周期的仿真之后,检查这个变量。如果为 true,这个时候才是表示一条指令真正意义上执行完成了(所有的 NPC 的状态已经更新)。此时,再触发 NEMU 的回调函数,更新相应执行信息。类似于异步和同步了。

DPI-C 的一些类型对应表

SystemVerilog C++
logic svLogic
int int
longint long long
byte char
logic [63:0] regs[] svOpenArrayHandler

对于最后寄存器数组的读取,参考代码如下:

仿真部分:

void output_gprs(const svOpenArrayHandle gprs) {
  update_gprs((uint64_t*)(((VerilatedDpiOpenVar *)gprs)->datap()));
}

SV 部分:

module(
  // ...
);
  // ...
  import "DPI-C" function void output_gprs(output logic [63:0] gprs[]);
  // ...
endmodule

3.4 编译链接

修改原有 NEMU 的 Makefile 脚本,以支持一下编译工作。

NEMU 仍然使用原有的 makefile 脚本。对于仿真代码,使用 verilator 进行编译。此处我们并不生成可执行文件,而是生成一个归档供后续 NEMU 链接使用。

如何生成归档文件?

RTFM,少 --exe 编译参数就可以了。同时,verilator 提供了一个 --lib-create 的选项,通过这个选项,可以生成一个 .a 静态链接文件和一个 .so 的动态链接文件。

最后将 .a 静态链接文件和 NEMU 生成的二进制对象文件一起链接即可。

如何开关波形?

NPC 仿真少不了使用波形进行 Debug 的时候。但是波形追踪的开关是在 NEMU 中设置,verilator 编译看不到这个变量,就非常的棘手,因为根据 2.3 节的描述,两者相互独立。

为了方便编译,我们还是会在 NEMU 的 Makefile 脚本中调用 verialtor 进行编译。Makefile 还是可以看到自己配置的追踪开关的,在这里根据追踪的开关选项向 verilator 传递 --CFLAGS 运行参数,例如 -DCONFIG_WTRACE

这里还有个小坑,--CFLAGS 必须在 verilator 命令的合适位置进行设置。

4 总结

通过阅读了 Verilator 的全局 Makefile 脚本,了解了 Verilator 编译当中的细节,为 NSIM 成功实现提供了可能。NSIM 便利了仿真环境的开发,减少了出错的可能性,也利于仿真环境代码的维护(基础代码仅 200 行左右);同时,可以使用 NEMU 上的各种功能,新功能的开发也可以直接在 NEMU 上开发,不需要对仿真环境单独进行移植。差分测试的时候,NSIM 也可以直接使用 NEMU 已有的 SpikeQEMU 接口进行测试。

NSIM 也留下了一些没有完成的工作。例如,Makefile 任务的前后依赖关系,并使其检测到 NPC 的 RTL 发生变化时,完成 RTL 的编译相关任务。

NSIMNPC 以及 NEMU 三者是相互独立又具有联系的,但是目前没有将 NPC 的 Makefile 和 NSIM 结合起来。当 NPC 的仿真代码发生变化的时候,仍然需要自己手动编译一次。

此外,NEMU 作为参考的差分测试并没有适配在其中,需要后面的完善。

参考文献

一生一芯 NEMU 项目

一生一芯讲义

verilator 手册

systemverilog dpi-c turtorial

评论

  1. 小张
    9 月前
    2024-8-09 16:48:08

    博主你好,我是第六期一生一芯的同学。最近在将npc接入验证环境的时候遇到了一些,问题。我现在知道am可以通过不同的编译方式,将测试程序调试到对应的platform下。无奈本人能力不够,始终不能理解,编译出来的riscv指令是如何直接放入到仿真环境的代码下的。特此询问博主,应该看哪一部分的代码,来了解这个过程。谢谢。

    • 博主
      小张
      9 月前
      2024-8-15 16:27:29

      抱歉这么晚回复(因为我这边没有配置邮件通知)。

      简化一下您的问题:经过 AM 编译的文件是如何加载到 NPC 的仿真环境的?
      其实这个过程和 NEMU 是相同的,仿真环境也会提供一个 「内存数组」,在环境初始化的时候,通过 load_img 函数将要运行的程序拷贝到内存数组中。这部分可以看看 NEMU 的初始化代码。

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇