节。
在本篇内容中,我们将围绕 Linux 内核中的时钟子系统核心架构 —— Common Clock Framework(简称 CCF)展开深入讲解,目标是帮助你全面理解其设计理念、主要数据结构、注册流程、驱动实现方式,以及如何基于 NXP i.MX8MP 平台进行完整的时钟驱动开发。
Common Clock Framework(CCF)是 Linux 内核自 v3.4 起引入的通用时钟架构,用于统一管理 SoC 上所有的时钟资源,解决平台异构、驱动分裂、代码冗余等问题。
CCF 的设计目标是:
clk_get()
/ clk_prepare()
/ clk_enable()
)。在上一篇中我们已概述三个核心角色:
角色 | 作用 | 示例 |
---|---|---|
时钟提供者(Provider) | 注册并实现时钟控制逻辑,如 PLL、分频器、门控器 | NXP CCM 时钟控制器驱动 |
时钟消费者(Consumer) | 使用时钟接口获取和启用时钟 | UART/I2C 等外设驱动 |
时钟框架(Framework) | 统一管理所有时钟,维护拓扑关系与接口 | drivers/clk/clk.c |
+---------------------+ +-------------------+
| UART 驱动 (Consumer)|<---->| CCF 框架层 |
+---------------------+ +-------------------+
↑
|
+---------------------+
| 时钟控制器驱动 |
| (如 fsl-imx8mp-ccm) |
+---------------------+
struct clk_hw
该结构体表示一个底层时钟硬件实体,由时钟提供者实现:
struct clk_hw {
struct clk_core *core;
struct clk_init_data *init;
// 可扩展的私有数据指针
};
struct clk_ops
用于定义操作时钟的函数集,是最关键的回调接口:
struct clk_ops {
int (*enable)(struct clk_hw *hw);
void (*disable)(struct clk_hw *hw);
unsigned long (*recalc_rate)(struct clk_hw *hw, unsigned long parent_rate);
long (*round_rate)(struct clk_hw *hw, unsigned long rate,
unsigned long *parent_rate);
int (*set_rate)(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate);
...
};
struct clk_init_data
用于注册时钟时的初始化信息:
struct clk_init_data {
const char *name;
const struct clk_ops *ops;
const char * const *parent_names;
u8 num_parents;
unsigned long flags;
};
Provider 通过 clk_register()
或 devm_clk_hw_register()
将 clk_hw
实例注册给框架,框架内部建立 clk_core
对象并添加至全局时钟树。
clk_ops
实现;clk_hw
;clk_init_data
;devm_clk_hw_register()
完成注册;of_clk_add_hw_provider()
提供给设备树接口。我们以 NXP i.MX8MP 的 UART2 根时钟 IMX8MP_CLK_UART2_ROOT
为例,讲解从时钟控制器提供者,到 UART2 驱动消费者的完整链路。
uart2: serial@30890000 {
compatible = "fsl,imx8mp-uart", "fsl,imx6q-uart";
reg = <0x30890000 0x10000>;
interrupts = ;
clocks = <&clk IMX8MP_CLK_UART2_ROOT>,
<&clk IMX8MP_CLK_UART2_ROOT>;
clock-names = "ipg", "per";
status = "disabled";
};
<&clk IMX8MP_CLK_UART2_ROOT>
表示设备节点引用了 ID 为 IMX8MP_CLK_UART2_ROOT
的时钟,provider 必须实现它。
static const struct imx8m_clk_root_clk uart2_root = {
.id = IMX8MP_CLK_UART2_ROOT,
.name = "uart2_root_clk",
.parent_names = (const char *[]){ "uart2_div" },
.num_parents = 1,
.flags = CLK_SET_RATE_PARENT,
};
注册流程:
hw = imx8m_clk_hw_register_composite(NULL, clk_root->name,
clk_root->parent_names, clk_root->num_parents,
mux_hw, &clk_mux_ops,
rate_hw, &clk_divider_ops,
gate_hw, &clk_gate_ops,
clk_root->flags);
注: 每个 root clock 通常由一个复用器、一个分频器和一个门控组成,符合 CCF 的复合时钟模型。
驱动中使用以下接口获取与控制时钟:
struct clk *clk = devm_clk_get(dev, "ipg");
clk_prepare_enable(clk);
...
clk_disable_unprepare(clk);
内核根据 clock-names
字符串与设备树中的 <&clk ID>
找到实际的 clk_core
实例,进一步调用对应 clk_ops
接口。
以下模拟一个名为 "demo-gate-clk"
的时钟,基于 GPIO 控制实现一个简单门控时钟:
static struct clk_hw *demo_clk_hw;
static struct clk_ops demo_clk_ops = {
.enable = demo_clk_enable,
.disable = demo_clk_disable,
};
static int demo_clk_probe(struct platform_device *pdev)
{
struct clk_init_data init;
demo_clk_hw = devm_kzalloc(&pdev->dev, sizeof(*demo_clk_hw), GFP_KERNEL);
init.name = "demo-gate-clk";
init.ops = &demo_clk_ops;
init.num_parents = 0;
init.flags = 0;
demo_clk_hw->init = &init;
clk_hw_register(&pdev->dev, demo_clk_hw);
of_clk_add_hw_provider(pdev->dev.of_node, of_clk_hw_simple_get, demo_clk_hw);
return 0;
}
demo_clk: demo-clk {
compatible = "vendor,demo-gate-clk";
#clock-cells = <0>;
};
my_peripheral@12340000 {
...
clocks = <&demo_clk>;
clock-names = "core";
};
Linux 内核 CCF 框架通过 clk_core
全局对象池,维持整个时钟拓扑结构,并提供统一接口进行操作与调试。
核心文件位于 drivers/clk/clk.c
,负责:
clk_get()
、clk_set_rate()
等函数;框架的调试接口如:
cat /sys/kernel/debug/clk/clk_summary
可查看当前系统所有时钟状态。
clk_hw
、clk_ops
,并注册给框架;clk_*()
接口,不直接操作硬件;imx8mp-clock.h
),再定位 clock controller 驱动;回答:
可以。CCF 支持多父时钟的复用器机制,注册 clk_hw
时传入多个 parent_names
,并实现 set_parent()
和 get_parent()
回调。使用者可调用 clk_set_parent()
选择目标父时钟。
回答:
可通过以下方式定位:
cat /sys/kernel/debug/clk/clk_summary
查看时钟是否启用;clk_prepare_enable()
;clk_ops.enable()
是否被成功执行;以上即为 Day 27 下篇全部内容,完整讲解了 CCF 架构设计与实际使用方式。
下一日我们将正式进入子系统与电源管理集成的驱动开发专题。
视频教程请关注 B 站:“嵌入式 Jerry”