皋陶 发表于 2020-8-23 20:41:07

从零开始实现一个基于RISC-V的流水线处理器

本帖最后由 皋陶 于 2020-8-26 15:10 编辑

从零开始实现一个基于RISC-V的流水线处理器
(1) :RISC-V指令集架构详解
本文目录

[*]基于RISC-V的流水线处理器

[*]RISC-V指令集
[*]RV32I

[*]R-Type
[*]I-Type
[*]J-Type
[*]B-Type
[*]Load & Store
[*]总结
[*]后记

基于RISC-V的流水线处理器
RISC-V是一个基于精简指令集原则的开源指令集架构。它由加州大学伯克利分校的David Patterson教授领导下的小组完成,至今已成为RISC处理器中一股强劲的新生力量。
至今github上已有多款优秀的基于RISC-V指令集的微处理器项目被发布,如蜂鸟E203、RocketChip等,但它们所涉及的知识层级较深,初学者很难迅速上手。而CSDN上也没有找到类似的RISC-V处理器的项目,因此笔者决定从零开始,由基础指令集做起,实现一款基于RISC-V指令集的微处理器。
RISC-V指令集
实现一款处理器,首先也是最重要的就是要确定处理器的指令集架构。
RISC-V指令集可以分为以下几个子集:

[*]RV32I:基本整数集,包括整数计算指令,LOAD/STORE,以及控制指令。RV32I拥有32位寻址空间,32个32位寄存器。
[*]RV32E:指令与RV32I相同,但是寄存器数量变为16个,用于嵌入式环境。
[*]RV64I:整数指令,拥有64位寻址空间,32个64位寄存器。
[*]RV128I:整数指令,拥有128位寻址空间,32个128位寄存器。


上述子集是RISC-V的基本指令集。在实际设计中,我们可以根据需要加入如下的扩展指令集:

[*]M:标准乘法和除法扩展,增加了乘法和除法的指令,并把结果保存在整数寄存器。
[*]A:标准原子指令扩展,增加了原子的读,修改,以及写存储器的指令。
[*]F:标准单精度浮点扩展,增加了浮点寄存器,单精度计算指令,以及单精度的LOAD/STORE指令。
[*]D:标准双精度浮点扩展,同样增加了浮点寄存器,且增加了双精度计算指令,双精度的LOAD/STORE指令。


上述均为RISC-V基金会认证的扩展指令集,此外还有V/P/T等处于草稿修改阶段的扩展指令集,这里就不再赘述了。
RV32I
在本次设计中,我们要实现的是基于最基本的整数指令集——RV32I的微处理器。就让我们从RV32I的具体指令格式看起。
下图展示了RV32I的基本指令格式。为了简化译码过程,源寄存器(rs1 和 rs2)和目标寄存器(rd)在RISC-V ISA 的所有指令格式中的位置保持一致。立即数被压缩在指令中最左边的可用位,并已经分配好,从而降低硬件复杂度。特别的,对于所有的立即数,指令的 31 位总是符号位,这样可以加速符号扩展电路。



下面我们将对各类型的指令进行具体的解释。
R-Type
R-Type的指令为寄存器-寄存器指令,其格式如下图所示。R-Type指令的特点是2个源操作数和目标操作数对象都是寄存器,在实际执行过程中需要经历寄存器的写回。





InstructionDescriptionFormatComment
ADDADD rd, rs1, rs2rd = rs1 + rs2,忽略溢出,保留低32bit
SUBSUB rd, rs1, rs2rd = rs1 - rs2,忽略溢出,保留低32bit
ANDAND rd, rs1, rs2rd = rs1 & rs2,按位与
OROR rd, rs1, rs2rd = rs1 | rs2,按位或
XORXOR rd, rs1, rs2rd = rs1 ^ rs2, 按位异或
SLTset less thanSLT rd, rs1, rs2if (rs1 < rs2) rd = 1 else rd = 0
SLTUset less than unsignedSLTU rd, rs1, rs2if((unsigned)rs1 < (unsigned)rs2) rd = 1 else rd = 0
SLLshift left logicalSLL rd, rs1, rs2rd = rs1 << rs2 ,逻辑左移,低位补零,rs2的lower 5 bits作为偏移量
SRLshift right logicalSRL rd, rs1, rs2rd = rs1 >> rs2 ,逻辑右移,高位补零,rs2的lower 5 bits作为偏移量
SRAshift rignt arithmetricSRA rd, rs1, rs2rd = rs1 >> rs2 ,算数右移,高位补符号位,rs2的lower 5 bits作为偏移量
NOPno operationNOP不进行任何操作,相当于ADDI x0, x0, 0

I-Type
I-Type的指令为寄存器-立即数指令,其指令格式如下图所示。R-Type指令中rs2和funct7的位置在I-Type指令中被imm替代,指令仅靠funct3来确定具体指令类型。
imm是一个12位的立即数,在与32位的寄存器进行逻辑运算时必须进行符号位扩展。同样地,I-Type指令也要经历寄存器的写回。




InstructionDescriptionFormatComment
ADDIadd immediateADD rd, rs1, immrd = rs1 + (sign-extended) imm,忽略溢出,保留低32bit
ANDIand immediateAND rd, rs1, immrd = rs1 & (sign-extended) imm,按位与
ORIor immediateOR rd, rs1, immrd = rs1 | (sign-extended) imm,按位或
XORIxor immediateXOR rd, rs1, immrd = rs1 ^ (sign-extended) imm, 按位异或
SLTIset less than immediateSLT rd, rs1, immif (rs1 < (sign-extended) imm) rd = 1 else rd = 0
SLTIUset less than unsigned immediateSLTU rd, rs1, immif((unsigned)rs1 < (unsigned)imm) rd = 1 else rd = 0

对于移位指令,使用立即数的低5位作为偏移量,高7位也作为判断具体指令的操作码,如下图所示。




InstructionDescriptionFormatComment
SLLIshift left logicalSLL rd, rs1, immrd = rs1 << shamt,逻辑左移,低位补零
SRLIshift right logicalSRL rd, rs1, immrd = rs1 >> shamt,逻辑右移,高位补零
SRAIshift rignt arithmetricSRA rd, rs1, immrd = rs1 >> shamt,算数右移,高位补符号位

J-Type
J-Type指令为无条件跳转指令,主要功能是更改PC的指向地址,让处理器在下一个时钟上升沿在指定的地址中取指令。
JAL的指令格式如下图所示。对于JAL,使用一个20位的有符号立即数作为偏移量,目标地址为pc + offset,并把原pc + 4的地址存入rd。



JALR的指令格式如下图所示。对于JALR,其使用的指令格式是I-Type的指令格式,使用一个12位的有符号立即数作为偏移量,目标地址为pc + offset后把LSB置为0后的结果,并把原pc + 4的地址存入rd。




由此,我们可以写出下表的指令。

InstructionDescriptionFormatComment
JALjump and linkJAL rd, offsetpc += (sign-extended) offset, rd = pc + 4
JALRjump and link registerJALR rd, rs1, offsetpc = &~ 1, rd = pc + 4
JRjump registerJR rs1pc = rs1

B-Type
B-Type指令为条件分支指令,仅在满足条件的情况下进行跳转,跳转的偏移量由一个12位的立即数给出。其指令格式如下图所示。




InstructionDescriptionFormatComment
BEQbranch equalBEQ rs1, rs2, offsetif (rs1 == rs2) pc += (sign-extended) offset
BNEbranch not equalBNE rs1, rs2, offsetif (rs1 != rs2) pc += (sign-extended) offset
BLTbranch less thanBLT rs1, rs2, offsetif (rs1 < rs2) pc += (sign-extended) offset
BGEbranch bigger than or equalBGE rs1, rs2, offsetif (rs1 >= rs2) pc += (sign-extended) offset
BGEUbranch bigger than or equal unsignedBGEU rs1, rs2, offsetif((unsigned) rs1 >= (unsigned) rs2) pc += (sign-extended) offset

Load & Store
在RV32I中,只有Load & Store指令拥有访问内存的权限。RV32I提供32-byte的寻址空间。
下图展示了Load & Store指令的具体格式。可以注意到,Load指令使用的是I-Type指令的格式,而Store指令使用的是S-Type指令的格式。Load和Store指令的偏移量均由一个12位的立即数给出,但是它们在指令中的位置不同,这是由指令的功能决定的:Load指令需要将内存中的数据转移到寄存器组,因此需要提供目标寄存器;而Store指令需要将寄存器组中的数据转移到内存,因此需要rs1提供内存中将要写入数据的地址,rs2提供需要转移的数据。
Load指令和Store指令的目标地址由rs1 + 符号位扩展的偏移量给出。当指令为LH/LB时,从内存的指定地址中取低16位/低8位,进行符号位扩展后存入rd。而当指令为LW时,直接从内存中取4字节的数据存入rd。


InstructionDescriptionFormatComment
LWload wordLW rd, rs1, offsetrd = mem
LHload halfLH rd, rs1, offsetrd = (sign-extended) mem
LBload byteLB rd, rs1, offsetrd = (sign-extended) mem
LBUload byte unsignedLBU rd, rs1, offsetrd = (zero-extended) mem
SWstore wordSW rs1, rs2, offsetmem = rs2

总结
至此,RV32I中基本指令的说明告一段落。
然而,这些说明并不完整——RV32I中还有FENCE指令、Environment Call and Breakpoints指令、HINT指令等尚未提及,这些指令对于本次的设计来说过于复杂,由于本设计完成的处理器仅包括基本的运算及存储功能,因此在这里就将对这些指令的描述略去了。感兴趣的读者可以通过这个链接下载RISC-V的中文手册,或自行下载英文原版手册,对其进行深入阅读。
后记
RISC-V作为一个新兴的RISC架构,在具体的实现中博采众长,真正做到了对其他早期RISC架构的“取其精华,去其糟粕”。例如,RISC-V取消了MIPS-32 ISA中的延迟分支,单纯依靠现代已经相当发达的硬件预测器预测分支结果,实现了架构和具体实现的分离,使得指令集和芯片具体实现的分离成为可能。
芯片商可以采用统一的、免费的开放指令集,但各个厂商可以有各自的内部模块实现,并可以申请专利予以保护。这样既可以构建同一个软件生态系统,又保持了芯片企业之间的独立性。
对于笔者而言,RISC-V和MIPS的区别不仅在于指令中各元素位置的变化,更在于其精心设计的、称得上优雅的具体指令集架构和强大的可扩展性。RV32I中在rs1和rd之间插入一个3 bit的funct,在I-Type的指令中使用12位而非16位的立即数,想必也是从可扩展性的角度考虑得来的结果。
笔者同样作为一个RISC-V的初学者,对于指令集的理解难免有纰漏或错误的地方,欢迎大家对笔者的文章进行批评指正,也欢迎大家与笔者进行交流。本篇完,感谢关注:RISC-V单片机中文网


页: [1]
查看完整版本: 从零开始实现一个基于RISC-V的流水线处理器