单周期数据通路:一条指令怎样走完五件事

中心问题:如果一条指令必须在一个时钟周期内完成,硬件要给它准备哪些路?

单周期 CPU 把取指、译码、执行、访存、写回放进同一个周期。它不表示真实高性能处理器都这样工作;它的价值在于把“指令语义需要的硬件路径”一次摊开。add 要读两个寄存器、进 ALU、写回寄存器;lw 还要访问数据存储器;sw 写内存但不写寄存器;分支既要比较寄存器,又要决定下一个 PC。

本篇覆盖 add/sub/and/or/addi/andi/ori/lw/sw/beq/bne/blt/bge。证据等级是“手算”和“功能执行”。功能执行指 Python 脚本生成控制表、穷举 ALU 断言并保存静态延迟算式;延迟部分不是周期仿真,也不推进阶段事件。不覆盖 jump、CSR、异常、压缩指令、流水线冒险、多周期控制或真实硬件频率。

依据和边界

RISC-V RV32I 文档给出本篇使用的指令语义:整数寄存器-寄存器指令读 rs1/rs2 并写 rd;立即数逻辑和加法读 rs1 加 sign-extended 12 位立即数;load/store 用 rs1 + offset 得到有效地址;条件分支比较两个寄存器,满足条件时把 sign-extended B-immediate 加到分支指令地址形成目标地址。依据见 RV32I Base Integer Instruction Set, Version 2.1

控制信号表对齐 CS61C 单周期控制材料。该材料把 PCSelImmSelBrUnASelBSelALUSelMemRWRegWEnWBSel 作为控制器输出,并指出分支 PCSel 依赖比较器产生的 BrEq/BrLT。依据见 CS61C Single-Cycle CPU Control: Control Logic Design

时序公式沿用第 05 篇:寄存器边沿后先经过 clk-to-q,组合逻辑传播,下一处寄存器在 setup 窗口前采样。关键路径仍是最长寄存器到寄存器路径。

一张足够小的数据通路

下面是本篇使用的教学数据通路。箭头只表示主要数据方向,控制线没有全部画出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data path:

PC -> IMEM -> instruction fields
|-> register file reads rs1/rs2 ----+
| |
+-> immediate gen -> ALU input mux --+-> ALU -> DMEM -> WB mux -> register file rd
^ |
| +------------> WB mux
register rs2 / immediate

next-PC path:

PC ---------------------------> +4 adder ----------------+
PC + branch immediate <-------- ALU / branch target path --+-> PC mux -> PC
branch comparator -------------- taken/not-taken select ---+

这张图只保留教学所需部件:PC、指令存储器、寄存器堆、立即数生成器、ALU、数据存储器、写回选择器和 PC 选择器。控制器读取指令字段,再决定每个选择器和写使能。

单周期设计的强约束是:每条受支持指令都必须在一个周期内走完自己的最长路径。不同指令不用同样多的部件,但周期长度必须照顾最慢的那一条。

四类指令走四条路

R 型算术逻辑指令的路径:

1
2
PC -> IMEM -> register file(rs1, rs2) -> ALU -> WB mux -> rd
next PC = PC + 4

add/sub/and/or 都读两个寄存器,差别只在 ALUSel。它们不访问数据存储器,MemRW = Read 或空闲,RegWEn = 1WBSel = ALU

I 型算术逻辑指令的路径:

1
2
3
PC -> IMEM -> register file(rs1)
immediate gen -> ALU -> WB mux -> rd
next PC = PC + 4

addi/andi/ori 的第二个 ALU 输入来自立即数,BSel = Imm。RISC-V 文档说明普通立即数会 sign-extend;因此立即数生成器必须在 ALU 计算前稳定输出。

load/store 的地址路径:

1
2
lw: PC -> IMEM -> register file(rs1) + imm -> ALU address -> DMEM read -> WB mux -> rd
sw: PC -> IMEM -> register file(rs1) + imm -> ALU address -> DMEM write(rs2)

lw 写寄存器,WBSel = Memsw 写内存,不写 rdRegWEn = 0。两者都用 ALU 做地址加法,所以 ALUSel = ADD

条件分支的路径:

1
2
PC -> IMEM -> register file(rs1, rs2) -> branch comparator -> PCSel
PC -> immediate gen -> ALU(PC + B-imm) ---------------------> PC mux

分支不写寄存器,也不写内存。它的难点是 PCSel 不是只由 opcode 决定。beq 要看 BrEqblt 要看 BrLT;比较器输出稳定后,控制器才能选 PC + 4 或分支目标。

控制表是指令语义到线路选择的翻译

本批脚本生成了控制表,附件见 control_table.txt。核心行如下:

1
2
3
4
5
inst | PCSel | ImmSel | BrUn | ASel | BSel | ALUSel | MemRW | RegWEn | WBSel
add | +4 | - | - | Reg | Reg | ADD | Read | 1 | ALU
lw | +4 | I | - | Reg | Imm | ADD | Read | 1 | Mem
sw | +4 | S | - | Reg | Imm | ADD | Write | 0 | -
beq taken | ALU | B | - | PC | Imm | ADD | Read | 0 | -

这张表不是从硬件自动综合出来的,而是把本篇支持的指令语义手工编码为教学控制信号,再由脚本保存和复查。它的验收意义有两个:

第一,能看出部件复用。ALU 既做算术结果,也做 rs1 + imm 地址计算,还做 PC + imm 分支目标计算。单周期教学 CPU 为了少放硬件,让这些计算共享一块 ALU;代价是同一周期内必须等它完成。

第二,能看出“不关心”不是“不存在”。sw 不写回寄存器,所以 WBSel 对最终状态无影响,可以标为 -。但控制器仍必须保证 RegWEn = 0,否则错误写回会污染寄存器状态。

手算一条 lw 路径

用第 05 篇同一份脚本里的延迟假设。lw 的地址路径里,寄存器读数、立即数生成和控制信号会并行到达 ALU 输入选择器,所以脚本先取三者最大到达时间,再加 mux 和 ALU 延迟:

1
2
3
4
5
6
7
8
9
10
clk_to_q_max = 35 ps
imem = 200 ps
reg_read = 120 ps
imm_gen = 60 ps
control_rom = 80 ps
mux = 30 ps
alu_32bit = 220 ps
dmem_read = 260 ps
wb_mux = 30 ps
setup = 25 ps

lw 的路径是:

1
2
3
4
5
6
7
8
9
10
11
12
inst    = clk_to_q_max + imem = 235 ps
regs = inst + reg_read = 355 ps
imm = inst + imm_gen = 295 ps
control = inst + control_rom = 315 ps

addr = max(regs, imm, control) + mux + alu_32bit
= 355 + 30 + 220
= 605 ps

writeback = max(addr + dmem_read, control) + wb_mux + setup
= max(865, 315) + 30 + 25
= 920 ps

脚本输出确认 single_cycle_lw 是当前模型最长路径:

1
2
critical_path.name = single_cycle_lw
critical_path.ps = 920

可复查附件:timing_report.txtverification.jsonlogic_cases.py

这就是单周期 CPU 的核心代价。add 在同一模型中只需要 660 ps,但它也必须使用至少 920 ps 的周期,因为所有指令共享同一个时钟周期。后面的多周期和流水线会把这个浪费拆开:慢指令多用阶段,快指令不必被最慢路径完全拖住。

分支为什么容易成为控制路径问题

beq 的分支目标可以和比较并行计算:

1
2
target = PC + B-imm
BrEq = (rs1 == rs2)

但最终 PC 只能选一个。脚本把 target 到达时间、select 到达时间和 PC + 4 到达时间分开计算,最后取最大值,再加 PC mux 和 PC setup。若比较器还没有稳定,PCSel 可能先指向 PC + 4,随后变成 target。只要 PC 寄存器采样前 PCSel 和目标地址都稳定,机器仍能得到正确下一条指令;若稳定得太晚,就会采到错误 PC。

因此控制器不是“译码后马上给出全部答案”的静态表。大多数控制信号只由指令字段决定,分支 PC 选择还要等数据路径的比较结果。CS61C 控制材料也把控制器拆成 ROM/解码和 branch-taken 组合逻辑两部分,本篇采用同样边界。

本篇验收

读完本篇,应能完成三件事:

  1. add/lw/sw/beq 分别画出经过的主要部件。
  2. 按表解释 RegWEnMemRWWBSelPCSel 为什么不同。
  3. 用声明的延迟假设算出最长单周期路径,并说明这个结果只对本模型有效。

没有完成的内容也必须清楚:本篇没有证明流水线正确性,没有证明真实 CPU 的周期时间,没有处理异常和跳转,也没有让第 07 篇参考执行器变成硬件。

练习

练习一:在本篇控制表里,sw x3, 8(x4)ImmSelASelBSelALUSelMemRWRegWEn 应该是什么?

答案:ImmSel = S,因为 store 使用 S 型立即数;ASel = Reg,第一个 ALU 输入来自 x4BSel = Imm,第二个输入是偏移 8;ALUSel = ADD,地址为 x4 + 8MemRW = WriteRegWEn = 0,因为 store 不写 rd

练习二:若数据存储器读延迟从 260 ps 降到 150 ps,其余假设不变,lw 路径变成多少?它还一定是最长路径吗?

答案:前半段 addr 仍为 605 ps。新的 lw 路径为 max(605 + 150, 315) + 30 + 25 = 810 ps。脚本原表中 sw 为 825 ps,taken branch 为 620 ps,add 为 660 ps。因此在这个修改后,sw 变成最长路径。这个练习说明关键路径是模型和参数共同决定的,不是某类指令永远固定最长。

参考资料