Featured image of post Risc-V 基本知识

Risc-V 基本知识

Risc-V 基本架构及指令集

本人数字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架构所需的实际指令数量更少:

  1. 恒定的0寄存器:任何需要0时,直接从x0中取。
  2. 复制寄存器:如将x6复制到x5时,可以直接add x5, x6, x0,将x6+0存入x5。
  3. 丢弃一个结果:如果只需要一个指令带来的其他效益而不需计算结果,可以将结果直接写入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的整数倍)。对齐式内存访问对硬件而言处理逻辑最为简洁。

映射:

RV32指令对照表
  • 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 即压缩指令

使用 Hugo 构建
主题 StackJimmy 设计