F6:我的第一块芯片
我们已经实现了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的相关内容, 回答下列问题: 第二章
- PC寄存器的位宽是多少?
- GPR共有多少个? 每个GPR的位宽是多少?
R[0]和sISA的R[0]有什么不同之处?- 指令编码的位宽是多少? 指令有多少种基本格式?
- 在指令的基本格式中, 需要多少位来表示一个GPR? 为什么?
add指令的格式具体是什么?- 还有一种基础指令集称为RV32E, 它和RV32I有什么不同?
接下来是我们的答案
- PC寄存器,其位宽为32位
- 32个32位宽的GPR
- 就是R【0】代表第一个寄存器,这一个寄存器的值永远为32个0,是一个RISC中的非常经典且美观的设计
- 指令编码位宽是32位,有4钟基本格式
- 5位来表示一个GPR,因为我们只有32个32位的寄存器
- 是一种R-type
- 指令精简,PC只有16个
了解RISC-V指令集的一些细节之后, 我们就可以给出minirv这一ISA的规范了, 具体如下:
只有两条指令的minirv处理器
- PC初值为
0 - GPR数量与RV32E中定义的GPR数量一致,即为16个
- 支持如下8条指令:
add,addi,lui,lw,lbu,sw,sb,jalr - 其他的ISA细节与RV32I相同
minirv有8条指令, 我们先实现其中的两条: addi, jalr. 首先考虑addi指令.
RTFM(2)
查阅RISC-V手册, 找到
addi指令的编码和相应的功能描述. 在第34章RV32/64G Instruction Set Listings中有一些指令表, 可以帮助你查阅addi指令的编码.
通过阅读第34章,我们可以找到:指令集的基本格式
RTFM(3)
为了了解RISC-V对存储器的若干约定, 你需要阅读RISC-V手册第1.4节的第一段, 从ISA的层面了解存储器的规格, 尤其是宽度的定义.
我们的寄存器位宽有32位,所以最后我们地址空间最后有二的32次方大小,接下来说的我不是很懂,先进行记录一下:
操作码编码较为稀疏, 使用译码器反而会带来一些不便. 因此, 我们建议你使用比较器, 直接比较指令中的操作码字段是否与addi指令的编码一致, 来进行译码操作. 例如, 可以通过以下操作判断一条指令是否为addi指令:
RTFM(4)
查阅RISC-V手册, 找到
jalr指令的编码和相应的功能描述.

找到了但没有很看懂
实现两条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


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


运行的结果本身是没有问题的,我们要注意,第一个寄存器的写入端要置为低电平,wire 0.
实现完整的minirv处理器
我们对于需要用到的指令的opcode进行了整理:
1 | +-------+--------+-----------+----------------+--------+---------+ |
我们准备对于opcode进行编码,这一次由于电路比较大,我们需要进行封装,所以我选择和上次一样地,只不过这次的封装加上了整个的32条支路的集线器
结果试验,整个的逻辑是没有什么问题的。
对指令的分析
我们开始思考如何配置寄存器,我们首先要明白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 | 31 20 19 15 14 12 11 7 6 0 |
二者都先算addr=x[rs1]+sext(imm)
sb和sw
指令结构:
1 | 31 25 24 20 19 15 14 12 11 7 6 0 |
调试
经过了16个小时的设计与调试,我们目前还是出现了问题,我们采用@ylin的一个测试指令来进行验证,验证各个指令是否正确:
1 | 00000000 <_start>: |
最后我们也是很好的完成了这个任务,运行完了以后的GPR情况如下:
如果这个验证集完成了,证明我们的八条指令就没有问题了。然后,我们通过添加简单的逻辑,就可以完成显示器的设置,结果如下:
一生一芯,YYDS!
一点小小的感想
本来还想在这一个mini的logisim上实现我的超级玛丽小游戏,但是没有条件跳转指令,也就是没有if判断,而真正的游戏几乎是不能够缺少条件判断的,所以我们很难去设计一个游戏,但是,我们能把自己的图片print到logisim,我觉得这本身就是一种幸福。至此,一生一芯F阶段到此结束,这一个F6我将近花了15个小时,速度还是有所欠缺,还需要更加的努力。
24hz的帧率,写不出我2048hz的心,心的同频,交相辉映。才会创造一个更好的未来,为了创造一个更好的未来,我愿意投入我全部的身心和热情,贡此身心,维护世间的美好与爱。
真的真的,这本身就是一种幸福。
爱是一种《英雄主义》,just for love, just for fun



