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:时钟名称,在设备树或时钟驱动里定义
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);
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;
4.3 其他常用API速查
除了上面四个,还有几个API你迟早会用到:
| 函数 | 作用 | 注意事项 |
|---|---|---|
| clk_get_rate | 获取当前实际频率 | 返回的是硬件配置值,不是请求值 |
| clk_disable_unprepare | 关闭时钟(逆操作) | 可以休眠,不要在中断里调 |
| clk_put | 释放时钟句柄 | 记得在错误路径和remove里调 |
| clk_get_parent | 获取父时钟 | 用于调试时钟树拓扑 |
| clk_set_parent | 切换时钟源 | 某些硬件切换时会有毛刺 |
4.4 时钟API调用流程图
下面这张图展示了典型的时钟操作流程,从获取到释放的完整生命周期:
4.5 实战中的避坑指南
讲完理论,说几个我实际项目中遇到的坑:
- clk_get返回的不是NULL,而是ERR_PTR。千万别用
if (!clk)判断,要用IS_ERR(clk)。我见过有人直接解引用ERR_PTR,内核直接oops。 - clk_set_rate可能向上取整也可能向下取整。不同驱动实现不一样,所以一定要用
clk_get_rate确认实际值。 - 频率切换时外设要暂停。如果你在UART发送数据时改了时钟频率,数据大概率会错乱。我一般在改频率前先disable外设,改完再重新配置波特率。
- 别忘了引用计数。每次
clk_get都要对应一次clk_put,否则时钟框架会泄漏。我在某个项目中漏了remove路径的clk_put,结果热插拔几次后系统就再也打不开那个时钟了。
clk_summary 文件,能快速定位哪个时钟没关。我每次做低功耗调试,第一件事就是cat这个文件。
嗯,以上就是时钟API的核心内容。记住一个原则:先获取,再准备,后使能;用完要逆序释放。这个顺序乱了,系统就会出各种奇怪的问题。