本人数字IC小白,一直对Risc-V处理器很感兴趣,打算仔细学习一下,并将学习过程记录下来,顺便敦促自己学习。
文章内容基于该网站教学。
Risc-V 基本知识
我要实现一个RV32I的CPU,因此需要了解RV32I的指令集架构以及内部结构。
指令集架构
基础整数指令集(I)提供了运算所需的全部基础指令,包含了加法、减法、逻辑运算、移位、跳转等指令。
部分指令如下表所示
| 类别 | 作用 | RV32I包含指令 |
|---|---|---|
| 计算(Arithmetic) | 整数加减法 | add, sub, addi |
| 逻辑(Logic) | 按位与、或、异或、移位 | and, or, xor, sll, srl |
| 比较(Compare) | 比较大小,结果1/0写入目标寄存器 | slt, slti |
| 加载/存储(Load/Store) | 从存储器中加载 / 存储数据到寄存器 | lw, sw, lb, sb |
| 分支(Branch) | 判断条件成立(if 判断、循环) | beq, bne, blt, bge |
| 跳转(Jump) | 固定跳转并保存返回地址,多用于函数调用、函数返回 | jal, jalr |
理解指令的构成
RISC-V指令由少量字段构成。掌握这七种字段,就能看懂几乎所有指令:
| 字段 | 位数 | 表示了什么 |
|---|---|---|
| opcode | 7bit | 指令类型(属于算术运算指令?读取加载指令?还是分支跳转指令?) |
| rd | 5bit | 目标寄存器(运算结果的存储位置) |
| rs1 | 5bit | 源寄存器1 |
| rs2 | 5bit | 源寄存器2 |
| funct3 | 3bit | 指令的子类型(选取opcode组别内具体的运算指令) |
| funct7 | 7bit | 指令的子类型(用于区分共用同一个 funct3 字段的运算指令) |
| imm | 视情况 | 立即数(自己输入常量数值,如“add 1”里的数字 “1”) |
并非每条指令都需要占用所有字段单元。加法指令(add)需要三个寄存器,无需常量;立即数加法指令(addi)需要一个寄存器搭配一个常量;跳转指令则需要一个大范围偏移量。
六种格式一览

每种格式的用途
- R-type (Register):两个源寄存器 → 一个输出寄存器。仅寄存器之间的计算,例如add x7, x5, x6 sub and xor sll
- I-type (Immediate):一个源寄存器、12位立即数 → 一个输出寄存器。该指令格式同样适用于加载指令等操作,例如:jalr、addi x6, x6, 1、lw x5, 0 (x10)
- S-type (Store):两个源寄存器(作为基址和要写入的数据)、12位立即数作为地址偏移量 → 写入存储器(地址rs1 + imm)。例如:sw x5, 8(x10)
- B-type (Branch):比较两个寄存器,若满足判定条件,则依据有符号偏移量完成跳转。该指令格式与 S 型指令相似,但其立即数用于跳转目标的地址。例如:beq x6, x7, loop
- U-type (Upper Immediate):20位立即数 → 一个输出寄存器。将立即数存储到寄存器中,例如lui x7, 1000
- J-type (Jump):20位立即数 → 程序计数器。将立即数存储到程序计数器中,并跳转到指定位置,例如:jal x1, 0x100 # 跳转到PC+0x100,同时x1保存PC+4
所有指令详解见本文
寄存器
采用寄存器不直接采用数据存储器的原因:寄存器访问速度快,内存访问速度慢
32个寄存器:x0–x31
RV32I给出了32个通用寄存器,分别命名为x0、x1、x2、……x31,每个寄存器宽度为32位。

寄存器x0
x0寄存器最特别,x0读出端恒接0电平,写入无效。对零寄存器x0的巧妙复用,这样能设计出很多高效指令写法,使RISC-V架构所需的实际指令数量更少:
- 恒定的0寄存器:任何需要0时,直接从x0中取。
- 复制寄存器:如将x6复制到x5时,可以直接add x5, x6, x0,将x6+0存入x5。
- 丢弃一个结果:如果只需要一个指令带来的其他效益而不需计算结果,可以将结果直接写入x0。
寄存器的ABI名称与作用
| 寄存器编号 | ABI 名称 功能说明 | |
|---|---|---|
| x0 | zero | 硬件固化常量 0 寄存器 |
| x1 | ra | 返回地址(函数执行完毕的返回跳转地址) |
| x2 | sp | 栈指针 |
| x3 | gp | 全局指针 |
| x4 | tp | 线程指针 |
| x5–x7 | t0 – t2 | 临时寄存器(暂存寄存器,调用者保存) |
| x8 | s0 / fp | 保存寄存器 / 栈帧指针 |
| x9 | s1 | 保存寄存器(被调用者保存) |
| x10–x11 | a0 – a1 | 函数参数、函数返回值 |
| x12–x17 | a2 – a7 | 其余函数入参 |
| x18–x27 | s2 – s11 | 保存寄存器(被调用者保存) |
| x28–x31 | t3 – t6 | 扩展临时寄存器(调用者保存) |
PC程序计数器
程序计数器(PC)并不包含在32个通用寄存器之中,但至关重要。该寄存器存储着当前正在执行指令的内存地址,PC的变化也决定接下来执行什么指令。正常情况下,CPU执行完一条指令后,程序计数器数值会自增4(因为每条指令占用4字节,也就是32位),从而指向下一条待执行指令;而分支指令或跳转指令会改写程序计数器的数值,这也是循环语句与条件判断语句实现的原理。
内存、栈
内存
寄存器运行速度快,但数量稀少,仅有32个。实际运行的程序需要存储的数据量要大得多,同时程序还会调用层层嵌套的函数。想要实现这些需求,就必须依靠内存、栈结构,以及一套被称为调用约定的通用规范。
在RV32架构中,地址为32位长度,因此最多拥有4GB的字节存储单元(总数量为2的32次方个)。想要操作内存,只需提供对应地址,执行读取或是写入操作。
RISC-V架构在内存中采用小端序存放数据:一个32位字数据存储时,最低有效字节存放在最低内存地址。
内存对齐:32位字数据的存放地址通常必须是4的整数倍(半字数据地址需为2的整数倍)。对齐式内存访问对硬件而言处理逻辑最为简洁。
映射:
- Text — 程序代码
- Data — 全局变量
- Heap — 动态分配的内存(向上扩展)
- Stack — 函数调用栈(向上扩展)
栈
栈是一块专用内存区域,工作原理如同一摞盘子:遵循后进先出规则。内存中栈的特性:
- 栈地址向低地址方向增长:执行入栈操作时,sp 寄存器数值会减小。
- 处理器没有专用的push(入栈)指令:需要手动实现入栈流程,先减小sp的值,再完成数据存入;出栈则先读取数据,再将sp数值恢复增加。
栈存在的意义:
- 执行嵌套函数调用————jal指令调用函数时会将返回地址存入ra寄存器。但倘若该函数内部又调用了另一个函数,第二条jal指令就会覆盖ra寄存器中的原有数据,致使外层函数丢失返回地址。
- 对应的解决办法为:每个函数在发起嵌套调用前,先将ra寄存器的值存入堆栈,调用结束后再从堆栈中恢复该数值。依托堆栈机制,函数能够实现任意深度的嵌套调用,且每一层调用都可保存自身对应的返回地址。入栈操作等价于先执行addi sp,sp,-N再执行sw存储指令,出栈操作等价于先执行lw加载指令再执行addi sp,sp,N。
调用约定(The calling convention)
不同人员(或编译器)编写的函数想要能够协同运行,所有参与方必须就参数传递方式、返回值存放位置以及需要保留的寄存器达成统一标准。这套统一标准即为调用约定(The calling convention)(RISC-V 架构的调用约定与 ARM 的 AAPCS 规范相近)。沿用上一节寄存器的ABI名称与作用命名规则说明如下:
| 寄存器 | 作用 | 保存责任方 |
|---|---|---|
| a0 ~ a7 | 函数入参;a0(搭配a1可存放多字节返回值)用作函数返回值 | 调用方保存 |
| ra | 函数返回地址 | 调用方保存(嵌套调用前务必手动保存) |
| t0 ~ t6 | 临时寄存器(临时计算值,随意覆写) | 调用方保存 |
| s0 ~ s11 | 保留寄存器(存放函数局部变量) | 被调用方保存 |
| sp | 栈指针寄存器 | 被调用方保存(函数返回前必须复原栈指针数值) |
- 调用方保存寄存器(t、a、ra)—— 被调用函数可随意覆盖这类寄存器的值。若调用函数后续还需要使用这些寄存器的数据,必须提前自行完成数据保存。
- 被调用方保存寄存器(s0 至 s11、栈指针 sp)—— 被调用函数执行完毕后,这类寄存器的数值
必须和调用之初保持一致;因此函数若要使用该类寄存器,需先将数据入栈保存,在函数返回前再将数据出栈恢复。
CPU的本质操作
按照五级流水线来划分RV-CPU的工作可以分为以下五步:
- Fetch(IF) ———— 从程序计数器(PC)指向的指令存储器(IMEM)中读取 32 位指令,计算 PC 值加 4 的结果。
- Decode(ID) ———— 将指令拆分为各个字段。从寄存器堆中读取源寄存器 1、源寄存器 2 的数据。对立即数进行符号扩展。
- Execute(EX) ———— 算术逻辑单元计算运算结果或分支目标地址。执行分支指令时,对操作数进行比对。
- Memory(MEM) ———— Load: 读取内存数据。Store: 数据存入内存。非存储器类指令直接跳过此步骤。
- Write-back(WB) ———— 结果写回目标寄存器
执行CPU数据通路所需的组成结构
| 结构 | 缩写 | 作用 |
|---|---|---|
| 程序计数器(Program Counter) | PC | 用于存放待读取下一条指令地址的32位寄存器 |
| 指令存储器(Instruction Memory)IMEM | 存储程序的只读存储器(组合逻辑电路,无需时钟信号) | |
| 指令译码器(Instruction Decoder) | DECODE | 用于将指令拆分为各个字段 |
| 寄存器堆(Register File) | REGFILE | 32个32位寄存器,两组异步读取端口与一组同步写入端口 |
| 立即数生成器(Immediate Generator) | IMMGEN | 根据指令格式对指令中的立即数字段进行符号扩展处理。 |
| 算数逻辑单元(ALU) | ALU | 接收两个32位操作数,计算后输出32位运算结果以及用于分支判断的零标志位。 |
| 算术逻辑单元多路选择器(ALU MUX) | ALUMUX | 选择ALU的第二个操作数取值为寄存器rs2或是立即数。 |
| 数据存储器(Data Memory) | DMEM | 暂存数据的存储器,可控制寄存器写入或读出。 |
| 写回多路选择器(Write-back MUX) | WBMUX | 选择写入目标寄存器rd的数据来源。 |
| 分支处理单元(Branch Unit) | BRANCH | 判断是否执行分支跳转,随后计算分支目标地址 |
| 程序计数器多路选择器(PC MUX) | PCMUX | 选择下一个程序计数器数值,也就是是否跳转 |
| 控制单元(Control Unit) | CTRL | 为内部单元提供控制信号 |
术语词表
记录在学习过程中遇到的专业名词
ABI
ABI(Application Binary Interface),应用二进制接口
ALU
ALU(Arithmetic Logic Unit),算术逻辑单元。包含加法器、减法器、移位器、逻辑运算器等功能。
CISC
CISC(Complex Instruction Set Computer),复杂指令集计算机。
CSR
CSR(Control and Status Register),控制和状态寄存器。
Immediate
立即数,表示CPU能直接调用的常量数值。
ISA
ISA(Instruction Set Architecture),指令集架构。
Pseudo-instruction
伪指令,不是汇编指令,在编译的时候会被编译器转换为对应的汇编指令。
RISC
RISC(Reduced Instruction Set Computer),精简指令集计算机。
RISC-V就是一种基于精简指令集(RISC)原则的开源指令集架构(ISA)
RV32I
RV -> RISC-V 32 -> 指令集长度32bit I -> base Integer instruction set 即整数型指令集
可扩展为RV32IMAFDC
RV -> RISC-V 32 -> 指令集长度32bit I -> base Integer instruction set 即整数型指令集 M -> multiply & divide 即乘除指令 A -> atomic operations 即原子操作 F/D -> floating-point 即浮点型 C -> compressed 16-bit instructions 即压缩指令