本文约4500字,今天遇到时钟频率引起的干涉问题了,还未解决,先来科普一下时钟频率的相关知识,补齐知识盲区。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
你是否曾因调整了一个外设时钟,无线吞吐量就急剧恶化?
或者系统出现了难以复现的定时抖动,最后发现根源竟是“频率值”这个不起眼的参数?
我们常会把时钟简单看作一个寄存器数字,但它的本质是一个物理波形。
本文将通过一些权威文档来分析时钟频率从晶振到内核驱动的完整生成与控制过程,再剖析这些频率值引发的电磁干涉、电源噪声和定时陷阱。一切基于 Linux 4.19 及之后的 Common Clock Framework(CCF)。
clk_set_rate 的全景一块电路板上的时钟,最初来自一个石英晶体振荡器,它提供某个固定频率(例如 24 MHz、32.768 kHz)。这个基准频率送入 SoC 内部的 PLL(锁相环),由 PLL 合成出各种所需速率。
一个典型的整数 PLL 结构如下:
参考频率 Fin ──→ 鉴频鉴相器 PFD ──→ 环路滤波器 ──→ 压控振荡器 VCO
↑ │
└──── 反馈分频器 (M) ←──────────────┘
输出频率 Fout = Fin × (M / (N × OD))
其中 N 为输入预分频器,M 为反馈分频器,OD 为输出后分频器(不同 SoC 的公式命名有差异,但原理一致)。
当我们需要一个非整数倍频时,分数 PLL(Fractional‑N PLL) 登场:它通过 Sigma‑Delta 调制器动态切换反馈分频系数,使长期平均为一个小数。例如 M 在 100 和 101 之间以 1/4 的占空比切换,则可等效为 M=100.25。代价是引入了量化噪声,表现为时钟的相位噪声和确定性抖动——这是后续抖动问题的根源。
Linux 4.19 之后的 CCF 将上述硬件映射为几种标准对象,并统一通过 struct clk_hw 和 clk_ops 操作:
rate 永不改变。enable/disable 不会改变频率。recalc_rate 计算 rate = parent_rate / div;round_rate 寻找可达到的最接近分频值。clk_hw,其 set_rate 会修改 M/N/OD 等寄存器。所有这些节点通过 clk_parent 指针连成一棵时钟树。设备树中的 clocks = <&clk_controller N> 就是这棵树的索引。当你在驱动中调用 clk_get + clk_set_rate,操作的是这棵树上的一个节点。
clk_set_rate 的内部之旅:请求如何向上传播当我们写下一行 clk_set_rate(clk, 50000000) 时,内核并不是简单地去修改一个寄存器。完整流程如下:
向上遍历请求
内核从当前 clk_core 出发,检查其 flags:
CLK_SET_RATE_PARENT,则将频率请求传递到父时钟,直到无法继续向上或遇到 PLL。自顶向下计算新频率 (clk_calc_new_rates)
从目标时钟树的最顶端开始,依次调用每个节点的 round_rate 或 determine_rate 回调。这些回调会根据硬件限制(分频表、PLL 的 VCO 范围)返回最接近的可行速率。该过程会同时确定是否需要更换父时钟(mux 切换)。
自顶向下更新硬件 (clk_change_rate)
计算完毕后,内核按照严格的顺序(通常是先设父时钟 PLL,再设分频器,最后开 gate)调用 set_rate 和 set_parent 回调,写入硬件寄存器。每完成一级,就通过 recalc_rate 重新读取实际速率并更新子树的缓存值。
通知链
设置完成后,内核发出 POST_RATE_CHANGE 通知,让注册的 notifier(如 DRM 驱动、音频驱动)能够及时更新自身的时序参数。
关键点:正是因为 CLK_SET_RATE_PARENT 的存在,一个看似独立的外设频率请求,可能引起整条时钟链的重配置。共用 PLL 的兄弟设备会在不知情的情况下被“换挡”。
assigned-clock-rates:一把更早、更猛的快刀我们经常在设备树中这样写:
&clk_ctrl {
assigned-clocks = <&clks CLK_UART1>, <&clks CLK_USB_PHY>;
assigned-clock-rates = <24000000>, <480000000>;
assigned-clock-parents = <&clks PLL_SYS>;
};
这些属性会在内核早期、对应 clock controller 驱动加载后立即生效(通过 of_clk_set_defaults -> clk_set_rate 完成)。它的优先级和强制性甚至高于驱动中的 clk_set_rate 调用,因为许多驱动的 probe 函数还没执行时,频率已经锁死。
从 Linux 4.19 起,对于带 CLK_SET_RATE_GATE 标志的时钟,设置频率前必须先关闭时钟(gate)。如果 assigned-clock-rates 指定的值需要重配 PLL,而 PLL 此刻正为某个已经在运行的硬件提供时钟,那么内核可能会因 __clk_set_rate 中的检查而直接失败或产生一个短暂的 glitch。这就是为何随意在设备树中填写频率值极其危险:它会在启动的最早阶段执行,缺乏驱动层的保护逻辑。
有了上述机制,我们再来看频率干涉的经典场景:
clk_set_rate 向上追溯,最终需要将 PLL_SYS 重设为 594 MHz(因为 594 ÷ 4 = 148.5)。这也是为什么我们在调试莫名复位或通信失败时,必须先用 /sys/kernel/debug/clk/clk_summary 查看时钟树是否被意外“牵连”。
每一个内核赋给时钟的频率,最终都会变成 PCB 上的方波。而方波,是由基波与无数奇次谐波构成的。
假设 SoC 的 MIPI‑DSI 时钟被设为 205 MHz。
其 7 次谐波频率为 205 × 7 = 1435 MHz。
这个频率非常接近 GPS L1 频段(中心频率 1575.42 MHz)和航空无线电导航频段。虽然基波可能被屏蔽,但高频谐波通过地平面或电源网络耦合到 RF 天线路径后,会抬高接收机噪底,导致 GPS 锁定时间变长甚至失锁。
实战策略:微调频率避开受害频段。
例如将 205 MHz 改为 207.36 MHz(这是常见音频晶振 12.288 MHz 的整数倍),7 次谐波变为 1451.52 MHz,错开了 GPS 的敏感区间,干扰功率可能下降 10 dB 以上。这类调整必须在频谱仪验证下进行,并在设备树的 assigned-clock-rates 中锁定。
现代 SoC 内部有多个独立时钟域,例如:
它们通过共享的电源分配网络(PDN)供电。每一个时钟域的数字开关动作都会在电源轨上造成对应频率的电压纹波。当两路纹波叠加时,会产生差频分量:
933 − 800 = 133 MHz,以及更低频率的包络(例如两者电源管理的周期性 burst 形成的 Hz~kHz 级差频)。
如果该差频刚好接近板上音频 DAC 的过采样频率,或触摸屏矩阵扫描频率,就会引发共振:
内核的 cpufreq / devfreq 会动态改变 CPU 和 GPU 频率,这使得拍频频率随负载漂移,问题变为“幽灵”般的概率事件。解决办法包括错开时钟域频率、优化 PDN 去耦,或在驱动层强制锁频(performance 电源策略)以排除干扰。
PCIe Gen3(8 GT/s)或 USB 3.1(5 Gbps)对时钟抖动有严苛要求。
CCF 中,很多 SoC 驱动为时钟源实现了 CLK_FRACTIONAL_DIVIDER。当你为了达到某个精确速率(例如从 594 MHz 得到 74.25 MHz)而使用分数分频时,Sigma‑Delta 调制引入的高频相位噪声将使时钟的 确定性抖动(Dj) 显著增加。
这种抖动直接导致串行链路的 眼图闭合,表现为误码率(BER)上升,链路反复 Retrain 或降速。
铁律:高速串行链路的参考时钟应优先使用整数分频(如 PLL 直接输出 100 MHz,或通过 /2、/4 等整数分频得到)。若必须使用分数 PLL 来兼容多种速率,请在硬件设计中预留展频时钟(SSC)选项,并在内核中通过 assigned-clock-parents 将该链路的时钟源指向具备展频能力的 PLL,牺牲一些频率绝对精度,换取抖动合规。
时钟频率不仅产生物理噪声,它还直接是 Linux 时间子系统的“心跳”。
clocksource 精度与时间漂移内核通过 clocksource 结构维持系统时间线。ktime_get() 最终依赖 clocksource.read() 返回的计数值,并由频率 mult 与 shift 转换为纳秒。
如果某个 clocksource 的基准时钟频率可在线改变,而该改变没有通过 clocksource.change_freq 或重注册机制通知内核,就会发生:
mult 值已经过时。clock_gettime 忽然快/慢百分之几十。Linux 4.19 中,CLOCK_SOURCE_IS_CONTINUOUS 标记的审查极为严格,内核推荐使用固定的 arch_sys_counter(ARMv8 通用定时器)作为主时钟源,就是因为它的频率恒等于基准晶振,不受 CPU 频率变动影响。某些早期的 i.MX 平台曾用 GPT 定时器作时钟源,其根时钟挂载在可变的 AHB 总线上,低功耗模式下频率从 66 MHz 突降到 24 MHz,造成时间跳跃事故。
sched_clock 的回退灾难sched_clock 是调度器内部使用的高性能时间戳,必须单调递增。在 ARM64 下,它通常直接映射到 arch_sys_counter,频率恒定。但如果你在异构系统中为某些 Cortex‑M 协处理器定制了共用的时间源,而该源的频率受 cpufreq 影响,则可能出现 sched_clock 回退。其后果是调度器的 PELT 负载统计产生负值,导致 loadavg 计算错误,进而使任务放置和频率调节决策完全错乱,出现全系统卡顿或不可复现的调度延迟。
在 tickless 内核(CONFIG_NO_HZ)中,clockevent 设备负责编程下一次事件触发时间。如果你错误地将一个外设的时钟设为 100 MHz,并因驱动 bug 将该外设定时器注册为 clockevent 设备,set_next_event 将尝试用极小的计数值编程下一中断。
一旦生效,每 1 μs 就会产生一次定时器中断。
在 /proc/interrupts 中会看到 timer 中断线被淹没,CPU 时间全部耗在中断上下文处理上,用户任务完全饥饿。务必养成习惯:每当出现莫名性能问题时,先 cat /sys/kernel/debug/clk/clk_summary 扫一眼有没有时钟被设到了离谱的数值。
debugfs 时钟树快照cat /sys/kernel/debug/clk/clk_summary
关注 enable_cnt 和 rate 列。一旦发现顶层 PLL 的输出频率与设计意图不符,百分之九十的原因是某个子设备的 assigned-clock-rates 或 clk_set_rate 通过 CLK_SET_RATE_PARENT 强制改变了它。
clk_set_rate 调用链echo 1 > /sys/kernel/debug/tracing/events/clk/clk_set_rate/enable
cat /sys/kernel/debug/tracing/trace_pipe
结合 clk_set_parent 事件,你可以绘制出系统运行过程中时钟树的真实变化图,揪出那个在背地里改了 PLL 频率的“肇事者”。
某款网关产品使用 SDIO Wi‑Fi 模块,初始时钟设为 50 MHz。在 2.4 GHz 的 11 信道(2462 MHz)工作时,50 MHz 的 49 次谐波(2450 MHz)正好落入带内,降低了接收灵敏度。
通过频谱仪扫描确认后,我们将 SDIO 时钟改为 49.152 MHz(常见音频晶振倍频),其 49 次谐波移至 2408.448 MHz,远离工作信道,接收灵敏度立即提升 3 dBm,丢包率大幅下降。
&usdhc1 {
assigned-clocks = <&clk IMX8MP_CLK_USDHC1>;
assigned-clock-rates = <49152000>;
};
对于 HDMI/DP 这类需要高速像素时钟的接口,内核驱动通常支持通过 assigned-clock-parents 将时钟源指向一个支持展频的 PLL。例如 Rockchip 部分平台的 DRM 驱动可解析 rockchip,ssc-mode 属性,通过调制 PLL 频率将能量扩散到 5000 ppm 宽的频带内,峰值 EMI 可降低 10 dB 以上。
这在消费类产品整机认证中,往往能省去一个昂贵的屏蔽罩。
我们写下的每一个时钟频率值,都不只是一个无符号整数。
它是 PLL 锁定后的物理波形,是 PCB 上辐射的谐波,是电源网络上叠加的差频噪声,还是内核计时系统不可动摇的基石。
Linux 4.19+ 的 CCF 给了我们强大而灵活的控制力,但也要求我们以系统级视野去驾驭:
debugfs、trace 实时监控时钟树,不让 PLL 在背后悄悄“背叛”你的设计。参考文档:
Linux Kernel Documentation: Common Clock Framework (clk.rst), Device Tree Clock Bindings
Kernel source (4.19+): drivers/clk/clk.c, include/linux/clk-provider.h
PCI Express Base Specification, USB 3.1 Specification — jitter and SSC requirements
“谢谢你看到这里”嵌入式Linux设备内部跨模块通信方案对比与统一消息架构设计
嵌入式Linux设备内部跨模块通信方案对比与统一消息架构设计
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助