固件说自己是 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

逻辑分析仪实测:PA8 翻转间隔的三种取值

三个细节值得单独说:

① 三档取值不是抖动分布。 相邻取值相差 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%,输出却是"精确命中"。原因是公式两边都用了同一个错误的假定值,约掉了。

对应的工程对策有三条,缺一不可:

  1. 假定值必须从源码解析,不要写死在脚本里(从 clock.h 的宏逐层代入)。
  2. 打印假定值的来源,让人一眼看出这次用的是哪个数。
  3. 假定值无法确定时拒绝出结论,而不是用默认值凑一个。

改前 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)

固件说自己是 400 MHz,怎么从外部证明它不是 380?

作者 JimmyZ,嵌入式 / 确定性控制方向。GitHub · 掘金