拖了大半个月,终于重新开始了一生一芯计划的学习。接下来要完成的部分是,一个有趣的项目
F6 功能完备的迷你RISC-V处理器
我们先前实现的简单的ISA只能实现一些简单的命令,但是并不具有完备性,导致很多功能我们难以正常实现,所以这一次我们要实现的是一个功能完备的RISC-V处理器,不过在此之前,我们需要更加深入的了解它。
迷你RISC-V指令集
在了解RISC-V指令集的细节之后,我们可以指定一个自己的minirv的ISA的规范
- PC初值为0
- GPR数量和RV32E一致(16)
- 支持以下八条指令
add、addi、lui、lw、lbu、sw、sb、jalr - 其他的ISA细节参考
RV32I
F6.1.1 RTFM
查阅RISC-V手册的目录, 你发现RV32I在哪一章进行介绍? 尝试在该章节中查阅RV32I的相关内容, 回答下列问题:
- PC寄存器的位宽是多少?
- GPR共有多少个? 每个GPR的位宽是多少?
R[0]和sISA的R[0]有什么不同之处?- 指令编码的位宽是多少? 指令有多少种基本格式?
- 在指令的基本格式中, 需要多少位来表示一个GPR? 为什么?
add指令的格式具体是什么?- 还有一种基础指令集称为RV32E, 它和RV32I有什么不同?
关于RV32I的介绍在第二章中可以看到
接下来按顺序回答这些问题:
- 在RV32I中PC的位宽是32bit
- 共有32个GPR,每个位宽均为32bit
R[0]中存放的数值恒为0,在我们先前的sISA中的R[0]只是我们的第一个寄存器- 指令编码的位宽是32,一共有四种指令格式
- 在指令的基本格式中需要5位来表示一个GPR,因为一共有32个寄存器
add指令是一种R-Type- RV32E是RV32I的精简版本,例如寄存器的数量被减少到了16个
只有两条指令的minirv处理器
我们先从两条指令开始实现addi和jalr
F6.2.1 RTFM(2)
查阅RISC-V手册, 找到
addi指令的编码和相应的功能描述. 在第34章RV32/64G Instruction Set Listings中有一些指令表, 可以帮助你查阅addi指令的编码.
我们可以在这里找到对addi的说明。
F6.2.2 RTFM(3)
为了了解RISC-V对存储器的若干约定, 你需要阅读RISC-V手册第1.4节的第一段, 从ISA的层面了解存储器的规格, 尤其是宽度的定义.
根据我们先前的ISA规则,我们的地址空间大小应该是一个按字节寻址地址空间,大小为232.
F6.2.3 RTFM(4)
查阅RISC-V手册, 找到
jalr指令的编码和相应的功能描述.
F6.2.4 实现两条指令的minirv处理器
理解
addi和jalr指令的功能后, 根据你之前设计sISA处理器的经验, 尝试设计一个支持这两条RISC-V指令的处理器.由于GPR需要进行较多的连线工作, 为了减轻大家的负担, 我们准备了一个预先完成大量连线工作的GPR子模块. 下载后, 通过Logisim打开文件
GPR.circ, 即可看到GPR的电路设计, 你可以整体选择这个电路后, 通过复制和粘贴将其加入到你工程中. 不过, 这个电路并没有实现GPR的完整功能, 你需要根据你对GPR的理解完善它.为了帮助你对处理器进行简单的测试, 我们准备了如下测试程序. 在下面的汇编指令中, GPR采用了ABI助记符(mnemonic), 名称更能反映其功能, 例如, 用
zero表示编号为0的GPR. 汇编指令中还有a0和ra, 你可以通过解析相应的指令编码得知对应的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中, 并尝试运行你的处理器, 然后检查处理器的运行结果是否符合预期.
首先,要找到两个指令的具体格式:
首先做出一个根据opcode和func3判断当前指令的逻辑:
接着要明确这两个指令的用途:
- addi rd,rs1,imm12: R[rd] = R[rs1] + extend(imm)
- jalr rd,rs1,imm12: R[rd] = PC + 4; PC = R[rs1] + extend(imm);
然后实现相关的功能即可,实现如下:
这是执行指令后的寄存器状态,也符合我们的预期:
F6.2.5 测试addi指令
在上述测试程序中,
addi指令的立即数比较小. 为了测试符号扩展的实现是否正确, 你需要让处理器执行一些立即数为负数的addi指令. 尝试编写若干条这种类型的addi指令, 并放置到ROM中, 检查你的实现是否正确.
我们只需要将上一次的测试指令改成:
1 | 00000000 <_start>: |
然后看运行的结果是否符合我们的期望:

确实没问题,x10从原来的20+10变成了20-10
实现完整的minirv处理器
接下来进一步实现其他的指令,由于我们还需要其他的内存空间用来存储数据,所以我们还需要引入RAM作为额外的存储空间
F6.3.1 实现完整的minirv处理器
实现
add和lui指令. 实现后, 尝试编写一些简单的指令序列放置到ROM中, 来初步检查你的实现是否正确.
首先找到相关的规范:

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

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

接下来通过组合这些信号和数据,进而实现我们的简单处理器,现在我们需要明确两个指令的行为:
- lui rd,imm20: R[rd] = imm20 << 12
- add rd,rs1,rs2: R[rd] = R[rs1] + R[rs2]
我们添加一些线来实现这些功能:

然后我们可以编写一个测试用例来运行:
1 | 00000000 <_start>: |
得到期望的结果:

F6.3.2 RTFM(5)
查阅RISC-V手册, 找到
lw,lbu,sw和sb这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)
实现
lw和sw指令, 然后编写一些简单的指令序列放置到ROM中, 来初步检查你的实现是否正确. 特别地, 你可以用鼠标右键点击RAM组件, 然后通过Edit Contents在RAM中放置一些数据, 来帮助你测试访存指令的行为.
这里编写一个测试程序:
1 | 00000000 <_start>: |
可以看到最终的运行结果是符合我们的期望的:


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 | 00000000 <_start>: |
在实现的过程中,我发现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 | 00000000 <_start>: |
测试结果十分准确

F6.3.6 在minirv处理器上执行C程序
分别加载并运行
mem.hex和sum.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.
