跳到内容

4.2 单周期 CPU 数据通路

老陈把散落在引擎室里的部件接到同一张图上,要求你们让一条指令在一个周期内走完全程。

ISA规定指令必须产生什么结果。本篇搭一台只支持少量 RV32I 指令的教学 CPU,关注控制信号与数据路径,而不是复刻现代处理器。

单周期意味着“一条指令一个长周期”

引擎室中央摆着寄存器文件、ALU、两块存储器接口和一排 MUX。目标是在一个时钟周期内,让当前指令从取出到产生全部结果;周期末,PC、目标寄存器或内存状态按指令语义更新。

概念路径可以画成:

text
                  +---------------- 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 寄存器当前指令地址取指地址
指令存储器PC32-bit 指令
主控制与 ALU 控制opcode、funct 字段MUX 选择与写使能
immediate generator分散的立即数字段符号扩展后的 32-bit 值
寄存器文件rs1、rs2、rd两个读值,周期末可写一个值
ALU两个选择后的操作数算术结果与比较条件
数据存储器接口地址、写数据、byte enableload 数据或写入效果
next-PC MUXPC+4、分支/跳转目标下一 PC
result MUXALU、load、PC+4rd 写回值

RV32I 的 x0 需要特殊处理:读端口对寄存器 0 返回零,写回地址为 0 时必须丢弃写入。它不是靠软件自觉维持。

add:先让一条指令穿过引擎

add x3, x1, x2 放进入口。它不访问 data memory,也不需要复杂 immediate;正适合先沿着取指、读寄存器、ALU、写回走一遍,再逐步增加 lwsw 和 branch 的岔路。

add x1, x2, x3 的路径是:

text
PC -> 取出指令 -> 读 x2/x3 -> ALU 加法 -> result MUX -> 写 x1
PC -> 加 4 -> next-PC MUX -> 写回 PC

控制逻辑识别 OP opcode,并结合 funct3=000funct7=0000000 选择 ADD。RegWrite=1,ALU 第二操作数来自 rs2,写回来源是 ALU。

寄存器文件读通常在这个教学模型中视为组合读,写发生在周期边沿。真实实现的端口时序由工艺和微架构决定。

lw:地址计算之后还要读数据

lw x1, 8(x2) 依赖两段组合工作:

text
读 x2 + 生成符号扩展立即数 8
        -> ALU 算有效地址 x2+8
        -> 数据存储器读 32 bit
        -> result MUX
        -> 写 x1

ALU 的第二输入由 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 立即数被拆在指令的高低两个区域:

text
x2 + sign_extend(8) -> 写地址
x3                  -> 写数据
MemWrite            -> 在周期末允许对应 byte lanes 更新

RegWrite=0,result MUX 的值无关紧要。控制表中的 X 表示 don't care,不代表硬件输入可以漂浮;综合器可在保证可观察行为的前提下选择方便实现的值。

beq:比较结果选择 next PC

beq x1, x2, target 比较两个寄存器。相等时:

text
next_pc = current_pc + sign_extend(branch_immediate)

否则顺序执行 next_pc = current_pc + 4。B-type 偏移的最低位隐含为零,目标相对的是当前分支指令地址,不是已经加 4 的地址。

简单数据通路可以让 ALU 做减法并检查 zero,也可配专门比较器。实现方式不是 ISA 契约。

在理想单周期模型中,taken branch 不额外消耗另一个周期,因为所有指令本来就占一个长周期。流水线处理器会面对控制 hazard,预测正确与否才会影响前端已经取入的指令。

一张刻意简化的控制表

下面只覆盖 addlwswbeq

指令RegWriteALUSrcMemWriteResultSrcBranchALUControl
add1rs20ALU0ADD
lw1imm0memory0ADD
sw0imm1X0ADD
beq0rs20X1compare equal

实际控制还要区分 load/store 宽度、signedness、跳转、异常、CSR、扩展指令和写掩码。表越简洁,越要明确它支持的指令子集。

控制器可以分层:主 decoder 根据 opcode 产生大类信号,ALU decoder 再结合 funct3funct7 和类别选择具体运算。这能复用逻辑,也让非法编码有集中处理位置。

立即数生成器为什么需要重排

RISC-V 把寄存器字段放在稳定位置,使译码和寄存器文件访问可尽早开始;不同指令的立即数 bit 因而分散。解码器用固定连线重新组合,并从指令 bit 31 执行符号扩展。

python
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,不需要逐条执行移位指令。

用编码器对解码器做往返测试

测试立即数时,正值、负值与范围端点都要覆盖:

python
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 必然最慢”仍取决于乘法器、分支逻辑、存储实现和物理布线。

周期下界大致包含:

text
源状态 clock-to-Q
+ 最慢组合/存储路径
+ 目的状态 setup
+ 时钟不确定度

短指令也必须等待这个统一周期结束,硬件资源往往不能在同一周期被不同阶段复用。这就是单周期设计容易理解却不适合高性能通用实现的主要原因。

多周期与流水线解决不同浪费

组织方式一条指令占用同时在途指令主要取舍
单周期1 个长周期1控制直观,周期由最慢路径决定
多周期多个较短周期通常 1可复用 ALU/存储端口,不同指令周期数不同
流水线多个阶段多条提高吞吐,但引入数据、控制与结构 hazard

流水线不保证一条指令的 latency 更小。它像装配线一样让不同指令占据不同阶段,理想稳态下提高完成吞吐。第 15 章会专门讨论冒险、转发、停顿和预测。

教学图隐藏了什么

  • cache miss 与主存延迟不是固定一个组合延迟;
  • 异常必须阻止不该发生的写回,并保存可恢复的架构状态;
  • 外设访存可能具有副作用,不能像普通 RAM 随意重放;
  • 乘除法、浮点和向量单元可能多周期或流水化;
  • 现代 CPU 会预测、乱序执行和重命名,但仍要维持 ISA 允许的软件观察结果;
  • 自修改代码、内存一致性与特权状态需要额外同步规则。

教学单周期核的价值不是代表“CPU 真身”,而是让每个 ISA 效果都能沿着一条明确线路解释。

动手追踪四条路径

  1. 在图上标出 addlw 共用哪些部件,哪个 MUX 选择不同。
  2. 推导 S-type 立即数 -16 的字段,并用解码函数验证。
  3. 解释为什么 sw 需要读取两个寄存器,却没有 rd
  4. jal 增加 PC+4 写回和跳转目标路径,列出新增 MUX 选择。
  5. 给控制器一个未知 opcode,设计“非法指令异常”而不是默默执行默认 ADD。
  6. 假设各部件延迟,计算 addlwswbeq 的路径并找出关键路径。

下一道瓶颈不在 ALU

数据通路已经能执行最小指令子集。接下来,取指和 load 都会碰到同一个现实:处理器计算速度与大容量存储访问之间相差巨大。下一章进入缓存与内存层次,从局部性、cache line 和 miss 类型开始。

Built with VitePress | Software Systems Atlas