3.2 时序逻辑:有记忆的计算
在时钟边沿,把 bit 存下来
你盯着示波器的屏幕。
通道 1 上是一个漂亮的矩形波——100MHz,周期 10ns。通道 2 上是数据信号——一堆 0 和 1 在疯狂翻转。你对着信号说了句话:
“在下一个时钟上升沿,把这个 bit 存下来。”
这一刻,你在要求电路拥有“记忆“。
上一节我们讲了组合逻辑——输出永远等于当前输入的函数。但这句话意味着:输出不再只是当前输入的函数了。输出取决于时钟边沿到来那一刻输入是什么——并且这个值会被保持,直到下一个边沿。
这需要一个全新的元件。
D 触发器:1 bit 的记忆
记忆的最基本单元,叫做 D 触发器(D Flip-Flop,FF)。
它只有一个数据输入(D)、一个数据输出(Q)、一个时钟输入(CLK)。行为简单到可以用一句话描述:
在每个 CLK 的上升沿:Q ← D
其余时间:Q 保持不变
CLK: _|¯|_|¯|_|¯|_|¯|_|¯|_
D : ___ ¯¯¯¯¯¯ _______ ¯¯¯¯
Q : _________ ¯¯¯¯¯¯ ______
↑ ↑
第一个上升沿 第二个上升沿
一个 D 触发器存储了 1 bit 的信息。它是怎么做到“保持“的?内部结构其实很优雅——用两个锁存器(latch)级联,每个锁存器用透明/锁存两种模式交替工作。CLK=0 时主锁存器透明、从锁存器锁存;CLK=1 时反过来。数据在 CLK 上升沿那一个瞬间从 D 被“拍“进 Q,然后被锁死。
把 N 个 D 触发器放在一起、用同一个 CLK——这就是一个 N 位寄存器。
你的 ARM Cortex-M4 有 16 个通用寄存器(R0-R15),每个 32 位。本质上是 16 × 32 = 512 个 D 触发器,被同一个 CLK 驱动。加上 PC、SP、LR、PSR——总共几百个 FF,构成了 CPU 的寄存器文件(register file)。
用 C 模拟 D 触发器
让我们在软件里模拟一个 D 触发器,包含建立时间和保持时间的检查:
#include <stdint.h>
#include <stdio.h>
typedef struct {
int q; // 当前输出
double t_setup; // 建立时间 (ps)
double t_hold; // 保持时间 (ps)
double t_cq; // Clock-to-Q 延迟 (ps)
int metastable; // 亚稳态标志
} D_FlipFlop;
void dff_clock(D_FlipFlop *ff, int d, double d_change_time,
double clk_edge_time) {
// 检查建立时间:数据必须在时钟边沿之前 t_setup 稳定
if (clk_edge_time - d_change_time < ff->t_setup) {
ff->metastable = 1;
ff->q = -1; // 不确定值
return;
}
// 检查保持时间:数据在时钟边沿之后 t_hold 内不能变
//(在完整模拟中需要 scheduler 支持,这里简化)
ff->metastable = 0;
ff->q = d; // 正常采样
}
void demo_dff() {
D_FlipFlop ff = {0, 50.0, 20.0, 100.0, 0};
// 正常采样:数据在边沿前 80ps 已稳定(>50ps 建立时间)
dff_clock(&ff, 1, 100.0, 180.0);
printf("Q = %d (正常)\n", ff.q); // Q = 1
// 建立时间违例:数据在边沿前 30ps 才变(<50ps 建立时间)
dff_clock(&ff, 0, 250.0, 280.0);
printf("Q = %d, metastable = %d (亚稳态!)\n",
ff.q, ff.metastable); // Q = 不确定
}
这个模拟抓住了关键:D 触发器的行为不是“即时“的。数据不能在时钟边沿附近变化——它需要一段安静时间。 违反这个要求,触发器就进入亚稳态。
同步时序电路:组合逻辑 + 寄存器
当你把组合逻辑和寄存器组合起来,就得到了同步时序电路:
+--------+ +--------+ +--------+
| 寄存器 |────>| 组合 |────>| 寄存器 |────> ...
| (FFs) | | 逻辑 | | (FFs) |
+--------+ +--------+ +--------+
↑ ↑
CLK CLK
每一个时钟周期:
- 前级寄存器输出稳定数据。
- 组合逻辑用这一拍的时间来传播和计算。
- 结果在下一个上升沿被“拍“进后级寄存器。
这就是所有同步数字电路的基本架构。 从最简单的计数器到最复杂的多核 SoC,都是这个架构的递归嵌套——寄存器、云、寄存器、云、寄存器……一层套一层。
一个 4 位计数器:时序逻辑的最简实例
#include <stdint.h>
typedef struct {
uint8_t value; // 4-bit 计数值
uint8_t enable; // 使能
uint8_t reset; // 同步复位
} Counter4Bit;
void counter_clock(Counter4Bit *ctr) {
// 同步复位(在时钟上升沿采样)
if (ctr->reset) {
ctr->value = 0;
return;
}
if (ctr->enable) {
// 用上一节定义的全加器逻辑:加 1
int a[4] = {ctr->value & 1, (ctr->value>>1)&1,
(ctr->value>>2)&1, (ctr->value>>3)&1};
int one[4] = {1, 0, 0, 0};
int sum[4], c_out;
ripple_carry_adder(a, one, 0, sum, &c_out);
// 4-bit 环绕:忽略 c_out
ctr->value = sum[0] | (sum[1]<<1) | (sum[2]<<2) | (sum[3]<<3);
}
// 无使能:保持原值
}
这个 4 位计数器——enabled 时在 CLK 上升沿加 1,reset 时归零。它只有 4 个 D 触发器和一个 4 位加法器。但把它放大到 64 位,加上一个起始值,再用一个捕获信号——你就得到了 PTP 硬件时钟(PHC)的核心。PTP 从时钟内部就是一个巨大的计数器从 0 开始昼夜不停地数——数的是本地振荡器的周期。PTP 报文到达时,硬件把那个瞬间的计数值锁存下来,这就是硬件时间戳。全部由一串 D 触发器和加法器完成。
移位寄存器
把 N 个 D 触发器串联——前一个的 Q 接到后一个的 D——就得到一个移位寄存器。每个时钟周期,数据向右移动一位。
CLK: _|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_
D_in: ___ ¯¯¯¯ _____ ______________
Q0: ______ ¯¯¯¯ _____ __________
Q1: _________ ¯¯¯¯ _____ _______
Q2: ____________ ¯¯¯¯ _____ ____
移位寄存器是串并转换(SPI 的数据接收)和并串转换(SPI 的数据发送)的物理基础。UART 的发送端也是一个移位寄存器——并行数据加载进去,每个波特率时钟移出一位到 TX 线。
三个关键时序参数
但“拍“这个动作不是魔法。D 触发器对时序有严格的要求。
- Setup time(建立时间):数据必须在时钟边沿之前稳定下来的最短时间。如果数据在建立时间窗口内还在变,FF 抓到的可能是 0 也可能是 1——甚至是一个中间电平。这叫亚稳态(metastability)。
- Hold time(保持时间):数据必须在时钟边沿之后继续保持稳定一段时间。
- Clock-to-Q delay:时钟边沿之后,Q 端新数据稳定输出所花的时间。
想象地铁车门。你必须在门开始关闭之前踏进车厢——这就是Setup Time。门关到一半的时候,你不能又伸一只脚进去——这就是Hold Time。如果违反了Setup——你被门夹住了(亚稳态)。如果违反了Hold——你的脚被夹断(数据丢失)。D触发器的时钟边沿就是那扇正在关闭的门。
整个数字电路的最大频率由这个公式决定:
f_max = 1 / (T_cq + T_comb + T_setup + T_skew)
其中 T_comb 是两级寄存器之间的组合逻辑最大路径延迟,T_skew 是时钟偏差——同一个 CLK 到达不同 FF 的时间差异。
在典型工艺下,两级寄存器之间放十几级逻辑门,每级约几十皮秒。T_cq、T_setup、T_skew 都在几十皮秒到上百皮秒级别。一个 15 级逻辑深度的路径,总延迟约几百皮秒,f_max 在 1-2 GHz 范围内。
但如果切换到最差工艺角(慢 NMOS、慢 PMOS)、高温、低压的条件,每级门延迟可能增加 50% 以上,T_cq 也会显著增加。同一个设计,在不同工艺角下最高频率可以差 50%。 这就是为什么芯片要标“工作频率范围“而不是“固定频率“——它不是不想稳定,是物理做不到稳定。
这也是为什么 IC 内部能跑 GHz、而分立芯片方案只有 MHz 的原因。 IC 内部的走线极短、时钟 skew 可控、门延迟极小。PCB 上走线长、电容大、电磁干扰多——T_comb 和 T_skew 都大得多。
亚稳态:当触发器“犹豫不决“
上面提到了亚稳态。这是一个值得专门讨论的概念——因为它是所有跨时钟域(CDC)设计的核心问题。
D 触发器的内部有一个正反馈环——两个反相器首尾相连。正常工作状态下,这个反馈环被强制拉到 0 或 1。但如果数据在建立时间窗口内变化,输入锁存器捕获到的可能是一个介于 V_IL 和 V_IH 之间的电压——既不是干净的 0,也不是干净的 1。
这时反馈环内的两个反相器会进入线性区——它们像一个放大器,试图把输入放大到满摆幅。但如果输入电压恰好是反馈环的亚稳态点(约 VDD/2),两个反相器的增益恰好让输出等于输入——整个环停留在中间电压上。
它不会永远停在中间——热噪声最终会打破平衡,让它跌落到 0 或 1。但这个过程需要的时间是不确定的——可能是一个时钟周期,也可能是几百个时钟周期。在这段时间内,后续的逻辑电路可能看到一个 0,可能看到一个 1,可能看到振荡——这意味着计算结果是不可重复的。
这就是为什么跨时钟域时必须放同步器——两级或三级 D 触发器串联,给第一个触发器足够的“平均故障间隔时间“(MTBF)从亚稳态恢复。两级同步器的 MTBF 通常在几十年以上(对于 200MHz 时钟域),但对于 ASIL-D 系统,通常还是需要三级同步器或异步 FIFO。
亚稳态不是设计错误,是物理规律。你无法消除它,只能把它发生的概率降到可接受的范围。
PTP 硬件时间戳:触发器的终极应用
现在回过头来看 PTP。
PTP 从时钟需要记录报文到达 MAC 层的精确时刻,精度要求在纳秒级。怎么做到的?
现代支持 PTP 的以太网 MAC(比如 TI DP83640)的做法:
- 在 MII/RMII 接口上监测 RX_DV(接收数据有效)信号的跳变。
- RX_DV 变为高电平的瞬间,硬件电路产生一个捕获信号。
- 这个捕获信号触发一组寄存器(即 D 触发器阵列),锁存当前 PTP 硬件时钟(PHC)的计数值。
整个过程在物理层完成,延迟只有几个纳秒,抖动在亚纳秒级别。没有中断响应时间,没有软件处理延时,没有操作系统的调度不确定性——因为全部是触发器和组合逻辑在干活。
PTP 的纳秒精度怎么来的?回答是:它把“记录时间“这件事,完全交给了一条以固定频率运转的计数器 + 一个边沿触发锁存的 D 触发器组。这是纯硬件,没有任何软件的不确定性。
而在 PTP 的 PI 伺服控制器中,频率校正值写入 PHC 的频率调整寄存器——本质上是一个加法器,每个时钟周期把频率校正值累加到一个相位累加器中,溢出时产生一个时钟 tick。这也是纯时序逻辑。整个 PTP 协议的精髓不在软件——在硬件。在那些昼夜不停翻转着的触发器上。
软件时间戳做不到这个精度,因为软件只有轮询或中断——即使在 RTOS 上,响应延迟也有微秒级。差了三个数量级。
你在数电课上学过的 D 触发器——这个看起来最基础的单元——是 PTP 硬件时间戳的物理基础。
从“死物“到“活物“
组合逻辑是“瞬间的真理“——输出永远是当前输入的函数。
时序逻辑是“历史的记忆“——输出不只是当前输入的函数,还带着过去的痕迹。
这一“记忆“能力,把电路从“死物“变成了“活物“。你在设计的任何系统,本质上都是在“计算“和“记忆“之间找平衡:
你在ECU里写CAN诊断状态机的时候,每一次状态转移——从“默认会话“到“扩展会话“——都是一组D触发器在时钟边沿更新自己的值。PTP的PI伺服控制器累计历史偏移用于频率校正——那个“积分项“的每个增量,都锁存在一个寄存器里,等待下一个Sync周期被读出来。记忆——就是电路学会等。
你的 ECU 不是“在计算“——它是在用记忆辅助计算,用计算保护记忆。
本篇小结
今天我们做了一件事:理解了时序逻辑——D触发器给了电路“记忆“,让它不再只是当前输入的函数。
关键结论:
- D触发器是1 bit记忆:在每个时钟上升沿Q←D,其余时间保持不变——N个串联就是寄存器,寄存器+组合逻辑就是所有同步数字电路的基本架构。
- 三个时序参数决定芯片能跑多快:T_cq + T_comb + T_setup + T_skew 共同限制最大频率,工艺角变化能让f_max差50%。
- 亚稳态不是设计错误,是物理规律:你无法消除它,只能把概率降到可接受范围——跨时钟域必须放同步器。
下一节,数据通路与控制器的双人舞——把加法器、寄存器、ALU编排成能执行程序的处理器。
【下集预告】
你现在手里有加法器、多路器、寄存器文件、ALU、程序计数器——都是前两节造出来的积木块。它们摊在桌面上,每个都能用。但从桌面上,你得不到一个能执行程序的处理器。
你需要一个指挥。一个能把“取指→译码→执行→写回“这四个动作编排成一支舞的东西。
下一节,数据通路与控制器的双人舞。