跳到内容

3.2 时序逻辑与同步

组合逻辑只描述 output = F(input)。加入存储状态后,输出还取决于过去;时钟、建立时间、保持时间与跨时钟域问题由此出现。

反馈让电路拥有历史

在地心控制台上,加法器已经能算出下一地址,但输入一撤走,结果也跟着消失。要构成程序计数器,电路必须保存当前地址,并在合适时刻接受新值。

把组合门的输出反馈到输入,可以形成两个稳定状态。这个直觉通向锁存器和触发器,但反馈也意味着传播延迟、竞争和模拟行为不能再被忽略。

时序电路可抽象为:

text
next_state = F(current_state, input)
output     = G(current_state, input)  # Mealy 型

output     = G(current_state)         # Moore 型

组合逻辑计算 next_state,存储元件在规定事件发生时把它变成新的 current_state

SR 锁存器:最小的保持结构

两个交叉耦合的 NOR 门可构成高有效 SR 锁存器。忽略传播过程后的功能表是:

SRQ 的下一状态含义
00保持保留旧值
101Set
010Reset
11不允许Q 与反相信号同时被压到 0

“不允许”不只是表格不美观。当 S、R 从 1 同时释放时,两条反馈路径可能竞争,最终状态由细小延迟差决定,还可能经历亚稳过程。若使用 NAND 构成低有效版本,输入极性和不允许组合会反过来;看电路前必须先确认约定。

SR 锁存器展示了状态怎样出现,却不适合作为软件读者心中唯一的同步存储模型。实际设计更常通过 D 输入与使能/时钟约束,避免调用者直接制造 S、R 冲突。

D 锁存器与 D 触发器不是同一种元件

D 锁存器是电平敏感的:使能有效期间,Q 跟随 D;使能撤销后,Q 保持最后值。这段开放窗口常称为 transparent phase。

边沿触发 D flip-flop 只在指定时钟边沿附近采样 D,其他时间保持 Q:

text
上升沿到来:Q(next) = D
其余时间:  Q(next) = Q

“只在一个数学瞬间读取”仍是数字抽象。物理触发器要求 D 在边沿前稳定至少 setup time,在边沿后继续稳定至少 hold time。

锁存器并非低劣或“随时会受干扰”。良好设计的 latch-based 流水线可以借用相邻阶段的时间,但时序分析更复杂。初学同步系统通常先用边沿触发模型,是因为阶段边界更清楚。

寄存器是并行采样的一组 bit

共享同一时钟边沿的一组触发器可以保存一个多 bit 字。再加入 enable 后,寄存器只在边沿且 enable 有效时更新:

text
if rising_edge(clock):
    if enable: Q(next) = D
    else:      Q(next) = Q

所有 bit 必须基于同一个旧状态判断边沿,再一起获得新输入。旧版示例在循环中让第一个 bit 提前更新共享的 previous_clock,导致后续 bit 看不到上升沿;这正说明软件顺序赋值不能随便冒充并发硬件。

下面的 C17 代码只是一个离散事件参考模型,但它正确地把边沿检测放在整组寄存器之外:

c
#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(&reg, false, true, UINT8_C(0x12));
    assert(reg.value == UINT8_C(0x00));

    drive_register8(&reg, true, true, UINT8_C(0x12));
    assert(reg.value == UINT8_C(0x12));

    drive_register8(&reg, true, true, UINT8_C(0x34));
    assert(reg.value == UINT8_C(0x12));

    drive_register8(&reg, false, true, UINT8_C(0x34));
    drive_register8(&reg, true, false, UINT8_C(0x34));
    assert(reg.value == UINT8_C(0x12));
    return 0;
}

C 局部变量不等于“一排固定触发器”。优化器可能把它放进物理寄存器、栈内存,直接消去,或在不同时刻复用存储位置。硬件描述语言中的寄存器语义与编译普通软件是两件不同的事。

一个时钟周期要容纳完整的数据路径

典型单时钟域寄存器到寄存器路径是:

text
源触发器 --clock-to-Q--> 组合逻辑与布线 --setup--> 目的触发器

忽略符号方向细节后的 setup 约束可概括为:

text
Tclock >= t_clk_to_q(max) + t_comb(max) + t_setup + 时钟不确定度

保持约束检查同一个边沿之后,新数据是否过早到达:

text
t_clk_to_q(min) + t_comb(min) >= t_hold + 最坏时钟偏差项

setup 失败通常靠降低频率、缩短组合路径、流水化或改善物理实现修复;hold 失败不能仅靠降低时钟频率解决,因为它关心同一边沿附近的最短路径,常需增加数据路径延迟或调整时钟树。

实际静态时序分析还会处理 clock skew、jitter、不同工艺/电压/温度角、时钟门控和例外路径。简单地说“周期大于门延迟”漏掉了触发器与时钟网络本身的预算。

同步更新要求先算 next,再统一提交

考虑两个状态寄存器在同一上升沿交换值:

text
A(next) = B
B(next) = A

硬件两边都读取边沿前的旧值,因此交换成功。若软件模型顺序执行 A = B; B = A;,第二句读到的是新 A,结果错误。

可靠模拟器通常分两阶段:

  1. 用旧状态计算所有 next-state;
  2. 在时钟事件上统一提交。

SystemVerilog 的时钟触发过程通常用 nonblocking assignment(<=)表达这类同步更新。blocking assignment(=)适合组合过程的局部计算;混用规则要遵循项目的 HDL 规范与工具检查。

亚稳态是模拟现实闯进数字抽象

若 D 在采样边沿附近违反 setup/hold,触发器内部节点可能暂时停在逻辑阈值之间,经过不可预测时间才解析为 0 或 1。下游观察到的风险是解析时间过长或不同路径采到不一致结果,而不是一种可供逻辑使用的稳定“第三值”。

来自按钮、外设或另一个异步时钟域的单 bit 控制信号,常用两级或更多级同步器降低亚稳传播概率:

text
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 模型生成输出的逻辑。

例如两步门禁控制器可有 LOCKEDBADGE_OKOPEN 三个状态。刷卡只把状态从 LOCKED 推到 BADGE_OK,在超时前输入正确 PIN 才进入 OPEN。这比把所有条件塞进一个巨大布尔表达式更容易表达历史依赖。

状态编码可用 binary、one-hot、Gray 等方案。one-hot 使用更多触发器,却可能缩短解码逻辑;最优选择与 FPGA/ASIC 目标、故障安全和时序约束有关。不要仅凭“bit 最少”判断实现成本。

从寄存器到处理器状态

处理器把组合数据路径与时序边界交替排列:

text
程序计数器 -> 取指逻辑 -> 流水线寄存器
             -> 译码/读寄存器 -> 流水线寄存器
             -> 执行单元 -> 流水线寄存器

这只是概念图。现代 CPU 可能乱序执行、重命名寄存器、推测分支,并让一条指令跨多个周期。源代码赋值也不会机械对应某一个时钟沿。数字逻辑提供实现积木,ISA 与微架构才决定这些积木如何组织。

动手画出时间关系

  1. 为高有效 NOR SR 锁存器画出 S、R、Q 波形,并标出不允许输入。
  2. 对同一组 D/enable 波形,分别画出高电平透明 D latch 与上升沿 D flip-flop 的 Q。
  3. 给定 t_clk_to_q=80 ps、组合路径 620 ps、setup 70 ps、不确定度 30 ps,求允许的最小时钟周期。
  4. 解释为什么降低频率通常修不了 hold violation。
  5. 设计一个跨时钟域传递 32-bit 配置字的 handshake,说明数据何时允许改变。
  6. 把一个三状态门禁 FSM 写成 current-state、next-state 两阶段模型,并覆盖非法状态恢复。

把门和状态焊成可执行引擎

现在已经具备组合运算和同步保存两类积木。下一章进入CPU 与 ISA,区分指令集给软件的契约与微架构的具体实现,再沿着取指、译码、执行和提交追踪一条指令。

Built with VitePress | Software Systems Atlas