L02 Simple Machine Implementations, Microcoding
上节课内容回顾
- 计算机架构不仅限于指令集和RTL:
- CS152课程涉及硬件和软件的交互,设计合适的抽象层。
- 技术与应用塑造计算机架构:
- 历史为未来的设计提供了重要的经验教训,理解过往的技术演进有助于应对未来挑战。
- 计算机架构的前130年:从巴贝奇到IBM 360:
- 计算机的发展从简单的计算器(无条件分支)转向完全可编程的机器。
- 尤其是在二战期间(1940年代中期),从机电设备到纯电子处理器的快速变革奠定了现代计算机的基础。
- 软件开发成本成为架构设计中的重要制约因素:
- 软件的兼容性需求极大地影响了架构设计,确保新技术能够与现有系统兼容以减少开发成本和时间。
指令集架构(ISA)
-
定义:ISA是硬件与软件之间的契约,通过描述所有程序员可见的状态(如寄存器和内存)以及操作该状态的指令的语义,定义计算机的操作方式。
-
作用:ISA为程序员提供一个抽象层,使得同一套指令可以在不同的硬件实现上运行。它本质上是机器语言的规范,定义了计算机能执行的指令。
-
历史:IBM 360是第一代将指令集架构与其具体实现(微架构)分离的机器。
- 多种实现:一个给定的ISA可以有不同的硬件实现。例如:
- 苏联曾开发出与IBM 360代码兼容的克隆机器。
- AMD、Intel和VIA的处理器都运行AMD64指令集。
- 许多智能手机使用ARM指令集架构,由包括苹果、华为在内的多家公司进行实现。
- 课程中使用的ISA:我们在课程中使用RISC-V作为标准的指令集架构,许多公司和开源项目都构建了基于RISC-V的实现。
指令集到微架构的映射
- 设计风格与微架构的关系:ISA通常是针对特定的微架构风格进行设计的。例如:
- 累加器架构:硬连线、无流水线设计。
- 复杂指令集计算(CISC):微代码控制。
- 精简指令集计算(RISC):硬连线、流水线设计。
- 超长指令字(VLIW):固定延迟、顺序执行的并行流水线。
- 灵活实现:尽管每种ISA都有典型的微架构风格,但它们可以用任何微架构风格来实现。例如:
- Intel Ivy Bridge:硬连线流水线CISC(x86)机器,带部分微代码支持。
- Apple M1:原生ARM ISA,但可以在软件中仿真x86。
- Spike:软件解释的RISC-V机器。
- 本讲将讨论一种微代码控制的RISC-V机器。
为什么学习微程序设计?
微程序设计是计算机架构中的重要概念,特别是在处理复杂指令集时。学习微程序设计有以下几个原因:
-
构建小型处理器:微程序设计展示了如何在复杂指令集下构建非常小的处理器。通过微程序控制器,能够有效地管理复杂指令的执行,而无需庞大的硬件电路。
-
理解CISC机器的起源:通过学习微程序设计,能够深入了解复杂指令集计算机(CISC)的起源及其设计理念。CISC机器以较少的指令数实现更多的功能,但每条指令可能更复杂且执行时间较长。
-
现实中的应用:微程序设计仍然被广泛应用在常见的计算机中,例如x86架构的处理器、IBM 360系列、PowerPC等。
-
机器结构入门:微程序设计为理解机器结构提供了一个温和的入门路径。通过微程序的方式,复杂指令的执行流程变得更加清晰易懂,降低了设计难度。
-
技术驱动的变革:通过学习微程序设计,能更好地理解技术如何推动了从CISC向精简指令集计算机(RISC)的转变。RISC架构是对复杂指令集的一种简化,其理念是用更简单、更快速的指令来提高处理器的性能。
值得注意的是,CISC和RISC的名称比它们所指的机器风格出现得要晚得多,早期的机器设计并没有明确的“CISC”和“RISC”分类。
控制器与数据路径
在处理器设计中,控制器与数据路径的区分是非常关键的概念。处理器的设计通常可以分为数据路径和控制器两个部分。
- 数据路径:数据路径负责存储数据并执行算术运算。它包括:
- 寄存器:存储操作数和结果的地方。
- 算术逻辑单元(ALU):进行算术和逻辑运算的部件。
- 程序计数器(PC):跟踪当前执行的指令地址。
- 指令寄存器(Inst. Reg.):保存当前正在执行的指令。
-
控制器:控制器的作用是对数据路径中的各个操作进行时序控制。它决定何时从寄存器中取数、何时开始执行ALU运算、何时存储结果等。控制器的设计是处理器设计中的一个重要挑战,尤其是在早期计算机设计中,如何正确设计控制电路成为了计算机设计者面临的最大难题。
- 微程序控制器:Maurice Wilkes在1958年发明了微程序设计的思想,用于设计EDSAC-II处理器的控制单元。这一设计概念帮助解决了复杂控制电路的问题,将控制逻辑简化为可编程的微指令序列。Wilkes的想法受到了Babbage早期设计中的“Barrel”机制的启发,这一设计理念也出现在早期的可编程计算机中。
微程序控制的CPU架构
微程序控制的CPU是一种通过使用微代码(microcode)来实现复杂指令执行的处理器架构。其核心部分由控制器和数据路径组成,具体架构如图所示:
- 主存储器(Main Memory):
- 存储由宏指令编写的用户程序(如x86、RISC-V的指令)。
- 宏指令通过地址和数据线传送到处理器中。
- 数据路径(Datapath):
- 数据路径是处理器的核心执行单元,负责执行算术逻辑运算(ALU),并处理寄存器之间的数据交换。
- 微程序计数器(μPC):
- 用于跟踪当前微指令的执行位置,每一条宏指令由多条微指令来分步完成。
- 解码器(Decoder):
- 解码宏指令并通过微程序计数器将控制信号传递到微代码存储器。
- 微代码ROM(Microcode ROM):
- 保存处理器执行微指令的固定存储区域。微指令规定了如何一步步完成复杂指令的执行。
- 微代码ROM通过控制线将信号传送给数据路径,指挥数据的处理过程。
- 控制信号(Control Lines):
- 微代码ROM产生的信号控制数据路径内的各个模块(如寄存器、ALU)的操作。
通过这种微程序控制架构,复杂的指令集可以被拆分为一系列更简单的微指令,以此来简化硬件设计并提高指令执行的灵活性。
技术影响
微代码设计的出现始于20世纪50年代,当时的硬件技术条件与今天大不相同,各种存储和逻辑实现手段在设计中扮演了重要角色。
- 逻辑元件:当时的逻辑单元主要依赖于真空管。
- 主存储器:采用的是磁芯存储,其存取速度和容量都受到了限制。
- 只读存储器(ROM):使用了二极管矩阵或穿孔金属卡片来实现。这些技术虽然原始,但可以高效地存储和访问微代码。
此外,几项技术特点对微代码的广泛应用产生了深远影响:
-
逻辑电路昂贵:相比只读存储器(ROM)或随机存取存储器(RAM),使用逻辑电路的成本高得多。因此,将复杂指令的控制逻辑转移到更便宜的微代码中成为一种高效的解决方案。
-
ROM比RAM便宜:当时的存储技术限制使得ROM比RAM更经济,因此使用ROM来存储微指令是一种可行的选择。
-
ROM速度远快于RAM:由于ROM的读取速度较快,微程序设计能够在不损失性能的前提下减少硬件复杂度,从而提高了整体系统的效率。
这些技术背景解释了微程序设计为何在当时成为复杂指令集处理器(如x86、IBM360)的主流设计选择。
RISC-V 指令集架构 (ISA)
RISC-V 是由加州大学伯克利分校开发的第五代精简指令集(RISC)设计,具有现实性和完整性,同时保持开放和小巧的特性。以下是 RISC-V ISA 的一些关键特点:
- 开放性与灵活性:RISC-V 是一种开放标准,不与特定的实现风格绑定,支持广泛的硬件实现。
- 32位与64位架构:支持 32 位(RV32)和 64 位(RV64)地址空间,适用于多处理器系统。
- 高效的指令编码:RISC-V 的指令集编码设计注重效率,使其适用于从嵌入式系统到高性能计算的各类应用。
- 扩展性强:便于教育、科研和实际工程中进行子集化和扩展。
- 行业应用:RISC-V 正逐渐获得工业界的广泛采用,许多公司和开源项目正在基于 RISC-V 构建其处理器架构。
RV32I 处理器状态
RV32I 是 RISC-V 的 32 位整数指令集变体,其处理器状态如下:
- 程序计数器 (pc):存储当前正在执行的指令的地址。
- 32 个 32 位整数寄存器(x0 - x31):
- 寄存器 x0 固定为 0,不能被更改,用于指令中的常数 0。
- 32 个浮点寄存器(f0 - f31):
- 每个浮点寄存器可以存储单精度或双精度浮点数(32 位或 64 位的 IEEE 浮点数)。
- 浮点状态寄存器 (fcsr):用于浮点操作的四舍五入模式和异常报告。
在处理器内部,整数寄存器和浮点寄存器分别用于处理整数运算和浮点运算。浮点寄存器的使用通常与高精度科学计算和多媒体处理相关联。
RISC-V 指令编码 (Instruction Encoding)
RISC-V 指令集架构 (ISA) 支持可变长度的指令格式,以灵活适应不同的系统需求。以下是 RISC-V 指令编码的关键点:
- 可变长度指令:RISC-V 支持从 16 位到 192 位及以上的指令长度,其中 32 位是最常见的基本指令长度。
- 固定的 32 位基本指令集:RV32 指令集总是使用 32 位的指令,且其最低两位固定为 11₂,用于标识基本指令。
- 分支与跳转指令的目标:所有分支与跳转指令的目标地址以 16 位为单位,尽管基本指令是 32 位长度,但这种设计优化了程序的分支处理。
RISC-V 指令格式 (Instruction Formats)
RISC-V 使用几种不同的指令格式,主要包括 R 型、I 型、S 型、B 型、U 型和 J 型,每种格式适应不同的指令功能需求。以下是不同指令格式的基本组成部分:
- R 型 (Register-Register Format):
opcode
(7 bits): 操作码,指令类型。rd
(5 bits): 目标寄存器。funct3
(3 bits): 功能码,用于区分不同的操作。rs1
,rs2
(5 bits): 源寄存器。funct7
(7 bits): 额外的功能码,进一步区分指令。
- I 型 (Immediate Format):
opcode
(7 bits): 操作码,指令类型。rd
(5 bits): 目标寄存器。funct3
(3 bits): 功能码。rs1
(5 bits): 源寄存器。imm[11:0]
(12 bits): 立即数,用于立即数操作指令。
- S 型 (Store Format):
opcode
(7 bits): 操作码,指令类型。imm[11:5]
(7 bits) 和imm[4:0]
(5 bits): 立即数部分,用于存储地址计算。rs1
,rs2
(5 bits): 源寄存器。funct3
(3 bits): 功能码。
- B 型 (Branch Format):
opcode
(7 bits): 操作码。imm[12|10:5]
,imm[4:1]
,imm[11]
,imm[20]
(13 bits): 分支地址的立即数。rs1
,rs2
(5 bits): 源寄存器。funct3
(3 bits): 功能码。
- U 型 (Upper Immediate Format):
opcode
(7 bits): 操作码。rd
(5 bits): 目标寄存器。imm[31:12]
(20 bits): 高位立即数。
- J 型 (Jump Format)
Single-Bus Datapath for Microcoded RISC-V
微代码 RISC-V 单总线数据通路
在微代码控制的 RISC-V 处理器中,单总线数据通路设计简化了数据传输的路径,使处理器各个单元通过共享总线来执行各种操作。以下是单总线数据通路的关键组成部分:
- 指令寄存器 (Instruction Reg.):存储当前的指令。
- 寄存器文件 (Register RAM):存储通用寄存器,包括程序计数器 (PC) 和用于计算的数据寄存器。
- 立即数扩展 (Immediate):从指令中提取立即数,并通过立即数选择 (ImmSel) 控制其使用。
- 算术逻辑单元 (ALU):执行算术和逻辑运算,使用源寄存器
rs1
和rs2
的数据进行计算。 - 主存储器 (Main Memory):在存储指令或数据时通过
MemW
控制数据写入,或通过MemEn
控制数据读取。
微指令 (Microinstructions)
微指令用寄存器传输形式表示,用于控制各单元的操作。例如:
MA := PC
:选择 PC 作为地址源,并设置控制信号RegSel = PC
,RegW = 0
,RegEn = 1
,MALd = 1
,表明从 PC 取地址。B := Reg[rs2]
:从源寄存器rs2
读取数据,设置控制信号RegSel = rs2
,RegW = 0
,RegEn = 1
,BLd = 1
,准备将数据送入 ALU。Reg[rd] := A + B
:通过 ALU 执行加法操作,将结果写入目标寄存器rd
,设置ALUOp = Add
,ALUEn = 1
,RegSel = rd
,RegW = 1
。
在单总线数据通路(Single-Bus Datapath)中,设计的核心思想是通过一个共享的总线来传递指令和数据。图中的每个符号和信号都对应着特定的功能,确保整个指令执行的每个步骤可以顺利完成。为了更好地理解,这里对图中涉及的寄存器、信号和控制信号进行详细讲解:
核心组件
- 指令寄存器(Instruction Register, IR):
- 作用:存储从内存中读取的当前指令,负责保存当前正在执行的指令。
- 输入信号:从内存加载的指令。
- 输出信号:指令的操作码 (Opcode) 和立即数 (Immediate) 被传递给解码器和 ALU 等后续阶段进行处理。
- 程序计数器(Program Counter, PC):
- 作用:保存当前指令的地址,同时用于计算下一条指令的地址。
- 输入信号:通常是当前 PC 值加上 4(在 RISC 架构中,大多数指令的长度为 4 个字节)。
- 输出信号:为指令地址提供到内存中,从而获取指令。
- 寄存器堆(Register File):
- 作用:包含 32 个通用寄存器,用于存储操作数和计算结果。
- 输入信号:
rs1
和rs2
:读取的两个源寄存器,分别用于 ALU 操作。rd
:写入目标寄存器,存储 ALU 或内存操作的结果。- 输出信号:
A
和B
:分别输出寄存器rs1
和rs2
中的值,输入到 ALU。控制信号和数据流向
- 立即数选择器(ImmSel):
- 作用:用于选择指令中的立即数,特别是与 R 型(寄存器类型)、I 型(立即数类型)和 J 型(跳转类型)指令相关的操作。
- 输入信号:指令的立即数字段。
- 输出信号:选择正确的立即数送到 ALU 或其他需要的单元。
- 寄存器选择器(RegSel):
- 作用:控制从寄存器堆读取的操作数,决定从寄存器
rs1
、rs2
等中读取的内容。- 输入信号:
rs1
、rs2
等。- 输出信号:将寄存器中的值传递到 ALU 或其他执行单元。
- 寄存器写信号(RegW):
- 作用:当需要将运算结果写入寄存器时,该信号为 1,允许数据写入寄存器堆中的
rd
。- 控制:由控制器根据指令类型(如算术操作或内存加载操作)来决定是否写入寄存器。
- 立即数使能(ImmEn):
- 作用:决定是否使用立即数,特别是用于 ALU 或内存地址计算时,立即数和寄存器值的结合。
- 控制:当操作需要立即数时,此信号使能;否则从寄存器读取值。
数据路径和控制单元
- ALU 操作符(ALUOp):
- 作用:决定 ALU 执行的具体算术或逻辑操作,例如加法、减法、逻辑与、或等操作。
- 输入信号:来自指令寄存器的操作码。
- 输出信号:将结果发送回寄存器堆或者用于其他阶段如内存写入等。
- ALU 使能(ALUEn):
- 作用:决定 ALU 是否激活以执行操作,主要用于算术和逻辑运算。
- 控制:该信号为 1 时,ALU 执行当前指定的操作,并将结果传递到输出寄存器。
- ALU 数据选择(ALUSel):
- 作用:决定 ALU 的操作数来源,是从寄存器还是从立即数中选择操作数。
- 控制:通过信号控制是否选择来自
rs1
、rs2
或者立即数。- 内存地址(MemAddr):
- 作用:用于访问内存的地址,通过寄存器和立即数计算得到,例如加载或存储数据时使用的地址。
- 输入信号:来自 ALU 的地址计算结果。
- 内存加载(MemLd):
- 作用:决定是否从内存中加载数据,并将其写入到寄存器堆。
- 控制:当执行
lw
(加载字)指令时,该信号使能。- 内存写(MemW):
- 作用:决定是否将寄存器中的数据写入到内存地址中。
- 控制:当执行
sw
(存储字)指令时,该信号为 1,将寄存器的内容写入到指定的内存地址。- 寄存器写使能(RegEn):
- 作用:允许 ALU 或内存操作的结果写入寄存器堆中的指定寄存器
rd
。- 控制:该信号根据指令类型(如算术运算或者从内存加载操作)控制。
RISC-V 指令执行阶段 (Instruction Execution Phases)
指令的执行过程可以划分为多个阶段,以下是 RISC-V 的基本指令执行流程:
- 取指 (Instruction Fetch):从程序计数器指定的内存地址中读取指令。
- 指令解码 (Instruction Decode):解析取出的指令,确定操作类型、源寄存器、目标寄存器等。
- 寄存器取值 (Register Fetch):从寄存器文件中读取源操作数。
- ALU 操作 (ALU Operations):根据指令类型执行算术或逻辑运算。
- 可选的存储操作 (Optional Memory Operations):对于需要访问内存的指令,执行读或写操作。
- 可选的寄存器写回 (Optional Register Writeback):将计算结果或从内存中读取的数据写回目标寄存器。
- 计算下一条指令的地址 (Calculate Next Instruction Address):更新 PC,准备执行下一指令。
微代码示例 (Microcode Sketches)
Instruction Fetch(取指)
-
MA, A := PC
:这是指令周期的起点,首先将程序计数器(PC)的当前值复制到地址寄存器(MA)和寄存器 A 中。PC 指向当前要执行的指令在内存中的地址,MA 则用于指示内存需要访问的地址。这一步确保后续内存访问能正确定位到需要取指的位置,并将当前 PC 备份在 A 中。 -
PC := A + 4
:RISC-V 处理器中,指令长度通常为固定的 4 字节,因此程序计数器每次执行后应递增 4,指向下一条指令。这一步的作用是预先为下次指令取值准备好新的 PC 地址,确保顺序执行的指令能依次从内存中取出。 -
wait for memory
:处理器通常以比内存更快的速度运行,因此需要等待内存响应。在这一阶段,处理器会暂停,直到内存返回当前 PC 所指向地址的指令值。 -
IR := Mem
:一旦内存响应完成,从内存中读取的指令被加载到指令寄存器(IR)中。IR 是一个特殊寄存器,用于临时存储当前正在执行的指令,以便后续进行解码和执行。 -
dispatch on opcode
:最后,处理器从 IR 中提取操作码(opcode),根据该操作码确定当前指令的类型,并进入相应的解码和执行流程。操作码通常指示这条指令是算术操作、逻辑操作、内存访问、跳转等。
ALU 指令
-
A := Reg[rs1]
:在 ALU 指令中,首先将源寄存器rs1
的值加载到寄存器 A 中。rs1
是指操作数来源寄存器的编号,它包含第一个操作数。 -
B := Reg[rs2]
:接着,将另一个源寄存器rs2
的值加载到寄存器 B 中。rs2
提供了第二个操作数,这两者将用于 ALU(算术逻辑单元)进行运算。 -
Reg[rd] := ALUOp(A, B)
:ALU 使用寄存器 A 和 B 中的值进行运算,具体的操作由操作码(opcode)决定,比如加法、减法、逻辑与或等。运算完成后,将结果存储到目标寄存器rd
中。 -
goto instruction fetch
:ALU 指令执行完毕后,处理器回到取指阶段,准备从 PC 指定的新地址取出下一条指令。
ALUI(带立即数的 ALU 指令)
-
A := Reg[rs1]
:类似于一般的 ALU 指令,首先将源寄存器rs1
的值加载到寄存器 A 中。 -
B := ImmI
:不同之处在于这里的第二个操作数不是寄存器值,而是一个 12 位的立即数ImmI
,该立即数扩展为与寄存器大小相同的 32 位,并加载到寄存器 B 中。立即数在指令中直接编码,适用于需要固定常量的运算。 -
Reg[rd] := ALUOp(A, B)
:ALU 使用寄存器 A 的值和扩展后的立即数 B 进行运算,运算结果存入目标寄存器rd
。 -
goto instruction fetch
:立即数运算完成后,处理器返回取指阶段,开始处理下一条指令。
LW(Load Word)
-
A := Reg[rs1]
:加载指令(如 LW)主要用于从内存中取数。首先从基址寄存器rs1
中取出基地址并存入 A,这是内存访问的基础地址。 -
B := ImmI
:符号扩展后的 12 位立即数ImmI
被加载到寄存器 B 中,作为内存地址计算的偏移量。立即数的扩展确保偏移量可以为负数,从而支持反向偏移访问。 -
MA := A + B
:基地址和偏移量相加得到实际内存地址,并存入内存地址寄存器 (MA) 中,这个地址是内存读取操作的目标地址。 -
wait for memory
:由于内存访问速度通常较慢,处理器需要等待内存响应,以确保数据已成功读取。 -
Reg[rd] := Mem
:内存响应完成后,处理器将从地址MA
读取的数据存入目标寄存器rd
。这一过程将从内存加载的值放入寄存器,供后续指令使用。 -
goto instruction fetch
:数据加载完成后,处理器进入下一个指令周期,再次从内存中取指。
JAL(Jump and Link)
-
Reg[rd] := A
:跳转并链接指令(JAL)用于跳转到目标地址,并将返回地址保存在寄存器rd
中,以便之后返回。首先,将当前 PC(保存于 A 寄存器中)的值,即返回地址,存储到目标寄存器rd
。 -
A := A - 4
:PC 在取指阶段已经递增,因此这里通过减去 4 来恢复跳转前的 PC 值。这确保了跳转指令能够正确保存返回地址。 -
B := ImmJ
:从跳转指令中提取立即数ImmJ
,作为跳转偏移量。这个立即数扩展后存储到寄存器 B 中。 -
PC := A + B
:计算新的跳转目标地址,将当前 PC 值加上立即数偏移量,并将结果存储回 PC,实现跳转。 -
goto instruction fetch
:跳转完成后,处理器回到取指阶段,继续执行新的指令。
Branch(分支指令)
-
A := Reg[rs1]
:分支指令根据条件决定是否跳转。首先,从寄存器rs1
中加载第一个操作数到 A 中,作为条件判断的第一个值。 -
B := Reg[rs2]
:然后,从寄存器rs2
中加载第二个操作数到 B 中,作为条件判断的第二个值。 -
if (!ALUOp(A, B)) goto instruction fetch
:ALU 执行比较操作,如等于或大于。如果条件不满足(即 ALU 运算结果为假),跳转至instruction fetch
,继续执行顺序指令。 -
A := PC
:如果条件满足,需要跳转时,首先将当前 PC 存储到 A 寄存器中,为后续的地址计算做准备。 -
A := A - 4
:由于 PC 在取指阶段已经递增,这一步通过减去 4 来恢复 PC,确保分支计算以正确的 PC 值为基准。 -
B := ImmB
:将分支指令中的立即数ImmB
提取出来,扩展后存储到寄存器 B 中。这个值作为跳转偏移量。 -
PC := A + B
:将恢复后的 PC 值与偏移量相加,得到跳转目标地址,并将结果存入 PC,实现条件跳转。 -
goto instruction fetch
:分支跳转完成后,处理器回到取指阶段,继续处理新的指令。
RISC-V 单总线数据通路指令流程分析
我们将使用前面讲解的微指令步骤来详细说明这一复杂流程,包括从内存加载操作数、执行加法、判断是否跳转以及写回内存的全过程。
1.
lw x1, 0(x2)
:从内存地址x2
处加载一个字到寄存器x1
取指阶段:
MA, A := PC
:将程序计数器(PC)的值加载到地址寄存器(MA)和寄存器 A 中。PC := A + 4
:更新 PC,指向下一条指令。wait for memory
:等待从内存读取指令。IR := Mem
:将内存中的指令加载到指令寄存器(IR)。dispatch on opcode
:识别为lw
加载指令。执行阶段:
A := Reg[rs1]
:将rs1
(这里为x2
)的值加载到寄存器 A 中,作为内存地址的基址。B := ImmI
:将指令中提取出的立即数(这里为0
)加载到寄存器 B 中。MA := A + B
:将基址和偏移量相加,计算内存访问的实际地址,并存入 MA。wait for memory
:等待内存返回该地址的数据。Reg[rd] := Mem
:将从内存中读取的值(x2
地址处的数据)存入目标寄存器rd
(这里为x1
)。goto instruction fetch
:取指完毕,准备下一条指令。2.
lw x3, 4(x2)
:从内存地址x2 + 4
处加载一个字到寄存器x3
取指阶段:
- 同上一条指令的取指阶段,加载
lw x3, 4(x2)
指令。执行阶段:
A := Reg[rs1]
:将rs1
(仍然是x2
)的值加载到寄存器 A 中,作为基址。B := ImmI
:将立即数4
加载到寄存器 B 中,作为偏移量。MA := A + B
:将基址x2
和偏移量4
相加,计算新的内存地址(x2 + 4
),并存入 MA。wait for memory
:等待内存从该地址返回数据。Reg[rd] := Mem
:将从内存中读取的数据存入目标寄存器rd
(这里为x3
)。goto instruction fetch
:取指完毕,准备下一条指令。3.
add x4, x1, x3
:将寄存器x1
和x3
中的值相加,结果存入寄存器x4
取指阶段:
- 与之前类似,取指执行
add
指令。执行阶段:
A := Reg[rs1]
:从寄存器x1
中读取第一个操作数,存入寄存器 A。B := Reg[rs2]
:从寄存器x3
中读取第二个操作数,存入寄存器 B。Reg[rd] := ALUOp(A, B)
:使用 ALU 进行加法运算,将 A 和 B 相加,结果存入寄存器x4
。goto instruction fetch
:加法运算完成,准备执行下一条指令。4.
beq x4, x0, label
:若x4
的值为零,则跳转到label
处取指阶段:
- 取指阶段执行
beq
指令。执行阶段:
A := Reg[rs1]
:从寄存器x4
中加载比较的第一个操作数(加法结果)。B := Reg[rs2]
:从寄存器x0
中加载第二个操作数(0
),存入寄存器 B。if (!ALUOp(A, B)) goto instruction fetch
:ALU 进行比较,如果 A 和 B 相等(即x4 == 0
),执行条件跳转,跳转到label
所指的地址。A := PC
:如果满足条件,保存当前 PC。A := A - 4
:恢复 PC 值,因为取指时 PC 已递增。B := ImmB
:将分支型立即数ImmB
加载到 B。PC := A + B
:将恢复的 PC 和分支偏移量相加,计算跳转地址,更新 PC。goto instruction fetch
:准备下一条指令,执行跳转。5.
sw x4, 8(x2)
:将寄存器x4
的值存入内存地址x2 + 8
处取指阶段:
- 执行
sw
指令的取指阶段。执行阶段:
A := Reg[rs1]
:从寄存器x2
中读取基地址,存入寄存器 A。B := ImmI
:将立即数8
加载到寄存器 B 中,作为偏移量。MA := A + B
:将基址和偏移量相加,得到存储的内存地址,并存入 MA。Mem := Reg[rs2]
:将寄存器x4
的值存入内存MA
指定的地址。goto instruction fetch
:存储操作完成后,回到取指阶段,准备执行下一条指令。完整流程总结
- 加载两个操作数:通过两条
lw
指令,先后从内存x2
处和x2 + 4
处加载操作数到寄存器x1
和x3
。- 执行加法:使用
add
指令,将寄存器x1
和x3
中的值相加,结果存入寄存器x4
。- 条件判断:通过
beq
分支指令,判断加法结果x4
是否为零。如果结果为零,跳转到label
处继续执行。- 存储结果:如果结果不为零,使用
sw
指令,将寄存器x4
的值存入内存地址x2 + 8
处。这一系列指令实现了从内存加载数据、执行运算、条件跳转和数据存储的复杂操作,展示了 RISC-V 指令集的典型使用流程和数据流动的具体过程。