2、内核时钟框架:clk框架介绍、时钟使能/关闭、时钟频率调节、时钟门控与电源域

好,咱们进入第二章。这一章聊时钟框架,也就是 Linux 内核里的 clk 子系统。

说实话,我刚开始接触驱动开发时,觉得时钟不就是给个频率嘛,有啥好讲的?后来踩了几个坑才明白——时钟管理要是搞不好,轻则外设不工作,重则系统死机、功耗失控。嗯,这一章咱们就把 clk 框架掰开揉碎了讲清楚。

2.1 clk 框架整体介绍

Linux 内核的时钟框架,说白了就是一套标准化的 API,用来管理和控制 SoC 上所有的时钟资源。它屏蔽了底层硬件的差异,让驱动开发者可以统一调用。

我个人习惯把 clk 框架想象成一个「时钟树」——根节点是晶振或 PLL,中间经过各种分频器、多路选择器、门控单元,最终到达各个外设模块。

核心数据结构:struct clk

每个时钟源在驱动里就是一个 struct clk 指针。你不需要关心它背后是 PLL 还是 OSC,只需要调用 API 就行。

我记得第一次看 clk 框架代码时,被 clk_prepare 和 clk_enable 搞晕了——为啥要分两步?后来才明白,这是为了支持原子上下文和非原子上下文的分离。

2.2 时钟使能与关闭

这是最基础的操作。你写任何一个外设驱动,第一步几乎都是打开它的时钟。

// 获取时钟句柄
struct clk *clk_get(struct device *dev, const char *id);

// 使能时钟(完整流程)
clk_prepare(clk);      // 可能睡眠,不能在中断上下文调用
clk_enable(clk);       // 原子操作,可在中断上下文调用

// 关闭时钟
clk_disable(clk);
clk_unprepare(clk);

// 释放时钟句柄
clk_put(clk);

这里有个坑,我曾经踩过——在 probe 函数里只调了 clk_enable,没调 clk_prepare。结果系统跑着跑着就崩了。为什么?因为有些时钟源在使能前需要做 PLL 锁定等待,这个操作会睡眠,而 clk_enable 是原子操作,不允许睡眠。

我的建议:写驱动时直接用 clk_prepare_enable()clk_disable_unprepare() 这对组合函数,一步到位,省心省力。

2.3 时钟频率调节

频率调节是 clk 框架的重头戏。你想想看,一个外设可能需要在不同场景下跑不同频率——比如 LCD 在待机时降频,在播放视频时提频。

// 设置时钟频率
long clk_round_rate(struct clk *clk, unsigned long rate);
int clk_set_rate(struct clk *clk, unsigned long rate);

// 获取当前频率
unsigned long clk_get_rate(struct clk *clk);

这里有个细节要注意:clk_round_rate 返回的是硬件实际能支持的最近频率。你设 100MHz,硬件可能只能给 99.99MHz 或 100.01MHz。我建议先 round 一下,再 set,避免误差累积。

避坑指南:我曾经在某个项目中,直接调 clk_set_rate 设了一个硬件不支持的频率,结果外设直接罢工。后来养成习惯——先 round,再 set,最后 get 确认实际频率。

另外,频率调节可能会影响整个时钟树。比如你改了 PLL 的输出频率,下游所有分频后的时钟都会跟着变。所以调频前最好看看 clk 的父子关系。

2.4 时钟门控与电源域

时钟门控,英文叫 clock gating。说白了就是用一个开关控制时钟信号是否传递到模块。门控关闭时,模块没有时钟,不工作,省电。

但这里有个更深的层次——电源域(Power Domain)。

电源域是一组共享同一电源开关的硬件模块。当整个电源域断电时,里面的所有模块都会掉电,包括它们的寄存器状态。而时钟门控只是停掉时钟,模块还在供电,寄存器内容保留。

特性 时钟门控 电源域断电
功耗节省 中等(动态功耗) 高(动态+静态功耗)
寄存器状态 保留 丢失,需重新初始化
恢复时间 快(微秒级) 慢(毫秒级)
适用场景 短时空闲 长时间休眠

在实际项目中,我通常这样设计:

  • 短时间不用某个外设(比如几毫秒),只关时钟门控
  • 长时间不用(比如几秒以上),考虑切电源域

嗯,这里要注意——电源域操作往往涉及 PM 框架的配合,不是 clk 框架单独能搞定的。但 clk 框架会提供一些回调,比如 clk_ops 里的 prepareunprepare,可以在里面做电源域的上电和下电操作。

关键点:时钟门控是细粒度的,电源域是粗粒度的。两者配合使用,才能达到最佳的功耗管理效果。

2.5 实战经验总结

最后,我分享几个实战中总结的经验:

  1. 时钟树要画清楚——接手一个新平台时,我第一件事就是找芯片手册里的时钟树图,搞清楚每个外设的时钟源路径。
  2. 调试时多用 clk_summary——内核的 /sys/kernel/debug/clk/clk_summary 节点能显示所有时钟的状态、频率、使能计数,调试利器。
  3. 注意引用计数——clk_enable 和 clk_disable 要成对调用,内核内部有引用计数,多调一次 enable 就会导致时钟关不掉。
  4. 别在原子上下文调 prepare——这个错误我犯过不止一次,每次都是 panic 后才想起来。

好了,这一章的内容就到这里。时钟框架是电源管理的基础,搞懂了它,后面的 DVFS、runtime PM 才能玩得转。下一章咱们聊 CPUFreq 和 DevFreq,那才是真正的动态调频实战。