时钟框架入门:Linux Common Clock Framework (CCF) 核心数据结构
时钟管理,说白了就是给芯片里的各个模块提供心跳。没有时钟,CPU、外设、总线全都歇菜。Linux 内核为了统一管理这些时钟,搞了一套 Common Clock Framework (CCF)。我刚开始接触这块时,也觉得数据结构又多又绕,但摸清门道后会发现,其实就那么几个核心角色。
这一章,咱们就直奔主题,把 CCF 的骨架——核心数据结构——彻底拆开看看。
1. 核心结构体:谁是谁?
CCF 里最重要的三个结构体,你必须要记住:struct clk_hw、struct clk_core、struct clk。它们仨的关系,有点像「演员、剧本、观众」。
| 结构体 | 角色 | 我个人的理解 |
|---|---|---|
struct clk_hw |
硬件抽象层 | 这是驱动开发者直接打交道的对象。你写时钟驱动时,主要就是填充这个结构体里的操作函数(比如 recalc_rate、set_rate)。 |
struct clk_core |
内核核心管理层 | 这是 CCF 内部真正干活的结构体。它维护了时钟树、引用计数、使能状态。驱动开发者一般看不见它,但它才是幕后大佬。 |
struct clk |
用户句柄 | 这是给消费者(比如某个外设驱动)用的。外设通过 clk_get() 拿到一个 struct clk *,然后调用 clk_enable()、clk_set_rate()。它本质上是一个指向 clk_core 的指针包装。 |
一句话总结:驱动写 clk_hw,内核管 clk_core,外设用 clk。各司其职,互不干扰。
2. 深入 struct clk_hw:驱动开发者的入口
你写一个时钟驱动,第一步就是定义一个 struct clk_hw 实例。它的定义大致长这样(我简化了,但核心字段都在):
struct clk_hw {
struct clk_core *core; // 指向内核核心结构,由 CCF 框架自动关联
struct clk_init_data *init; // 初始化数据,包含名字、操作函数、父时钟等
};
这里最关键的其实是 struct clk_init_data,它才是你真正要填的东西:
struct clk_init_data {
const char *name; // 时钟名字,比如 "uart_clk"
const struct clk_ops *ops; // 操作函数集,这是灵魂
const char * const *parent_names; // 父时钟名字列表
u8 num_parents; // 父时钟数量
unsigned long flags; // 标志位,比如 CLK_IS_CRITICAL
};
我的习惯:在定义 parent_names 时,我通常会用一个静态数组,并在 num_parents 里用 ARRAY_SIZE() 宏。这样加父时钟时不容易漏改数量,少踩一个坑是一个。
3. struct clk_ops:时钟的操作函数集
这个结构体就是你的驱动「灵魂」。每个函数指针都对应一个硬件操作。我挑几个最常用的说说:
| 函数指针 | 作用 | 我在项目中遇到过的问题 |
|---|---|---|
prepare / unprepare |
可能睡眠的使能/禁能操作(比如操作 I2C 寄存器) | 有人把 spinlock 保护的寄存器操作放这里,导致内核调度器报错。记住:prepare 可以睡,enable 不能睡。 |
enable / disable |
原子操作,不能睡眠(比如直接写内存映射寄存器) | 我曾经在 enable 里调了 msleep(),结果系统直接死锁。嗯,从那以后我每次写 enable 都会先问自己一句:这里能睡吗? |
recalc_rate |
重新计算当前时钟频率 | 这个函数会被 CCF 频繁调用,尤其是调试时。我建议你在这里加个 pr_debug,方便跟踪频率变化。 |
set_rate |
设置时钟频率 | 注意返回值:成功返回实际设置的频率,失败返回负值。很多新手直接返回 0,导致上层以为设置成功了。 |
round_rate |
查询某个频率是否支持,并返回最接近的频率 | 这个函数是 set_rate 的「侦察兵」。我习惯先调 round_rate 确认硬件支持,再调 set_rate。 |
注意:enable 和 disable 是在 spinlock 保护的上下文中调用的。千万别在里面做任何可能睡眠的操作,比如 kmalloc、mutex_lock、printk(虽然 printk 不会睡,但会影响实时性)。
4. 时钟树:数据结构怎么串起来?
CCF 内部用 struct clk_core 构建了一棵「时钟树」。每个 clk_core 都有指向父时钟和子时钟的指针:
struct clk_core {
const char *name;
const struct clk_ops *ops;
struct clk_hw *hw; // 指向对应的 clk_hw
struct module *owner;
struct clk_core *parent; // 父时钟
struct clk_core *children; // 子时钟链表头
struct list_head child_node; // 用于挂入父时钟的 children 链表
unsigned long rate; // 当前频率
int enable_count; // 使能引用计数
// ... 还有很多其他字段
};
你想想看,整个 SoC 的时钟源(比如晶振)是根节点,然后 PLL、分频器、门控依次往下挂,最终到达外设。CCF 通过这个树形结构,实现了「级联操作」——你改一个 PLL 的频率,所有子时钟都会收到通知。
关键点:引用计数 enable_count 保证了时钟不会被意外关闭。只有所有使用者都调用了 clk_disable(),时钟才会真正被禁能。这个机制救过我很多次,尤其是在多驱动共享一个时钟的场景下。
5. 核心关系图:一张 SVG 看懂 CCF 数据结构
下面这张图,我把三个核心结构体以及它们之间的关系画了出来。你看完应该能对 CCF 的「演员、剧本、观众」模型有个直观印象。
6. 注册与注销:数据结构怎么「活」起来?
定义好结构体只是第一步。要让时钟真正工作,你需要把它注册到 CCF 框架中。常用的 API 是:
int clk_hw_register(struct device *dev, struct clk_hw *hw);
void clk_hw_unregister(struct clk_hw *hw);
注册时,CCF 内部会做几件事:
- 分配并初始化一个
struct clk_core。 - 根据
parent_names查找父时钟,建立父子关系。 - 将新时钟挂入全局时钟树。
- 创建
struct clk句柄,供消费者通过clk_get()获取。
避坑指南:我曾经在模块卸载时忘了调 clk_hw_unregister(),结果导致内核 panic。原因是父时钟的 children 链表里还挂着已释放的节点。所以,注册和注销一定要成对出现,最好在 probe() 和 remove() 里对称写好。
7. 调试接口:怎么验证数据结构对不对?
CCF 提供了几个调试手段,我平时用得最多的是:
/sys/kernel/debug/clk/clk_summary:打印整个时钟树,包括每个时钟的使能计数、频率、父时钟。这是排查问题的第一站。/sys/kernel/debug/clk/<clock_name>/clk_rate:查看某个时钟的当前频率。trace_clock_enable/disable:通过 ftrace 跟踪时钟使能/禁能事件,适合分析动态功耗问题。
我的习惯:每次写完一个新时钟驱动,我都会先挂上 clk_summary 看一眼。如果时钟树里没出现我注册的时钟,或者频率不对,那就说明 clk_hw_register 或者 recalc_rate 有问题。先查数据,再查代码,效率高很多。