一、前言
在嵌入式Linux开发中,无论是CPU、外设控制器,还是简单的GPIO扩展器,大多数硬件模块都离不开时钟信号的支撑。
时钟子系统(Clock Subsystem),作为Linux内核中基础设施的一部分,为设备提供统一、灵活、可控的时钟管理机制。
然而,时钟子系统的工作方式常常隐藏在设备树配置与驱动框架之下,让初学者难以直观感知。
今天,我们将通过一个完整、清晰、实战化的例子,带你从设备树定义,到内核驱动调用核心API,全面理解时钟子系统的核心知识点和实际用途。
二、实战场景介绍
本次实战选取的例子是 —— UART 控制器(串口)。
在 i.MX8MP 平台上,UART1
控制器是一个非常典型的时钟消费者。它需要一个稳定的时钟源来驱动波特率生成器,确保数据收发的准确性。
三、设备树中的时钟配置
在设备树中,为每个时钟消费者(比如 UART)指定它需要的时钟源。来看具体的设备树片段(源自 i.MX8MP EVK 板):
&uart1 { /* BT UART */pinctrl-names = "default";pinctrl-0 = <&pinctrl_uart1>;assigned-clocks = <&clk IMX8MP_CLK_UART1>;assigned-clock-parents = <&clk IMX8MP_SYS_PLL1_80M>;uart-has-rtscts;status = "okay";
};
解释
assigned-clocks
:指定要配置的时钟节点(这里是UART1的根时钟)。assigned-clock-parents
:指定该时钟的上游父时钟(这里是系统PLL1输出的80MHz频率)。status = "okay"
:使能设备。
小结
在设备树中,UART1通过 assigned-clocks 指定了自己依赖的时钟源。
这一步是让内核启动时,能够正确配置 UART 所需时钟。
四、时钟子系统在内核中的处理流程
内核解析设备树时,会执行以下步骤:
-
解析 assigned-clocks 属性
- 调用
of_clk_get()
获取时钟phandle。
- 调用
-
设置父时钟(if assigned-clock-parents)
- 调用
clk_set_parent()
。
- 调用
-
设置频率(if assigned-clock-rates)
- 调用
clk_set_rate()
。
- 调用
-
将时钟与设备绑定
- 供后续驱动使用
clk_get()
提取并控制时钟。
- 供后续驱动使用
总结一句话:内核在启动时,根据设备树,把设备需要的时钟准备好,驱动层可以直接使用。
五、驱动中的核心函数调用链
进入驱动注册时,通常在 probe()
函数中,设备驱动需要主动申请并启用时钟。
来看典型的 Linux 串口驱动核心代码(以drivers/tty/serial/fsl_lpuart.c
为例):
static int lpuart_probe(struct platform_device *pdev)
{struct resource *res;struct clk *clk;struct lpuart_port *sport;// ... 省略其他初始化代码clk = devm_clk_get(&pdev->dev, NULL);if (IS_ERR(clk))return PTR_ERR(clk);clk_prepare_enable(clk);// 后续设置UART寄存器,开始工作return 0;
}
解释
devm_clk_get(&pdev->dev, NULL)
:从时钟子系统中获取设备绑定的时钟(根据设备树内容解析)。clk_prepare_enable(clk)
:准备并使能时钟,确保模块时钟打开,才能访问寄存器或工作。
这两个函数是驱动中最核心的标准时钟处理流程。
六、核心知识点总结
过程 | 内容 | 说明 |
---|---|---|
设备树定义 | 通过 assigned-clocks assigned-clock-parents | 指定设备需要使用的时钟及父时钟关系 |
内核解析设备树 | 使用 of_clk_get() 、clk_set_parent() 等接口 | 完成设备初步时钟配置 |
驱动中申请时钟 | 使用 devm_clk_get() 获取时钟句柄 | 与设备生命周期绑定,自动管理释放 |
启用时钟 | 使用 clk_prepare_enable() | 确保时钟开启,模块能够正常读写工作 |
关闭时钟 | 使用 clk_disable_unprepare() (通常在 remove 时) | 释放功耗资源,避免悬空开启 |
七、完整链路小结(可视化)
[设备树]|v
解析 assigned-clocks → 绑定 clk_hw → 初始化频率和父时钟|v
驱动 probe 阶段|v
devm_clk_get() 获取时钟|v
clk_prepare_enable() 打开时钟|v
模块正常运行
八、实战小问答(加深记忆)
-
Q:如果不调用
clk_prepare_enable()
会怎么样?
➔ A:设备可能无法正常访问寄存器(时钟门控),导致挂死或者硬件超时错误。 -
Q:为什么用
devm_clk_get()
?
➔ A:简化驱动资源管理,避免忘记释放时钟资源导致内存泄漏。 -
Q:时钟源一定是固定的吗?
➔ A:不一定,有些时钟(如CPU频率)是动态可变的,需要动态控制频率。
九、driver/clk/子系统的核心组成
在 Linux 内核中,driver/clk/ 目录下是整个 Common Clock Framework(CCF) 的具体实现。
它包含两大部分:
部分 | 作用 |
---|---|
核心框架代码(Generic) | 提供统一时钟注册、获取、启用、频率调整等API |
平台时钟驱动(Platform-specific) | 各芯片平台的具体时钟树实现,比如 imx8mp |
十、实战中最关键的核心文件和功能
我们只讲跟设备树解析、驱动使用真正相关的代码,不浪费篇幅。
目录/文件 | 作用 | 备注 |
---|---|---|
drivers/clk/clk.c | 核心通用时钟框架逻辑 | 所有 clk_register, clk_get, clk_prepare_enable 都在这里最终处理 |
drivers/clk/clk-fixed-rate.c | 实现固定频率的时钟(如设备树里的 fixed-clock) | 常用于 camera、USB、PCIE refclk |
drivers/clk/imx/clk-imx8mp.c | i.MX8MP 平台特有的时钟树搭建 | 把 IMX8MP 平台的所有时钟源、分频器、复用器建立起来 |
十一、真正重要的核心函数讲解(来自 driver/clk/clk.c)
11.1 clk_register()
🔵 作用:
在内核启动时,或者平台驱动初始化时,把一个新的时钟节点注册到内核的时钟树中。
🔵 主要流程:
- 将
struct clk_hw
转换为struct clk_core
- 将
clk_core
挂到内核全局时钟链表 - 处理时钟父子关系
🔵 关键源码位置:
struct clk *clk_register(struct device *dev, struct clk_hw *hw)
{struct clk_core *core;...core = kzalloc(sizeof(*core), GFP_KERNEL);core->hw = hw;...hlist_add_head(&core->node, &clk_root_list);return __clk_create_clk(core, NULL);
}
这里的 clk_core
是真正管理时钟状态的内部结构。
11.2 of_clk_get()
和 devm_clk_get()
🔵 作用:
of_clk_get()
:从设备树 phandle 提取时钟节点。devm_clk_get()
:在驱动中调用,帮你自动结合生命周期管理。
🔵 主要流程:
- 根据设备树 phandle 查找 clk_provider
- 通过 provider的
of_clk_src_onecell_get()
或类似接口拿到clk
🔵 关键源码位置:
struct clk *of_clk_get(struct device_node *np, int index)
{...provider = of_parse_phandle(np, "clocks", index);...return provider->get(provider->data, index);
}
最终就是在驱动层 devm_clk_get()
里面,调用 of_clk_get()
,并返回一个 clk 结构体给设备驱动用。
11.3 clk_prepare_enable()
和 clk_disable_unprepare()
🔵 作用:
- clk_prepare_enable():打开时钟,并确保硬件模块有时钟供给。
- clk_disable_unprepare():关闭时钟,释放资源。
🔵 核心调用逻辑:
int clk_prepare_enable(struct clk *clk)
{int ret;ret = clk_prepare(clk);if (ret)return ret;ret = clk_enable(clk);if (ret)clk_unprepare(clk);return ret;
}
本质就是调用底层时钟驱动注册时定义好的 enable/disable
函数指针。
比如:
- PLL的 enable
- gate clock 的开关
- mux clock 的切换
十二、fixed-clock 举例(drivers/clk/clk-fixed-rate.c)
在设备树中,如果定义了:
pcie0_refclk: pcie0-refclk {compatible = "fixed-clock";#clock-cells = <0>;clock-frequency = <100000000>;
};
内核会自动匹配到 drivers/clk/clk-fixed-rate.c
中的 of_fixed_clk_setup()
:
static void __init of_fixed_clk_setup(struct device_node *node)
{struct clk_fixed_rate *fixed;...fixed->hw.init = &init;clk_register(NULL, &fixed->hw);
}
➔ 最后也是通过 clk_register()
注册到内核时钟树。
所以:
fixed-clock
→ 固定频率的时钟节点- 设备(比如 PCIe PHY)可以直接引用它做为参考时钟
✨ 最重要总结(一句话)
✅ 无论是外设驱动,还是内核框架,
✅ 设备树 → of_clk_get → clk_register → clk_prepare_enable
✅ 整个链路最终就是围绕 clk_core 和 clk_ops 把时钟管理起来,
✅ 保证所有设备能统一管理时钟、动态调频、动态开关。
📋 最后再给你一张核心流程小图(助记忆)
[设备树 clocks属性]↓
[of_clk_get()]↓
[clk_register() 把hw挂到core]↓
[devm_clk_get()]↓
[clk_prepare_enable()]↓
[设备驱动正常工作]