CPU 内部包含许多重要组件:寄存器、ALU、控制逻辑、内存接口,以及大量辅助电路。
但是,这些组件只有连接起来、协同工作,才能真正组成一颗能够执行程序的处理器。
CPU 内部最重要的数据连接之一,就是:
寄存器文件(Register File)与算术逻辑单元(Arithmetic Logic Unit,ALU)之间的数据通路。
寄存器文件保存 CPU 当前正在使用的数据。
ALU 则负责对这些数据执行算术和逻辑运算。
因此,一个最基本的 CPU 数据流可以表示为:
Register File
↓
ALU
↓
Register File
本文将跟踪一条非常简单的 MIPS 指令:
add $t0, $t1, $t2
它的含义是:
$t0 = $t1 + $t2
从汇编语言来看,这条指令非常简单。
但是在 CPU 内部,为了完成这一行代码,多个硬件模块必须精确配合。
1. 从 ADD 指令开始
先来看这条 MIPS 指令:
add $t0, $t1, $t2
这里涉及三个寄存器。
$t1 是第一个源寄存器。
$t2 是第二个源寄存器。
$t0 是目标寄存器。
概念上就是:
$t1 + $t2 → $t0
假设:
$t1 = 25
并且:
$t2 = 17
那么处理器必须计算:
25 + 17 = 42
然后把结果 42 写入:
$t0
从程序员角度来看,这就是全部操作。
但是在 CPU 内部,这个过程要复杂得多,也有趣得多。
2. CPU 需要两个源操作数
一次加法需要两个输入值。
因此,ALU 必须得到:
Operand A
和:
Operand B
对于这条指令:
add $t0, $t1, $t2
两个操作数分别来自:
$t1
和:
$t2
这也是为什么典型 MIPS 寄存器文件通常提供:
两个读端口(Two Read Ports)
这样 CPU 就可以同时读取两个寄存器。
概念上:
$t1 → Read Data 1
$t2 → Read Data 2
然后,这两个值可以同时送入 ALU。
3. 寄存器文件并不认识寄存器名称
在汇编语言中,我们使用:
$t0
$t1
$t2
这样的名字。
但是硬件并不真正理解这些名称。
MIPS CPU 内部使用的是:
寄存器编号。
在传统 MIPS 命名方式中:
$t0 = register 8
$t1 = register 9
$t2 = register 10
因此:
add $t0, $t1, $t2
对于硬件来说,实际涉及的是:
8, 9, 10
经典 MIPS 有 32 个通用寄存器。
要从 32 个寄存器中选择一个,需要:
5 bits
因为:
2^5 = 32
所以,每个寄存器编号都可以用 5 位二进制数表示。
这些 5 位寄存器地址直接编码在机器指令内部。
4. 指令本身包含寄存器地址
经典 MIPS R-type 指令长度为:
32 bits
其中主要包含以下字段:
Opcode
rs
rt
rd
shamt
funct
对于 ADD 指令:
rs
指定第一个源寄存器。
rt
指定第二个源寄存器。
rd
指定目标寄存器。
对于:
add $t0, $t1, $t2
对应关系是:
rs = $t1
rt = $t2
rd = $t0
因此,CPU 可以直接从指令中的二进制字段得到寄存器地址。
也就是说:
机器指令本身就告诉寄存器文件应该读取哪些寄存器,以及最终把结果写入哪个寄存器。
5. 两个寄存器地址进入寄存器文件
寄存器文件会接收两个读地址。
第一个来自:
rs
第二个来自:
rt
对于我们的例子:
rs → $t1
rt → $t2
这些地址进入寄存器文件后,会分别选择两个已经存储的 32 位数据。
概念上:
Read Register 1 → $t1
Read Register 2 → $t2
寄存器文件输出:
Read Data 1
和:
Read Data 2
这里最重要的一点是:
两个寄存器的数据可以同时被读取出来。
6. 为什么寄存器文件需要两个读端口
假设 CPU 的寄存器文件只有一个读端口。
执行:
$t0 = $t1 + $t2
时,就可能需要先读取 $t1,然后再读取 $t2。
这样会让指令执行过程变得更加复杂。
因此,典型 MIPS 寄存器文件支持两个独立的读端口。
数据流变成:
$t1 → Read Data 1
$t2 → Read Data 2
两个值可以在基本相同的执行阶段被读取出来。
这种结构非常适合:
ADD
SUB
AND
OR
SLT
等需要两个寄存器操作数的指令。
这也说明了一个重要问题:
寄存器文件的硬件结构,与指令集的设计是密切相关的。
7. 寄存器文件把数据送入 ALU
现在来到最关键的一步。
寄存器文件的两个输出开始流向 ALU。
概念上:
Register File
Read Data 1 → ALU Input A
Read Data 2 → ALU Input B
对于:
add $t0, $t1, $t2
就是:
$t1 → ALU Input A
$t2 → ALU Input B
假设:
$t1 = 25
$t2 = 17
那么 ALU 得到:
Input A = 25
Input B = 17
现在 ALU 已经获得了执行运算所需的数据。
但是,还缺少一个非常重要的信息:
到底应该执行什么运算?
8. ALU 不只是做加法
ALU 并不是简单的加法器。
根据不同处理器设计,它通常可以执行:
Addition
Subtraction
AND
OR
XOR
Comparison
等操作。
某些处理器还可能让 ALU 负责移位操作,也可能使用独立的移位硬件。
因此,仅仅把 $t1 和 $t2 送入 ALU 是不够的。
CPU 还必须告诉 ALU:
你现在究竟应该执行什么操作?
这就需要:
控制信号(Control Signals)。
9. 控制单元对指令进行解码
CPU 会分析机器指令中的各个字段,以确定当前到底是什么指令。
对于经典 MIPS R-type ADD 指令:
主 Opcode 表明这是一条 R-type 指令。
然后,额外的 funct 字段进一步指定具体运算。
控制逻辑对这些字段进行解码。
对于 ADD 指令,最终会生成一个 ALU 控制信号:
ALU Control = ADD
概念上:
Instruction
↓
Control Logic
↓
ALU Control = ADD
↓
ALU
与此同时,寄存器文件已经把两个操作数送了过来。
因此,现在 ALU 同时知道了两件事:
数据是什么。
以及:
应该执行什么运算。
10. ALU 执行加法
ALU 接收到:
Input A = $t1 的值
Input B = $t2 的值
ALU Control = ADD
在我们的例子中:
Input A = 25
Input B = 17
ALU 内部的算术电路开始工作,最终产生:
ALU Result = 42
从更底层的硬件角度来看,这次加法最终可能由:
Full Adders
或者更先进的加法器结构实现。
因此,ALU 实际上把 CPU 的两个世界连接起来了。
底层:
Logic Gates
构成:
Adders
加法器成为:
ALU
的一部分。
然后 ALU 执行机器指令要求的算术运算。
这正是数字逻辑逐渐演变成 CPU 指令执行的过程。
11. ALU 得到结果后,工作还没有结束
ALU 已经完成:
25 + 17 = 42
但是指令还没有真正结束。
CPU 还有一个重要任务:
把结果保存下来。
对于:
add $t0, $t1, $t2
结果必须写入:
$t0
因此:
42
必须从 ALU 返回到寄存器文件。
这条路径通常属于:
Write-Back Path,写回路径。
概念上:
Register File
↓
ALU
↓
ALU Result
↓
Write Back
↓
Register File
所以,寄存器文件既是:
操作数的来源
也是:
计算结果的目的地。
12. 目标寄存器来自 rd
CPU 已经知道结果应该写到哪里。
因为目标寄存器编号已经编码在指令中。
对于 R-type ADD 指令:
目标寄存器来自:
rd
例如:
add $t0, $t1, $t2
目标是:
$t0
而 $t0 对应:
Register 8
因此,寄存器文件最终收到:
Write Register = 8
Write Data = 42
但是,仅仅把这些信号送到寄存器文件还不够。
CPU 还需要一个信号告诉寄存器文件:
现在允许写入。
13. RegWrite 控制寄存器更新
CPU 并不是执行任何指令时都要写寄存器。
有些指令会修改寄存器。
有些不会。
例如:
ADD 会写寄存器。
SUB 会写寄存器。
LW 会写寄存器。
但是:
SW 不会写通用寄存器。
普通分支指令通常也不会写通用寄存器。
因此,控制单元会生成一个非常重要的控制信号:
RegWrite
对于 ADD:
RegWrite = 1
它告诉寄存器文件:
把输入的数据写入指定的目标寄存器。
于是:
Write Register = $t0
Write Data = ALU Result
RegWrite = 1
现在硬件已经准备好更新 $t0。
14. 寄存器读取和写入的行为不同
经典寄存器文件通常采用这样一种设计:
读取是组合逻辑的。
写入是时钟控制的。
这两者的区别非常重要。
当 Read Register 地址改变以后,对应寄存器中的数据可以通过组合逻辑传播到读输出。
通常不需要再等待另一个时钟边沿。
但是写入不同。
目标寄存器一般会在指定的有效时钟边沿真正更新。
因此,在执行:
add $t0, $t1, $t2
时,CPU 可以先读取:
$t1
和:
$t2
然后把它们送入 ALU。
ALU 完成运算后:
$t0
再在正确的时钟控制点被更新。
这种:
组合数据流 + 时钟存储
的结合,是同步 CPU 设计的基础。
15. 完整的 ADD 数据通路
现在,我们可以把前面的所有步骤连起来。
从:
add $t0, $t1, $t2
开始。
指令确定:
Source 1 = $t1
Source 2 = $t2
Destination = $t0
完整数据流如下:
Instruction
↓
Extract rs, rt, rd
↓
Register File
↓
Read $t1 and $t2
↓
Read Data 1 and Read Data 2
↓
ALU
↓
ADD
↓
ALU Result
↓
Write Data
↓
Register File
↓
Write $t0
如果加入实际数据:
$t1 = 25
$t2 = 17
那么:
25
和:
17
进入 ALU。
ALU 计算得到:
42
然后 CPU 将:
42
写入:
$t0
到这里:
一条 ADD 指令才算真正完成。
16. 这就是数据通路 Datapath
我们前面一直在跟踪的这些硬件连接,在计算机体系结构中统称为:
Datapath,数据通路。
Datapath 包含 CPU 内部负责:
传输数据
以及:
转换数据
的硬件结构。
典型 Datapath 包括:
Register File
ALU
Multiplexers
Program Counter
Adders
Immediate-generation hardware
Memory interfaces
等等。
而控制单元则决定:
每条指令执行时,这些硬件应该如何工作。
可以用两句话理解 CPU:
Datapath 负责移动和处理数据。
Control Unit 负责告诉 Datapath 应该做什么。
理解这两部分之间的区别,是学习 CPU 架构非常重要的一步。
17. 为什么很快就会需要多路选择器
我们使用的 ADD 例子非常简单。
因为 ALU 的两个操作数都直接来自寄存器。
但是,并不是所有指令都这样。
例如,有些指令需要把:
一个寄存器值
与:
一个立即数
送入 ALU。
于是 ALU 的第二个输入就出现了两个可能来源:
Register Value
或者:
Immediate Value
CPU 必须从中选择一个。
这就是:
Multiplexer,MUX,多路选择器
存在的重要原因。
同样,不同指令可能需要选择不同的:
目标寄存器
或者:
Write-Back 数据来源。
随着 CPU 支持越来越多的指令类型,Multiplexer 会大量出现在 Datapath 中。
这也是简单的:
Register File → ALU
逐渐演变成完整 CPU Datapath 的关键一步。
18. ADD 和 LW 使用 ALU 的方式不同
ALU 并不仅仅用于算术指令。
例如:
lw $t0, 8($t1)
CPU 必须先计算内存地址:
address = $t1 + 8
这个加法仍然由 ALU 完成。
但是此时,ALU 的第二个输入已经不是另一个寄存器。
而是:
Immediate Offset
也就是立即数偏移量。
因此:
ADD:
Register + Register → ALU
而 LW:
Register + Immediate → ALU
这就是为什么在典型 CPU Datapath 中,ALU 某个输入前面经常会放一个 Multiplexer。
它根据当前指令选择正确的数据来源。
19. SW 同样使用寄存器文件和 ALU
再来看:
sw $t0, 8($t1)
这条指令也需要从寄存器文件读取两个值。
但是它们的用途不同。
$t1
提供:
Base Address
而:
$t0
提供:
需要写入内存的数据。
ALU 负责计算:
$t1 + 8
得到内存地址。
与此同时:
$t0
中的数据会流向内存的:
Write Data Input
这说明一个重要事实:
寄存器文件的两个输出,并不总是承担完全相同的任务。
它们具体做什么,取决于当前正在执行什么指令。
而控制系统负责决定:
Datapath 应该如何被使用。
20. 寄存器文件处在 CPU 执行的中心
寄存器文件在 CPU 内部处于一个非常关键的位置。
大量指令都会从读取一个或多个寄存器开始。
这些数据随后可能进入:
ALU
Memory Address Generation Logic
Comparison Logic
或者:
其他 Execution Units
执行完成以后,很多结果又重新返回寄存器文件。
因此,CPU 指令执行经常呈现这样的基本模式:
Read Operands
↓
Execute Operation
↓
Write Result
更加具体地说:
Register File
↓
Execution Unit
↓
Register File
这种模式并不是 MIPS 独有的。
在现代处理器架构中,我们都可以看到类似思想。
21. 从寄存器文件到 ALU,再到 CPU
我们之前可以把 Register File 当成一个独立的硬件模块来学习。
这样可以理解 CPU 如何保存和快速访问少量但极其重要的数据。
但是,当 Register File 与 ALU 连接起来以后,它的意义就完全不同了。
现在,一个更大的 CPU 结构开始出现:
Registers 保存操作数。
Register File 提供操作数。
ALU 执行运算。
结果通过 Write-Back Path 返回。
Control Signals 协调整个过程。
这些硬件模块连接在一起以后,就构成了:
CPU 执行机器指令的基础。
22. 从逻辑门一直到 ADD 指令
这条数据通路还展示了计算机中非常重要的一条抽象链。
最底层是:
Transistors
晶体管构成:
Logic Gates
逻辑门构成:
Adders
加法器成为:
ALU
的一部分。
ALU 的数据来自:
Register File
Register File 与 ALU 一起执行:
Machine Instructions
而机器指令最终又构成:
Programs
因此,看起来非常简单的一条汇编语句:
add $t0, $t1, $t2
实际上连接了两个完全不同的世界:
上层的软件
和:
最底层的数字逻辑硬件。
这正是计算机体系结构最重要的思想之一。
23. 下一步是什么?
现在,我们已经把 CPU 中两个非常重要的组件连接起来:
Register File
和:
ALU
但是,这仍然只是完整处理器的一部分。
真正的 CPU 还必须回答很多问题:
指令是怎样取出来的?
CPU 怎么知道下一条应该执行什么指令?
Program Counter 在 Datapath 中处于什么位置?
Load 和 Store 指令怎样与内存通信?
Branch 指令如何改变程序执行流程?
Control Unit 如何选择正确的数据路径?
当这些组件全部连接起来以后,我们就不再是在学习几个独立的数字电路。
而是在逐渐构建:
一条完整的 CPU Datapath。
Conclusion
一条 MIPS 指令:
add $t0, $t1, $t2
从汇编语言来看非常简单。
但是在 CPU 内部,它会启动一套高度组织化的数据流。
首先,指令指定两个源寄存器:
$t1
和:
$t2
寄存器文件同时读取两个值。
然后:
Read Data 1 → ALU Input A
Read Data 2 → ALU Input B
控制逻辑告诉 ALU:
执行 ADD。
ALU 完成加法,产生结果。
随后,这个结果沿着:
Write-Back Path
返回寄存器文件。
最后,结果被写入:
$t0
因此,最核心的数据流就是:
Instruction
↓
Register File
↓
ALU
↓
Write Back
↓
Register File
这不仅仅是:
寄存器文件与 ALU 之间的一条连接。
它实际上已经是:
CPU Datapath 的起点。
一旦理解这条路径,我们就可以继续追踪一条完整的机器指令:
从 Instruction Fetch 开始,
经过 Register Access,
进入 ALU Execution,
然后可能访问 Memory,
最后完成 Write Back。
而正是在这里,一个个看似独立的:
Logic Gates
Adders
Registers
ALU
Multiplexers
开始真正连接起来。
最终变成:
一颗能够执行程序的 CPU。
