《计算机组成原理》从零设计 CPU:深度拆解现代 RISC 处理器的通用数据通路与控制逻辑
本文内容深度参考了计算机体系结构领域的经典著作——《计算机组成与设计:硬件/软件接口》(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。读的是src1和src2,写的是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 位的指令:
- 物理上的麻烦:内存硬件通常是按“块”读取的(比如一次读 4 字节)。如果要读地址 1 开始的 4 字节(即 1, 2, 3, 4),硬件可能需要先读出 0-3 这一块,再读出 4-7 这一块,然后把它们拼凑起来。这会让 CPU 变慢。
- 逻辑上的混乱:如果指令可以随便放,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
- 0:
总结
“地址必须是 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 1115∼11 位定义的
rd寄存器。 - I 型:比如
lw或addi,结果要写回机器码第 20∼1620 \sim 1620∼16 位定义的rt寄存器。 - 映射到硬件:必须在寄存器堆的“写寄存器编号”输入端加一个 MUX,由
RegDst信号控制。
C. 解决“写什么”的冲突(寄存器写数据)
- R 型:写回的数据直接来自 ALU 的计算结果。
- I 型 (仅指 lw):写回的数据来自 Data Memory(数据存储器)。
- 映射到硬件:在写回路径上加一个 MUX,由
MemtoReg信号控制。
3. 408 计组中的核心考向
在 408 的大题里,考官最喜欢考的就是这三个 MUX 的状态。
场景模拟:
如果现在要执行指令 addi $t1, $t2, 5:
- ALUSrc 必须选 1(选择立即数 5,而不是读出来的寄存器值)。
- RegDst 必须选 0(把结果存入第 20∼1620 \sim 1620∼16 位指定的
$t1)。 - MemtoReg 必须选 0(存入 ALU 算出来的和,而不是从内存读数)。
一个指令(指令字(Instruction Word)的物理构成)中都包含哪些字段?其中的ALUOp 和 funct 字段是什么?
R 型指令(如 add) —— 纯自给自足
- Opcode (6位):包裹的封皮,告诉你这是 R 型。
- rs, rt, rd (各5位):三个寄存器的编号。
- shamt (5位):位移量(做位移指令用)。
- funct (6位):指令自带! 具体的“操作说明书”(加还是减)。
I 型指令(如 lw, beq) —— 外部协作
- Opcode (6位):告诉你这是
lw或beq。 - 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 型指令走的是“分级转接”。 - 存取指令(lw/sw):依赖加法信号。这个信号是由主控根据
关于三种指令字段详解
三种指令物理结构的“领土划分”

为了让你在 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 型指令中放
rd、shamt和funct的那块地盘(共 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型指令)访存? 这部分讨论的问题。
-
容易混淆的点:
- 指令的高 6 位 (
[31:26]):这是 Opcode,所有指令都有,由主控制器处理。 - 指令的低 6 位 (
[5:0]):这是 funct,只有 R 型指令有,由 ALU 控制单元处理。
在电路图中:
- 指向 “主控制单元” 的是 高 6 位(Opcode)。
- 指向 “ALU 控制” 气泡的是 低 6 位(funct)。
- 指令的高 6 位 (
[25-0] 位:剩下的部分。根据 Opcode 的不同,这些位会被拆分成不同的字段,比如源寄存器编号、目标寄存器编号、立即数或偏移量。
- 附:这 7 个控制信号有效/无效时候的含义:

原理拆解

可以通过以下四个步骤拆解其工作原理:
1. 通用的“车头”:取指阶段 (Instruction Fetch)
无论是什么指令(R型、I型还是分支),第一步都是一样的:
- PC 寄存器输出地址,从指令存储器中取出 32 位的指令码。
- 同时,PC 会通过最上方的加法器(Add)自动加 4,指向下一条指令的地址。
- 这部分是完全公共的,不分指令类型。
2. 中央调度站:指令译码与控制信号 (Decoding)
当指令码取出来后,它会被拆解成不同的位段:
- 指令 [31-26](Opcode):进入控制单元(图中中间的椭圆)。这就是“大脑”,它识别出当前是
add、lw还是beq。 - 控制单元的输出:它会瞬间产生一排 0 和 1 的信号(如
RegWrite、ALUSrc、MemRead等)。这些信号就像开关,决定了数据流向哪条支路。
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型指令(如
lw或addi):第二个操作数是指令中自带的16位立即数(经过符号扩展变为32位)。 - 兼容方式:通过 ALU 前面的那个 Mux。如果是 R 型,
ALUSrc=0选寄存器;如果是 I 型,ALUSrc=1选立即数。
C. 写回寄存器的是什么数据?(MemtoReg 信号)
- 计算指令(如
add):写回的是 ALU 的计算结果。 - 访存指令(如
lw):写回的是 数据存储器 读出的内容。 - 兼容方式:最右侧的 Mux。通过
MemtoReg信号决定是把“工厂加工的(ALU)”还是“仓库搬运的(Memory)”数据存入寄存器。
4. 逻辑分流:分支指令的特殊性 (Branch)
对于 beq(相等则跳转)指令,它需要同时做两件事:
- 计算比较:利用 ALU 做减法,看结果是否为 0(零标志 Zero 为 1)。
- 计算目标地址:利用下方的“符号扩展”和“左移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 |
核心结论:
硬件并没有为每种指令造一套新电路,而是造了一套包含所有可能路径的“全集”电路。每条特定的指令在执行时,通过控制信号激活其中一部分路径,并屏蔽(或忽略)不相关的部分。这就是单周期处理器用“一套公共数据通路”解决所有问题的奥秘。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)