4. 通用时钟接口API:clk_get、clk_prepare_enable、clk_set_rate、clk_round_rate等核心函数详解

时钟框架的API,说白了就是一套标准化的「操作手册」。你不需要关心底层时钟树怎么搭,调这几个函数就行。我刚开始接触时也觉得函数太多记不住,后来发现核心的就那么几个——今天咱们把它们掰开揉碎了讲清楚。

4.1 时钟API的「三板斧」:获取、准备、使能

操作一个时钟,基本流程就三步:拿到句柄、准备好、打开它。对应到代码里就是 clk_get、clk_prepare、clk_enable。不过实际项目中,大家更习惯用组合函数 clk_prepare_enable,一步到位。

4.1.1 clk_get:拿到时钟的「身份证」

这个函数的作用很简单——根据设备名和时钟名,找到对应的时钟结构体指针。

struct clk *clk_get(struct device *dev, const char *id);

参数说明:

  • dev:设备指针,可以为NULL
  • id:时钟名称,在设备树或时钟驱动里定义
我的习惯:能传dev就传dev,别偷懒传NULL。因为传dev时,框架会自动帮你解析设备树里的"clocks"属性,省得你手动拼名字。我在调试某款AI芯片时,就因为传了NULL导致时钟名拼写错误,查了两天才发现。

4.1.2 clk_prepare与clk_enable:为什么分两步?

很多新手会问:为啥不合成一个函数?其实这是Linux内核的精心设计——clk_prepare可能休眠(比如操作I2C去配置PLL),而clk_enable必须在原子上下文中调用(比如中断处理函数)。

函数 能否休眠 调用上下文 典型耗时
clk_prepare 可以 进程上下文 毫秒级(PLL锁定)
clk_enable 不能 原子上下文 微秒级(寄存器操作)
clk_prepare_enable 可以 进程上下文 两者之和

实际项目中,我99%的情况都用 clk_prepare_enable。只有写中断处理函数或DMA回调时,才会手动拆开调用。

// 推荐写法:一步到位
clk = clk_get(dev, "uart_clk");
if (IS_ERR(clk))
    return PTR_ERR(clk);

ret = clk_prepare_enable(clk);
if (ret)
    goto err_clk;

// 中断上下文写法:必须分开
clk_prepare(clk);  // 在probe里提前准备好
// 在中断处理中:
clk_enable(clk);
// ... 操作硬件 ...
clk_disable(clk);
我曾经踩过的坑:在某个网络驱动里,我在中断处理函数中直接调了clk_prepare_enable,结果系统死锁。因为clk_prepare内部拿了mutex,而中断上下文不允许睡眠。从那以后,我养成了习惯——先看函数注释里有没有"may sleep"字样。

4.2 频率控制:clk_set_rate与clk_round_rate

拿到时钟并打开后,下一步就是调频率。这两个函数是频率控制的核心。

4.2.1 clk_set_rate:我要这个频率!

int clk_set_rate(struct clk *clk, unsigned long rate);

这个函数会沿着时钟树向上传播,找到能改变频率的时钟节点(通常是PLL或分频器)。返回值是实际设置的频率,但注意——它可能不是你请求的那个值。

为什么会这样?因为硬件分频器只能做整数分频,或者PLL有固定的频率步进。比如你请求100MHz,但硬件只能输出99.99MHz或100.01MHz,那它就会选一个最接近的。

4.2.2 clk_round_rate:先问问能不能做到

long clk_round_rate(struct clk *clk, unsigned long rate);

这个函数不会真的改变频率,它只是「询价」——返回硬件能支持的最接近频率。我个人的习惯是:在调用 clk_set_rate 之前,先用 clk_round_rate 探一下底。

unsigned long target_rate = 100000000;  // 100MHz
unsigned long actual_rate;

// 先询价
actual_rate = clk_round_rate(clk, target_rate);
if (actual_rate < 0) {
    pr_err("clock rate not supported\n");
    return -EINVAL;
}

// 检查误差是否在可接受范围内
if (abs(actual_rate - target_rate) > 1000000) {  // 误差超过1MHz
    pr_warn("rate mismatch: request %lu, actual %lu\n",
            target_rate, actual_rate);
}

// 设置频率
ret = clk_set_rate(clk, target_rate);
if (ret)
    return ret;
关键点:clk_round_rate返回的是long类型,负值表示错误。clk_set_rate返回的是int,0表示成功。别搞混了——我见过有人把clk_set_rate的返回值当频率用,结果调试了一整天。

4.3 其他常用API速查

除了上面四个,还有几个API你迟早会用到:

函数 作用 注意事项
clk_get_rate 获取当前实际频率 返回的是硬件配置值,不是请求值
clk_disable_unprepare 关闭时钟(逆操作) 可以休眠,不要在中断里调
clk_put 释放时钟句柄 记得在错误路径和remove里调
clk_get_parent 获取父时钟 用于调试时钟树拓扑
clk_set_parent 切换时钟源 某些硬件切换时会有毛刺

4.4 时钟API调用流程图

下面这张图展示了典型的时钟操作流程,从获取到释放的完整生命周期:

时钟API调用生命周期 clk_get() clk_prepare() clk_enable() clk_set_rate() clk_disable() clk_unprepare() clk_put() 快捷组合 clk_prepare_enable() = clk_prepare() + clk_enable() clk_disable_unprepare() = clk_disable() + clk_unprepare()

4.5 实战中的避坑指南

讲完理论,说几个我实际项目中遇到的坑:

  1. clk_get返回的不是NULL,而是ERR_PTR。千万别用 if (!clk) 判断,要用 IS_ERR(clk)。我见过有人直接解引用ERR_PTR,内核直接oops。
  2. clk_set_rate可能向上取整也可能向下取整。不同驱动实现不一样,所以一定要用 clk_get_rate 确认实际值。
  3. 频率切换时外设要暂停。如果你在UART发送数据时改了时钟频率,数据大概率会错乱。我一般在改频率前先disable外设,改完再重新配置波特率。
  4. 别忘了引用计数。每次 clk_get 都要对应一次 clk_put,否则时钟框架会泄漏。我在某个项目中漏了remove路径的clk_put,结果热插拔几次后系统就再也打不开那个时钟了。
调试小技巧:在/sys/kernel/debug/clk目录下,你可以看到所有时钟的树形结构、当前频率、使能计数。配合 clk_summary 文件,能快速定位哪个时钟没关。我每次做低功耗调试,第一件事就是cat这个文件。

嗯,以上就是时钟API的核心内容。记住一个原则:先获取,再准备,后使能;用完要逆序释放。这个顺序乱了,系统就会出各种奇怪的问题。