我们已经实现了sCPU,可以计算数列求和,但是这个处理器确实让我们对处理器如何工作有着更为深入的认识。但由于限制,不能运行复杂的程序,由于其指令集的宽度,PC的数量,GPR的位宽,指令的功能。所以,在接下来我们要实现一个功能完备的RISC-V处理,可以运行更多的程序。

Just for fun! let’s go!

迷你RISC-V指令集

RISC-V是近十年流行起来的开放指令集架构, 它采用模块化的思想, 把指令划分成不同模块。除了基础指令集RV32I, 还有各种指令扩展, 包括乘除扩展, 浮点扩展, 原子操作扩展等. 开发者可以根据自身需求选择一个或多个扩展, 也可以一个扩展都不选, 这种灵活性受到了开发者的喜爱.

RV32I共有42条指令, 通过实现RV32I, 处理器已经足够完成绝大部分的计算工作. 不过为了进一步降低开发的工作量, 我们提出了一个”迷你RISC-V”指令集minirv, 从RV32I中选出了8条指令, 用它们来替代其他RV32I指令的功能, 使得RV32I能完成的工作, minirv也能完成. 这样, 我们就不必实现完整的42条RV32I指令, 也能让处理器运行更复杂的程序了.

RTFM

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

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

接下来是我们的答案

  1. PC寄存器,其位宽为32位
  2. 32个32位宽的GPR
  3. 就是R【0】代表第一个寄存器,这一个寄存器的值永远为32个0,是一个RISC中的非常经典且美观的设计
  4. 指令编码位宽是32位,有4钟基本格式
  5. 5位来表示一个GPR,因为我们只有32个32位的寄存器
  6. 是一种R-type
  7. 指令精简,PC只有16个

了解RISC-V指令集的一些细节之后, 我们就可以给出minirv这一ISA的规范了, 具体如下:

只有两条指令的minirv处理器

  • PC初值为0
  • GPR数量与RV32E中定义的GPR数量一致,即为16个
  • 支持如下8条指令: addaddiluilwlbuswsbjalr
  • 其他的ISA细节与RV32I相同

minirv有8条指令, 我们先实现其中的两条: addijalr. 首先考虑addi指令.

RTFM(2)

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

通过阅读第34章,我们可以找到:指令集的基本格式
image.png

RTFM(3)

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

我们的寄存器位宽有32位,所以最后我们地址空间最后有二的32次方大小,接下来说的我不是很懂,先进行记录一下:
image.png
操作码编码较为稀疏, 使用译码器反而会带来一些不便. 因此, 我们建议你使用比较器, 直接比较指令中的操作码字段是否与addi指令的编码一致, 来进行译码操作. 例如, 可以通过以下操作判断一条指令是否为addi指令:

RTFM(4)

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

image.png
找到了但没有很看懂

实现两条minirv处理器

这是我们搭建的GPR堆,包含16组32位寄存器,写入为单路,由wdata和waddr控制,输出由raddr1和raddr2两个输入控制。同时我们为以后预留了复位端,用以清空寄存器.
我们来缕一缕需要实现的两条指令:
ADDI: R[rd] = R[rs1] + extend(imm)

  • 需要一个加法器,实现立即数与内存读取数据的相加
  • 需要先读取立即数
  • 需要打开写使能端
    jalr:R[rd] = PC + 4; PC = R[rs1] + extend(imm)
  • 需要读取当前PC值,加四传入rd,并且通过mux,写入wdata
  • 和ADDI一样,做一个加法器获取值,但是传入PC,由jalr信号控制的mux控制,传入PC
    image.png

image.png
我们看到这个典型的结构,对于一个RISC-V架构,JALR命令就是上述的格式,opcode为1100111,ADDI命令也是上述的格式,opcode为0010011。我还是没有非常的理解jalr指令的具体的意义。
image.png
我们要对于这个minirv进行一些检测
首先,我们需要设计一个测试集,我们检验addi和jalr这两个指令

1
2
3
4
5
6
7
8
9
10
11
12
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)

image.png
image.png
运行的结果本身是没有问题的,我们要注意,第一个寄存器的写入端要置为低电平,wire 0.

实现完整的minirv处理器

我们对于需要用到的指令的opcode进行了整理:

1
2
3
4
5
6
7
8
9
10
11
12
+-------+--------+-----------+----------------+--------+---------+
| 指令 | 类型 | opcode | opcode(16进制) | funct3 | funct7 |
+-------+--------+-----------+----------------+--------+---------+
| add | R-type | 0110011 | 0x33 | 000 |0000000 |
| addi | I-type | 0010011 | 0x13 | 000 | - |
| lui | U-type | 0110111 | 0x37 | - | - |
| lw | I-type | 0000011 | 0x03 | 010 | - |
| lbu | I-type | 0000011 | 0x03 | 100 | - |
| sw | S-type | 0100011 | 0x23 | 010 | - |
| sb | S-type | 0100011 | 0x23 | 000 | - |
| jalr | I-type | 1100111 | 0x67 | 000 | - |
+-------+--------+-----------+----------------+--------+---------+

我们准备对于opcode进行编码,这一次由于电路比较大,我们需要进行封装,所以我选择和上次一样地,只不过这次的封装加上了整个的32条支路的集线器
image.png
结果试验,整个的逻辑是没有什么问题的。

对指令的分析

我们开始思考如何配置寄存器,我们首先要明白8个指令的存储与读取的权限,以及他们要进行的运算

  • ADD:读取rs1 rs2进入加法器,输出赋予WE权限,接入Wdata写入rd
  • ADDI:读取rs1,和imm进入加法器,后面保持一致

所以rs2和imm在加法时,需要进入一个多路选择器

  • JALR:R[rd]=PC+1,PC=R[rs1]+imm。需要有寄存器rd的写入权限,数据流来自于PC,同时经过加法器后,进入PC前有一个多路选择器
  • LUI:放入R[rd]高二十位立即数,低位补0,需要WE和wdata权限

lw和lbu是一对指令,都是从内存中读取数据,LW是读取一整个一整个字(四个字节),而LBU是只写入最低的8位

  • 二者需要在一开始经过一个多路选择器,代表需要写入的数据
  • 需要WE权限和Wdata的注入,总体而言并没有非常复杂

sb和sw也是一组指令,其中的sb代表向内存写一个字节,,而sw是对内存写一整个词(4字节)

  • 这是唯二的需要向ROM写的指令
  • 这个sb很烦,需要写入RAM中的任意一个位置,这边不简单

我们对于寄存器进行梳理,发现对于寄存器we的权限:
ADD ADDI JALR LUI LW LBU
其wdata分别来自于:

  • ADD ADDI加法后的结果
  • lw和lbu的指令,需要从内存中移数据过来
  • LUI的数据
  • JALR的劫后余生
    其waddr都是写入在地址位rd的地方

对内存指令的具体分析

LW和LBU

指令结构:

1
2
3
4
31          20 19    15 14   12 11     7 6      0
+-------------+--------+-------+--------+--------+
| imm[11:0] | rs1 |funct3 | rd | opcode |
+-------------+--------+-------+--------+--------+

二者都先算addr=x[rs1]+sext(imm)

sb和sw

指令结构:

1
2
3
4
31        25 24    20 19    15 14   12 11       7 6      0
+-----------+--------+--------+-------+-----------+--------+
| imm[11:5] | rs2 | rs1 |funct3 | imm[4:0] | opcode |
+-----------+--------+--------+-------+-----------+--------+

调试

经过了16个小时的设计与调试,我们目前还是出现了问题,我们采用@ylin的一个测试指令来进行验证,验证各个指令是否正确:

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) # 无限循环

最后我们也是很好的完成了这个任务,运行完了以后的GPR情况如下:
00eab926fc7f4b401c5b5b92e1a704ca.png
如果这个验证集完成了,证明我们的八条指令就没有问题了。然后,我们通过添加简单的逻辑,就可以完成显示器的设置,结果如下:
a622fe7b6292621343167a69379b8744.png
一生一芯,YYDS!

一点小小的感想

本来还想在这一个mini的logisim上实现我的超级玛丽小游戏,但是没有条件跳转指令,也就是没有if判断,而真正的游戏几乎是不能够缺少条件判断的,所以我们很难去设计一个游戏,但是,我们能把自己的图片print到logisim,我觉得这本身就是一种幸福。至此,一生一芯F阶段到此结束,这一个F6我将近花了15个小时,速度还是有所欠缺,还需要更加的努力。

24hz的帧率,写不出我2048hz的心,心的同频,交相辉映。才会创造一个更好的未来,为了创造一个更好的未来,我愿意投入我全部的身心和热情,贡此身心,维护世间的美好与爱

真的真的,这本身就是一种幸福。

爱是一种《英雄主义》,just for love, just for fun