早上好啊大伙,上一期我们将指令按照格式分成4类,然后我们这一期来看看,这些不同的格式在执行中会有哪些不同。
事先说明,这一期个人感觉很重要,这一期的知识储备决定你后面能不能知道怎么去设计一个CPU
这一期我们就根据指令的处理流程,大概梳理一下一个CPU所需要的一些模块,然后下一期来设计我们的CPU。
在前面我们知道一条指令的运行需要经过四个阶段:
取指 译码 执行 写回
取指(Fetch)阶段是指令执行流水线的第一步,负责从内存中读取指令并更新程序计数器(PC)。
程序计数器(PC,Program Counter)
存储当前正在执行的指令地址,是取指阶段的核心寄存器。
RISC-V 指令长度固定为32 位(RV32I)或 64 位(RV64I),因此指令地址需按4 字节(RV32)或 8 字节(RV64)对齐(即 PC 最低两位 / 三位为 0)。
指令存储器(IMEM,Instruction Memory)
只读存储器,存储程序的机器指令,通过 PC 地址读取指令。
PC 增量逻辑
顺序执行时:每取一条指令,PC 自动递增一个指令长度(RV32:PC += 4;RV64:PC += 8)。
分支 / 跳转时:新 PC 值由指令执行结果决定(见下文 “分支指令处理”)。
将 PC 的值作为地址,从 IMEM 中读取 32 位(RV32I)或 64 位(RV64I)的指令,存入指令寄存器(IR,Instruction Register)。
默认情况(无分支):
PC_next = PC_current + 指令长度(如 RV32I 中PC_next = PC + 4)。
分支 / 跳转指令:
新 PC 值在 ** 执行阶段(Execute)** 计算完成后,反馈到取指阶段(需处理流水线冒险,见下文)。
若取指时发生异常(如地址未对齐、访问越权),CPU 会:
将当前 PC 保存到mcause或mepc寄存器(取决于异常类型)。
PC 跳转至异常向量表地址(由mtvec寄存器指定),开始执行异常处理程序。
RISC-V 的分支指令分为两类:条件分支(如beq、bne)和无条件跳转(如jal、jalr),其取指逻辑略有不同。
根据处理阶段我们需要准备 **PC(下一个需要处理的指令地址), NPC(PC的前置运算), IM(指令寄存器)**三个模块。
且此部分我们主要只处理PC 读取指令并更新地址,核心逻辑围绕顺序执行和分支跳转
而对于如何判断应该是顺序执行还是分支跳转并不在这部分来进行判断。
负责解析从取指阶段获取的机器指令,提取操作码(opcode)、寄存器编号、立即数等信息,并生成控制信号以指导后续阶段的操作。
RISC-V 指令集采用模块化设计,核心指令分为 7 种基本格式:
R 型(寄存器 - 寄存器):如add x1, x2, x3(x1 = x2 + x3)
I 型(立即数):如addi x1, x2, 10(x1 = x2 + 10)
S 型(存储):如sw x1, 20(x2)(将x1存入x2 + 20地址)
B 型(分支):如beq x1, x2, label(若x1 == x2则跳转)
U 型(长立即数):如lui x1, 0x12345(加载高 20 位立即数)
J 型(跳转):如jal x1, label(跳转并链接)
压缩指令(C 型):16 位长度,用于提高代码密度(RV32C/RV64C 扩展)
所有格式的最低 7 位均为 opcode,用于标识指令类型。
具体每条指令的格式,可以回看前面的文章。
操作码(opcode):确定指令类型(如算术、加载存储、分支等)。
功能码(funct3/funct7):配合 opcode 进一步细分指令。
从 ** 寄存器堆(Register File)** 中读取源操作数寄存器(如rs1、rs2)的值。
RISC-V 有 32 个通用寄存器(x0~x31),其中x0固定为 0。
控制信号用于指导后续阶段的操作,例如:
ALU 控制信号:指定 ALU 执行的操作(加法、减法、逻辑与等)。
存储器控制信号:控制加载 / 存储操作(如lw/sw)。
分支控制信号:决定是否跳转。
寄存器写使能:控制是否将结果写回寄存器。
根据指令格式提取并扩展立即数(如符号扩展、零扩展)。
不同格式的立即数编码方式不同(如 I 型取 12 位,S 型分为两部分拼接)。
不同指令格式的立即数生成方式:
I 型:imm[11:0]直接符号扩展为 32 位。
S 型:imm[11:5](位 31-25)和imm[4:0](位 11-7)拼接后符号扩展。
B 型:imm[12|10:5|4:1|11](位 31、30-25、11-8、7)拼接,左移 1 位后符号扩展。
U 型:imm[31:12](位 31-12)直接补零扩展到 32 位(用于lui、auipc)。
J 型:imm[20|10:1|11|19:12](位 31、30-21、20、19-12)拼接,左移 1 位后符号扩展。
控制单元生成的关键控制信号包括:
ALU 操作控制:
决定 ALU 执行的运算(如 ADD、SUB、AND、OR 等)。
例如,add指令对应 ALU 加法操作,and指令对应逻辑与操作。
存储器访问控制:
MemRead:控制是否从内存读取数据(如lw指令)。
MemWrite:控制是否向内存写入数据(如sw指令)。
MemSize:指定访问字节数(字节 / 半字 / 字)。
寄存器写控制:
RegWrite:控制是否将结果写回寄存器。
RdSelect:选择写回数据的来源(ALU 结果、内存数据、PC 值等)。
分支控制:
Branch:是否为分支指令。
BranchType:分支类型(如 BEQ、BNE、BLT 等)。
指令寄存器(IM):
存储从取指阶段获取的指令。
寄存器堆(Register File):
双读端口:同时读取rs1和rs2的值。
单写端口:在写回阶段将结果写入rd寄存器。
控制单元(Control Unit):
基于 opcode、funct3、funct7 生成控制信号。
可通过硬布线逻辑或微程序实现。
立即数生成器(Immediate Generator):
根据指令格式提取并扩展立即数。
整理一下,想要完成这个部分的功能,我们需要去准备 模块:
IM(指令寄存器)、RF(寄存器堆)、Control(判断指令,然后给出信号)、Imm_Gen(立即数拓展)
RISC-V 的译码阶段通过解析指令格式、读取寄存器、生成控制信号和立即数,为后续执行阶段做准备。
执行(Execute)阶段是指令执行流水线的第三步,负责完成指令指定的运算操作。该阶段利用译码阶段生成的控制信号和操作数,通过算术逻辑单元(ALU)或其他功能单元执行具体运算,并处理分支跳转的目标地址计算。
ALU 运算
执行算术运算(如加法、减法)、逻辑运算(如与、或、非)、移位运算等。
运算类型由译码阶段的控制信号决定。
分支目标地址计算
对于分支指令(如beq、jal),计算跳转目标地址。
分支条件判断(如比较寄存器值是否相等)。
内存地址计算
对于加载 / 存储指令(如lw、sw),计算内存访问的有效地址。
有效地址 = 基址寄存器值(如rs1) + 立即数偏移量。
生成执行结果
将运算结果或分支判断结果传递给后续阶段(如访存或写回)。
算术逻辑单元(ALU)
功能:执行各类运算,支持多种操作(如 ADD、SUB、AND、OR、SLL 等)。
输入:
操作数 1:通常来自寄存器rs1或 PC(如分支指令)。
操作数 2:来自寄存器rs2或立即数(由译码阶段生成)。
输出:运算结果。
分支目标计算单元
功能:计算分支指令的目标地址。
公式:
目标地址 = PC + 符号扩展(立即数 << 1)
(RISC-V 分支指令的立即数最低位为 0,需左移 1 位补全地址)。
条件判断单元
功能:判断分支条件是否成立(如beq需比较rs1和rs2是否相等)。
输出:布尔值,表示是否跳转。
多路选择器(MUX)
选择 ALU 的操作类型(如加法、减法)。
选择 PC 的下一个值(顺序执行或分支跳转)。
ALU 运算结果:
用于算术指令(如add)、内存地址计算(如lw)或分支目标地址计算(如beq)。
分支判断结果:
布尔值,表示分支条件是否成立(如beq中rs1是否等于rs2)。
用于决定是否更新 PC 为分支目标地址。
算术指令(add x1, x2, x3)
操作数:rs1 = x2,rs2 = x3。
ALU 操作:执行加法(x2 + x3)。
结果:将加法结果存入rd = x1。
分支指令(beq x1, x2, label)
操作数:rs1 = x1,rs2 = x2,立即数imm(跳转偏移量)。
步骤:
计算目标地址:target = PC + 符号扩展(imm << 1)。
判断条件:若x1 == x2,则将 PC 更新为target;否则继续执行PC + 4。
加载指令(lw x5, 8(x10))
操作数:rs1 = x10,立即数8。
步骤:
计算地址:address = x10 + 8。
后续访存阶段从该地址读取数据到x5。
整理一下,想要完成这个部分的功能,我们需要去准备 模块:
ALU(指令执行)、MUX(多路选择器)
RISC-V 的执行阶段通过 ALU 和其他功能单元完成指令的核心运算,包括算术逻辑操作、分支目标地址计算和内存地址生成。
写回(Write Back)阶段是指令执行流水线的最后一步,负责将执行结果(如 ALU 运算结果或内存读取数据)写回目标寄存器。
从以下来源中选择需要写回寄存器的数据:
ALU 运算结果(如算术、逻辑运算结果)。
内存读取数据(如lw指令从内存加载的数据)。
PC 值(如jal/jalr指令需将下一条指令地址写入ra寄存器)。
将选定的数据写入目标寄存器(rd)。
RISC-V 规范中,x0寄存器恒为 0,写入x0的操作会被忽略。
若指令执行过程中发生异常(如访存越界),需将异常信息写入专用寄存器(如mcause、mtval)。
寄存器堆(Register File)
双读端口:在译码阶段读取rs1和rs2。
单写端口:在写回阶段写入rd。
写使能控制:由译码阶段生成的RegWrite信号控制是否写入。
多路选择器(MUX)
选择写回数据的来源(ALU 结果、内存数据、PC 值等)。
控制逻辑
接收译码阶段的控制信号(如RegWrite、RdSelect),指导写回操作。
算术指令(add x1, x2, x3)
数据来源:ALU 运算结果(x2 + x3)。
操作:
RegWrite = 1,RdAddr = 1(对应x1)。
将 ALU 结果写入寄存器堆的x1位置。
加载指令(lw x5, 8(x10))
数据来源:内存读取数据(地址为x10 + 8)。
操作:
RegWrite = 1,RdAddr = 5(对应x5)。
将内存数据写入寄存器堆的x5位置。
跳转指令(jal x1, label)
数据来源:PC 值(PC + 4)。
操作:
RegWrite = 1,RdAddr = 1(对应x1)。
将PC + 4写入寄存器堆的x1位置,同时 PC 更新为跳转目标地址。
这部分的模块在前面都已经有了,在实现的时候只需要的把线连上就行了。
RISC-V 的写回阶段通过将运算结果或内存数据写回寄存器,完成指令对处理器状态的修改。其设计简洁高效,通过控制信号灵活选择数据来源,并与寄存器堆交互。
这一期主要是说了指令执行的过程,然后对于每个过程所需要的模块及功能,对于从零开始学CPU的FPGA设计最难的部分就在这里了,很难去想象这件事情。
所以慢慢来,多尝试几次就会有自己的经验,等真正自主实现一个CPU,后面就都会很顺利,这就是就是难入门的知识,后面有方向学习过程就不会和刚开始那么难熬了。