MIPS 寄存器文件详解:32个寄存器如何为CPU提供数据
中央处理器每秒可以执行数百万甚至数十亿条指令。它可以完成加法、比较、地址计算、函数调用、循环控制和数据搬运。
这些正在被指令使用的数据,通常不会每次都直接从主内存读取。主内存容量很大,但速度远远跟不上处理器内部的执行电路。
CPU真正用来保存当前工作数据的地方,是寄存器。
在经典的32位MIPS处理器中,32个通用寄存器被组织成一个独立的硬件模块,称为:
Register File,寄存器文件。
寄存器文件不仅仅是32个寄存器的集合。它还包含读取选择、写入选择、译码器、控制信号和数据端口。
它可以同时读取两个寄存器,并在时钟边沿把一个结果写回另一个寄存器。
这就是MIPS寄存器文件最核心的结构:
两个读取端口,一个写入端口。
一、什么是寄存器?
寄存器是CPU内部的一种高速存储电路。
一个32位寄存器可以保存一个32位二进制数值。这个数值可能表示:
- 一个整数
- 一个内存地址
- 一个字符
- 一个位掩码
- 一个状态值
- 一次计算的中间结果
32位寄存器中的位通常从0编号到31。
- bit 0 是最低有效位
- bit 31 是最高有效位
每一位只能保存0或1,32个位组合起来,构成一个完整的32位数值。
单个寄存器容量非常小,只能保存一个数值。但寄存器位于CPU核心内部,距离执行电路非常近,因此访问速度非常快。
CPU需要同时保存很多正在使用的数据,所以不能只有一个寄存器,而需要把多个寄存器组织在一起。
这就形成了寄存器文件。
二、什么是Register File?
Register File中的“File”不是磁盘上的文件,而是指一组统一组织和控制的寄存器。
经典MIPS寄存器文件包含:
- 32个通用寄存器
- 每个寄存器32位
- 两个读取端口
- 一个写入端口
- 读取地址
- 写入地址
- 写入数据
- 写使能信号
- 时钟信号
寄存器文件接收寄存器编号,然后选择对应的寄存器。
CPU需要告诉它:
因此,寄存器文件不是把32个寄存器简单地放在一起,而是一个完整的多端口硬件模块。
三、32乘32的寄存器结构
经典MIPS寄存器文件通常表示为:
32 × 32 Register File
第一个32表示有32个寄存器。
第二个32表示每个寄存器宽32位。
可以把它想象成一个表格:
- 从上到下有32行
- 每一行对应一个寄存器
- 每一行包含32个位
寄存器编号从0到31。
当CPU提供一个寄存器编号时,寄存器文件就会选择对应的一行,并输出其中的32位数据。
执行写入时,CPU也会指定一个寄存器编号,再把新的32位数据保存到对应位置。
四、整个寄存器文件只有128字节
一个32位寄存器等于4个字节。
MIPS共有32个通用寄存器,因此总容量为:
|
1 2 |
32 × 4 bytes = 128 bytes |
整个通用寄存器文件只有128字节。
与其他存储设备相比,这个容量非常小。
- 主内存通常以GB计算
- 高速缓存可能有几十KB或几MB
- 一个普通图片可能有几MB
- 一个视频文件可能有几百MB甚至几十GB
但是,寄存器文件并不是为了保存大量数据。
不需要立即使用的数据,可以留在缓存或者主内存中。
需要频繁使用的数据,则尽量保存在寄存器中。
五、为什么MIPS有32个寄存器?
经典MIPS指令长度是32位。
在许多MIPS指令中,都有字段用于指定源寄存器和目标寄存器。
如果处理器拥有32个寄存器,就需要5位二进制数来选择其中一个。
因为:
|
1 2 |
2的5次方 = 32 |
5位二进制一共可以表示32种组合,范围从0到31。
因此,指令中的一个5位寄存器字段,可以选择寄存器0到寄存器31中的任意一个。
如果寄存器数量更多,程序就可以在CPU内部保存更多工作数据。
但是,指令中用于保存寄存器编号的字段也必须更大。
MIPS选择32个通用寄存器,是寄存器数量和指令编码空间之间的一种平衡。
六、寄存器编号和软件名称
从硬件角度看,MIPS寄存器只有编号:
|
1 2 3 4 5 6 |
Register 0 Register 1 Register 2 ... Register 31 |
但是,汇编程序员通常使用更容易理解的名称。
| 编号 | 名称 | 常见用途 |
|---|---|---|
| 0 | $zero |
永远读取为0 |
| 1 | $at |
汇编器临时寄存器 |
| 2–3 | $v0–$v1 |
函数返回值 |
| 4–7 | $a0–$a3 |
函数参数 |
| 8–15 | $t0–$t7 |
临时寄存器 |
| 16–23 | $s0–$s7 |
保存寄存器 |
| 24–25 | $t8–$t9 |
临时寄存器 |
| 28 | $gp |
全局指针 |
| 29 | $sp |
栈指针 |
| 30 | $fp / $s8 |
帧指针或保存寄存器 |
| 31 | $ra |
返回地址 |
这些名称主要来自软件调用约定。
例如:
$t0实际上是寄存器8$s0实际上是寄存器16$sp实际上是寄存器29$ra实际上是寄存器31
硬件并不知道“T”代表临时,也不知道“S”代表保存。
汇编器会把这些名称转换成5位寄存器编号,CPU最终看到的只是数字。
七、为什么需要两个读取端口?
考虑下面这条MIPS指令:
|
1 2 |
add $t0, $t1, $t2 |
它的含义是:
|
1 2 |
$t0 = $t1 + $t2 |
CPU需要完成三个动作:
- 读取
$t1 - 读取
$t2 - 把结果写入
$t0
由于加法需要两个源操作数,寄存器文件必须同时提供两个数值。
因此,MIPS寄存器文件拥有两个独立的读取端口。
第一个读取端口选择$t1。
第二个读取端口选择$t2。
两个32位数据可以同时离开寄存器文件,并进入后面的执行电路。
这不只适用于加法。
减法、逻辑运算、比较、分支和存储指令,也经常需要同时读取两个寄存器。
八、读取端口内部如何工作?
在教学模型中,一个读取端口可以看作一个大型多路选择器。
32个寄存器的输出全部连接到一个32选1选择结构。
5位读取地址决定最终选择哪个寄存器。
例如:
- 地址8选择寄存器8
- 地址9选择寄存器9
- 地址16选择寄存器16
由于寄存器文件拥有两个读取端口,因此需要两套独立的读取选择结构。
|
1 2 3 |
Read Address 1 → Read Data 1 Read Address 2 → Read Data 2 |
两个读取端口可以选择不同的寄存器,也可以同时读取同一个寄存器。
读取不会删除寄存器中的数据,只是把选中的数值复制到输出端。
真实处理器中的多端口寄存器文件,物理实现会比简单的多路选择器图复杂得多,但外部行为基本一致:
两个寄存器编号输入,两个32位数据输出。
九、写入端口如何工作?
寄存器文件的写入端口通常包含:
- 5位写地址
- 32位写数据
- Reg Write写使能
- 时钟信号
写地址决定哪个寄存器接收结果。
写数据提供需要保存的新数值。
Reg Write决定当前是否允许写入。
当Reg Write等于0时,不修改任何通用寄存器。
当Reg Write等于1时,目标寄存器可以在有效时钟边沿保存新数据。
可以把它概括为:
|
1 2 3 4 5 |
Write Address → 选择目标寄存器 Write Data → 提供新数值 Reg Write → 是否允许写入 Clock → 何时完成写入 |
十、5到32译码器
5位写地址需要从32个寄存器中选择一个目标。
一种常见教学实现,是使用5到32译码器。
译码器接收5位二进制输入,并产生32条选择线。
正常情况下,只有一条选择线被激活。
例如:
- 写地址为8,选择寄存器8
- 写地址为16,选择寄存器16
- 写地址为31,选择寄存器31
译码器的输出还需要与Reg Write信号结合。
只有当目标寄存器被选中,并且Reg Write等于1时,它才会在时钟边沿保存写数据。
虽然所有寄存器都可以连接到同一组写数据线上,但一次正常写入只会修改一个寄存器。
十一、读取是组合逻辑
在经典MIPS教学模型中,读取通常被看作组合逻辑操作。
也就是说,读取输出直接取决于当前的读取地址和寄存器中保存的数据。
当读取地址变化时,经过短暂的传播延迟,读取数据也会发生变化。
读取操作不需要等待新的时钟边沿。
例如:
|
1 2 3 |
Read Address 1 = 8 Read Data 1 = Register 8 |
如果地址改为9:
|
1 2 3 |
Read Address 1 = 9 Read Data 1 = Register 9 |
真实电路并不是完全没有延迟,但读取过程不需要通过时钟边沿触发。
十二、写入发生在时钟边沿
寄存器写入通常是时钟控制的。
在一个时钟周期中,处理器首先准备好:
- 写地址
- 写数据
- Reg Write信号
目标寄存器不会随着这些信号的每一次微小变化而不断修改。
它会等待有效时钟边沿。
当时钟边沿到来,并且Reg Write等于1时,选中的寄存器才会保存新的数据。
这种设计可以让CPU内部状态只在明确的时间点发生变化。
在时钟周期的大部分时间里,组合逻辑可以读取操作数、完成计算并生成结果。
到了时钟边沿,结果再被写入目标寄存器。
因此,在经典模型中:
读取通常是组合逻辑,写入通常是时钟控制。
十三、一条add指令的完整流程
继续使用下面这条指令:
|
1 2 |
add $t0, $t1, $t2 |
在经典MIPS R型指令中:
rs字段选择第一个源寄存器rt字段选择第二个源寄存器rd字段选择目标寄存器
对于这条指令:
|
1 2 3 4 |
rs = $t1 rt = $t2 rd = $t0 |
执行过程如下:
$t1的编号进入第一个读取地址$t2的编号进入第二个读取地址- 寄存器文件输出两个32位操作数
- 两个操作数进入执行电路
- 执行电路完成加法
- 结果返回寄存器文件
$t0成为写地址- Reg Write被设置为1
- 在时钟边沿,结果写入
$t0
这一集暂时可以把ALU看成黑盒。
两个操作数进入,一个结果输出。
下一篇再详细解释ALU内部如何完成加法、减法和逻辑运算。
十四、永远为零的寄存器
MIPS中的寄存器0非常特殊。
读取$zero时,结果永远是0。
即使程序试图向$zero写入其他数值,写入也会被忽略。
例如:
|
1 2 |
add $t0, $t1, $zero |
相当于:
|
1 2 |
$t0 = $t1 |
因为任何数加0都不会改变。
零寄存器还可以用于:
- 与0进行比较
- 判断一个数是否为0
- 生成固定的零值
- 丢弃不需要的计算结果
硬件可以直接阻止对寄存器0的写入,也可以在读取时强制输出0。
无论具体电路如何实现,软件看到的规则始终相同:
$zero永远读取为0,所有写入都会被忽略。
十五、立即数指令如何使用寄存器文件?
立即数指令不一定需要读取两个通用寄存器。
例如:
|
1 2 |
addi $t0, $t1, 10 |
它的含义是:
|
1 2 |
$t0 = $t1 + 10 |
这条指令从寄存器文件读取$t1。
第二个操作数10,不来自另一个寄存器,而是直接来自指令中的立即数字段。
寄存器文件仍然输出正常的读取数据。
后面的选择电路会决定,执行单元的第二个输入是寄存器数据,还是立即数。
这部分将在完整MIPS数据通路中继续展开。
十六、load指令如何使用寄存器文件?
考虑一条加载指令:
|
1 2 |
lw $t0, 4($t1) |
它的含义是:
从$t1 + 4所指向的内存位置读取一个32位数据,并保存到$t0。
执行过程是:
- 寄存器文件读取
$t1 $t1提供基地址- 处理器把基地址与偏移量4相加
- 内存系统返回数据
- 返回的数据成为寄存器文件的Write Data
$t0成为Write Address- Reg Write被设置为1
- 数据在时钟边沿写入
$t0
这里还不需要深入讨论缓存和总线。
可以暂时把内存系统看成一个黑盒。
十七、store指令为什么也需要两个读取端口?
考虑下面这条存储指令:
|
1 2 |
sw $t0, 4($t1) |
它的含义是:
把$t0中的数据,写入$t1 + 4所指向的内存位置。
这条指令需要同时读取两个寄存器:
$t1提供基地址$t0提供需要写入内存的数据
因此:
|
1 2 3 |
Read Port 1 → $t1 Read Port 2 → $t0 |
这再次说明了两个读取端口的重要性。
但是,store指令不会产生新的通用寄存器结果。
它修改的是内存,而不是寄存器文件。
所以执行store时:
|
1 2 |
Reg Write = 0 |
没有通用寄存器被写入。
十八、寄存器文件不是CPU的全部状态
32个通用寄存器非常重要,但它们并不是CPU内部的全部存储状态。
经典MIPS处理器还可能包含:
- Program Counter,程序计数器
- HI寄存器
- LO寄存器
- 浮点寄存器
- 状态寄存器
- 异常寄存器
- 地址转换和系统控制寄存器
- 流水线寄存器
程序计数器通常独立于通用寄存器文件。
传统MIPS中的HI和LO寄存器,用于保存乘法和除法的特殊结果。
浮点运算单元也可能拥有独立的浮点寄存器文件。
因此,32个通用寄存器是程序员最常直接使用的一部分CPU状态,但不是处理器内部的全部寄存器。
十九、寄存器和Cache不是一回事
寄存器和高速缓存都位于CPU附近,但两者的工作方式不同。
寄存器由机器指令直接指定。
例如:
|
1 2 |
add $t0, $t1, $t2 |
这条指令明确要求读取$t1和$t2,再写入$t0。
Cache则通常由硬件自动管理。
程序不会直接要求:
|
1 2 |
把这个变量放入第7条缓存行 |
缓存保存的是内存中某些数据块的副本。
寄存器保存的是指令当前直接使用的数值。
寄存器文件只有128字节,而缓存可能有几十KB或几MB。
寄存器容量更小,但与指令执行的联系更加直接。
二十、总结
经典32位MIPS寄存器文件包含32个通用寄存器。
每个寄存器宽32位。
5位寄存器编号可以选择32个寄存器中的任意一个。
它拥有两个独立的读取端口,可以同时提供两个源操作数。
它还拥有一个写入端口,可以把一个结果保存到目标寄存器。
读取通常由组合逻辑完成。
写入通常发生在有效时钟边沿。
5到32译码器选择目标寄存器。
Reg Write信号决定是否允许写入。
寄存器0永远输出0,并忽略所有写入操作。
加法、立即数、加载和存储指令,都以不同方式使用同一个寄存器文件。
寄存器文件本身不负责完成加法、减法或者逻辑运算。
它的任务是:
- 保存当前操作数
- 向执行电路提供数据
- 接收指令执行之后产生的结果
两个操作数离开寄存器文件之后,通常会进入中央处理器中最重要的执行电路之一:
Arithmetic Logic Unit,算术逻辑单元,也就是ALU。
下一篇,我们将继续介绍ALU内部如何使用数字电路完成加法、减法、比较和按位逻辑运算。
