目录
- 基于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个源操作数和目标操作数对象都是寄存器,在实际执行过程中需要经历寄存器的写回。
Instruction | Description | Format | Comment |
---|---|---|---|
ADD | ADD rd, rs1, rs2 | rd = rs1 + rs2,忽略溢出,保留低32bit | |
SUB | SUB rd, rs1, rs2 | rd = rs1 - rs2,忽略溢出,保留低32bit | |
AND | AND rd, rs1, rs2 | rd = rs1 & rs2,按位与 | |
OR | OR rd, rs1, rs2 | rd = rs1 | rs2,按位或 | |
XOR | XOR rd, rs1, rs2 | rd = rs1 ^ rs2, 按位异或 | |
SLT | set less than | SLT rd, rs1, rs2 | if (rs1 < rs2) rd = 1 else rd = 0 |
SLTU | set less than unsigned | SLTU rd, rs1, rs2 | if((unsigned)rs1 < (unsigned)rs2) rd = 1 else rd = 0 |
SLL | shift left logical | SLL rd, rs1, rs2 | rd = rs1 << rs2 [4 : 0],逻辑左移,低位补零,rs2的lower 5 bits作为偏移量 |
SRL | shift right logical | SRL rd, rs1, rs2 | rd = rs1 >> rs2 [4 : 0],逻辑右移,高位补零,rs2的lower 5 bits作为偏移量 |
SRA | shift rignt arithmetric | SRA rd, rs1, rs2 | rd = rs1 >> rs2 [4 : 0],算数右移,高位补符号位,rs2的lower 5 bits作为偏移量 |
NOP | no operation | NOP | 不进行任何操作,相当于ADDI x0, x0, 0 |
I-Type
I-Type的指令为寄存器-立即数指令,其指令格式如下图所示。R-Type指令中rs2和funct7的位置在I-Type指令中被imm替代,指令仅靠funct3来确定具体指令类型。
imm是一个12位的立即数,在与32位的寄存器进行逻辑运算时必须进行符号位扩展。同样地,I-Type指令也要经历寄存器的写回。
Instruction | Description | Format | Comment |
---|---|---|---|
ADDI | add immediate | ADD rd, rs1, imm | rd = rs1 + (sign-extended) imm,忽略溢出,保留低32bit |
ANDI | and immediate | AND rd, rs1, imm | rd = rs1 & (sign-extended) imm,按位与 |
ORI | or immediate | OR rd, rs1, imm | rd = rs1 | (sign-extended) imm,按位或 |
XORI | xor immediate | XOR rd, rs1, imm | rd = rs1 ^ (sign-extended) imm, 按位异或 |
SLTI | set less than immediate | SLT rd, rs1, imm | if (rs1 < (sign-extended) imm) rd = 1 else rd = 0 |
SLTIU | set less than unsigned immediate | SLTU rd, rs1, imm | if((unsigned)rs1 < (unsigned)imm) rd = 1 else rd = 0 |
对于移位指令,使用立即数的低5位作为偏移量,高7位也作为判断具体指令的操作码,如下图所示。
Instruction | Description | Format | Comment |
---|---|---|---|
SLLI | shift left logical | SLL rd, rs1, imm | rd = rs1 << shamt,逻辑左移,低位补零 |
SRLI | shift right logical | SRL rd, rs1, imm | rd = rs1 >> shamt,逻辑右移,高位补零 |
SRAI | shift rignt arithmetric | SRA rd, rs1, imm | rd = 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。
由此,我们可以写出下表的指令。
Instruction | Description | Format | Comment |
---|---|---|---|
JAL | jump and link | JAL rd, offset | pc += (sign-extended) offset, rd = pc + 4 |
JALR | jump and link register | JALR rd, rs1, offset | pc = [rs + (sign-extended) offset] &~ 1, rd = pc + 4 |
JR | jump register | JR rs1 | pc = rs1 |
B-Type
B-Type指令为条件分支指令,仅在满足条件的情况下进行跳转,跳转的偏移量由一个12位的立即数给出。其指令格式如下图所示。
Instruction | Description | Format | Comment |
---|---|---|---|
BEQ | branch equal | BEQ rs1, rs2, offset | if (rs1 == rs2) pc += (sign-extended) offset |
BNE | branch not equal | BNE rs1, rs2, offset | if (rs1 != rs2) pc += (sign-extended) offset |
BLT | branch less than | BLT rs1, rs2, offset | if (rs1 < rs2) pc += (sign-extended) offset |
BGE | branch bigger than or equal | BGE rs1, rs2, offset | if (rs1 >= rs2) pc += (sign-extended) offset |
BGEU | branch bigger than or equal unsigned | BGEU rs1, rs2, offset | if((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。
Instruction | Description | Format | Comment |
---|---|---|---|
LW | load word | LW rd, rs1, offset | rd = mem[rs1 + (sign-extended) offset] [31:0] |
LH | load half | LH rd, rs1, offset | rd = (sign-extended) mem[rs1 + (sign-extend) offset)] [15:0] |
LB | load byte | LB rd, rs1, offset | rd = (sign-extended) mem[rs1 + (sign-extend) offset)] [7:0] |
LBU | load byte unsigned | LBU rd, rs1, offset | rd = (zero-extended) mem[rs1 + (sign-extend) offset)] [7:0] |
SW | store word | SW rs1, rs2, offset | mem[rs1 + (sign-extend) offset)] = 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的初学者,对于指令集的理解难免有纰漏或错误的地方,欢迎大家对笔者的文章进行批评指正,也欢迎大家与笔者进行交流。