[TOC]
drivers/clk 时钟管理框架(Common Clock Framework) SoC时钟资源的统一抽象
历史与背景
这项技术是为了解决什么特定问题而诞生的?
drivers/clk
目录中的代码实现了Linux内核的“通用时钟框架”(Common Clock Framework, CCF)。这项技术的诞生是为了解决在现代片上系统(SoC)中一个极其复杂且普遍存在的问题:时钟资源的管理混乱、缺乏统一抽象和代码不可移植。
一个现代SoC内部包含着一个庞大而复杂的时钟树,里面有成百上千个独立的时钟源,用于驱动不同的硬件模块(CPU核、GPU、内存控制器、UART、I2C、SPI、显示引擎等)。在CCF出现之前:
- ** vendor特定实现**:每个SoC厂商(如TI, NXP, Samsung, Rockchip)都在其BSP(板级支持包)中实现一套自己独有的、非标准的API来管理时钟。
- 驱动不可移植:一个为TI芯片编写的UART驱动,如果想移植到NXP的芯片上,其所有与时钟相关的代码(如何使能时钟、如何设置波特率所需的时钟频率)都必须完全重写。
- 电源管理困难:精确的电源管理依赖于在硬件模块不使用时能够关闭(gate)其时钟,以节省功耗。在没有统一框架的情况下,这种操作难以系统化地实现和验证。
- 缺乏依赖关系管理:时钟之间存在父子依赖关系(例如,一个UART的时钟可能来自于一个PLL的分频)。手动管理这些依赖关系非常容易出错,比如在父时钟未开启时就尝试使能子时钟。
CCF的诞生就是为了提供一个统一的、与具体SoC硬件无关的API,将时钟消费者(Consumer)(即设备驱动)与时钟提供者(Provider)(即SoC特定的时钟控制器驱动)彻底分离开来。
它的发展经历了哪些重要的里程碑或版本迭代?
CCF是内核演进中解决硬件碎片化问题的典范。
- 概念的形成与引入:该框架被引入内核,旨在取代各种vendor特定的时钟代码。其核心是定义了一套标准的时钟操作API(
clk_prepare_enable
,clk_set_rate
等)。 - 与设备树(Device Tree)的深度融合:这是CCF发展中最关键的里程碑。整个SoC的时钟树拓扑结构——哪个时钟是哪个时钟的父节点,时钟的类型(门控、分频器、复用器、PLL)及其参数——被完全从C代码中剥离出来,用设备树进行描述。内核的CCF核心在启动时解析设备树,动态地在内存中重建整个时钟依赖关系图。
- 时钟提供者模型的丰富:框架内部不断增加对各种新型、复杂时钟硬件的支持,提供了丰富的标准时钟类型(
clk_gate
,clk_divider
,clk_mux
,clk_pll
,clk_fixed_rate
等),使得为新SoC编写时钟提供者驱动变得更加模块化和简单。 - 成为事实标准:如今,任何向Linux主线提交的新的SoC平台支持,都必须使用Common Clock Framework来管理其时钟。
目前该技术的社区活跃度和主流应用情况如何?
CCF是Linux内核在嵌入式和移动领域最核心、最基础的框架之一,极其稳定和成熟。它是所有现代SoC平台(ARM, ARM64, RISC-V等)正常工作的基石。几乎所有SoC上的设备驱动都是CCF的消费者,包括:
- 核心系统:CPUfreq驱动(用于调节CPU频率)、内存控制器。
- 外设驱动:UART, I2C, SPI, SD/MMC, Ethernet, USB等。
- 多媒体驱动:GPU, 显示控制器, VPU (视频处理单元)。
核心原理与设计
它的核心工作原理是什么?
CCF的核心是提供者/消费者模型,并通过设备树作为中间的“数据粘合剂”。
提供者(Provider):
- 这是SoC特定的时钟控制器驱动,例如
drivers/clk/rockchip/clk-rk3399.c
。 - 它负责解析设备树中描述时钟硬件的节点。
- 它根据设备树的描述,将硬件时钟源(如一个PLL、一个分频器)实例化为内核中的
struct clk_hw
对象,并向CCF核心注册这些时钟,同时提供操作这些硬件的回调函数(.enable
,.set_rate
等)。 - CCF核心根据设备树中的父子关系,将所有
clk_hw
对象链接起来,形成一个完整的时钟树图。
- 这是SoC特定的时钟控制器驱动,例如
消费者(Consumer):
- 这是标准的设备驱动,例如一个I2C控制器驱动。
- 在其设备树节点中,会通过
clocks
和clock-names
属性来声明它需要哪些时钟。例如:clocks = <&cru SCLK_I2C1>; clock-names = "i2c";
。 - 驱动在
.probe
函数中,调用统一的、硬件无关的APIdevm_clk_get(dev, "i2c")
来获取一个时钟句柄(struct clk *
)。CCF核心会根据设备树中的链接,找到并返回正确的时钟句柄。 - 在需要访问硬件之前,驱动调用
clk_prepare_enable(clk)
。CCF核心会自动处理所有依赖关系(例如,递归地使能所有父时钟),并最终调用提供者驱动注册的回调函数来打开时钟门控。 - 如果需要,驱动可以调用
clk_set_rate(clk, rate)
来改变时钟频率。 - 当不再需要硬件时,调用
clk_disable_unprepare(clk)
。
它的主要优势体现在哪些方面?
- 完全的抽象和可移植性:消费者驱动完全不知道时钟的来源和物理实现,其代码可以在任何支持CCF的平台上不经修改地运行。
- 数据驱动的配置:时钟树的配置完全由设备树数据决定,硬件工程师或系统集成者可以修改时钟配置而无需修改任何C代码。
- 集中的电源管理:CCF提供了对时钟门控的统一管理,是实现精细化电源管理、降低系统功耗的关键。
- 健壮的依赖管理:框架自动处理复杂的时钟依赖关系,防止了因操作顺序错误导致的系统崩溃。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 初始实现的复杂性:为一个全新的SoC家族编写时钟提供者驱动并正确地描述整个设备树是一项复杂且细致的工作,需要深入理解硬件手册。
- 对设备树的强依赖:CCF的正确工作极度依赖于设备树的准确性。设备树中的任何一个错误(如父子关系错误、分频/倍频系数错误)都可能导致整个系统无法启动或工作异常。
- 调试难度:当时钟出现问题时,调试可能比较困难。需要借助debugfs(
/sys/kernel/debug/clk
)来检查时钟树的状态、频率和使能计数,这需要一定的专业知识。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请例举说明。
CCF是任何需要在SoC上运行的设备驱动程序管理其时钟资源的唯一且标准的解决方案。
- 场景一:UART驱动
为了能以正确的波特率(如115200 bps)进行通信,UART控制器需要一个精确的输入时钟频率。UART驱动会通过clk_set_rate()
请求一个合适的频率(例如,115200的整数倍),然后通过clk_prepare_enable()
打开时钟,之后才能配置波特率分频器并开始通信。 - 场景二:CPU动态变频(CPUfreq)
CPUfreq驱动是CCF的一个主要消费者。当系统负载变化时,它会调用clk_set_rate()
来动态地升高或降低CPU核心的时钟频率,以在性能和功耗之间取得平衡。 - 场景三:显示控制器
显示控制器需要一个精确的像素时钟(pixel clock)来匹配显示屏的分辨率和刷新率。当显示模式改变时(例如从1080p切换到4K),显示驱动会调用clk_set_rate()
来设置新的像素时钟频率。
是否有不推荐使用该技术的场景?为什么?
- 非SoC内部时钟:CCF主要用于管理SoC内部的时钟资源。对于通过标准总线(如PCIe, USB)连接的外设,它们通常有自己的时钟生成和管理机制,不受SoC时钟树的直接控制。
- 固定时钟晶振:如果一个简单的外设直接连接到一个固定的晶体振荡器,并且这个时钟不能被软件控制(开关或变频),那么它可能只需要一个
clk_fixed_rate
的简单描述,但复杂的CCF功能(变频、父子切换)对其并不适用。
对比分析
请将其 与 其他相似技术 进行详细对比。
CCF的主要对比对象就是它所取代的、前CCF时代的各种vendor特定的时钟管理实现。
特性 | Common Clock Framework (CCF) | Vendor-Specific 实现 (旧方式) |
---|---|---|
抽象级别 | 高。提供了统一的、硬件无关的API。 | 低。驱动直接调用与特定SoC寄存器绑定的私有函数。 |
驱动可移植性 | 非常高。消费者驱动代码可在不同平台间直接复用。 | 几乎为零。驱动与特定SoC的BSP紧密耦合。 |
硬件配置方式 | 数据驱动。通过设备树描述硬件拓扑和配置。 | 代码驱动。通常在C代码(板级文件)中硬编码时钟配置。 |
电源管理 | 标准化、集中化。统一的clk_enable /disable 接口,易于实现系统级功耗优化。 |
分散、临时。每个驱动自行实现时钟开关,难以协同和验证。 |
依赖管理 | 自动化、健壮。框架根据设备树自动处理父子依赖。 | 手动、易错。开发者必须在代码中手动保证正确的时钟操作顺序。 |
维护成本 | 低。API稳定,硬件改动只需修改设备树。 | 高。任何时钟相关的硬件改动都可能需要修改大量驱动C代码。 |
include/linux/clk-provider.h
CLK_OF_DECLARE CLK_OF_DECLARE_DRIVER
1 |
|
include/linux/clk.h
clk_disable_unprepare
1 | /* clk_disable_unprepare 有助于在非原子上下文中使用 clk_disable 的情况。 */ |
clk_bulk_prepare_enable: 批量准备并使能时钟
这是一个定义在内核头文件中的静态内联函数, 它的核心作用是提供一个方便且安全的封装, 用于一次性地”准备”(prepare)并”使能”(enable)一组时钟。它将一个典型的两阶段时钟启动过程合并为一个单一的API调用, 并内置了关键的错误恢复逻辑。
该函数的原理基于Linux通用时钟框架(Common Clock Framework)中的两个核心概念:
- 准备 (Prepare): 这是一个预备步骤。调用
clk_bulk_prepare
会递归地确保该组时钟所需的所有上游父时钟都已经被”准备”并”使能”。它还会执行一些必要的硬件预操作, 但通常不会真正地打开通往最终外设的时钟门控。这就像在打开水龙头之前, 确保总水管和分水管的阀门都已打开。 - 使能 (Enable): 这是实际开启时钟的步骤。调用
clk_bulk_enable
会打开直连外设的最后一个时钟门控, 让时钟信号可以真正地驱动外设工作。这相当于拧开水龙头本身。
此函数最重要的价值在于其原子性保证和错误处理:
- 它首先尝试”准备”所有时钟。如果这一步就失败了, 它会立即返回错误, 系统状态保持不变。
- 如果”准备”成功, 它会接着尝试”使能”所有时钟。如果”使能”失败了, 它不会直接返回, 而是会自动调用
clk_bulk_unprepare
来撤销第一步成功的”准备”操作, 将系统恢复到调用此函数之前的状态。只有在完成这个”回滚”操作之后, 它才会返回错误码。
对于STM32H750这样的系统, 这个函数极大地简化了设备驱动程序的编写。例如, 一个需要AHB总线时钟和自身内核时钟的复杂外设驱动, 在其probe
函数中只需调用一次clk_bulk_prepare_enable
即可。驱动开发者无需手动编写”如果第二步失败则撤销第一步”的繁琐逻辑, 从而使代码更简洁、更健壮, 有效地防止了因资源状态不一致而导致的系统故障。__must_check
属性会提醒编译器, 如果调用者忽略了此函数的返回值(即不检查操作是否成功), 就产生一个编译警告, 从而强制实施了良好的编程实践。
1 | /* |
drivers/clk/clk.c
of_clk_init 的 clk init
1 | 0x00000000c02a0e10 __clk_of_table = . |
1 | extern struct of_device_id __clk_of_table; |
clk_core_lookup 查找时钟
1 | static struct clk_core *clk_core_lookup(const char *name) |
clk_core_get 查找 clk 的 clk_core 父级
1 | /** |
clk_core_fill_parent_index Clk 核心填充父索引
1 | static void clk_core_fill_parent_index(struct clk_core *core, u8 index) |
clk_core_get_parent_by_index Clk core 按索引获取父级
1 | static struct clk_core *clk_core_get_parent_by_index(struct clk_core *core, |
__clk_init_parent Clk init 父级
1 | static struct clk_core *__clk_init_parent(struct clk_core *core) |
clk_core_get_phase 获取时钟的 phase
1 | static int clk_core_get_phase(struct clk_core *core) |
clk_core_update_duty_cycle_nolock 更新时钟的占空比
1 | static void clk_core_reset_duty_cycle_nolock(struct clk_core *core) |
clk_core_get_rate_nolock Clk core 获取速率 nolock
1 | static unsigned long clk_core_get_rate_nolock(struct clk_core *core) |
__clk_core_init 在 struct clk_core 中初始化数据结构
1 | /** |
clk_prepare_lock clk_prepare_unlock
1 | /*** locking ***/ |
clk_core_link_consumer 将 clk 使用者添加到clk_core中的使用者列表中
1 | /** |
clk_core_populate_parent_map Clk 核心填充父映射
1 | static int clk_core_populate_parent_map(struct clk_core *core, |
__clk_register 注册时钟
1 | static struct clk * |
dev_or_parent_of_node 获取 @dev 或 @dev 的父节点的设备节点
1 | /** |
clk_hw_register 注册 clk_hw 并返回错误代码
1 | /** |
clk_core_update_orphan_status 更新 orphan 状态
1 | /* |
clk_reparent 重新父级
1 | static void clk_reparent(struct clk_core *core, struct clk_core *new_parent) |
__clk_set_parent_before __clk_set_parent_after
1 | static struct clk_core *__clk_set_parent_before(struct clk_core *core, |
__clk_recalc_accuracies Clk 重新计算精度
1 | /** |
clk_recalc 重新计算速率
1 | static unsigned long clk_recalc(struct clk_core *core, |
__clk_recalc_rates 重新计算数率
1 | /** |
clk_core_reparent_orphans 重新为孤立的时钟(orphan clocks)分配父级时钟
- 孤立时钟是指在注册时没有父级时钟的时钟节点
- 该函数的主要目的是遍历孤立时钟列表(clk_orphan_list),为每个找到新父级的孤立时钟更新其时钟树拓扑结构,并重新计算相关的时钟参数。
1 | static void clk_core_reparent_orphans_nolock(void) |
of_clk_add_hw_provider 为节点注册时钟提供程序
1 | /** |
of_clk_hw_onecell_get 获取时钟
1 | struct clk_hw * |
of_clk_get_hw_from_clkspec of clk 从提供商处获取 hw
1 | static struct clk_hw * |
of_parse_clkspec() 解析给定设备节点的 DT 时钟说明符
1 | /** |
alloc_clk 分配一个 clk 消费者,但将其取消链接到 clk_core
1 | /** |
clk_hw_create_clk 分配 clk consumer 并将其链接到给定 clk_hw 的 clk_core
1 | /** |
of_clk_get_hw 从hw获取时钟
1 | struct clk_hw *of_clk_get_hw(struct device_node *np, int index, |
clk_prepare clk 准备时钟
1 | static int clk_core_prepare(struct clk_core *core) |
clk_enable_lock clk_enable_unlock
1 | static unsigned long clk_enable_lock(void) |
clk_core_enable 启用时钟
1 | static int clk_core_enable(struct clk_core *core) |
clk_enable 启用时钟
1 | /** |
clk_disable 禁用时钟
1 | static void clk_core_disable(struct clk_core *core) |
clk_prepare_enable 准备并启用时钟
1 | /* clk_prepare_enable helps cases using clk_enable in non-atomic context. */ |
of_clk_get_parent_name 获取时钟的父节点名称
1 | const char *of_clk_get_parent_name(const struct device_node *np, int index) |
clk_get_rate 获取时钟频率
1 | static unsigned long clk_core_get_rate_recalc(struct clk_core *core) |
drivers/clk/clk-composite.c
__clk_hw_register_composite 注册复合时钟
1 | static struct clk_hw *__clk_hw_register_composite(struct device *dev, |
drivers/clk/clk-conf.c
__set_clk_parents Set clk 父项
1 | static int __set_clk_parents(struct device_node *node, bool clk_supplier) |
__set_clk_rates 设置 clk 速率
1 | static int __set_clk_rates(struct device_node *node, bool clk_supplier) |
of_clk_set_defaults 解析和设置 assigned clocks 配置
1 | /** |
drivers/clk/clk-devres.c
devm
框架下的时钟获取函数
此代码片段展示了Linux内核通用时钟框架(Common Clock Framework)中, 用于获取时钟句柄的devm
(设备资源管理)系列接口。这套API的核心作用是为设备驱动程序提供一个绝对安全、高度健壮且极其便利的方式来获取其所需的时钟资源, 并将该资源的释放操作与驱动的生命周期完全自动化地绑定。
devm
框架的根本原理是解决了驱动probe
函数中一个最常见也最棘手的编程难题: 错误处理和资源清理。一个典型的probe
函数会按顺序获取多个资源(如时钟、内存、中断等)。如果在获取第N个资源时失败, 代码必须手动地、按相反的顺序释放前N-1个已成功获取的资源。这通常会导致复杂的、容易出错的goto
链。devm
框架通过将资源的获取和释放函数的注册绑定在一起, 彻底消除了这种手动清理的需要。
数据结构与释放函数 (devm_clk_state
, devm_clk_release
)
这是devm_clk_get
机制的基础。devm_clk_state
是一个小型的”管理票据”, 用于记录一次成功的时钟获取; devm_clk_release
则是这张”票据”对应的清理函数。
1 | /* |
核心与封装函数 (__devm_clk_get
, devm_clk_get
)
__devm_clk_get
是实现了所有核心逻辑的内部函数, 而devm_clk_get
是提供给大多数驱动使用的、简洁的API封装。
1 | /* |
drivers/clk/clk-divider.c
1 | /* |
clk_div_readl 读取寄存器的数据
1 | static inline u32 clk_div_readl(struct clk_divider *divider) |
_get_table_div 获取表格中的分频系数
1 | struct clk_div_table { |
_get_div 获取分频系数
1 | static unsigned int _get_div(const struct clk_div_table *table, |
DIV_ROUND_UP_ULL 执行分频并向上取整
1 |
divider_recalc_rate 重新计算数率
1 | unsigned long divider_recalc_rate(struct clk_hw *hw, unsigned long parent_rate, |
clk_divider_recalc_rate 重新计算数率
1 | static unsigned long clk_divider_recalc_rate(struct clk_hw *hw, |
clk_divider_set_rate Clk 分频器设置速率
1 | static int clk_divider_set_rate(struct clk_hw *hw, unsigned long rate, |
clk_divider_ops clk_divider_ro_ops
1 | const struct clk_ops clk_divider_ops = { |
clk_hw_register_divider Clk hw 注册分频器
1 | struct clk_hw *__clk_hw_register_divider(struct device *dev, |
drivers/clk/clk-fixed-rate.c
clk_hw_register_fixed_rate_with_accuracy 硬件时钟注册 固定速率和精度
1 | const struct clk_ops clk_fixed_rate_ops = { |
_of_fixed_clk_setup
1 | static struct clk_hw *_of_fixed_clk_setup(struct device_node *node) |
clk_hw_register_fixed_rate 向 clock 注册固定速率 clock
1 | struct clk_hw *__clk_hw_register_fixed_rate(struct device *dev, |
clk_hw_register_fixed_factor Clk hw 寄存器固定系数
1 | static struct clk_hw * |
stm32
- clk-hse clk-lse i2s_ckin
1 | clocks { |
drivers/clk/clk-gate.c
1 | /** |
clk_gate_readl 读取寄存器
1 | static inline u32 clk_gate_readl(struct clk_gate *gate) |
clk_gate_writel 写入寄存器
1 | static inline void clk_gate_writel(struct clk_gate *gate, u32 val) |
clk_gate_endisable
1 | /* |
clk_gate_ops
1 | const struct clk_ops clk_gate_ops = { |
clk_hw_register_gate 注册门控时钟
1 | struct clk_hw *__clk_hw_register_gate(struct device *dev, |
drivers/clk/clk-mux.c
1 | /* |
clk_mux_readl 返回mux的寄存器地址内的数据
1 | static inline u32 clk_mux_readl(struct clk_mux *mux) |
clk_mux_val_to_index 根据读取值返回索引
1 | int clk_mux_val_to_index(struct clk_hw *hw, const u32 *table, unsigned int flags, |
clk_mux_get_parent 复用时钟获取父设备
1 | static u8 clk_mux_get_parent(struct clk_hw *hw) |
clk_mux_ops clk_mux_ro_ops
1 | const struct clk_ops clk_mux_ops = { |
clk_hw_register_mux 注册多路复用器时钟
1 | struct clk_hw *__clk_hw_register_mux(struct device *dev, struct device_node *np, |
drivers/clk/clkdev.c
clk_get
1 | struct clk *clk_get(struct device *dev, const char *con_id) |
drivers/clk/clk-stm32h7.c
stm32_mclk stm32 mux clk 复合时钟
1 | /* System clock parent */ |
clk_register_stm32_timer_ker 注册STM32定时器KER
1 | static unsigned long timer_ker_recalc_rate(struct clk_hw *hw, |
register_core_and_bus_clocks 寄存器 core 和 bus clocks
1 | struct clk_div_table { |
stm32_oclk stm32 晶振时钟
1 | /* Oscillary clock configuration */ |
clk_register_ready_gate Clk 注册门控时钟
1 |
|
get_cfg_composite_div 获取 cfg 复合 div
1 | /* ODF CLOCKS */ |
clk_register_stm32_pll
1 | /* PLL configuration */ |
pclk
1 | /* 外设 时钟 */ |
stm32h7_rcc_init
1 | static void __init stm32h7_rcc_init(struct device_node *np) |
sys_ck的时钟源不是默认值的分析
- 日志可以看出sys_ck读取后选择的时钟源是索引3,即
pll1_p
1 | [2025/5/24 22:06:33 042] [ 0.000000] sys_ck: get_parent = 3 |
- 查看手册说法,默认时钟源是HSI
1 | /* Bits 2:0 SW[2:0]: System clock switch |
- 查看u-boot的代码,可以看到在
drivers/clk/stm32/clk-stm32h7.c
中,在configure_clocks
函数中,会设置时钟源为PPL1
1 | /* select PLL1 as system clock source (sys_ck)*/ |