4、时钟驱动开发(一):注册一个简单的固定频率时钟提供者
时钟驱动,说白了就是给系统提供一个「心跳」。你想想看,CPU要跑、外设要工作、总线要传输数据,哪个离得开时钟?在Linux里,时钟框架(clk framework)就是干这个的——它把各种时钟源抽象成统一的接口,让驱动开发者不用关心底层硬件怎么配。
这一章,咱们就从最简单的场景入手:注册一个固定频率的时钟提供者。别小看这个“固定频率”,很多嵌入式板子上的低速外设(比如UART、I2C)用的就是这种时钟。我刚开始做驱动时,觉得时钟框架很复杂,后来发现从固定频率入手,反而能最快理解它的核心逻辑。
4.1 时钟框架的核心角色
在动手写代码之前,你得先搞清楚三个角色:
- 时钟提供者(Clock Provider):能产生时钟信号的硬件,比如晶振、PLL、分频器。
- 时钟消费者(Clock Consumer):使用时钟信号的设备,比如UART控制器、GPIO模块。
- 时钟框架(clk core):内核提供的中间层,负责管理提供者和消费者之间的交互。
它们的关系可以用一张图说清楚:
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,让消费者等一等。
4.7 验证与调试
驱动加载后,怎么确认它工作正常?我一般用这几个方法:
- 看 dmesg:驱动 probe 成功会打印 "Registered fixed clock: 25000000 Hz"。
- 查 /sys/kernel/debug/clk:如果内核开启了
CONFIG_DEBUG_FS,在/sys/kernel/debug/clk/clk_summary里能看到所有注册的时钟树。 - 用 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)的基础。下一类时钟驱动,无非是在这个骨架上增加更多的回调函数和状态管理。
嗯,代码量不大,但每一步都有它的道理。你动手试一遍,比看十遍文档都管用。