第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 还是晶振。

关键点:struct clk 是消费者看到的接口,struct clk_hw 才是提供者实现的硬件抽象。两者通过 clk_core 连接。

2.2 clk_ops:操作集的“五脏六腑”

clk_ops 是时钟提供者必须实现的操作函数集。我见过不少新手写时钟驱动时,把 ops 里的函数全实现了,结果一半用不上。其实你只需要根据硬件特性实现必要的回调。

回调函数 作用 是否必须
prepare 可能睡眠的准备工作(如获取锁、申请资源) 推荐实现
unprepare prepare 的逆操作 推荐实现
enable 原子操作,真正使能时钟 必须
disable 原子操作,关闭时钟 必须
recalc_rate 重新计算当前频率 建议实现
round_rate 返回最接近目标频率的可达值 可调频时实现
set_rate 设置时钟频率 可调频时实现

我曾经在一个项目中,只实现了 enable/disable 和 recalc_rate,因为那个时钟是固定的,不需要调频。结果跑起来完全没问题。所以别被那么多回调函数吓到,按需实现就好。

我的经验:prepare/unprepare 和 enable/disable 的区别在于能否睡眠。prepare 里可以做内存分配、互斥锁操作;enable 里只能做寄存器读写这类原子操作。搞反了会导致内核报“scheduling while atomic”的错。

2.3 时钟提供者的注册流程

提供者,就是真正控制硬件时钟源的驱动。比如一个 SoC 内部的 PLL 驱动,或者一个外部晶振的驱动。注册流程大致分三步:

  1. 定义 clk_hw:填充 struct clk_hw,绑定 clk_ops
  2. 注册到框架:调用 clk_hw_register() 或 devm_clk_hw_register()
  3. 添加到时钟树:通过设备树或板级代码建立父子关系
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> 就能找到它。

避坑:我曾经忘记调用 of_clk_add_hw_provider,结果 clk_get() 永远返回 -EPROBE_DEFER。排查了半天才发现是提供者没注册到设备树接口上。注册完 clk_hw 后,一定要检查这一步。

2.4 时钟消费者的注册流程

消费者,就是使用时钟的驱动,比如 I2C、UART、MMC 等外设驱动。它们不关心时钟怎么产生的,只关心能不能拿到句柄、能不能调频率。

消费者的流程更简单:

  1. 获取时钟:clk_get(dev, "clk_name") 或 devm_clk_get()
  2. 准备并使能:clk_prepare_enable()
  3. 使用:需要时调用 clk_get_rate()、clk_set_rate()
  4. 释放: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,也看不到硬件寄存器。这就是内核时钟框架的设计哲学——提供者与消费者解耦。

核心思想:提供者实现硬件操作,消费者只调用 API。中间通过 struct clk 和 clk_core 做桥接。这种分层设计让驱动代码更干净,也更容易移植。

2.5 整体流程:从注册到使用

为了让你更直观地理解,我画了一张流程图。它展示了提供者注册、消费者获取、以及中间的核心调度过程。

时钟框架:提供者与消费者交互流程 时钟提供者驱动 实现 clk_ops 回调 填充 struct clk_hw clk_hw_register() of_clk_add_hw_provider() clk_core 调度层 维护时钟树 转发 clk_ops 调用 频率计算与约束 时钟消费者驱动 clk_get() clk_prepare_enable() clk_get_rate() clk_disable_unprepare() 说明: 提供者通过 clk_hw_register() 注册到 clk_core,再通过 of_clk_add_hw_provider() 暴露给设备树。 消费者通过 clk_get() 获取 struct clk 句柄,后续所有操作都通过这个句柄完成。 clk_core 层负责维护时钟树拓扑、转发操作、做频率约束检查。

从这张图你能看到,整个流程是单向的:提供者注册到核心,消费者从核心获取。核心层像是一个“调度中心”,它不直接操作硬件,但负责协调所有时钟关系。

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 的时钟。

小技巧:我习惯在设备树里给每个时钟起一个有意义的名字,比如 "i2c_clk"、"uart_clk"。这样驱动代码里用名字查找,比用索引更可读,也更容易维护。

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() 导致时钟泄漏。

避坑:我曾经在一个项目中,发现某个外设的时钟 enable_cnt 一直累加,排查后发现是驱动在中断上下文里调用了 clk_prepare()。prepare 可能睡眠,不能在中断上下文调用。后来改成在 tasklet 里做 prepare,问题就解决了。

嗯,这一章的内容差不多就这些。struct clk、clk_ops、提供者与消费者的注册流程,是理解内核时钟框架的三块基石。你写驱动时,多看看 /sys/kernel/debug/clk/ 下的信息,能帮你快速定位问题。