3.2 时序逻辑与同步
组合逻辑只描述
output = F(input)。加入存储状态后,输出还取决于过去;时钟、建立时间、保持时间与跨时钟域问题由此出现。
反馈让电路拥有历史
在地心控制台上,加法器已经能算出下一地址,但输入一撤走,结果也跟着消失。要构成程序计数器,电路必须保存当前地址,并在合适时刻接受新值。
把组合门的输出反馈到输入,可以形成两个稳定状态。这个直觉通向锁存器和触发器,但反馈也意味着传播延迟、竞争和模拟行为不能再被忽略。
时序电路可抽象为:
next_state = F(current_state, input)
output = G(current_state, input) # Mealy 型
或
output = G(current_state) # Moore 型组合逻辑计算 next_state,存储元件在规定事件发生时把它变成新的 current_state。
SR 锁存器:最小的保持结构
两个交叉耦合的 NOR 门可构成高有效 SR 锁存器。忽略传播过程后的功能表是:
| S | R | Q 的下一状态 | 含义 |
|---|---|---|---|
| 0 | 0 | 保持 | 保留旧值 |
| 1 | 0 | 1 | Set |
| 0 | 1 | 0 | Reset |
| 1 | 1 | 不允许 | Q 与反相信号同时被压到 0 |
“不允许”不只是表格不美观。当 S、R 从 1 同时释放时,两条反馈路径可能竞争,最终状态由细小延迟差决定,还可能经历亚稳过程。若使用 NAND 构成低有效版本,输入极性和不允许组合会反过来;看电路前必须先确认约定。
SR 锁存器展示了状态怎样出现,却不适合作为软件读者心中唯一的同步存储模型。实际设计更常通过 D 输入与使能/时钟约束,避免调用者直接制造 S、R 冲突。
D 锁存器与 D 触发器不是同一种元件
D 锁存器是电平敏感的:使能有效期间,Q 跟随 D;使能撤销后,Q 保持最后值。这段开放窗口常称为 transparent phase。
边沿触发 D flip-flop 只在指定时钟边沿附近采样 D,其他时间保持 Q:
上升沿到来:Q(next) = D
其余时间: Q(next) = Q“只在一个数学瞬间读取”仍是数字抽象。物理触发器要求 D 在边沿前稳定至少 setup time,在边沿后继续稳定至少 hold time。
锁存器并非低劣或“随时会受干扰”。良好设计的 latch-based 流水线可以借用相邻阶段的时间,但时序分析更复杂。初学同步系统通常先用边沿触发模型,是因为阶段边界更清楚。
寄存器是并行采样的一组 bit
共享同一时钟边沿的一组触发器可以保存一个多 bit 字。再加入 enable 后,寄存器只在边沿且 enable 有效时更新:
if rising_edge(clock):
if enable: Q(next) = D
else: Q(next) = Q所有 bit 必须基于同一个旧状态判断边沿,再一起获得新输入。旧版示例在循环中让第一个 bit 提前更新共享的 previous_clock,导致后续 bit 看不到上升沿;这正说明软件顺序赋值不能随便冒充并发硬件。
下面的 C17 代码只是一个离散事件参考模型,但它正确地把边沿检测放在整组寄存器之外:
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
struct Register8 {
uint8_t value;
bool previous_clock;
};
static void drive_register8(struct Register8 *reg, bool clock,
bool enable, uint8_t data) {
bool rising_edge = clock && !reg->previous_clock;
if (rising_edge && enable) {
reg->value = data;
}
reg->previous_clock = clock;
}
int main(void) {
struct Register8 reg = {0, false};
drive_register8(®, false, true, UINT8_C(0x12));
assert(reg.value == UINT8_C(0x00));
drive_register8(®, true, true, UINT8_C(0x12));
assert(reg.value == UINT8_C(0x12));
drive_register8(®, true, true, UINT8_C(0x34));
assert(reg.value == UINT8_C(0x12));
drive_register8(®, false, true, UINT8_C(0x34));
drive_register8(®, true, false, UINT8_C(0x34));
assert(reg.value == UINT8_C(0x12));
return 0;
}C 局部变量不等于“一排固定触发器”。优化器可能把它放进物理寄存器、栈内存,直接消去,或在不同时刻复用存储位置。硬件描述语言中的寄存器语义与编译普通软件是两件不同的事。
一个时钟周期要容纳完整的数据路径
典型单时钟域寄存器到寄存器路径是:
源触发器 --clock-to-Q--> 组合逻辑与布线 --setup--> 目的触发器忽略符号方向细节后的 setup 约束可概括为:
Tclock >= t_clk_to_q(max) + t_comb(max) + t_setup + 时钟不确定度保持约束检查同一个边沿之后,新数据是否过早到达:
t_clk_to_q(min) + t_comb(min) >= t_hold + 最坏时钟偏差项setup 失败通常靠降低频率、缩短组合路径、流水化或改善物理实现修复;hold 失败不能仅靠降低时钟频率解决,因为它关心同一边沿附近的最短路径,常需增加数据路径延迟或调整时钟树。
实际静态时序分析还会处理 clock skew、jitter、不同工艺/电压/温度角、时钟门控和例外路径。简单地说“周期大于门延迟”漏掉了触发器与时钟网络本身的预算。
同步更新要求先算 next,再统一提交
考虑两个状态寄存器在同一上升沿交换值:
A(next) = B
B(next) = A硬件两边都读取边沿前的旧值,因此交换成功。若软件模型顺序执行 A = B; B = A;,第二句读到的是新 A,结果错误。
可靠模拟器通常分两阶段:
- 用旧状态计算所有 next-state;
- 在时钟事件上统一提交。
SystemVerilog 的时钟触发过程通常用 nonblocking assignment(<=)表达这类同步更新。blocking assignment(=)适合组合过程的局部计算;混用规则要遵循项目的 HDL 规范与工具检查。
亚稳态是模拟现实闯进数字抽象
若 D 在采样边沿附近违反 setup/hold,触发器内部节点可能暂时停在逻辑阈值之间,经过不可预测时间才解析为 0 或 1。下游观察到的风险是解析时间过长或不同路径采到不一致结果,而不是一种可供逻辑使用的稳定“第三值”。
来自按钮、外设或另一个异步时钟域的单 bit 控制信号,常用两级或更多级同步器降低亚稳传播概率:
async input -> flip-flop 1 -> flip-flop 2 -> synchronous logic同步器只能把失效概率压低到工程目标,不能从数学上消灭亚稳态。第一级输出不应分叉给普通逻辑。
多 bit 总线不能简单地每一位各接两级同步器,因为各位可能在不同周期被接收,拼出从未发送过的组合。脉冲、计数器和数据流通常需要 handshake、toggle protocol、Gray code、async FIFO 等针对语义设计的 CDC 方案。
复位同样是时序设计的一部分
同步复位只在时钟边沿生效,容易纳入普通时序分析;异步复位可在时钟停止时立即让状态进入已知值,但解除复位必须满足 recovery/removal 等时序要求,常采用“异步拉起、同步释放”。
不是每一个 data-path 寄存器都必须复位。大量复位网络会增加面积、布线和时序压力;只要控制状态保证未初始化数据在使用前被覆盖,就可能无需复位。选择取决于安全需求、上电协议和实现工艺。
不要用普通 AND/OR 门随意拼接时钟。使能在门延迟中变化会制造窄脉冲或额外边沿。需要时钟门控时,应使用库提供的集成门控单元和工具认可的约束;许多逻辑也可直接使用寄存器 enable。
状态机把控制台流程落成电路
地心控制台不能只算“输入是什么”,还要记得上一拍停在哪个阶段。状态机把这种历史压进寄存器,再由组合逻辑计算下一状态和输出。
一个同步有限状态机通常包含:
- 保存 current state 的状态寄存器;
- 根据 current state 与输入计算 next state 的组合逻辑;
- 根据 Moore 或 Mealy 模型生成输出的逻辑。
例如两步门禁控制器可有 LOCKED、BADGE_OK、OPEN 三个状态。刷卡只把状态从 LOCKED 推到 BADGE_OK,在超时前输入正确 PIN 才进入 OPEN。这比把所有条件塞进一个巨大布尔表达式更容易表达历史依赖。
状态编码可用 binary、one-hot、Gray 等方案。one-hot 使用更多触发器,却可能缩短解码逻辑;最优选择与 FPGA/ASIC 目标、故障安全和时序约束有关。不要仅凭“bit 最少”判断实现成本。
从寄存器到处理器状态
处理器把组合数据路径与时序边界交替排列:
程序计数器 -> 取指逻辑 -> 流水线寄存器
-> 译码/读寄存器 -> 流水线寄存器
-> 执行单元 -> 流水线寄存器这只是概念图。现代 CPU 可能乱序执行、重命名寄存器、推测分支,并让一条指令跨多个周期。源代码赋值也不会机械对应某一个时钟沿。数字逻辑提供实现积木,ISA 与微架构才决定这些积木如何组织。
动手画出时间关系
- 为高有效 NOR SR 锁存器画出 S、R、Q 波形,并标出不允许输入。
- 对同一组 D/enable 波形,分别画出高电平透明 D latch 与上升沿 D flip-flop 的 Q。
- 给定
t_clk_to_q=80 ps、组合路径 620 ps、setup 70 ps、不确定度 30 ps,求允许的最小时钟周期。 - 解释为什么降低频率通常修不了 hold violation。
- 设计一个跨时钟域传递 32-bit 配置字的 handshake,说明数据何时允许改变。
- 把一个三状态门禁 FSM 写成 current-state、next-state 两阶段模型,并覆盖非法状态恢复。
把门和状态焊成可执行引擎
现在已经具备组合运算和同步保存两类积木。下一章进入CPU 与 ISA,区分指令集给软件的契约与微架构的具体实现,再沿着取指、译码、执行和提交追踪一条指令。