从逻辑到状态:一块 ALU 为什么还需要时钟

中心问题:只靠一张真值表能不能得到一台会执行程序的机器?

答案是否定的。真值表和组合逻辑能说明“给定输入会产生什么输出”,但程序执行还需要“什么时候保存这个输出”。组合逻辑负责计算,寄存器负责记住,时钟把“旧状态参与计算”和“新状态进入机器”分成两个时刻。没有这个分界,CPU 里的寄存器、PC、内存写回都会变成一团互相追赶的信号。

本篇只讨论一条最短的桥:布尔函数怎样变成小型 ALU,ALU 输出怎样进入寄存器,为什么最长组合路径决定时钟周期。证据等级是“手算”和“功能执行”。功能执行指 Python 脚本对 4 位 ALU 穷举断言并保存输出;时序部分只是静态延迟手算,不是周期仿真。这里没有 RTL、综合报告、FPGA 上板或真实 CPU 测量。

边界与依据

RISC-V 官方 ISA Specifications 20260120 的 RV32I 章节说明:RV32I 有 32 个 32 位 x 寄存器,x0 恒为 0,另有 pc 保存当前指令地址;R 型指令从 rs1rs2 读数并向 rd 写结果,ADD/SUB/AND/OR 属于整数寄存器-寄存器指令,溢出不产生算术异常,结果写回低 XLEN 位。这些语义决定了后续 ALU 至少要能接收两个寄存器操作数、选择一种运算、产生写回结果。依据见 RISC-V RV32I 文档的程序员模型、R 型整数指令和立即数/编码说明:RV32I Base Integer Instruction Set, Version 2.1

时序概念采用 CS61C 的同步数字系统说明:寄存器输入需要在时钟边沿前满足 setup time,在边沿后满足 hold time;寄存器输出不会在时钟边沿后立即稳定,而是在 clk-to-q 延迟后变化;关键路径延迟可写为 clk-to-q + combinational logic delay + setup time。依据见 CS61C Synchronous Digital Systems Summary

本文的教学模型固定为 4 位 ALU,只覆盖 ADD/SUB/AND/OR/SLT。4 位足够把加法器、取反加一、逻辑门、选择器、zero/carry 标志和关键路径讲清楚;它不能替代 32 位 RV32I ALU,也不能证明任何硬件频率。

组合逻辑只回答同一拍里的问题

组合逻辑的特点是没有记忆。输入变化后,经过门延迟,输出跟着变化。一个 1 位全加器可以用三输入真值表描述:

1
2
3
4
5
6
7
8
9
a b cin | sum cout
0 0 0 | 0 0
0 0 1 | 1 0
0 1 0 | 1 0
0 1 1 | 0 1
1 0 0 | 1 0
1 0 1 | 0 1
1 1 0 | 0 1
1 1 1 | 1 1

sum = a xor b xor cincout 在至少两个输入为 1 时为 1。把四个全加器串起来,就是 4 位 ripple-carry adder。低位的进位必须先算出来,再交给下一位,所以这条路径比单个与门或或门长。

减法可以复用加法器。a - b 等价于 a + (~b) + 1,其中 ~b 是按位取反,初始进位设为 1。这样 ALU 不需要第二套减法器,只需要给 b 前面加一组反相/选择逻辑,并把初始 carry 接成 1。这个复用是教学模型里的第一条设计取舍:少放硬件,换来更长一点的组合路径。

逻辑运算更直接:

1
2
AND: y[i] = a[i] & b[i]
OR : y[i] = a[i] | b[i]

SLT 在 4 位二补码模型里输出 0 或 1。它把 ab 解释成有符号数后比较大小:0x8 表示 -8,0xF 表示 -1。这个比较说明同一串比特在不同运算里有不同解释;第 01 篇处理数值表示,第 05 篇只关心 ALU 怎样实现选择。

ALU 的本质是一组候选结果加一个选择器

小型 ALU 可以画成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                 +------------------+
a ----+--------->| add/sub result |----+
| +------------------+ |
| +------------------+ |
+--------->| and result |----+
| +------------------+ | +------+
| +------------------+ +--->| mux |--> y
b ----+--------->| or result |----+ +------+
| +------------------+ |
| +------------------+ |
+--------->| slt result |----+
+------------------+

op -------------------------------> selector

所有候选结果都可以并行产生,op 只决定最后哪一路进入输出。控制信号不是魔法,它只是选择器的输入。后面的单周期 CPU 会把指令里的 opcode、funct3、funct7 翻译成 ALUSel,再交给这块 ALU。

本批脚本对 4 位 ADD/SUB/AND/OR/SLT 做了穷举验证。运行命令如下:

1
python3 examples/computer-architecture/logic/src/logic_cases.py

脚本保存了完整 1 位全加器真值表、ALU 样例表和验证摘要。读者可下载同名素材目录中的 logic_cases.pyfull_adder_truth_table.txtalu_sample_table.txtverification.json 复查。

输出摘要中的关键事实:

1
2
3
4
evidence_level: 手算、功能执行
timing_scope: static delay arithmetic with parallel arrival times, not cycle simulation
alu_bits: 4
alu_exhaustive_checks: 1280

1280 来自 16 个 a、16 个 b、5 种运算的断言。它证明这份 4 位教学模型的函数输出符合脚本里的定义,不证明 32 位 ALU、RTL 或物理实现正确。延迟计算只保存题设和算式,不推进阶段事件。

状态元素把一拍和下一拍隔开

只要 ALU 输出直接接回 ALU 输入,电路就会随着门延迟不断传播,难以定义“本条指令结束时状态是什么”。寄存器给机器一个离散边界:

1
2
3
4
5
6
7
     old state                         new state
+---------------+ combinational +---------------+
| register bank | --> ALU -->| register bank |
+---------------+ +---------------+
^ |
| |
+-------------- clock edge ------------+

在一个时钟周期中,寄存器先把旧值稳定送出。组合逻辑用旧值计算结果。下一个有效时钟边沿到来时,如果目标寄存器允许写入,并且输入已经稳定足够久,新值才进入寄存器。边沿之间输入怎么抖动,不会立刻改写寄存器保存的状态。

这就是程序状态能逐条更新的原因。第 03 篇追踪 pc、寄存器和内存变化时,每一行状态都对应一次“已提交”的结果;第 05 篇补上硬件侧的条件:寄存器在时钟边沿采样,组合逻辑在两个边沿之间完成计算。

关键路径限制时钟周期

教学延迟假设如下:

1
2
3
4
5
6
7
clk_to_q_max = 35 ps
clk_to_q_min = 18 ps
mux = 30 ps
alu_4bit = 160 ps
setup = 25 ps
hold = 20 ps
min_combo = 10 ps

寄存器到寄存器的 ALU 路径为:

1
2
3
clk_to_q_max + mux + alu_4bit + setup
= 35 + 30 + 160 + 25
= 250 ps

因此在这组假设下,周期不能小于 250 ps。若周期只有 200 ps,寄存器在边沿到来前还拿不到稳定结果;输出即使在逻辑上最终正确,也已经错过采样窗口。

hold 约束检查另一种失败:数据传播太快,也可能在同一个边沿后立刻冲进目标寄存器的保持窗口。hold 不能拿最长 clk-to-q 或最长组合逻辑来凑数;脚本单独给出最小题设,使用 clk_to_q_min + min_combo >= hold

1
18 + 10 = 28 ps >= 20 ps

所以这组假设下 hold 检查通过。这个结果只说明模型里的最短路径满足约束;真实芯片要面对布线偏斜、时钟树、工艺角和工具报告,不能从这个手算推出真实频率。

时序报告保存在 timing_report.txt。其中 article05_reg_to_reg_alu 为 250 ps,与上面的手算一致。

控制信号只在边界内有意义

ALU 的 op、选择器的 sel、寄存器写使能 RegWEn 都是控制信号。它们本身也是组合逻辑输出,必须在本周期足够早地稳定下来,才能让数据走对路径。

比如一条 add 指令,控制器要把 ALUSel 设为 ADD,写回选择设为 ALU 输出,寄存器写使能设为 1。若 ALUSel 在周期末还不稳定,ALU 可能先输出 AND,又输出 ADD;只要最终在 setup 窗口前稳定,寄存器仍能采到正确值。若稳定得太晚,本周期就不能安全采样。

因此“控制”和“数据”不是两套相互独立的东西。控制信号决定数据走哪条路,数据路径的比较结果又可能反过来影响控制选择。第 06 篇的分支 PCSel 就属于这种情况:比较器先产生 BrEq/BrLT,控制逻辑再决定下一个 PC 是 pc + 4 还是分支目标。

本篇验收

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

  1. 从 1 位全加器真值表推出 4 位加法器的进位传播。
  2. 解释 SUB 为什么可以复用 ADD 的硬件。
  3. clk-to-q + combinational delay + setup 计算一个寄存器到寄存器路径的最小时钟周期。

证据边界也要同时成立:这里的 ALU 是教学模型;时序数字是声明假设;没有任何 RTL 或硬件实测结论。

练习

练习一:在 4 位二补码中,计算 0xE + 0x3。写出结果、carry 和 zero 标志。

答案:0xE 表示 -2,0x3 表示 3。按 4 位加法,1110 + 0011 = 1 0001,低 4 位为 0x1,carry 为 1,zero 为 0。若解释成二补码结果,0x1 是 1。这里没有溢出标志;脚本只记录 carry 与 zero。

练习二:某路径的 clk-to-q = 40 ps,组合逻辑最长延迟为 310 ps,setup 为 30 ps。时钟周期至少是多少?若最短组合路径为 5 ps,hold 为 60 ps,hold 是否满足?

答案:周期至少 40 + 310 + 30 = 380 ps。hold 检查为 40 + 5 = 45 ps,小于 60 ps,不满足。修复方向不是降低周期,而是处理最短路径或时钟/寄存器约束;这类问题在真实设计里由时序分析工具报告。

参考资料