第二讲:时钟框架入门——Linux clk框架概述、时钟消费者与提供者、常见的时钟API函数
各位同学,大家好。今天我们聊聊Linux时钟框架。说实话,时钟这东西,在嵌入式开发里太基础了,基础到很多人容易忽略它。但你要知道,一个芯片跑不起来,十有八九是时钟没配好。我在项目中遇到过好几次,板子焊好了,系统死活起不来,最后发现是某个外设的时钟没开——嗯,这种坑,踩过一次就记住了。
2.1 Linux clk框架是干什么的?
Linux clk框架,说白了就是一套管理时钟的软件层。它把硬件上那些五花八门的时钟源、PLL、分频器、门控,抽象成统一的接口。你想想看,一个SoC里可能有几十个时钟域,每个外设需要的频率还不一样。如果没有框架,驱动工程师得直接操作寄存器,那代码就乱成一锅粥了。
我个人习惯把clk框架理解成“时钟的配电箱”。提供者是发电厂,消费者是各个电器。框架就是中间那套开关和变压器。你只需要说“我要多少伏”,框架帮你搞定一切。
核心思想:时钟提供者(Clock Provider)负责产生和调整时钟,时钟消费者(Clock Consumer)只管使用。两者通过clk框架解耦。
2.2 时钟消费者与提供者
先说说消费者。消费者就是使用时钟的设备,比如UART、I2C、SPI、GPU等等。消费者不关心时钟是怎么来的,只关心能不能拿到想要的频率。消费者通过设备树里的clocks属性来声明自己需要哪些时钟。
举个例子,一个UART节点可能长这样:
uart0: serial@10000000 {
compatible = "my-uart";
reg = <0x10000000 0x1000>;
clocks = <&clkc 15>;
clock-names = "uart_clk";
};
这里clocks指向了时钟控制器clkc的第15号输出。消费者驱动里通过名字"uart_clk"来获取这个时钟句柄。
再说提供者。提供者通常是时钟控制器(Clock Controller),它管理着晶振、PLL、分频器、门控等硬件资源。提供者要注册到clk框架里,告诉框架“我能提供哪些时钟,每个时钟有什么特性”。
提供者的设备树节点一般长这样:
clkc: clock-controller@20000000 {
compatible = "my-clk-controller";
reg = <0x20000000 0x1000>;
#clock-cells = <1>;
};
#clock-cells表示消费者需要提供几个参数来指定具体时钟。等于1的话,消费者传一个索引号就行。
小提示:我在调试时经常用/sys/kernel/debug/clk/clk_summary来查看当前所有时钟的状态。这个文件会列出每个时钟的使能计数、准备计数、频率等信息,排查问题非常有用。
2.3 常见的时钟API函数
接下来是重头戏——API函数。这些函数是驱动工程师每天都要打交道的。我按使用场景把它们分成几类:
2.3.1 获取和释放时钟
| 函数 | 作用 |
|---|---|
clk_get(dev, name) |
从设备或设备树获取时钟句柄 |
devm_clk_get(dev, name) |
带资源管理的版本,自动释放 |
clk_put(clk) |
释放时钟句柄 |
我个人强烈推荐用devm_clk_get。为什么?因为省心。你不需要手动调用clk_put,驱动卸载时框架会自动帮你清理。我曾经在早期代码里忘了clk_put,结果模块反复加载卸载后,时钟引用计数乱了,系统直接挂掉。从那以后,我所有新代码都用devm版本。
2.3.2 时钟使能与禁能
| 函数 | 作用 |
|---|---|
clk_prepare(clk) |
准备时钟(可能睡眠) |
clk_enable(clk) |
使能时钟(原子操作) |
clk_disable(clk) |
禁能时钟 |
clk_unprepare(clk) |
取消准备 |
这里有个容易踩的坑:clk_prepare和clk_enable是分开的。为什么这么设计?因为有些时钟的准备工作(比如PLL锁定)需要睡眠等待,不能在中断上下文里做。而clk_enable只是操作寄存器,可以原子执行。
注意:千万不要在中断上下文里调用clk_prepare!它可能会调用mutex_lock,导致内核崩溃。我曾经见过一个同事在中断处理里直接调clk_prepare_enable,结果系统死锁——嗯,调试了一整天。
为了方便,框架提供了组合函数:
clk_prepare_enable(clk)= prepare + enableclk_disable_unprepare(clk)= disable + unprepare
2.3.3 频率操作
| 函数 | 作用 |
|---|---|
clk_get_rate(clk) |
获取当前频率 |
clk_set_rate(clk, rate) |
设置频率 |
clk_round_rate(clk, rate) |
查询最接近的可实现频率 |
你想想看,设置频率时,硬件不一定能精确达到你想要的数值。比如你想要100MHz,但PLL只能输出96MHz或108MHz。这时候clk_round_rate就派上用场了——它告诉你实际能拿到多少,不会让你白忙活。
我在项目中习惯这样用:
long target = 100000000; // 100MHz
long actual;
actual = clk_round_rate(clk, target);
if (actual < 0) {
dev_err(dev, "invalid rate\n");
return actual;
}
if (actual != target) {
dev_warn(dev, "rate adjusted from %ld to %ld\n", target, actual);
}
clk_set_rate(clk, actual);
先查再设,这是标准做法。别直接设,设完再查,那样可能已经晚了。
2.3.4 其他实用函数
clk_get_parent(clk):获取父时钟clk_set_parent(clk, parent):切换父时钟clk_is_enabled(clk):检查时钟是否使能
切换父时钟这个功能,在低功耗场景下很常用。比如系统空闲时,把CPU时钟从PLL切到晶振,省电。我做过一个项目,切换父时钟时没注意glitch问题,结果时钟线上出现毛刺,外设直接挂了。后来加了clk_set_parent前后的保护逻辑才搞定。
2.4 时钟框架的核心流程
为了让你更直观地理解,我画了一张图。这张图展示了从设备树解析到驱动使用时钟的完整链路。
从这张图你能看到,整个流程分四层:设备树描述、OF解析、框架核心、提供者驱动。消费者驱动只跟框架核心打交道,不直接操作硬件。这就是抽象的好处——换一个SoC,驱动代码基本不用改。
2.5 实际开发中的注意事项
最后,我总结几个实战中容易出问题的地方:
- 时钟顺序:先prepare再enable,先disable再unprepare。别搞反了。
- 引用计数:clk框架内部有引用计数。你调几次enable,就得调几次disable。否则时钟关不掉,功耗下不来。
- 频率范围:不是所有时钟都能随便设。有些外设有频率上限,设太高会跑飞。记得看datasheet。
- 调试手段:除了
clk_summary,还可以用trace_clk事件跟踪时钟操作。我在定位一个时钟门控bug时,就是靠trace抓到某个驱动多调了一次enable。
我的习惯:写驱动时,我会在probe函数里先获取所有需要的时钟,然后统一prepare。在设备真正工作时才enable。这样既保证了初始化顺序,又避免了不必要的功耗。
好了,这一讲就到这里。时钟框架是嵌入式Linux的基石,理解透了,后面学电源管理、DVFS、cpuidle都会轻松很多。下一讲我们会深入时钟提供者的实现,手把手教你写一个时钟控制器驱动。
可可精品资料库