4、时钟驱动开发(一):注册一个简单的固定频率时钟提供者

时钟驱动,说白了就是给系统提供一个「心跳」。你想想看,CPU要跑、外设要工作、总线要传输数据,哪个离得开时钟?在Linux里,时钟框架(clk framework)就是干这个的——它把各种时钟源抽象成统一的接口,让驱动开发者不用关心底层硬件怎么配。

这一章,咱们就从最简单的场景入手:注册一个固定频率的时钟提供者。别小看这个“固定频率”,很多嵌入式板子上的低速外设(比如UART、I2C)用的就是这种时钟。我刚开始做驱动时,觉得时钟框架很复杂,后来发现从固定频率入手,反而能最快理解它的核心逻辑。

4.1 时钟框架的核心角色

在动手写代码之前,你得先搞清楚三个角色:

  • 时钟提供者(Clock Provider):能产生时钟信号的硬件,比如晶振、PLL、分频器。
  • 时钟消费者(Clock Consumer):使用时钟信号的设备,比如UART控制器、GPIO模块。
  • 时钟框架(clk core):内核提供的中间层,负责管理提供者和消费者之间的交互。

它们的关系可以用一张图说清楚:

时钟框架核心角色关系图 时钟提供者 clk_hw + clk_ops 时钟框架 clk_core 注册/查找 时钟消费者 clk_get / clk_enable 注册 查找 关键流程 1. 提供者实现 clk_ops,填充 clk_hw,调用 clk_hw_register() 2. 框架维护全局时钟树,提供 clk_get() / clk_get_rate() 等 API 3. 消费者通过设备树或板级代码获取时钟句柄,操作时钟
我的习惯:写时钟驱动前,先在纸上画出这三个角色和它们之间的接口。框架图一画,代码结构就清晰了。

4.2 固定频率时钟的场景

什么时候需要固定频率时钟?我举几个实际例子:

  • 板子上有一颗 32.768kHz 的 RTC 晶振,专门给实时时钟模块用。
  • 某颗外设芯片需要 25MHz 参考时钟,由主控 SoC 的专用引脚输出。
  • 调试阶段,你想给某个模块提供一个稳定的测试时钟,频率写死就行。

这些场景的共同点:频率不会变,也不需要调。所以驱动实现起来很简单——你只需要告诉框架“我这个时钟是多少 Hz”,然后提供几个空操作的回调函数。

4.3 数据结构与回调函数

Linux 时钟框架用 struct clk_hw 描述一个时钟提供者硬件。你需要填充它,并实现一组操作函数(struct clk_ops)。

对于固定频率时钟,核心数据结构长这样:

struct clk_fixed_rate {
    struct clk_hw hw;
    unsigned long fixed_rate;
    unsigned long flags;
};

你只需要实现两个回调:

回调函数 作用 固定频率实现
recalc_rate 返回当前时钟频率 直接返回 fixed_rate
round_rate 返回最接近请求频率的可用频率 直接返回 fixed_rate(不支持调频)
注意:固定频率时钟不需要实现 enable/disable 回调。因为硬件上时钟一直存在,不需要软件开关。如果你强行实现了,反而可能引入不必要的功耗管理问题。

4.4 完整代码实现

下面是一个完整的固定频率时钟驱动。我习惯把提供者、操作函数、注册逻辑放在一个文件里,结构清晰:

#include <linux/clk-provider.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>

struct clk_fixed_rate {
    struct clk_hw hw;
    unsigned long fixed_rate;
};

static unsigned long fixed_recalc_rate(struct clk_hw *hw,
                                       unsigned long parent_rate)
{
    struct clk_fixed_rate *fixed = to_clk_fixed_rate(hw);
    return fixed->fixed_rate;
}

static long fixed_round_rate(struct clk_hw *hw, unsigned long rate,
                             unsigned long *parent_rate)
{
    struct clk_fixed_rate *fixed = to_clk_fixed_rate(hw);
    return fixed->fixed_rate;
}

static const struct clk_ops fixed_clk_ops = {
    .recalc_rate = fixed_recalc_rate,
    .round_rate  = fixed_round_rate,
};

static int fixed_clk_probe(struct platform_device *pdev)
{
    struct clk_fixed_rate *fixed;
    struct clk_init_data init = { };
    int ret;

    fixed = devm_kzalloc(&pdev->dev, sizeof(*fixed), GFP_KERNEL);
    if (!fixed)
        return -ENOMEM;

    /* 从设备树或平台数据获取频率 */
    if (pdev->dev.of_node) {
        if (of_property_read_u32(pdev->dev.of_node, "clock-frequency",
                                 &fixed->fixed_rate))
            return -EINVAL;
    } else {
        /* 回退到平台数据,这里演示写死 25MHz */
        fixed->fixed_rate = 25000000;
    }

    init.name = pdev->dev.of_node ?
                pdev->dev.of_node->name : "fixed-clk";
    init.ops = &fixed_clk_ops;
    init.num_parents = 0;  /* 固定时钟没有父时钟 */

    fixed->hw.init = &init;

    ret = devm_clk_hw_register(&pdev->dev, &fixed->hw);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "Registered fixed clock: %lu Hz\n",
             fixed->fixed_rate);
    return 0;
}

static const struct of_device_id fixed_clk_of_match[] = {
    { .compatible = "fixed-clock" },
    { }
};
MODULE_DEVICE_TABLE(of, fixed_clk_of_match);

static struct platform_driver fixed_clk_driver = {
    .driver = {
        .name = "fixed-clk",
        .of_match_table = fixed_clk_of_match,
    },
    .probe = fixed_clk_probe,
};
module_platform_driver(fixed_clk_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Fixed rate clock provider driver");
关键点:
  • devm_clk_hw_register() 是资源管理的注册接口,驱动卸载时自动注销。
  • num_parents = 0 表示这个时钟没有父时钟,它是根节点。
  • 设备树 compatible 用 "fixed-clock",这是内核社区约定俗成的名字。

4.5 设备树绑定

驱动写好了,怎么让内核找到它?设备树里加一个节点就行:

/ {
    my_fixed_clk: my_fixed_clk {
        compatible = "fixed-clock";
        clock-frequency = <25000000>;
        #clock-cells = <0>;
    };
};

#clock-cells = <0> 表示这个时钟提供者只输出一路时钟,消费者引用时不需要额外参数。比如:

&uart0 {
    clocks = <&my_fixed_clk>;
    clock-names = "uart_clk";
};

4.6 避坑指南

我在项目中遇到过几个坑,写出来给你提个醒:

  • 频率单位搞错:设备树里写的是 Hz,不是 kHz 或 MHz。有人把 25MHz 写成 25,结果时钟跑得比乌龟还慢。
  • 忘记设置 #clock-cells:如果不写这个属性,消费者引用时会报错,因为框架不知道这个提供者输出几路时钟。
  • probe 顺序依赖:如果消费者在提供者之前 probe,clk_get 会失败。解决办法是用 EPROBE_DEFER,让消费者等一等。
我曾经:调试一个 I2C 控制器,死活不工作。折腾了半天,发现是时钟提供者的 probe 函数返回了 0,但时钟根本没注册成功——因为 devm_clk_hw_register 返回了错误,我忘了检查返回值。从那以后,我每个 probe 函数都会仔细检查所有注册调用的返回值。

4.7 验证与调试

驱动加载后,怎么确认它工作正常?我一般用这几个方法:

  1. 看 dmesg:驱动 probe 成功会打印 "Registered fixed clock: 25000000 Hz"。
  2. 查 /sys/kernel/debug/clk:如果内核开启了 CONFIG_DEBUG_FS,在 /sys/kernel/debug/clk/clk_summary 里能看到所有注册的时钟树。
  3. 用 clk_get_rate 测试:写一个小模块,调用 clk_get 和 clk_get_rate,看返回值是不是 25000000。

举个例子,查看时钟摘要:

# cat /sys/kernel/debug/clk/clk_summary
   clock                         enable_cnt  prepare_cnt  rate
 ---------------------------------------------------------------------
 my_fixed_clk                           0            0  25000000

看到 rate 列显示 25000000,说明注册成功了。

4.8 小结

这一章我们走通了固定频率时钟提供者的完整流程:从数据结构、回调函数,到驱动代码、设备树绑定,再到验证方法。别看它简单,这是理解复杂时钟驱动(PLL、分频器、MUX)的基础。下一类时钟驱动,无非是在这个骨架上增加更多的回调函数和状态管理。

嗯,代码量不大,但每一步都有它的道理。你动手试一遍,比看十遍文档都管用。