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 里的 prepare 和 unprepare,可以在里面做电源域的上电和下电操作。
关键点:时钟门控是细粒度的,电源域是粗粒度的。两者配合使用,才能达到最佳的功耗管理效果。
2.5 实战经验总结
最后,我分享几个实战中总结的经验:
- 时钟树要画清楚——接手一个新平台时,我第一件事就是找芯片手册里的时钟树图,搞清楚每个外设的时钟源路径。
- 调试时多用 clk_summary——内核的
/sys/kernel/debug/clk/clk_summary节点能显示所有时钟的状态、频率、使能计数,调试利器。 - 注意引用计数——clk_enable 和 clk_disable 要成对调用,内核内部有引用计数,多调一次 enable 就会导致时钟关不掉。
- 别在原子上下文调 prepare——这个错误我犯过不止一次,每次都是 panic 后才想起来。
好了,这一章的内容就到这里。时钟框架是电源管理的基础,搞懂了它,后面的 DVFS、runtime PM 才能玩得转。下一章咱们聊 CPUFreq 和 DevFreq,那才是真正的动态调频实战。