4.2 单周期 CPU 数据通路
老陈把散落在引擎室里的部件接到同一张图上,要求你们让一条指令在一个周期内走完全程。
ISA规定指令必须产生什么结果。本篇搭一台只支持少量 RV32I 指令的教学 CPU,关注控制信号与数据路径,而不是复刻现代处理器。
单周期意味着“一条指令一个长周期”
引擎室中央摆着寄存器文件、ALU、两块存储器接口和一排 MUX。目标是在一个时钟周期内,让当前指令从取出到产生全部结果;周期末,PC、目标寄存器或内存状态按指令语义更新。
概念路径可以画成:
+---------------- control ----------------+
| |
PC -> instruction memory -> fields -> register file -> ALU -> result MUX
| | | | |
+-> PC + 4 / branch target | immediate data memory |
+------------------------------->|图中把 instruction memory 与 data memory 分开,是为了允许同一周期同时取指和访问数据。实际机器可以通过独立 cache 端口呈现这种效果,底层再汇入统一内存层次;ISA 不要求物理上一定有两块存储芯片。
一条指令“经过取指、译码、执行、访存、写回”是数据依赖顺序,并不表示单周期 CPU 使用五个时钟。组合信号在一个周期中传播,边沿到来时统一提交状态。
数据通路的核心部件
| 部件 | 读取什么 | 产生什么 |
|---|---|---|
| PC 寄存器 | 当前指令地址 | 取指地址 |
| 指令存储器 | PC | 32-bit 指令 |
| 主控制与 ALU 控制 | opcode、funct 字段 | MUX 选择与写使能 |
| immediate generator | 分散的立即数字段 | 符号扩展后的 32-bit 值 |
| 寄存器文件 | rs1、rs2、rd | 两个读值,周期末可写一个值 |
| ALU | 两个选择后的操作数 | 算术结果与比较条件 |
| 数据存储器接口 | 地址、写数据、byte enable | load 数据或写入效果 |
| next-PC MUX | PC+4、分支/跳转目标 | 下一 PC |
| result MUX | ALU、load、PC+4 等 | rd 写回值 |
RV32I 的 x0 需要特殊处理:读端口对寄存器 0 返回零,写回地址为 0 时必须丢弃写入。它不是靠软件自觉维持。
add:先让一条指令穿过引擎
把 add x3, x1, x2 放进入口。它不访问 data memory,也不需要复杂 immediate;正适合先沿着取指、读寄存器、ALU、写回走一遍,再逐步增加 lw、sw 和 branch 的岔路。
add x1, x2, x3 的路径是:
PC -> 取出指令 -> 读 x2/x3 -> ALU 加法 -> result MUX -> 写 x1
PC -> 加 4 -> next-PC MUX -> 写回 PC控制逻辑识别 OP opcode,并结合 funct3=000、funct7=0000000 选择 ADD。RegWrite=1,ALU 第二操作数来自 rs2,写回来源是 ALU。
寄存器文件读通常在这个教学模型中视为组合读,写发生在周期边沿。真实实现的端口时序由工艺和微架构决定。
lw:地址计算之后还要读数据
lw x1, 8(x2) 依赖两段组合工作:
读 x2 + 生成符号扩展立即数 8
-> ALU 算有效地址 x2+8
-> 数据存储器读 32 bit
-> result MUX
-> 写 x1ALU 的第二输入由 MUX 切换到立即数,写回来源切换到内存数据。RV32I 的 lw 把 32-bit 值写入 32-bit 寄存器;在 RV64I 中,lw 会把 32-bit 结果符号扩展到 XLEN,lwu 才做零扩展。
地址还可能触发未对齐、访问权限、页错误或总线错误。单周期入门图常把存储器画成固定延迟组合块,只是在暂时隐藏异常与可变延迟;真实 cache miss 不可能靠无限拉长一个物理时钟周期等待。
sw:没有 rd,也不写寄存器
sw x3, 8(x2) 使用 rs1 保存基址、rs2 保存待写数据,S-type 立即数被拆在指令的高低两个区域:
x2 + sign_extend(8) -> 写地址
x3 -> 写数据
MemWrite -> 在周期末允许对应 byte lanes 更新RegWrite=0,result MUX 的值无关紧要。控制表中的 X 表示 don't care,不代表硬件输入可以漂浮;综合器可在保证可观察行为的前提下选择方便实现的值。
beq:比较结果选择 next PC
beq x1, x2, target 比较两个寄存器。相等时:
next_pc = current_pc + sign_extend(branch_immediate)否则顺序执行 next_pc = current_pc + 4。B-type 偏移的最低位隐含为零,目标相对的是当前分支指令地址,不是已经加 4 的地址。
简单数据通路可以让 ALU 做减法并检查 zero,也可配专门比较器。实现方式不是 ISA 契约。
在理想单周期模型中,taken branch 不额外消耗另一个周期,因为所有指令本来就占一个长周期。流水线处理器会面对控制 hazard,预测正确与否才会影响前端已经取入的指令。
一张刻意简化的控制表
下面只覆盖 add、lw、sw 与 beq:
| 指令 | RegWrite | ALUSrc | MemWrite | ResultSrc | Branch | ALUControl |
|---|---|---|---|---|---|---|
add | 1 | rs2 | 0 | ALU | 0 | ADD |
lw | 1 | imm | 0 | memory | 0 | ADD |
sw | 0 | imm | 1 | X | 0 | ADD |
beq | 0 | rs2 | 0 | X | 1 | compare equal |
实际控制还要区分 load/store 宽度、signedness、跳转、异常、CSR、扩展指令和写掩码。表越简洁,越要明确它支持的指令子集。
控制器可以分层:主 decoder 根据 opcode 产生大类信号,ALU decoder 再结合 funct3、funct7 和类别选择具体运算。这能复用逻辑,也让非法编码有集中处理位置。
立即数生成器为什么需要重排
RISC-V 把寄存器字段放在稳定位置,使译码和寄存器文件访问可尽早开始;不同指令的立即数 bit 因而分散。解码器用固定连线重新组合,并从指令 bit 31 执行符号扩展。
def sign_extend(value: int, width: int) -> int:
sign = 1 << (width - 1)
return (value ^ sign) - sign
def decode_i_immediate(instruction: int) -> int:
return sign_extend((instruction >> 20) & 0xFFF, 12)
def decode_s_immediate(instruction: int) -> int:
encoded = ((instruction >> 25) << 5) | ((instruction >> 7) & 0x1F)
return sign_extend(encoded & 0xFFF, 12)
def decode_b_immediate(instruction: int) -> int:
encoded = (
(((instruction >> 31) & 0x1) << 12)
| (((instruction >> 7) & 0x1) << 11)
| (((instruction >> 25) & 0x3F) << 5)
| (((instruction >> 8) & 0xF) << 1)
)
return sign_extend(encoded, 13)这是软件解码模型,硬件中对应的是连线、符号扩展和 MUX,不需要逐条执行移位指令。
用编码器对解码器做往返测试
测试立即数时,正值、负值与范围端点都要覆盖:
def encode_i_immediate(value: int) -> int:
assert -2048 <= value <= 2047
return (value & 0xFFF) << 20
def encode_s_immediate(value: int) -> int:
assert -2048 <= value <= 2047
encoded = value & 0xFFF
return ((encoded >> 5) << 25) | ((encoded & 0x1F) << 7)
def encode_b_immediate(value: int) -> int:
assert -4096 <= value <= 4094 and value % 2 == 0
encoded = value & 0x1FFF
return (
(((encoded >> 12) & 0x1) << 31)
| (((encoded >> 11) & 0x1) << 7)
| (((encoded >> 5) & 0x3F) << 25)
| (((encoded >> 1) & 0xF) << 8)
)
for immediate in (-2048, -1, 0, 1, 2047):
assert decode_i_immediate(encode_i_immediate(immediate)) == immediate
assert decode_s_immediate(encode_s_immediate(immediate)) == immediate
for immediate in (-4096, -2, 0, 2, 4094):
assert decode_b_immediate(encode_b_immediate(immediate)) == immediate往返测试能发现 bit 11 与 bit 12 放反之类的接线错误,但不能单独证明 opcode、寄存器字段和非法指令处理都正确。完整处理器验证还会使用 ISA reference model、随机指令流、形式性质与 compliance tests。
时钟周期由最慢合法路径约束
单周期 CPU 必须让所有支持的指令在同一个周期预算内完成。若 lw 依次经过取指存储器、寄存器文件、ALU、数据存储器和写回 MUX,它常成为很长的候选关键路径;但“load 必然最慢”仍取决于乘法器、分支逻辑、存储实现和物理布线。
周期下界大致包含:
源状态 clock-to-Q
+ 最慢组合/存储路径
+ 目的状态 setup
+ 时钟不确定度短指令也必须等待这个统一周期结束,硬件资源往往不能在同一周期被不同阶段复用。这就是单周期设计容易理解却不适合高性能通用实现的主要原因。
多周期与流水线解决不同浪费
| 组织方式 | 一条指令占用 | 同时在途指令 | 主要取舍 |
|---|---|---|---|
| 单周期 | 1 个长周期 | 1 | 控制直观,周期由最慢路径决定 |
| 多周期 | 多个较短周期 | 通常 1 | 可复用 ALU/存储端口,不同指令周期数不同 |
| 流水线 | 多个阶段 | 多条 | 提高吞吐,但引入数据、控制与结构 hazard |
流水线不保证一条指令的 latency 更小。它像装配线一样让不同指令占据不同阶段,理想稳态下提高完成吞吐。第 15 章会专门讨论冒险、转发、停顿和预测。
教学图隐藏了什么
- cache miss 与主存延迟不是固定一个组合延迟;
- 异常必须阻止不该发生的写回,并保存可恢复的架构状态;
- 外设访存可能具有副作用,不能像普通 RAM 随意重放;
- 乘除法、浮点和向量单元可能多周期或流水化;
- 现代 CPU 会预测、乱序执行和重命名,但仍要维持 ISA 允许的软件观察结果;
- 自修改代码、内存一致性与特权状态需要额外同步规则。
教学单周期核的价值不是代表“CPU 真身”,而是让每个 ISA 效果都能沿着一条明确线路解释。
动手追踪四条路径
- 在图上标出
add与lw共用哪些部件,哪个 MUX 选择不同。 - 推导 S-type 立即数
-16的字段,并用解码函数验证。 - 解释为什么
sw需要读取两个寄存器,却没有rd。 - 为
jal增加PC+4写回和跳转目标路径,列出新增 MUX 选择。 - 给控制器一个未知 opcode,设计“非法指令异常”而不是默默执行默认 ADD。
- 假设各部件延迟,计算
add、lw、sw、beq的路径并找出关键路径。
下一道瓶颈不在 ALU
数据通路已经能执行最小指令子集。接下来,取指和 load 都会碰到同一个现实:处理器计算速度与大容量存储访问之间相差巨大。下一章进入缓存与内存层次,从局部性、cache line 和 miss 类型开始。