本文内容深度参考了计算机体系结构领域的经典著作——《计算机组成与设计:硬件/软件接口》(Computer Organization and Design,简称 COAD)。
在学习 CPU 设计的过程中,我发现书中对数据通路的刻画极为精妙,但也存在一定的理解门槛。因此,我结合书中的核心理论,尝试用更直观的视角(如 3D 逻辑拆解、信号流向追踪等)将这一套通用 RISC 处理器的底层逻辑进行了重新梳理。希望这篇笔记能帮助同样在研读此书或对底层架构感兴趣的同学,更丝滑地跨越从“指令字”到“硬件实现”的这道坎。

区分组合单元状态单元

  • 组合单元:没有记忆,不受控于时钟,比如用于加工数据
  • 状态单元:有记忆,受控于时钟,比如用于存储数据

设计数据通路的第一个元素需要哪三个部件?

① 存储单元:存储指令

② 程序计数器:保存当前指令地址

③ 加法器:计算PC值指向下一个地址

什么是 R型、I型、J型指令?

  • R型:(Register - 寄存器型) :

    所有参与者都在 CPU 内部寄存器里。通常是两个寄存器里的数做运算,结果存入第三个寄存器。

  • I 型:(Immediate - 立即数型):
    包含了一个具体的常数(立即数),或者是涉及到 CPU 与内存之间的数据交换。

  • J型:(Jump - 跳转型):专门用于无条件地跳向一个非常远的地址。

R型指令需要读两个寄存器,对它们的内容进行ALU操作,再写回结果,如何理解读两个寄存器?

  • R 型指令通常是 OP dest, src1, src2。读的是 src1src2,写的是 dest。在 CPU 内部,有一个专门存放通用寄存器的小型存储阵列,叫做寄存器堆(Register File)。

  • 读两个:意味着这个存储阵列拥有两个独立的读取端口
    在这里插入图片描述

为什么可以在同一时钟周期内读出写入同一寄存器

想象一下,第一条指令要把结果存入 R1,第二条指令紧接着要从 R1 读数,硬件通过**时钟边沿(Edge-triggered)**巧妙地解决了这个问题:

  • 写入(Write)发生在“上升沿”:当周期开始的瞬间,电平一跳变,前一条指令的结果就“咔哒”一声锁存进了寄存器。
  • 读取(Read)是“电平触发/组合逻辑”:只要寄存器里的值定下来了,读电路(组合逻辑)就能在剩下的半个周期里把新值传输出去。

简要描述 beq 指令是如何处理分支的?

当分支条件为真时,则分支目标地址成为新的PC,成为分支发生;当分支条件为假时,则PC自增后取代当前PC(像其他普通指令执行一样),无事发生。

存储指令和取数指令除了需要寄存器堆和ALU外,为什么还需要“符号扩展单元”?

ALU(算术逻辑单元)是一个 32 位的加工厂。它的两个输入端必须都是 32 位的导线。但是,像 lw(取数)或 addi(立即数加法)这类指令,它们自带的偏移量或常数只有 16 位。所以需要把 16 位补齐成 32 位,才能送入 ALU。

符号扩展单元所扩展的对象通常是什么?

  • 16位立即数字段: 在执行诸如 addi(加立即数)、slti(小于立即数则置位)等算术逻辑指令时,指令中包含的16位常数需要扩展为32位才能与寄存器中的32位数进行运算。
  • 16位地址偏移量: 在执行 lw(取字)、sw(存字)或 beq(相等则分支)等指令时,指令中给出的16位偏移地址(offset)需要扩展为32位,以便与基址寄存器相加计算出目标内存地址。

符号扩展单元 是怎么实现扩展的?

符号扩展的规则非常简单:看最高位(符号位)。如果最高位是 0 就补 0,是 1 就补 1。

场景 A:正数(最高位是 0)

假设你要加一个常数 7

  • 16 位表示0000 0000 0000 0111
  • 符号扩展到 32 位:前面全部补 0。
  • 结果0000 0000 0000 0000 0000 0000 0000 0111(值依然是 7)

场景 B:负数(最高位是 1)

假设你要减一个数,即加一个负数 -2(补码表示)。

  • 16 位表示1111 1111 1111 1110(最高位是 1,代表负数)
  • 符号扩展到 32 位:前面全部补 1。
  • 结果1111 1111 1111 1111 1111 1111 1111 1110(值依然是 -2)

为什么补 1? 如果补 0,它就会变成一个巨大的正数,改变了计算的本意。

为什么在这种 MIPS 分支指令中,要先进行符号扩展再左移 “2 位”?

左移两位意味着二进制地址的最后两位永远是 00(地址是4的倍数)

要理解为什么地址必须是 4 的倍数,我们需要从**“内存的编址方式”和“指令的对齐”**这两个层面来看。

1. 内存是按“字节”编址的

计算机内存就像一排排的小抽屉,每个抽屉(地址)只能放 **1 字节(8 bits)**的数据。

  • 地址 0 放 1 字节
  • 地址 1 放 1 字节
  • 地址 2 放 1 字节
  • 地址 3 放 1 字节
  • … 以此类推。

2. MIPS 指令是“固定长度”的

MIPS 设计者为了让硬件简单高效,规定所有指令必须正好是 32 位(也就是 4 字节)

如果你要把一条指令放进内存,它会占掉 4 个连续的抽屉

  • 第一条指令:占用地址 0, 1, 2, 3。它的起始地址(也就是 CPU 寻找它的地址)是 0
  • 第二条指令:紧接着放,占用地址 4, 5, 6, 7。它的起始地址是 4
  • 第三条指令:占用地址 8, 9, 10, 11。它的起始地址是 8

3. 为什么不能从地址 1, 2 或 3 开始?

想象一下,如果 CPU 允许从地址 1 开始取一条 32 位的指令:

  1. 物理上的麻烦:内存硬件通常是按“块”读取的(比如一次读 4 字节)。如果要读地址 1 开始的 4 字节(即 1, 2, 3, 4),硬件可能需要先读出 0-3 这一块,再读出 4-7 这一块,然后把它们拼凑起来。这会让 CPU 变慢。
  2. 逻辑上的混乱:如果指令可以随便放,PC(程序计数器)每次加 1 可能会指到一条指令的“肚子”中间,取出来的就是半条指令 A 和半条指令 B,这根本没法运行。

4. 结论:强制对齐

为了追求极致的速度,MIPS 规定:

指令必须“对齐”存放。

这意味着:

  • 地址只能是 0,4,8,12,16…0, 4, 8, 12, 16 \dots0,4,8,12,16
  • 这些数字在二进制下有一个共同特征:最后两位永远是 00
    • 0: ...0000
    • 4: ...0100
    • 8: ...1000
    • 12: ...1100

总结

“地址必须是 4 的倍数”是为了让硬件取指令时能“一刀准”。

既然最后两位一定是 00,MIPS 就把这多余的信息从指令码里抠掉,把宝贵的位数省下来去表示更远的跳转距离。


⭐⭐⭐ 如何让一套硬件(一个寄存器堆 + 一个 ALU)既能跑 R 型(R型指令)运算,又能跑 I 型(I型指令)访存?

自己理解:R型和I型在数据通路上涉及到的组件不相同,R型指令不需要涉及数据存储器;而I型指令需要设计数据存储器,另外还需要一个符号扩展器。只需要在经过相同的组件后通过多路选择器将其”**分流“**即可实现”同套设备支持两套指令”。

在这里插入图片描述

设计思路拆解

我们要解决两个核心“冲突”,每个冲突都需要一个 MUX 来化解:

1. ALU 第二个输入的来源冲突

  • R 型指令:需要两个寄存器值。ALU 的第二个输入应来自 Read Data 2
  • 访存指令 (lw/sw):需要计算基址地址。ALU 的第二个输入应来自指令里的 16 位立即数(经过符号扩展)。
  • 解决方案:在 ALU 的第二个输入端加一个 MUX (ALUSrc)
    • 选 0:来自寄存器(R 型)。
    • 选 1:来自符号扩展后的立即数(I 型)。

2. 写回寄存器堆的数据来源冲突

  • R 型指令:计算完就结束了。写回的数据来自 ALU 的计算结果
  • 取数指令 (lw):写回的数据来自 数据存储器 (Data Memory)
  • 解决方案:在寄存器堆的写入端(Write Data)加一个 MUX (MemtoReg)
    • 选 0:来自 ALU(R 型)。
    • 选 1:来自存储器(lw)

“如何用同一套硬件适配不同的操作数来源”
我们可以从指令格式、数据通路和硬件成本三个层面来分析。


1. R型 vs. I型:本质区别

特性 R 型 (Register) I 型 (Immediate)
操作数来源 全是寄存器(两个源寄存器) 一个寄存器 + 一个立即数
指令格式 `op rs
典型代表 add, sub, and addi, lw, sw, beq
目的寄存器 明确由 rd 字段指定 结果通常存入 rt 字段

2. 为什么在设计题中要将两者区分?

在那道设计题中,如果不区分这两者,CPU 就无法正常工作。区分它们是为了解决**“硬件冲突”**:

A. 解决 ALU 输入端的“二选一”冲突

  • R 型:需要把从寄存器堆读出的两个数据(Read Data 1 和 Read Data 2)都送进 ALU。
  • I 型:只需要一个寄存器数据(Read Data 1),另一个输入必须是指令里的那个 16 位立即数(经过符号扩展)。
  • 映射到硬件:必须在 ALU 的第二个输入端加一个 MUX(多路选择器),由控制信号 ALUSrc 来决定:现在是该听寄存器的,还是该听指令里立即数的。

B. 解决“写给谁”的冲突(寄存器写地址)

  • R 型:结果写回机器码第 15∼1115 \sim 111511 位定义的 rd 寄存器。
  • I 型:比如 lwaddi,结果要写回机器码第 20∼1620 \sim 162016 位定义的 rt 寄存器。
  • 映射到硬件:必须在寄存器堆的“写寄存器编号”输入端加一个 MUX,由 RegDst 信号控制。

C. 解决“写什么”的冲突(寄存器写数据)

  • R 型:写回的数据直接来自 ALU 的计算结果
  • I 型 (仅指 lw):写回的数据来自 Data Memory(数据存储器)
  • 映射到硬件:在写回路径上加一个 MUX,由 MemtoReg 信号控制。

3. 408 计组中的核心考向

在 408 的大题里,考官最喜欢考的就是这三个 MUX 的状态。
场景模拟:
如果现在要执行指令 addi $t1, $t2, 5

  1. ALUSrc 必须选 1(选择立即数 5,而不是读出来的寄存器值)。
  2. RegDst 必须选 0(把结果存入第 20∼1620 \sim 162016 位指定的 $t1)。
  3. MemtoReg 必须选 0(存入 ALU 算出来的和,而不是从内存读数)。

一个指令(指令字(Instruction Word)的物理构成)中都包含哪些字段?其中的ALUOpfunct 字段是什么?

R 型指令(如 add —— 纯自给自足
  • Opcode (6位):包裹的封皮,告诉你这是 R 型。
  • rs, rt, rd (各5位):三个寄存器的编号。
  • shamt (5位):位移量(做位移指令用)。
  • funct (6位)指令自带! 具体的“操作说明书”(加还是减)。
I 型指令(如 lw, beq —— 外部协作
  • Opcode (6位):告诉你这是 lwbeq
  • rs, rt (各5位):源寄存器和目标/比较寄存器。
  • Immediate (16位)指令自带! 立即数、偏移量或常数。
J 型指令(如 j —— 目标导向
  • Opcode (6位):告诉你是跳转指令。
  • Address (26位)指令自带! 跳转目标的局部地址。

ALUOp字段 呢?

ALUOp 是由主控制单元生成的内部控制信号,通常为 2 位,它并不直接存在于原始指令字中,而是指令译码后的产物,严格来说每种指令(R型、I型、J型)都会生成 ALUOp,与其说是生成,不如说是指令经过数据通路时**,主控制器产生的一套控制信号中的固定成员**

funct字段呢?

funct 字段物理上位于 R 型指令字的 第 0 到 5 位,共占用 **6 位,**在 MIPS 指令集中,funct 字段物理上仅存在于 R 型指令的结构中。

问:图中说:“当ALUOp 为 00 或 01 时,ALU 的动作不依赖于funct字段”,是否意味着**存取指令(00)分支指令(01)**不依赖于funct,而funct字段用于设置ALU的控制信号,所以R型指令依赖ALU的控制信号,存取指令和分支指令不依赖于控制信号?

在这里插入图片描述

  • : 所有指令(包括存取和分支)都百分之百依赖 ALU 的控制信号

    • 存取指令(lw/sw)依赖加法信号。这个信号是由主控根据 Opcode 直接硬性指派的。
    • 分支指令(beq)依赖减法信号。同样由主控硬性指派。
    • R型指令依赖多种信号。因为 Opcode 相同,主控指派不了,只能委派给 funct 字段去查表决定,funct 字段决定设置ALU控制信号是加、减、与、或还是小于则置位信号

    总结: 大家都要看控制信号,只是 lw/beq 走的是“专线直达”,而 R 型指令走的是“分级转接”。

关于三种指令字段详解

三种指令物理结构的“领土划分”

在这里插入图片描述

为了让你在 408 考试中不掉进陷阱,我们需要从“物理空间”的角度来看看 32 位指令是如何被“瓜分”的。


1. 指令物理结构的“领土划分”

想象 32 位指令是一个长度固定的长条。由于总长度只有 32 位,每种类型的指令必须根据需求来分配这有限的空间。

R 型指令:功能复杂,需要 funct

R 型指令需要指定 3 个寄存器(rs, rt, rd),还要预留位移量(shamt)。

  • 布局Opcode(6) + rs(5) + rt(5) + rd(5) + shamt(5) + funct(6)
  • 物理存在:最后的 6 位被专门定义为 funct 字段。
I 型指令:需要大空间放立即数

I 型指令(如 lw, sw, beq)需要放一个 16 位的立即数。

  • 布局Opcode(6) + rs(5) + rt(5) + Immediate(16)
  • 物理真相:原本 R 型指令中放 rdshamtfunct 的那块地盘(共 5+5+6=165+5+6=165+5+6=16 位),在 I 型指令里被整块强征了,用来存放 Immediate
J 型指令:需要极大空间放目标地址
  • 布局Opcode(6) + Address(26)
  • 物理真相:除了开头的 Opcode,剩下的 26 位全部上交,用来存地址。

⭐⭐⭐ 在单周期处理器设计中,硬件如何做到只用一套公共的数据通路,就能兼容并蓄地执行 R 型、I 型和分支等多种逻辑完全不同的指令?

如图:

在这里插入图片描述

自己理解:先看指令

[31-26] 位 :这指的是32位机器指令的最高 6 位,在MIPS架构中,这部分被称为Opcode(操作码),这6位的值能够决定7位个控制信号流入不同的组件时候是有效还是无效。这步又回到了 ⭐⭐⭐ 如何让一套硬件(一个寄存器堆 + 一个 ALU)既能跑 R 型(R型指令)运算,又能跑 I 型(I型指令)访存? 这部分讨论的问题。

  • 容易混淆的点:

    1. 指令的高 6 位 ([31:26]):这是 Opcode,所有指令都有,由主控制器处理。
    2. 指令的低 6 位 ([5:0]):这是 funct,只有 R 型指令有,由 ALU 控制单元处理。

    在电路图中:

    • 指向 “主控制单元” 的是 高 6 位(Opcode)。
    • 指向 “ALU 控制” 气泡的是 低 6 位(funct)。

[25-0] 位:剩下的部分。根据 Opcode 的不同,这些位会被拆分成不同的字段,比如源寄存器编号、目标寄存器编号、立即数或偏移量。

  • 附:这 7 个控制信号有效/无效时候的含义:

在这里插入图片描述

原理拆解

在这里插入图片描述

可以通过以下四个步骤拆解其工作原理:


1. 通用的“车头”:取指阶段 (Instruction Fetch)

无论是什么指令(R型、I型还是分支),第一步都是一样的:

  • PC 寄存器输出地址,从指令存储器中取出 32 位的指令码。
  • 同时,PC 会通过最上方的加法器(Add)自动加 4,指向下一条指令的地址。
  • 这部分是完全公共的,不分指令类型。

2. 中央调度站:指令译码与控制信号 (Decoding)

当指令码取出来后,它会被拆解成不同的位段:

  • 指令 [31-26](Opcode):进入控制单元(图中中间的椭圆)。这就是“大脑”,它识别出当前是 addlw 还是 beq
  • 控制单元的输出:它会瞬间产生一排 0 和 1 的信号(如 RegWriteALUSrcMemRead 等)。这些信号就像开关,决定了数据流向哪条支路。

3. 关键的“交通岔路口”:多路选择器 (Mux)

这是实现“兼容”的核心。图中标记了几个关键的 Mux,它们决定了不同指令的差异化路径:

A. 写哪个寄存器?(RegDst 信号)

  • R型指令(如 add $rd, $rs, $rt):目标寄存器是 bits [15-11]
  • I型指令(如 lw $rt, offset($rs)):目标寄存器是 bits [20-16]
  • 兼容方式:通过左侧第一个 Mux,由 RegDst 信号控制。如果是 R 型,选下面的路;如果是 I 型,选上面的路。

B. ALU 的第二个操作数来自哪里?(ALUSrc 信号)

  • R型指令:第二个操作数来自寄存器堆输出的数据2(Read data 2)。
  • I型指令(如 lwaddi):第二个操作数是指令中自带的16位立即数(经过符号扩展变为32位)。
  • 兼容方式:通过 ALU 前面的那个 Mux。如果是 R 型,ALUSrc=0 选寄存器;如果是 I 型,ALUSrc=1 选立即数。

C. 写回寄存器的是什么数据?(MemtoReg 信号)

  • 计算指令(如 add):写回的是 ALU 的计算结果
  • 访存指令(如 lw):写回的是 数据存储器 读出的内容。
  • 兼容方式:最右侧的 Mux。通过 MemtoReg 信号决定是把“工厂加工的(ALU)”还是“仓库搬运的(Memory)”数据存入寄存器。

4. 逻辑分流:分支指令的特殊性 (Branch)

对于 beq(相等则跳转)指令,它需要同时做两件事:

  1. 计算比较:利用 ALU 做减法,看结果是否为 0(零标志 Zero 为 1)。
  2. 计算目标地址:利用下方的“符号扩展”和“左移2位”,加上当前的 PC 值。
  • 兼容方式:图中最右上角的那个与门(AND Gate)。只有当 Branch 信号有效且 ALU 输出 Zero 为真时,右上角的 Mux 才会切换,让 PC 跳转到目标地址,否则继续顺序执行(PC+4)。

总结:它是如何做到“兼容”的?

我们可以用下表概括不同指令在同一通路上的“开关”状态:

指令类型 ALUSrc (ALU来源) MemtoReg (写回来源) RegWrite (是否写寄存器) 行为描述
R型 (add) 0 (寄存器) 0 (ALU结果) 1 (是) 两个寄存器相加,存回寄存器
Load (lw) 1 (立即数) 1 (内存数据) 1 (是) 寄存器加偏移量找地址,读内存存回寄存器
Store (sw) 1 (立即数) X (无所谓) 0 (否) 寄存器加偏移量找地址,把数据写入内存
Branch (beq) 0 (寄存器) X (无所谓) 0 (否) 两个寄存器做减法,若为0则改变PC

核心结论:

硬件并没有为每种指令造一套新电路,而是造了一套包含所有可能路径的“全集”电路。每条特定的指令在执行时,通过控制信号激活其中一部分路径,并屏蔽(或忽略)不相关的部分。这就是单周期处理器用“一套公共数据通路”解决所有问题的奥秘。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐