ISA 是契约
ISA 规定语义,微架构风格是实现选择,二者并非绑死
CS152 · 第 2 课
上一课讲完 IBM 360 如何用统一的 ISA 隐藏了 8 位到 64 位数据通路的差异。这一课深入其中一种关键的实现风格——微编码(microcoding)——并用一台微编码的 RISC-V 机器作为具体例子,讲清楚"控制"与"数据通路"到底是如何分离、又如何协作的。
指令集架构(ISA)是软件和硬件之间的契约:它通过列出所有程序员可见的状态(寄存器 + 内存)以及作用在这些状态上的指令语义来定义。IBM 360 是历史上第一条把 ISA 与实现(即微架构)彻底分离的产品线。
同一个 ISA 可以有很多种不同的实现:
ISA 设计时常常带有某种特定的微架构风格作为默认预期:
| ISA 风格 | 典型对应的微架构风格 |
|---|---|
| Accumulator(累加器型) | 硬连线、不流水(hardwired, unpipelined) |
| CISC | 微编码(microcoded) |
| RISC | 硬连线、流水线(hardwired, pipelined) |
| VLIW | 定长延迟的顺序并行流水线 |
但这只是"典型对应",并非强制规则——同一个 ISA 完全可以用任何微架构风格实现:
为什么要学微编码?
处理器设计可以分成两部分:
早期计算机设计者面临的最大挑战,是把控制电路设计正确。Maurice Wilkes(EDSAC 的设计者)在 1958 年为 EDSAC-II 发明了"微编程"(microprogramming)的思想来设计控制单元——这个想法其实早已被 Babbage 分析机中的"the Barrel"(滚筒)以及更早的可编程计算器所预示。
微码在 1950 年代出现时,三种关键存储/逻辑技术的相对成本是:
关键的成本/速度关系是:逻辑门远比 ROM 或 RAM 贵;ROM 比 RAM 便宜;ROM 比 RAM 快得多。
这就直接解释了 IBM 360 Model 30 的设计选择(第 1 课):与其为每条复杂指令都设计一套专用的硬连线逻辑(很贵),不如把"如何执行每条指令"编码成一系列存储在 ROM 里的微指令(更便宜、更快),再用一个通用的译码器去执行这些微指令——这正是微编码机器的核心思想。而 Model 70 定位高端、性能优先,直接用专用电路(硬连线)实现控制逻辑。
本课程使用 RISC-V(RV32I 基础整数指令集)作为教学 ISA。它是伯克利设计的"第五代"RISC 指令集:现实、完整,但开源且精简;不为某一种特定实现风格过度设计;同时支持 32 位(RV32)和 64 位(RV64)地址空间;为多处理器设计;指令编码高效;便于教学/研究中的裁剪与扩展。
处理器状态:
pcx0–x31(x0 恒为 0)f0–f31(可存 32 位或 64 位 IEEE 浮点值)fcsr(舍入模式与异常报告)指令编码:RV32 基础指令集固定为 32 位,最低两位恒为 11(为将来扩展变长指令预留)。指令共有 6 种格式:
| 格式 | 用途 | 关键字段 |
|---|---|---|
| R-type | 寄存器-寄存器运算 | funct7, rs2, rs1, funct3, rd, opcode |
| I-type | 立即数运算、load | imm[11:0], rs1, funct3, rd, opcode |
| S-type | store | imm[11:5]+imm[4:0], rs2, rs1, funct3, opcode |
| B-type | 分支(branch) | imm 拆分编码, rs2, rs1, funct3, opcode |
| U-type | 高位立即数 | imm[31:12], rd, opcode |
| J-type | 跳转(jump,如 JAL) | imm 拆分编码, rd, opcode |
无论基础指令是否定长 32 位,所有分支/跳转的目标地址都以 16 位为最小单位对齐。
本课用一个简化的"单总线"(single-bus)数据通路来实现微编码的 RISC-V 机器:指令寄存器(Instruction Reg.)、PC、寄存器堆(Registers,含 PC 作为可寻址的一员)、两个锁存器 A/B、ALU、主存(Main Memory)都挂在同一条总线上,每个部件的读写由控制信号(如 RegEn、RegW、ALUEn、MemEn、MALd 等)逐拍触发。
微指令用寄存器传输(register-transfer)记号书写,每一行对应一组具体的控制信号:
MA:=PC 意味着 RegSel=PC; RegW=0; RegEn=1; MALd=1(把 PC 的值放上总线,锁存进内存地址寄存器 MA)B:=Reg[rs2] 意味着 RegSel=rs2; RegW=0; RegEn=1; BLd=1(把 rs2 寄存器的值放上总线,锁存进 B)Reg[rd]:=A+B 意味着 ALUop=Add; ALUEn=1; RegSel=rd; RegW=1(ALU 计算 A+B 放上总线,写回 rd 寄存器)一条 RISC-V 指令的完整执行被拆分成几个阶段:取指(Instruction Fetch)→ 译码(Instruction Decode)→ 寄存器读取(Register Fetch)→ ALU 运算 → 可选的内存操作 → 可选的寄存器写回 → 计算下一条指令地址。每个阶段对应一到多条微指令,由 Microcode ROM 中固化的微程序驱动。
取指(Instruction Fetch)——所有指令共享的第一步:
Instruction Fetch: MA,A := PC
PC := A+4
(等待内存)
IR := Mem
(根据 opcode 跳转分派)
关键细节:取指同时把 当前指令的 PC 锁存进 A,并立即把 PC 更新为 PC+4(下一条顺序指令的地址)。这意味着分派到具体指令的微码时,A 中保存的是"这条指令自己的地址",而 PC 已经指向"下一条指令"——这个先后关系是后面 JAL/Branch 需要正确处理 PC 相对寻址的关键。
ALU 型指令(R-type)与立即数 ALU 型指令(I-type):
ALU: A := Reg[rs1]
B := Reg[rs2]
Reg[rd] := ALUOp(A,B)
goto 取指
ALUI: A := Reg[rs1]
B := ImmI // 12 位立即数符号扩展
Reg[rd] := ALUOp(A,B)
goto 取指
Load 指令(LW):
LW: A := Reg[rs1]
B := ImmI // 12 位立即数符号扩展
MA := A+B
(等待内存)
Reg[rd] := Mem
goto 取指
JAL 与 Branch——需要利用取指阶段保存在 A 中的"本指令 PC"来计算 PC 相对跳转目标:
JAL: Reg[rd] := A // A 中是取指时保存的本指令 PC
A := A-4 // 恢复到"取指前"的 PC(因为取指阶段已 +4)
B := ImmJ // 跳转型立即数
PC := A+B
goto 取指
Branch: A := Reg[rs1]
B := Reg[rs2]
if (!ALUOp(A,B)) goto 取指 // 不跳转:条件不成立
A := PC // 跳转:取当前 PC 重新计算
A := A-4
B := ImmB // 分支型立即数
PC := A+B
goto 取指
这是一份简化的教学"草图"(microcode sketch),不是可直接综合的 RTL。 原始幻灯片对 JAL 中 A 的处理方式给出了两种略有出入的写法(含一条"备选"写法的注释),这提示我们:PC 相对寻址在寄存器传输级别很容易出现"多减一次 4"或"少减一次 4"这类差一错误(off-by-one)。真正精确的行为,要以 Sodor 微编码核心的 microcode.scala 源码为准——这也是 Lab 1 中要求大家去阅读真实 RTL、而不是只信任幻灯片伪代码的原因之一。
问题 1:1950 年代,为什么用 ROM 存储微指令、用通用译码器执行,比为每条复杂指令直接设计专用硬连线逻辑更划算?
问题 2:下列哪个例子最能说明"ISA 风格并不强制决定微架构实现风格"?
问题 3:在取指微码 MA,A:=PC; PC:=A+4 执行完毕、跳转到具体指令的微码之后,寄存器 A 和 PC 分别保存着什么?
ISA 规定语义,微架构风格是实现选择,二者并非绑死
数据通路存数算数,控制给数据通路的操作排序
逻辑贵、ROM 比 RAM 便宜又快,催生了微编码机制
微指令就是一组具体的控制信号,如 MA:=PC
sp23/lecture_slides/L02_Simple_Machine_Implementations_Microcoding.pdf(UC Berkeley CS152/CS252A, Spring 2023, Sophia Shao)