4. 固定时钟适配:fixed-clock驱动分析、在设备树中定义固定频率时钟、驱动中获取固定时钟

各位同学,今天我们来聊聊固定时钟。说白了,就是那种频率不会变、不需要PLL配置、不需要外部晶振控制的时钟源。你想想看,很多嵌入式系统里都有这种场景:一个外设需要个参考时钟,但频率是固定的,比如24MHz、50MHz这种。

我在项目中遇到过好几次,有人把固定时钟当成普通时钟去配,结果驱动加载时卡在时钟框架里出不来。嗯,这里要注意,固定时钟虽然简单,但它的适配方式跟普通时钟还是有区别的。

4.1 fixed-clock驱动分析

Linux内核里,fixed-clock驱动在 drivers/clk/clk-fixed-rate.c 里。我习惯先看它的结构体定义:

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

这个结构体很精简,就四个字段。核心是 fixed_rate,它存的就是我们设备树里配的频率值。

驱动注册时,会调用 clk_register_fixed_rate() 函数。我贴一下关键代码:

struct clk *clk_register_fixed_rate(struct device *dev, const char *name,
        const char *parent_name, unsigned long flags,
        unsigned long fixed_rate)
{
    struct clk_fixed_rate *fixed;
    struct clk_init_data init;
    struct clk *clk;

    fixed = kzalloc(sizeof(*fixed), GFP_KERNEL);
    if (!fixed)
        return ERR_PTR(-ENOMEM);

    init.name = name;
    init.ops = &clk_fixed_rate_ops;
    init.flags = flags;
    init.parent_names = (parent_name ? &parent_name : NULL);
    init.num_parents = (parent_name ? 1 : 0);

    fixed->fixed_rate = fixed_rate;
    fixed->hw.init = &init;

    clk = clk_register(dev, &fixed->hw);
    if (IS_ERR(clk))
        kfree(fixed);

    return clk;
}

注意看 clk_fixed_rate_ops,它只实现了两个回调:

  • recalc_rate:直接返回 fixed_rate
  • round_rate:返回最接近的固定频率

没有 set_rate,因为固定时钟不支持调频。我曾经见过有人试图在驱动里调用 clk_set_rate() 去改固定时钟的频率,结果返回 -EINVAL,折腾了半天才发现是操作对象选错了。

4.2 在设备树中定义固定频率时钟

设备树里定义固定时钟,用 fixed-clock 这个 compatible。我一般这样写:

clk_24m: clk-24m {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;
    clock-output-names = "clk_24m";
};

几个关键点:

  • #clock-cells = <0>:表示这个时钟输出只有一个,不需要额外索引
  • clock-frequency:单位是Hz,直接写数值
  • clock-output-names:可选,但建议加上,调试时一眼能看出来

如果你需要多个固定时钟,可以这样:

clocks {
    clk_50m: clk-50m {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <50000000>;
    };

    clk_32k: clk-32k {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <32768>;
    };
};

我习惯把固定时钟统一放在一个 clocks 节点下,这样设备树看起来整洁。当然,你也可以散落在各处,但维护起来就麻烦了。

注意: 固定时钟的节点名不要跟其他节点冲突。我曾经遇到过有人把固定时钟节点命名为 clock,结果跟某个SoC内部的时钟控制器重名了,编译设备树时直接报错。

4.3 驱动中获取固定时钟

驱动里获取固定时钟,跟获取普通时钟一样,用 clk_get() 或者设备树API。我贴一个完整的例子:

#include <linux/clk.h>
#include <linux/err.h>

struct my_device {
    struct clk *clk;
    unsigned long rate;
};

static int my_probe(struct platform_device *pdev)
{
    struct my_device *mydev;
    int ret;

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

    // 获取固定时钟
    mydev->clk = devm_clk_get(&pdev->dev, "my_clk");
    if (IS_ERR(mydev->clk)) {
        dev_err(&pdev->dev, "failed to get my_clk\n");
        return PTR_ERR(mydev->clk);
    }

    // 获取时钟频率
    mydev->rate = clk_get_rate(mydev->clk);
    dev_info(&pdev->dev, "clock rate: %lu Hz\n", mydev->rate);

    // 使能时钟(固定时钟也需要使能)
    ret = clk_prepare_enable(mydev->clk);
    if (ret) {
        dev_err(&pdev->dev, "failed to enable clock\n");
        return ret;
    }

    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_device *mydev = platform_get_drvdata(pdev);

    clk_disable_unprepare(mydev->clk);
    return 0;
}

对应的设备树里,外设节点要引用固定时钟:

my_device: my-device {
    compatible = "my,device";
    clocks = <&clk_24m>;
    clock-names = "my_clk";
};

这里有个细节:clk_get_rate() 返回的就是设备树里配的 clock-frequency。你不需要做任何转换,内核时钟框架已经帮你处理好了。

小技巧: 如果你不确定时钟是否获取成功,可以在 clk_get_rate() 之后加个打印。我调试时经常这么干,一眼就能看出频率对不对。

4.4 固定时钟的常见坑

我总结几个实际项目中踩过的坑:

  • 频率单位搞错:设备树里 clock-frequency 的单位是Hz,不是MHz。有人写成 24 以为就是24MHz,结果驱动里读出来是24Hz。
  • 忘记使能时钟:固定时钟虽然频率不变,但也要调用 clk_prepare_enable()。有些SoC的时钟控制器会默认关闭所有时钟,你不使能,外设就罢工。
  • 时钟名不匹配:设备树里 clock-names 跟驱动里 devm_clk_get() 的第二个参数必须一致。我见过有人设备树里写 "clk_24m",驱动里写 "24m_clk",结果返回 -ENOENT。

4.5 知识体系图

下面这张图展示了固定时钟适配的完整流程:

固定时钟适配流程 设备树定义 compatible = "fixed-clock"; clock-frequency = <24000000>; 内核解析 clk_register_fixed_rate() → 注册固定时钟 驱动获取 devm_clk_get() → clk_get_rate() → clk_prepare_enable() 外设正常工作 第1步 第2步 第3步 第4步

这张图把固定时钟适配的四个步骤串起来了。从设备树定义开始,到内核解析注册,再到驱动获取使用,最后外设正常工作。每一步都有对应的API和操作,你照着这个流程走,基本不会出错。

核心要点: 固定时钟适配的关键在于三点——设备树配对频率、驱动正确获取、记得使能时钟。这三步走完,固定时钟就稳了。

好了,关于固定时钟适配的内容就这些。实际项目中,固定时钟虽然简单,但它是整个时钟树的基础。你想想看,很多PLL的参考时钟就是固定时钟,如果这个源头配错了,后面所有时钟都会跑偏。所以,别因为它简单就掉以轻心。