2. 时钟框架入门

各位同学,大家好。今天我们聊聊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 + enable
  • clk_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 时钟框架的核心流程

为了让你更直观地理解,我画了一张图。这张图展示了从设备树解析到驱动使用时钟的完整链路。

Linux时钟框架核心流程 设备树(DTS) clocks = <&clkc 15> OF时钟解析(of_clk_get) 解析#clock-cells,获取时钟句柄 clk框架核心层 clk_prepare / clk_enable / clk_set_rate 时钟提供者驱动 clk_register / clk_ops回调 硬件寄存器 消费者侧 提供者侧

从这张图你能看到,整个流程分四层:设备树描述、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都会轻松很多。下一讲我们会深入时钟提供者的实现,手把手教你写一个时钟控制器驱动。

可可精品资料库