RTL difftest搭建
difftest在测试集中可以起到十分重要的作用,可以快速找到发生问题的指令和pc寄存器地址。在nemu作为dut,参考其他模拟器(比如spike)的功能中,大部分代码已经完成,我们只需要完成寄存器的比对即可。但在RTL中重新实现这一功能或者类似功能时,我们需要完成更多函数,但大体的框架已经完成,我们可以参考nemu中已有的difftest功能去补全函数。
讲义中已经提示我们需要完成哪些函数:
// 在DUT host memory的`buf`和REF guest memory的`addr`之间拷贝`n`字节, // `direction`指定拷贝的方向, `DIFFTEST_TO_DUT`表示往DUT拷贝, `DIFFTEST_TO_REF`表示往REF拷贝 void difftest_memcpy(paddr_t addr, void *buf, size_t n, bool direction); // `direction`为`DIFFTEST_TO_DUT`时, 获取REF的寄存器状态到`dut`; // `direction`为`DIFFTEST_TO_REF`时, 设置REF的寄存器状态为`dut`; void difftest_regcpy(void *dut, bool direction); // 让REF执行`n`条指令 void difftest_exec(uint64_t n); // 初始化REF的DiffTest功能 void difftest_init();
我们要完成的ref.c的函数。我们可以以spike作为参考,spike的代码位于/nemu/tools/spike-diff/difftest.cc 。但是这只是一部分,difftest.cc只是提供了工具函数,主要的逻辑则位于dut.c中。除过ref.c,我们还需要修改npc的函数。其实这二者是对应的:
difftest.cc |
ref.c |
dut.c | main.cpp |
ref_difftest_memcpy(RESET_VECTOR, guest_to_host(RESET_VECTOR), img_size, DIFFTEST_TO_REF);
ref_difftest_regcpy(&cpu, DIFFTEST_TO_REF);
随后,在正常情况下,dut每执行一条指令,就暂停下来让ref也执行一条指令,然后对比二者的寄存器值是否一一对应。样例dut.c中多了很多逻辑,因为有的指令可能需要跳过,我们的npc与nemu对比时暂无此情况,不需要进行处理。
------------------------------
在清楚思路以后,我们可以仿照difftest.cc,完成ref.c的三个函数:
首先需要注意,在nemu为dut时,DIFFTEST_TO_REF是从nemu到spike,我们实现时,DIFFTEST_TO_REF就是从npc到nemu了。反之也是同理。
regcpy的基本功能就是根据方向,决定是把nemu的寄存器值给npc,还是把npc的值给nemu。内部交换值是使用memcpy()还是直接cpu.gpr=xxx均可。我自己是选择的memcpy。
difftest_memcpy同样。根据方向,使用paddr_write或者paddr_read()即可。注意:传入的dut是void*,我自己是转换word_t *后用下标赋值的,这里隐含了小端序。如果你的传值不一样,最好小心这一点,可以在传值后原地检查一次nemu的内存,一方面可以看赋值是否成功,一方面可以检查值是否正确。比如0x0000 0413这样一条指令,其实0x00位是13,不是00,从左到右内存地址递减。小端序的内存最低字节在最低位。不过c语言内部会处理,我们自己做RTL时选择什么内存模型会有影响。
--------------------------------
npc的main里面除了调用这几个函数,还需要自己实现两个函数:get_regs,负责获取当前的通用寄存器和pc值。check_regs,负责对比寄存器值。
关于如何获取npc里通用寄存器的值,有两种主要的方法:一种是在reg元件里利用DPI-C机制声明函数给C++调用,一种是根据编译出的头文件直接查找寄存器的名字调用。声明DPI-C函数的代码如下:
export "DPI-C" function get_reg_value; function int get_reg_value(input int index); begin if (index < 2**ADDR_WIDTH) begin get_reg_value = rf[index]; end else begin $display("wrong reg index, %0d",index); get_reg_value = -1; end end endfunction
同时npc的C++部分也要先声明再使用。此外有很多注意事项:
1. 声明时需要extern "C";
2.verilog的function参数不能出现output,并且用export声明函数时不要写参数。
3.verilog的function和c++侧的参数类型声明要对应:
Verilog byte 对应 C char Verilog int 对应 C int Verilog shortint 对应 C short Verilog longint 对应 C long long Verilog real 对应 C double Verilog bit 和 logic 对应 C int(通常用于单个位)
有时候有/无符号和logic/bit等的对应会造成额外麻烦。需要注意的是,Verilog中的byte
是一个8位的有符号整数,与C语言中的char
在大多数平台上是等价的(也是8位有符号整数)
我最终没有选择这种方法,因为使用时编译器反复报错,不管是否使用return ,怎么修改函数声明,或者是修改内部函数,都在出问题。最终选择了方法二。
-------------------------------------------
方法二只需要根据编译出的头文件寻找元件名:在我们编译出的obj文件里,会有Vtop.h Vtop__dpi.h和Vtop___xxxroot.h。第一个头文件表示顶层元件的一些功能性函数接口,比如eval() 以及DPI-C函数。 第二个是dpi-c机制的接口。第三个root.h就是从顶层元件开始所有连接元件内变量的标记,reg型和wire型都有:
如图,变量名中间的DOT就相当于verilog语言里的"." ,通过这个我们就可以通过顶层元件访问到连接的元件,一层层向内。我的通用寄存器是顶层元件->执行器->寄存器,所以我的寄存器变量名就叫 "top__DOT__exec_unit__DOT__RegFile__DOT__rf',访问方法也很简单,像数组一样直接下标就可以访问。但是,在C++元件里要访问,首先需要引用头文件root.h,其次需要用top访问,讲义里也提到,对于我而言,想要访问这个变量,就需要写成:
top->rootp->top__DOT__exec_unit__DOT__RegFile__DOT__rf
这个寄存器变量不能直接用top->访问,中间还需要rootp。只要查阅了两个头文件就可以看明白。
--------------------
在搞清楚怎么获取寄存器的值以后,剩下的事就很简单了。在开始前初始化,把npc的代码img和寄存器状态给nemu。在每个时钟周期后:
获取npc寄存器值
获取nemu寄存器值
对比
不过还有一点需要注意:如果你的寄存器使用了时序逻辑+非阻塞赋值,那么寄存器赋值会晚一个时钟周期,在对比时需要注意。在这里我没有写出让nemu运行一条指令这个函数应该在哪里,因为个人情况可能不同。