《计算机组成原理》从零设计 CPU:深度拆解现代 RISC 处理器的通用数据通路与控制逻辑

张开发
2026/6/1 10:04:00 15 分钟阅读
《计算机组成原理》从零设计 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:...00004:...01008:...100012:...1100总结“地址必须是 4 的倍数”是为了让硬件取指令时能“一刀准”。既然最后两位一定是00MIPS 就把这多余的信息从指令码里抠掉把宝贵的位数省下来去表示更远的跳转距离。⭐⭐⭐ 如何让一套硬件一个寄存器堆 一个 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来自 ALUR 型。选 1来自存储器lw“如何用同一套硬件适配不同的操作数来源”。我们可以从指令格式、数据通路和硬件成本三个层面来分析。1. R型 vs. I型本质区别特性R 型 (Register)I 型 (Immediate)操作数来源全是寄存器两个源寄存器一个寄存器 一个立即数指令格式oprs典型代表add, sub, andaddi, 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, 5ALUSrc必须选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 型指令走的是“分级转接”。关于三种指令字段详解三种指令物理结构的“领土划分”为了让你在 408 考试中不掉进陷阱我们需要从“物理空间”的角度来看看 32 位指令是如何被“瓜分”的。1. 指令物理结构的“领土划分”想象 32 位指令是一个长度固定的长条。由于总长度只有 32 位每种类型的指令必须根据需求来分配这有限的空间。R 型指令功能复杂需要 functR 型指令需要指定 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的那块地盘共556165561655616位在 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。[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型指令第二个操作数来自寄存器堆输出的数据2Read data 2。I型指令如lw或addi第二个操作数是指令中自带的16位立即数经过符号扩展变为32位。兼容方式通过 ALU 前面的那个 Mux。如果是 R 型ALUSrc0选寄存器如果是 I 型ALUSrc1选立即数。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 跳转到目标地址否则继续顺序执行PC4。总结它是如何做到“兼容”的我们可以用下表概括不同指令在同一通路上的“开关”状态指令类型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核心结论硬件并没有为每种指令造一套新电路而是造了一套包含所有可能路径的“全集”电路。每条特定的指令在执行时通过控制信号激活其中一部分路径并屏蔽或忽略不相关的部分。这就是单周期处理器用“一套公共数据通路”解决所有问题的奥秘。

更多文章