第2章 内核时钟框架:struct clk结构体、clk_ops操作集、时钟提供者与消费者的注册流程
时钟框架,说白了就是内核里管理时钟的那一套机制。你写驱动时,经常要开时钟、关时钟、调频率,这些操作背后都离不开 struct clk 和 clk_ops。我刚开始接触这块时,总觉得它很抽象——不就是个时钟吗?后来调一个外设死活不工作,才发现是时钟树没配对。嗯,这一章咱们就把这个框架拆开看。
2.1 struct clk:时钟的“身份证”
在内核里,每个时钟源都有一个 struct clk 结构体。它不直接暴露给驱动开发者,而是通过 clk_get() 拿到一个句柄。你想想看,这有点像文件描述符——你不需要知道底层怎么实现的,拿着句柄就能操作。
struct clk {
const char *name;
const struct clk_ops *ops;
struct clk_hw *hw;
struct clk_core *core;
unsigned long min_rate;
unsigned long max_rate;
...
};
我个人习惯把 struct clk 看作一个“代理”。它不存真正的硬件寄存器地址,而是通过 ops 函数指针去操作硬件。这样做的好处很明显:驱动代码只需要调用 clk_prepare_enable(),不用管底层是 PLL 还是晶振。
2.2 clk_ops:操作集的“五脏六腑”
clk_ops 是时钟提供者必须实现的操作函数集。我见过不少新手写时钟驱动时,把 ops 里的函数全实现了,结果一半用不上。其实你只需要根据硬件特性实现必要的回调。
| 回调函数 | 作用 | 是否必须 |
|---|---|---|
prepare |
可能睡眠的准备工作(如获取锁、申请资源) | 推荐实现 |
unprepare |
prepare 的逆操作 | 推荐实现 |
enable |
原子操作,真正使能时钟 | 必须 |
disable |
原子操作,关闭时钟 | 必须 |
recalc_rate |
重新计算当前频率 | 建议实现 |
round_rate |
返回最接近目标频率的可达值 | 可调频时实现 |
set_rate |
设置时钟频率 | 可调频时实现 |
我曾经在一个项目中,只实现了 enable/disable 和 recalc_rate,因为那个时钟是固定的,不需要调频。结果跑起来完全没问题。所以别被那么多回调函数吓到,按需实现就好。
2.3 时钟提供者的注册流程
提供者,就是真正控制硬件时钟源的驱动。比如一个 SoC 内部的 PLL 驱动,或者一个外部晶振的驱动。注册流程大致分三步:
- 定义 clk_hw:填充
struct clk_hw,绑定clk_ops - 注册到框架:调用
clk_hw_register()或devm_clk_hw_register() - 添加到时钟树:通过设备树或板级代码建立父子关系
static const struct clk_ops my_fixed_clk_ops = {
.enable = my_clk_enable,
.disable = my_clk_disable,
.recalc_rate= my_clk_recalc_rate,
};
static int my_clk_probe(struct platform_device *pdev)
{
struct clk_hw *hw;
int ret;
hw = devm_kzalloc(&pdev->dev, sizeof(*hw), GFP_KERNEL);
if (!hw)
return -ENOMEM;
hw->init = CLK_HW_INIT("my_clk", &my_fixed_clk_ops, 0);
ret = devm_clk_hw_register(&pdev->dev, hw);
if (ret)
return ret;
return of_clk_add_hw_provider(pdev->dev.of_node, of_clk_hw_simple_get, hw);
}
注意最后一行 of_clk_add_hw_provider,这个函数把时钟提供者注册到设备树框架中。消费者通过设备树里的 clocks = <&my_clock_controller 0> 就能找到它。
2.4 时钟消费者的注册流程
消费者,就是使用时钟的驱动,比如 I2C、UART、MMC 等外设驱动。它们不关心时钟怎么产生的,只关心能不能拿到句柄、能不能调频率。
消费者的流程更简单:
- 获取时钟:
clk_get(dev, "clk_name")或devm_clk_get() - 准备并使能:
clk_prepare_enable() - 使用:需要时调用
clk_get_rate()、clk_set_rate() - 释放:
clk_disable_unprepare()+clk_put()
static int my_i2c_probe(struct platform_device *pdev)
{
struct clk *clk;
unsigned long rate;
clk = devm_clk_get(&pdev->dev, "i2c_clk");
if (IS_ERR(clk))
return PTR_ERR(clk);
clk_prepare_enable(clk);
rate = clk_get_rate(clk);
dev_info(&pdev->dev, "I2C clock rate: %lu Hz\n", rate);
// ... 初始化硬件 ...
return 0;
}
static int my_i2c_remove(struct platform_device *pdev)
{
struct my_i2c_dev *i2c = platform_get_drvdata(pdev);
clk_disable_unprepare(i2c->clk);
return 0;
}
你想想看,消费者驱动里根本看不到 clk_ops,也看不到硬件寄存器。这就是内核时钟框架的设计哲学——提供者与消费者解耦。
2.5 整体流程:从注册到使用
为了让你更直观地理解,我画了一张流程图。它展示了提供者注册、消费者获取、以及中间的核心调度过程。
从这张图你能看到,整个流程是单向的:提供者注册到核心,消费者从核心获取。核心层像是一个“调度中心”,它不直接操作硬件,但负责协调所有时钟关系。
2.6 设备树中的时钟绑定
实际项目中,提供者和消费者通过设备树“认识”。设备树里用 clocks 属性和 clock-names 来指定时钟连接。
// 时钟提供者
clock_controller: clock-controller@ff760000 {
compatible = "my,pll-clock";
reg = <0xff760000 0x1000>;
#clock-cells = <1>; // 一个 cell 表示时钟索引
};
// 时钟消费者
i2c@ff3f0000 {
compatible = "my,i2c";
reg = <0xff3f0000 0x1000>;
clocks = <&clock_controller 3>; // 使用索引为 3 的时钟
clock-names = "i2c_clk";
};
消费者驱动里调用 devm_clk_get(&pdev->dev, "i2c_clk"),内核就会根据 clock-names 找到对应的 clocks 条目,然后去 clock_controller 里拿到索引 3 的时钟。
2.7 常见问题与调试方法
调试时钟框架时,我遇到过几个典型问题:
- clk_get 返回 -EPROBE_DEFER:提供者还没注册。检查提供者驱动是否加载成功,设备树节点是否匹配。
- clk_prepare_enable 卡死:可能是 prepare 里做了睡眠操作,但 enable 里又调用了 prepare。检查 ops 实现是否把 prepare 和 enable 的逻辑搞混了。
- clk_get_rate 返回 0:recalc_rate 回调没实现,或者实现有 bug。我建议至少实现一个简单的 recalc_rate,返回固定值也行。
调试时,我常用 /sys/kernel/debug/clk/clk_summary 查看时钟树状态。这个文件会列出所有注册的时钟、当前频率、使能计数等信息。比如:
cat /sys/kernel/debug/clk/clk_summary
clock enable_cnt prepare_cnt rate
---------------------------------------------------------------------
i2c_clk 1 1 24000000
uart_clk 0 0 48000000
pll_main 1 2 500000000
看到 enable_cnt 和 prepare_cnt 不一致时,基本就能断定是驱动里忘了调用 clk_disable_unprepare() 导致时钟泄漏。
嗯,这一章的内容差不多就这些。struct clk、clk_ops、提供者与消费者的注册流程,是理解内核时钟框架的三块基石。你写驱动时,多看看 /sys/kernel/debug/clk/ 下的信息,能帮你快速定位问题。