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_rateround_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 知识体系图
下面这张图展示了固定时钟适配的完整流程:
这张图把固定时钟适配的四个步骤串起来了。从设备树定义开始,到内核解析注册,再到驱动获取使用,最后外设正常工作。每一步都有对应的API和操作,你照着这个流程走,基本不会出错。
核心要点: 固定时钟适配的关键在于三点——设备树配对频率、驱动正确获取、记得使能时钟。这三步走完,固定时钟就稳了。
好了,关于固定时钟适配的内容就这些。实际项目中,固定时钟虽然简单,但它是整个时钟树的基础。你想想看,很多PLL的参考时钟就是固定时钟,如果这个源头配错了,后面所有时钟都会跑偏。所以,别因为它简单就掉以轻心。