0%

113:一生一芯F6

拖了大半个月,终于重新开始了一生一芯计划的学习。接下来要完成的部分是,一个有趣的项目

F6 功能完备的迷你RISC-V处理器

我们先前实现的简单的ISA只能实现一些简单的命令,但是并不具有完备性,导致很多功能我们难以正常实现,所以这一次我们要实现的是一个功能完备的RISC-V处理器,不过在此之前,我们需要更加深入的了解它。

迷你RISC-V指令集

在了解RISC-V指令集的细节之后,我们可以指定一个自己的minirv的ISA的规范

  • PC初值为0
  • GPR数量和RV32E一致(16)
  • 支持以下八条指令addaddiluilwlbuswsbjalr
  • 其他的ISA细节参考RV32I

F6.1.1 RTFM

查阅RISC-V手册的目录, 你发现RV32I在哪一章进行介绍? 尝试在该章节中查阅RV32I的相关内容, 回答下列问题:

  1. PC寄存器的位宽是多少?
  2. GPR共有多少个? 每个GPR的位宽是多少?
  3. R[0]和sISA的R[0]有什么不同之处?
  4. 指令编码的位宽是多少? 指令有多少种基本格式?
  5. 在指令的基本格式中, 需要多少位来表示一个GPR? 为什么?
  6. add指令的格式具体是什么?
  7. 还有一种基础指令集称为RV32E, 它和RV32I有什么不同?

关于RV32I的介绍在第二章中可以看到

image

接下来按顺序回答这些问题:

  1. 在RV32I中PC的位宽是32bit
  2. 共有32个GPR,每个位宽均为32bit
  3. R[0]中存放的数值恒为0,在我们先前的sISA中的R[0]只是我们的第一个寄存器
  4. 指令编码的位宽是32,一共有四种指令格式
  5. 在指令的基本格式中需要5位来表示一个GPR,因为一共有32个寄存器
  6. add指令是一种R-Type
  7. RV32E是RV32I的精简版本,例如寄存器的数量被减少到了16个

只有两条指令的minirv处理器

我们先从两条指令开始实现addijalr

F6.2.1 RTFM(2)

查阅RISC-V手册, 找到addi指令的编码和相应的功能描述. 在第34章RV32/64G Instruction Set Listings中有一些指令表, 可以帮助你查阅addi指令的编码.

image

我们可以在这里找到对addi的说明。

F6.2.2 RTFM(3)

为了了解RISC-V对存储器的若干约定, 你需要阅读RISC-V手册第1.4节的第一段, 从ISA的层面了解存储器的规格, 尤其是宽度的定义.

根据我们先前的ISA规则,我们的地址空间大小应该是一个按字节寻址地址空间,大小为232.

F6.2.3 RTFM(4)

查阅RISC-V手册, 找到jalr指令的编码和相应的功能描述.

image

F6.2.4 实现两条指令的minirv处理器

理解addijalr指令的功能后, 根据你之前设计sISA处理器的经验, 尝试设计一个支持这两条RISC-V指令的处理器.

由于GPR需要进行较多的连线工作, 为了减轻大家的负担, 我们准备了一个预先完成大量连线工作的GPR子模块. 下载后, 通过Logisim打开文件GPR.circ, 即可看到GPR的电路设计, 你可以整体选择这个电路后, 通过复制和粘贴将其加入到你工程中. 不过, 这个电路并没有实现GPR的完整功能, 你需要根据你对GPR的理解完善它.

为了帮助你对处理器进行简单的测试, 我们准备了如下测试程序. 在下面的汇编指令中, GPR采用了ABI助记符(mnemonic), 名称更能反映其功能, 例如, 用zero表示编号为0的GPR. 汇编指令中还有a0ra, 你可以通过解析相应的指令编码得知对应的GPR编号.

1
2
3
4
5
6
7
8
9
10
11
00000000 <_start>:
0: 01400513 addi a0,zero,20
4: 010000e7 jalr ra,16(zero) # 10 <fun>
8: 00c000e7 jalr ra,12(zero) # c <halt>

0000000c <halt>:
c: 00c00067 jalr zero,12(zero) # c <halt>

00000010 <fun>:
10: 00a50513 addi a0,a0,10
14: 00008067 jalr zero,0(ra)

尝试通过指令集的状态机理解这个程序的功能. 理解后, 将程序其放置在ROM中, 并尝试运行你的处理器, 然后检查处理器的运行结果是否符合预期.

首先,要找到两个指令的具体格式:

image

首先做出一个根据opcodefunc3判断当前指令的逻辑:

image

接着要明确这两个指令的用途:

  • addi rd,rs1,imm12: R[rd] = R[rs1] + extend(imm)
  • jalr rd,rs1,imm12: R[rd] = PC + 4; PC = R[rs1] + extend(imm);

然后实现相关的功能即可,实现如下:

image

这是执行指令后的寄存器状态,也符合我们的预期:

image

F6.2.5 测试addi指令

在上述测试程序中, addi指令的立即数比较小. 为了测试符号扩展的实现是否正确, 你需要让处理器执行一些立即数为负数的addi指令. 尝试编写若干条这种类型的addi指令, 并放置到ROM中, 检查你的实现是否正确.

我们只需要将上一次的测试指令改成:

1
2
3
4
5
6
7
8
9
10
11
00000000 <_start>:
0: 01400513 addi a0,zero,20
4: 010000e7 jalr ra,16(zero) # 10 <fun>
8: 00c000e7 jalr ra,12(zero) # c <halt>

0000000c <halt>:
c: 00c00067 jalr zero,12(zero) # c <halt>

00000010 <fun>:
10: ff650513 addi a0,a0,-10
14: 00008067 jalr zero,0(ra)

然后看运行的结果是否符合我们的期望:

确实没问题,x10从原来的20+10变成了20-10

实现完整的minirv处理器

接下来进一步实现其他的指令,由于我们还需要其他的内存空间用来存储数据,所以我们还需要引入RAM作为额外的存储空间

F6.3.1 实现完整的minirv处理器

实现addlui指令. 实现后, 尝试编写一些简单的指令序列放置到ROM中, 来初步检查你的实现是否正确.

首先找到相关的规范:

由于之后的取指会越来越复杂所以我们把取值的部分封装起来,不过到目前为止,我们也能注意到,无论是哪种指令格式,我们都需要func3rdopcode。所以我们首先需要一个根据opcodefunc3生成对应指令信号的选择器:

然后我们从指令中提取出我们需要的数据字段和信号:

接下来通过组合这些信号和数据,进而实现我们的简单处理器,现在我们需要明确两个指令的行为:

  • lui rd,imm20: R[rd] = imm20 << 12
  • add rd,rs1,rs2: R[rd] = R[rs1] + R[rs2]

我们添加一些线来实现这些功能:

然后我们可以编写一个测试用例来运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000 <_start>:
0: 01400513 addi a0, zero, 20 # a0 = 20
4: 000015b7 lui a1, 0x1 # a1 = 0x1000
8: 00b50533 add a0, a0, a1 # a0 = 0x1014
c: 018000e7 jalr ra, 24(zero) # 跳转到 0x18 <fun>
10: 014000e7 jalr ra, 20(zero) # 跳转到 0x14 <halt>

00000014 <halt>:
14: 01400067 jalr zero, 20(zero) # 跳转到自身 0x14

00000018 <fun>:
18: ff650513 addi a0, a0, -10 # a0 = 0x100A
1c: 000015b7 lui a1, 0x1 # a1 = 0x1000
20: 00b50533 add a0, a0, a1 # a0 = 0x200A
24: 00008067 jalr zero, 0(ra) # 返回

得到期望的结果:

F6.3.2 RTFM(5)

查阅RISC-V手册, 找到lw, lbu, swsb这4条指令的编码和相应的功能描述. 手册中还介绍了EEI和不对齐访存的相关内容, 目前暂不使用, 因此你可以忽略这些内容.

接着找然后,明确每个指令需要产生的信号和数据:

由于数据和信号的提取我们已经完成了,所以我们只需要简单的设计电路就好了。首先需要明确这几个指令的功能分别是什么:

  • lw rd,rs1,imm12: R[rd] = MEM[rs1 + offset][31:0]
  • lbu rd, offset(rs1): R[rd] = 零扩展( MEM[rs1 + offset][7:0] )
  • sb rs2, offset(rs1): MEM[rs1 + offset][7:0] = R[rs2][7:0]
  • sw rs2, offset(rs1): MEM[rs1 + offset][31:0] = R[rs2][31:0]

这里还应该注意到我们需要引入内存了

F6.3.3 实现完整的minirv处理器(2)

实现lwsw指令, 然后编写一些简单的指令序列放置到ROM中, 来初步检查你的实现是否正确. 特别地, 你可以用鼠标右键点击RAM组件, 然后通过Edit Contents在RAM中放置一些数据, 来帮助你测试访存指令的行为.

这里编写一个测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00000000 <_start>:
0: 00100093 addi x1, zero, 1 # x1 = 1
4: 00200113 addi x2, zero, 2 # x2 = 2
8: 00300193 addi x3, zero, 3 # x3 = 3
#c: 00400213 addi x4, zero, 4 # x4 = 4

# 写入不同地址
10: 00102023 sw x1, 0(zero) # MEM[0] = 1
14: 00202223 sw x2, 4(zero) # MEM[4] = 2
18: 00302423 sw x3, 8(zero) # MEM[8] = 3
#1c: 00402623 sw x4, 12(zero) # MEM[12] = 4

# 读取验证
20: 00002503 lw x10, 0(zero) # x10 = MEM[0] = 1
24: 00402583 lw x11, 4(zero) # x11 = MEM[4] = 2
28: 00802603 lw x12, 8(zero) # x12 = MEM[8] = 3
#2c: 00c02683 lw x13, 12(zero) # x13 = MEM[12] = 4

# 跳转到 halt (使用 jalr)
30: 03400067 jalr zero, 52(zero) # 跳转到 0x34

00000034 <halt>:
34: 03400067 jalr zero, 52(zero) # 跳转到自身 0x34

可以看到最终的运行结果是符合我们的期望的:

F6.3.4 实现完整的minirv处理器(3)

实现lbu指令, 并通过一些指令序列来初步检查你的实现是否正确.

Hint: 你可以先在RAM中放置一个4字节的数据0x12345678, 并通过lw指令读出它(假设数据位于内存地址a), 确认读出结果为0x12345678. 然后通过若干条lbu指令分别从内存地址a, a+1, a+2, a+3中读出数据, 我们预期这些lbu指令分别读出0x78(对应地址a), 0x56, 0x34, 0x12(对应地址a+3)

接下来需要实现字节的内存写入功能,由于我先前实现写入字节控制的实现比较粗糙,所以这里我们需要重新修改这一部分以实现字节的写入/读取:

这一部分的信号处理需要更新以支持我们的字节读取。

我们的测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000 <_start>:
# 在地址 0 存入测试数据 0x12345678
0: 123450b7 lui x1, 0x12345
4: 67808093 addi x1, x1, 0x678 # x1 = 0x12345678
8: 00102023 sw x1, 0(zero) # MEM[0] = 0x12345678

# 测试 lw (funct3 = 010)
c: 00002503 lw x10, 0(zero) # x10 = 0x12345678

# 测试 lbu (funct3 = 100)
10: 00004583 lbu x11, 0(zero) # x11 = 0x78
14: 00104603 lbu x12, 1(zero) # x12 = 0x56
18: 00204683 lbu x13, 2(zero) # x13 = 0x34
1c: 00304703 lbu x14, 3(zero) # x14 = 0x12

# 结束循环
20: 02400067 jalr zero, 36(zero) # 跳转到 0x24
00000024 <halt>:
24: 02400067 jalr zero, 36(zero) # 自循环

在实现的过程中,我发现lbu的字节选择是针对RAM输出的四字节来进行处理的。一开始总是读出双字而不是单字节,搜了之后才知道:

这个是程序执行的结果:

F6.3.5 实现完整的minirv处理器(4)

实现sb指令, 并通过一些指令序列来初步检查你的实现是否正确.

Hint: 你可以先在RAM中放置一个4字节的数据0x12345678, 并通过lw指令读出它(假设数据位于内存地址a), 确认读出结果为0x12345678. 然后通过若干条sb指令分别往内存地址a+3, a+2, a+1, a+0 中写入0x90(对应地址a+3), 0xab, 0xcd, 0xef(对应地址a); 写入之前, 可以通过addi指令配合零号寄存器, 来向目的寄存器写入一个立即数, 从而实现sISA中li指令的效果. 最后再次通过lw指令读出新数据, 我们预期读出结果为0x90abcdef.

有了刚刚的教训我们可以很快的实现我们的字节写入指令。

这个指令的处理最为复杂,sb需要先指定四字节中的偏移值,然后将要复写的数据移动到对应的数据位置上才能实现覆盖,可以通过多路复用器和移位器来实现。

细节如下:

我们可以使用以下程序进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00000000 <_start>:
0: 123450b7 lui x1,0x12345 # x1 = 0x12345000
4: 67808093 addi x1,x1,0x678 # x1 = 0x12345678
8: 00102023 sw x1,0(zero) # MEM[0] = 0x12345678
c: 00002503 lw x10,0(zero) # x10 = 0x12345678 (验证)

10: 09000593 addi x11,zero,0x90 # x11 = 0x90
14: 0ab00613 addi x12,zero,0xab # x12 = 0xab
18: 0cd00693 addi x13,zero,0xcd # x13 = 0xcd
1c: 0ef00713 addi x14,zero,0xef # x14 = 0xef

20: 00b001a3 sb x11,3(zero) # MEM[3] = 0x90
24: 00c00123 sb x12,2(zero) # MEM[2] = 0xab
28: 00d000a3 sb x13,1(zero) # MEM[1] = 0xcd
2c: 00e00023 sb x14,0(zero) # MEM[0] = 0xef

30: 00002503 lw x10,0(zero) # x10 = 0x90abcdef

34: 03800067 jalr zero,56(zero) # 跳转到 0x38
38: 03800067 jalr zero,56(zero) # 无限循环

测试结果十分准确

F6.3.6 在minirv处理器上执行C程序

分别加载并运行mem.hexsum.hex. 运行指定时间后, 检查处理器的状态, 若PC位于halt函数附近, 且a0寄存器为0, 则说明程序运行正确. 两个程序的预期运行时间如下:

  • mem.hex - 6000周期
  • sum.hex - 6000周期

如果你发现运行指定时间后, PC位于其他位置, 或a0寄存器不为0, 则说明程序运行错误. 但由于这个过程中已经运行了上千条指令, 很难发现是哪一条指令执行出错, 因此我们还是推荐你做好上一道必做题的验证工作, 通过一些简单的指令序列来检查你的处理器实现是否正确.

已完成:

为minirv处理器添加图形显示功能

现在已经实现了基础的minirv处理器,现在我们尝试为它设置一个屏幕:

  • Cursor(光标) - No Cursor
  • Reset Behavior(重置行为) - Asynchronous
  • Color Model(颜色模式) - 888 RGB (24 bit)
  • Width(宽度) - 256
  • Height(高度) - 256

根据上述的配置,我们的一个像素需要三个字节来表示颜色,但是为了更好的对齐,这里我们使用四个字节来表示一个像素,所以整个屏幕我们需要256*256*4B = 256KB的空间。而这里,我们规定内存地址空间[0x20000000,0x20040000]映射我们的屏幕显存。由于我们的实际用到的地址空间并没有这么大,所以我们需要通过检测地址是否在这个空间,从而设置isVGA信号,同时我们还约定,只有store指令可以向屏幕中写入像素。

F6.4.1 为minirv处理器添加图形显示功能

在处理器的数据通路上添加RGB Video组件. 我们约定, 屏幕像素对应的存储区域是[0x20000000, 0x20040000).

添加组件后, 加载并运行vga.hex程序. 这个程序的预期运行时间是628000周期, 你可能需要等待1~2分钟. 如果你的实现正确, 你将看到程序运行结束时, RGB Video组件中显示”一生一芯”logo.