时钟框架入门: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 的「演员、剧本、观众」模型有个直观印象。

CCF 核心数据结构关系图 CCF 核心数据结构关系图 驱动开发者层 struct clk_hw 硬件抽象,驱动填充 ops struct clk_init_data 名字、ops、父时钟列表 包含 内核核心层 struct clk_core 时钟树节点、引用计数、频率 core 指针 消费者层(外设驱动) struct clk 用户句柄 clk_get() 返回 💡 驱动开发者只操作 clk_hw,消费者只操作 clk,内核通过 clk_core 串联整个时钟树。

6. 注册与注销:数据结构怎么「活」起来?

定义好结构体只是第一步。要让时钟真正工作,你需要把它注册到 CCF 框架中。常用的 API 是:

int clk_hw_register(struct device *dev, struct clk_hw *hw);
void clk_hw_unregister(struct clk_hw *hw);

注册时,CCF 内部会做几件事:

  1. 分配并初始化一个 struct clk_core。
  2. 根据 parent_names 查找父时钟,建立父子关系。
  3. 将新时钟挂入全局时钟树。
  4. 创建 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 有问题。先查数据,再查代码,效率高很多。