一生一芯 C1 支持RV32IM的NEMU

发布于:2025-06-27 ⋅ 阅读:(10) ⋅ 点赞:(0)
[wushf@wushf-server ysyx-workbench]$ cd am-kernels/tests/cpu-tests/
[wushf@wushf-server cpu-tests]$ make ARCH=$ISA-nemu ALL=dummy run
# Building dummy-run [-nemu]
/home/wushf/Desktop/ysyx-workbench/abstract-machine/Makefile:30: *** Expected $ARCH in {loongarch32r-nemu minirv-logisim minirv-nemu minirv-npc mips32-nemu native riscv32-nemu riscv32e-nemu riscv32e-npc riscv64-nemu spike x86-nemu x86-qemu x86_64-qemu}, Got "-nemu".  Stop.
test list [1 item(s)]: dummy
[         dummy] ***FAIL***

$ISA填啥我是不确定的,但是好在输错了会提示参数列表:

  • loongarch32r-nemu minirv-logisim minirv-nemu minirv-npc mips32-nemu native riscv32-nemu riscv32e-nemu riscv32e-npc riscv64-nemu spike x86-nemu x86-qemu x86_64-qemu

这些参数取自文件名:

  • abstract-machine/scripts/*.mk

编译RISCV32

现在编译一下riscv32

[wushf@wushf-server cpu-tests]$ make ARCH=riscv32-nemu ALL=dummy run
# Building dummy-run [riscv32-nemu]
# Building am-archive [riscv32-nemu]
+ CC src/platform/nemu/trm.c
In file included from /usr/riscv64-linux-gnu/usr/include/features.h:535,
                 from /usr/riscv64-linux-gnu/usr/include/bits/libc-header-start.h:33,
                 from /usr/riscv64-linux-gnu/usr/include/stdint.h:26,
                 from /usr/lib/gcc/riscv64-linux-gnu/14.2.0/include/stdint.h:9,
                 from /home/wushf/Desktop/ysyx-workbench/abstract-machine/am/include/am.h:4,
                 from /home/wushf/Desktop/ysyx-workbench/abstract-machine/am/src/platform/nemu/trm.c:1:
/usr/riscv64-linux-gnu/usr/include/gnu/stubs.h:8:11: fatal error: gnu/stubs-ilp32.h: No such file or directory
    8 | # include <gnu/stubs-ilp32.h>
      |           ^~~~~~~~~~~~~~~~~~~
compilation terminated.
make[2]: *** [/home/wushf/Desktop/ysyx-workbench/abstract-machine/Makefile:101: /home/wushf/Desktop/ysyx-workbench/abstract-machine/am/build/riscv32-nemu/src/platform/nemu/trm.o] Error 1
make[1]: *** [/home/wushf/Desktop/ysyx-workbench/abstract-machine/Makefile:132: /home/wushf/Desktop/ysyx-workbench/abstract-machine/am/build/am-riscv32-nemu.a] Error 2
test list [1 item(s)]: dummy
[         dummy] ***FAIL***

按照手册的要求注释掉头文件:/usr/riscv64-linux-gnu/usr/include/gnu/stubs.h

现在停在了:

invalid opcode(PC = 0x80000000):
        13 04 00 00 17 91 00 00 ...
        00000413 00009117...
There are two cases which will trigger this unexpected exception:
1. The instruction at PC = 0x80000000 is not implemented.
2. Something is implemented incorrectly.
Find this PC(0x80000000) in the disassembling result to distinguish which case it is.

根据手册:

这是因为你还没有实现<font style="color:rgb(78, 110, 142);background-color:rgb(238, 238, 238);">0x00000413</font>的指令, 因此, 你需要开始在NEMU中添加指令了.

要实现哪些指令才能让<font style="color:rgb(78, 110, 142);background-color:rgb(238, 238, 238);">dummy</font>在NEMU中运行起来呢? 答案就在其反汇编结果(<font style="color:rgb(78, 110, 142);background-color:rgb(238, 238, 238);">am-kernels/tests/cpu-tests/build/dummy-$ISA-nemu.txt</font>)中

Disassembly of section .text:

80000000 <_start>:
80000000:	00000413          	li	s0,0
80000004:	00009117          	auipc	sp,0x9
80000008:	ffc10113          	addi	sp,sp,-4 # 80009000 <_end>
8000000c:	00c000ef          	jal	80000018 <_trm_init>

80000010 <main>:
80000010:	00000513          	li	a0,0
80000014:	00008067          	ret

80000018 <_trm_init>:
80000018:	ff010113          	addi	sp,sp,-16
8000001c:	00000517          	auipc	a0,0x0
80000020:	01c50513          	addi	a0,a0,28 # 80000038 <_etext>
80000024:	00112623          	sw	ra,12(sp)
80000028:	fe9ff0ef          	jal	80000010 <main>
8000002c:	00050513          	mv	a0,a0
80000030:	00100073          	ebreak
80000034:	0000006f          	j	80000034 <_trm_init+0x1c>

在手册中搜索li指令,可以找到:

根据之前分析INSTPAT,是可以判断rd是否为0的,目前还看不懂啥是HINT,暂时当作addi处理。

addi

搜索一下addi

对比26页的表格,发现属于I-Type

捋清楚宏定义的作用,手册指明了语义,实现起来还是比较简单的:

  • INSTPAT("??????? ????? ??? ??000 ????? 0010011", addi   , I, R(rd) = imm + src1);

jal

但是目前没有提供J-Type,需要看一下是否可以复用已有函数,有可能是官方等待我们实现的。

J-Type的格式只有immrd

为什么imm有效位置是[1:20],第0位去哪了?并且文档中说jal命令的地址空间是正负1M

因为偏移量都是以2字节为倍数。偶数字节的第0位一定是0

所以imm其实有21位,只是最低位始终为0,最多能表示2M的空间,补码形式就是正负1M

内存地址通常是以4为倍数或以8为倍数,为什么要约定以2为倍数?

  • 推测是为了保持对16位的支持,并允许向后兼容。
#define immJ() do { \
  *imm = SEXT( \
    ((BITS(i, 31, 31) << 20) | \
     (BITS(i, 19, 12) << 12) | \
     (BITS(i, 20, 20) << 11) | \
     (BITS(i, 30, 21) << 1)), 21); \
} while (0)

按照手册的指示实现:

  • INSTPAT("??????? ????? ??? ????? ????? 1101111", jal    , J, R(rd) = s->snpc; s->dnpc = s->pc + imm);

ret

ret还是一个伪指令。

dummy-riscv32-nemu.txt中的ret位于函数结尾的位置,猜测是作为过程返回。

那么会被编码为jalr指令。

jalr指令的细节就在上面这一段,是I-Type,指令操作是:

  1. imm+rs1
  2. 将结果的最低有效位强制置零
  3. 下一条指令写入rd

funct3部分长度是3位,值是0

opcode部分的值在手册末尾的附录中:

sw

在文档的Page34页:

sw的作用是把rs2的值写入到内存rs1+imm的位置。

源代码中已经实现了sb

INSTPAT("??????? ????? ??? ??000 ????? 0100011", sb     , S, Mw(src1 + imm, 1, src2));

写入宽度为1字节。

sw32位,也就是4字节。

Page609页给出了各部分的细节:

mv是伪指令,会被编译成addiaddi已经在上文实现。

j也是伪指令,会被编译成jaljal已经在上文实现。

Welcome to riscv32-NEMU!
For help, type "help"
(nemu) c
[src/cpu/cpu-exec.c:137 cpu_exec] nemu: HIT BAD TRAP at pc = 0x80000030
[src/cpu/cpu-exec.c:97 statistic] host time spent = 79 us
[src/cpu/cpu-exec.c:98 statistic] total guest instructions = 11
[src/cpu/cpu-exec.c:100 statistic] simulation frequency = 139240 inst/s
(nemu) 

运行更多程序

尝试运行addmake ARCH=riscv32-nemu ALL=add run

发现需要实现更多指令。

beqz

beqz是伪指令,会被编译为beq

beq的指令语义在Page32

格式要求在Page609

目前还没有实现B-Type,实现一下:

#define immB()                     \
  do {                             \
    *imm = SEXT(                   \
        ((BITS(i, 31, 31) << 12) | \
         (BITS(i, 30, 25) << 5) |  \
         (BITS(i, 11, 8) << 1) |   \
         (BITS(i, 7, 7) << 11)),   \
        13);                       \
  } while (0)

立即数的每一位离散在指令中,拼凑有个技巧:

  • 手册已经给出了在指令中的位置
  • 手册已经给出了在立即数中的位置

<<右侧取立即数的位置的最低位。表述有点麻烦,RTFSC!

可以顺手把bne也实现出来。

INSTPAT("??????? ????? ????? 000 ????? 1100011", beq, B, if (src1 == src2) s->dnpc = s->pc + imm);
INSTPAT("??????? ????? ????? 001 ????? 1100011", bne, B, if (src1 != src2) s->dnpc = s->pc + imm);

lw

nemu已经给出了lbu指令,可以参考上面的sw实现lw

先考虑实现下lblblbu的区别是是否符号扩展。

u后缀的表示零扩展,也就是无符号扩展。

nemu自带的lbu是直接从内存中取出一个字节,传递的过程是以无符号的形式。

  • INSTPAT("???????????? ??? ??100 ????? 0000011", lbu, I, R(rd) = Mr(src1 + imm, 1));

通过之前的分析,宏定义SEXT的作用就是有符号扩展,用这个包裹一下就有了lb

  • INSTPAT("???????????? ??? ??000 ????? 0000011", lb, I, R(rd) = SEXT(Mr(src1 + imm, 1), 8));

另外几条内存加载指令也可以照着葫芦画瓢,一并实现:

INSTPAT("???????????? ??? ??000 ????? 0000011", lb, I, R(rd) = SEXT(Mr(src1 + imm, 1), 8));
INSTPAT("???????????? ??? ??100 ????? 0000011", lbu, I, R(rd) = Mr(src1 + imm, 1));
INSTPAT("???????????? ??? ??001 ????? 0000011", lh, I, R(rd) = SEXT(Mr(src1 + imm, 2), 16));
INSTPAT("???????????? ??? ??101 ????? 0000011", lhu, I, R(rd) = Mr(src1 + imm, 2));
INSTPAT("???????????? ??? ??010 ????? 0000011", lw, I, R(rd) = Mr(src1 + imm, 4));

add/sub

add属于R-Type,还需要实现R-Type的立即数取值。

发现R-Type的指令结构中没有立即数,无需实现immR

可以顺手实现sub指令:

INSTPAT("0000000 ????? ????? 000 ????? 0110011", add, R, R(rd) = src1 + src2);
INSTPAT("0100000 ????? ????? 000 ????? 0110011", sub, R, R(rd) = src1 - src2);

seqz/sltiu

seqz是伪指令,会被编译成sltiu

参考上面lhu/lhlb/lbuu的作用是标记是否为无符号数。

INSTPAT("???????????? ????? 010 ????? 0010011", slti, I, R(rd) = ((sword_t)src1 < (sword_t)imm) ? 1 : 0);
INSTPAT("???????????? ????? 011 ????? 0010011", sltiu, I, R(rd) = (src1 < imm ? 1 : 0));

现在有个问题是,imm是负号扩展后得到的,到底是有符号还是无符号,比较是否正确。

现在的解决方案是与上0xfff,恢复到扩展之前。

INSTPAT("???????????? ????? 010 ????? 0010011", slti, I, R(rd) = ((sword_t)src1 < (sword_t)imm) ? 1 : 0);
INSTPAT("???????????? ????? 011 ????? 0010011", sltiu, I, R(rd) = (src1 < (imm & 0xfff) ? 1 : 0));

更快地实现更多指令

RISCV官方手册搜索起来有点缓慢。

可以去看《RISC-V 开放架构设计之道》:

在末尾提供了指令列表,明确地给出了每个指令的格式和动作。

div/rem

除法和取余数,分母是零的情况,在书中的指令列表中没有给出。

在手册的Page70

  • 有符号将rd赋值成-1
  • 无符号不存在溢出。

BugFix

之前版本的笔记中,将jal实现为:

  • INSTPAT("??????? ????? ??? ????? ????? 1101111", jal    , J, R(rd) = s->snpc; s->dnpc = s->snpc + imm);

导致没有通过dummy测试。

原因是snpc在进入指令匹配的时候,已经是pc+4了,应该取s->snpc + imm - 4s->pc + imm


网站公告

今日签到

点亮在社区的每一天
去签到