固件说自己是 400 MHz,怎么从外部证明它不是 380?
用逻辑分析仪量 PA8 翻转周期反推:4,907 个跳变、有效间隔均值 99.9975 µs,反推 400.010 MHz(+25 ppm,标准误 1.6 ppm)。同一公式若把假定值填成 450,会打印出一个误差 +0.002% 的完美结论。
固件里写着 400 MHz,那只是它自己算出来的。要证明它,得让外部仪器量一个由该时钟驱动的物理信号,再反推。实测一份 4,907 个跳变的抓取:PA8 翻转间隔均值 99.9975 µs,反推 400.010 MHz,相对标称 +25 ppm;而间隔只有三种取值,彼此相差 41.7 ns,正好是仪器的时间戳栅格——看到的"抖动"是量化,不是电路。
现象
主频由几个宏乘出来(HSE 25 MHz → PLL 分频 → SYSCLK),寄存器读回也只能证明"配置写对了",不能证明"时钟真的跑在那个频率":晶振偏、分频算错、电压不足降频,读数都可能依然是好看的。
| 证据类型 | 能证明什么 | 不能证明什么 |
|---|---|---|
| 固件打印频率 | 宏算出来是多少 | 实际跑的是多少 |
| SWD 读 RCC 寄存器 | 配置位写成了什么 | 时钟树是否按配置输出 |
| 外部仪器量引脚 | 引脚真的按什么节奏在变 | 内部计算是否正确 |
第三种才是物理证据。代价是你得引一根线出来。
原因
这段推理的关键,是"被测信号的周期由固件假定的频率算出来",所以实测周期与真实频率成反比:
定时器计数 = (TIMxCLK / 1e6) × 100 // 固件按假定时钟算出 20000
实际半周期 = 计数 ÷ 真实 TIMxCLK
⇒ cpu_actual = cpu_assumed × 100 µs ÷ 实测半周期
- 若假定值正确:测到 100 µs,反推等于假定值。
- 若真实频率比假定低 5%:同一份固件会输出 105 µs,反推 380 MHz。
这就是为什么它算"外部证据":周期是引脚上真实发生的,不依赖固件怎么想。
怎么做
固件侧:让一个定时器每 100 µs 翻转一次引脚(此处 PA8),其余什么都不做。若时钟初始化失败,就让引脚闪错误码次数后停住——这样"没有方波"本身就是一条信息。
主机侧:逻辑分析仪抓该通道,导出二进制;Saleae 的跳变列表格式是 44 字节头 + 每个跳变 8 字节 double 时间戳:
import struct, statistics
raw = open("digital_4.bin", "rb").read()
assert raw[:8] == b"<SALEAE>"
ntr = struct.unpack_from("<Q", raw, 36)[0] # 跳变数
assert len(raw) == 44 + 8 * ntr # 长度自校验
ts = struct.unpack_from("<%dd" % ntr, raw, 44) # 单位: 秒
iv = [ts[i+1] - ts[i] for i in range(ntr - 1)]
iv = iv[1:] # 剔除首个间隔(含抓取起点伪影)
mean = statistics.mean(iv)
sem = statistics.pstdev(iv) / len(iv) ** 0.5 # 均值的标准误
cpu_assumed = 400e6
cpu_actual = cpu_assumed * 100e-6 / mean
print(f"edges={ntr} mean={mean*1e6:.7f}us "
f"SEM={sem*1e9:.3f}ns -> {cpu_actual/1e6:.4f}MHz")
要点有三个:长度自校验(44 + 8×N 对不上就说明格式判断错了)、剔除首个间隔(它跨越"开始采集"那一刻,必然偏大)、报标准误而不是标准差——单个间隔的分辨率受仪器栅格限制,但均值的标准误可以小两个数量级。
验证
同一份抓取文件(39,300 字节,对应 44 + 8×4,907)的全部关键量:
| 量 | 实测值 | 说明 |
|---|---|---|
| 跳变数 | 4,907 | 跨度 0.490628 s |
| 有效间隔 | 4,905 | 剔除首个 140.208 µs |
| 间隔取值 | 99,958.3 / 100,000.0 / 100,041.7 ns | 仅三种,台阶 41.7 ns |
| 均值 | 99.9974516 µs | 与设计的 100 µs 差 2.5 ns |
| 标准差 / 标准误 | 11.25 ns / 0.161 ns | 标准误 = 1.6 ppm |
| 反推主频 | 400.0102 MHz | 假定 400 MHz,偏差 +25 ppm |

三个细节值得单独说:
① 三档取值不是抖动分布。 相邻取值相差 41.7 ns,等于 1/24 MHz,也就是仪器时间戳的栅格。样本标准差 11.25 ns 比这个栅格还小,说明真实抖动低于仪器分辨率——这里测到的是量化,不是电路。
② 均值可以用分辨率换精度。 单样本分辨率 41.7 ns(约 417 ppm),但 4,905 个样本取均值后标准误降到 0.161 ns(1.6 ppm)。前提是量化误差在真值两侧来回跳(此处 338 次偏低、38 次偏高),否则系统偏差不会被平均掉。
③ 反推结果与固件自洽。 固件按 200 MHz 定时器时钟算出 20,000 个计数,正是 100 µs;实测 99.9975 µs 与之相符,等价于"晶振、PLL 分频、定时器时钟三者都对"。
一个会给出完美结论的假阳性
这套方法的公式是自洽的,所以假定值填错时,它会给你一个看起来无懈可击的结果。
同一份 400 MHz 固件,如果脚本默认假定值是 450 MHz:
实测 100.000 µs → 反推 450.01 MHz → 打印「偏差 +0.002%,时钟树正确」
误差 12.5%,输出却是"精确命中"。原因是公式两边都用了同一个错误的假定值,约掉了。
对应的工程对策有三条,缺一不可:
- 假定值必须从源码解析,不要写死在脚本里(从
clock.h的宏逐层代入)。 - 打印假定值的来源,让人一眼看出这次用的是哪个数。
- 假定值无法确定时拒绝出结论,而不是用默认值凑一个。
改前 450 MHz → 改后 400 MHz:把默认假定值改为从源码解析,并把来源打印在首行——同一份数据,结论才从"看起来对"变成"可以核对"。
边界
- 反推得到的是"真实值 ÷ 假定值"的比值,不是绝对频率:晶振本身偏离多少 ppm 是未知量,要拿绝对精度得用外部参考时基或校准过的频率计。
- 单次间隔的分辨率由仪器栅格决定(本例 41.7 ns,约 417 ppm);只有均值能达到 ppm 级,且要求量化误差双向分布。
- 首个间隔必须剔除,否则一个 140 µs 的起点伪影会把均值拉偏约 8 ns(相当于 80 ppm)。
- 结论只在"该引脚翻转完全由这个定时器决定"时成立;若主循环里还有别的路径翻转同一引脚,测到的是混合节奏。
- 低功耗模式下测的频率不能直接外推到满载:本次抓取来自
wfi空载主循环,满载电流更大,需要重测。 - 探头地线要就近接:远处地会引入窄毛刺,把"没有抖动"读成"有抖动"。
最后验证于 2026-10-29 · Saleae Logic 8 抓取文件(.la/raw/digital_4.bin)· STM32H723ZG · 固件 src/clock.h(HSE 25 MHz,PLL 80 倍频,TIMxCLK 200 MHz)