4. 时钟子系统:时钟框架、clk provider、clk consumer、设备树时钟绑定
时钟子系统是嵌入式 Linux 驱动开发的基石。对于 V4L2 设备而言,正确的时钟配置直接决定了摄像头传感器、MIPI 接口、ISP 等模块能否在正确的频率下工作。本章将深入剖析 Linux 通用时钟框架(CCF)的设计理念、核心数据结构、Provider/Consumer 模型以及设备树中的时钟绑定规范。
4.1 通用时钟框架(CCF)概述
Linux 通用时钟框架(Common Clock Framework, CCF)旨在统一 SoC 上种类繁多的时钟硬件,为驱动开发者提供一套标准化的 API。CCF 的核心思想是将时钟硬件抽象为树状结构,通过 Provider(提供者)和 Consumer(使用者)模型实现解耦。
4.2 核心数据结构与 API
CCF 框架围绕几个关键数据结构构建,理解它们是编写 Provider 和 Consumer 驱动的前提。
| 数据结构 | 所属角色 | 作用描述 |
|---|---|---|
struct clk_hw |
Provider | 硬件时钟的抽象,包含指向 struct clk_ops 的指针和私有数据。 |
struct clk_ops |
Provider | 操作函数集,包括 prepare、unprepare、enable、disable、recalc_rate、round_rate、set_rate 等。 |
struct clk |
Consumer | Consumer 看到的时钟句柄,通过 clk_get() 获得,用于调用所有 Consumer API。 |
struct clk_init_data |
Provider | 注册时钟时提供的初始化数据,包含时钟名称、操作函数集、父时钟列表等。 |
struct clk_hw 是 Provider 视角的硬件表示,而 struct clk 是 Consumer 视角的软件句柄。驱动开发中,Provider 驱动主要操作 clk_hw,Consumer 驱动只操作 clk。
4.3 clk provider:时钟提供者驱动编写
Provider 驱动负责向 CCF 注册一个或多个时钟。典型的注册流程如下:
- 分配并初始化
struct clk_hw结构体。 - 填充
struct clk_init_data,设置时钟名称、父时钟、标志位和操作函数集。 - 调用注册函数,如
clk_hw_register()、devm_clk_hw_register()或专用注册器(如clk_register_fixed_rate())。 - 实现 clk_ops 中的回调函数,操作底层硬件寄存器。
以下是一个简单的固定频率时钟 Provider 示例:
#include <linux/clk-provider.h>
#include <linux/platform_device.h>
static struct clk_hw *my_fixed_clk_hw;
static int my_clk_probe(struct platform_device *pdev)
{
struct clk_init_data init = { };
int ret;
init.name = "my_fixed_clk";
init.ops = &clk_fixed_rate_ops; // 使用通用固定频率操作函数
init.flags = CLK_IS_CRITICAL; // 标记为关键时钟,防止被关闭
my_fixed_clk_hw = devm_kzalloc(&pdev->dev, sizeof(*my_fixed_clk_hw), GFP_KERNEL);
if (!my_fixed_clk_hw)
return -ENOMEM;
my_fixed_clk_hw->init = &init;
ret = devm_clk_hw_register(&pdev->dev, my_fixed_clk_hw);
if (ret)
return ret;
return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_simple_get, my_fixed_clk_hw);
}
devm_of_clk_add_hw_provider() 将时钟暴露给设备树,否则 Consumer 无法通过 clk_get() 找到该时钟。对于可变频率时钟(如 PLL),必须实现 recalc_rate、round_rate 和 set_rate 回调。
4.4 clk consumer:时钟使用者驱动编写
Consumer 驱动(如 V4L2 摄像头驱动)通过标准 API 获取和操作时钟。典型的使用模式如下:
#include <linux/clk.h>
struct device *dev = &pdev->dev;
struct clk *sensor_clk;
int ret;
// 1. 获取时钟句柄(时钟名称需与设备树中的 clock-names 匹配)
sensor_clk = devm_clk_get(dev, "xvclk");
if (IS_ERR(sensor_clk)) {
dev_err(dev, "Failed to get xvclk\n");
return PTR_ERR(sensor_clk);
}
// 2. 准备并启用时钟(prepare 可能睡眠,enable 是原子操作)
ret = clk_prepare_enable(sensor_clk);
if (ret) {
dev_err(dev, "Failed to enable xvclk\n");
return ret;
}
// 3. 设置时钟频率(可选,通常由设备树指定)
ret = clk_set_rate(sensor_clk, 24000000); // 设置为 24MHz
if (ret)
dev_warn(dev, "Failed to set xvclk rate\n");
// 4. 获取实际频率(用于配置传感器 PLL)
unsigned long rate = clk_get_rate(sensor_clk);
dev_info(dev, "xvclk rate: %lu Hz\n", rate);
// ... 进行 V4L2 设备初始化 ...
// 5. 关闭时钟(在驱动移除或休眠时调用)
clk_disable_unprepare(sensor_clk);
| API 函数 | 功能 | 是否可睡眠 |
|---|---|---|
clk_get(dev, id) |
获取时钟句柄 | 是 |
clk_prepare(clk) |
准备时钟(可能涉及 PLL 锁定等耗时操作) | 是 |
clk_enable(clk) |
使能时钟输出(原子操作) | 否 |
clk_prepare_enable(clk) |
组合操作:prepare + enable | 是 |
clk_set_rate(clk, rate) |
设置时钟频率 | 是 |
clk_get_rate(clk) |
获取当前时钟频率 | 否 |
clk_disable_unprepare(clk) |
组合操作:disable + unprepare | 是 |
4.5 设备树时钟绑定
设备树是连接 Provider 和 Consumer 的桥梁。时钟绑定遵循 clocks 和 clock-names 属性规范。
4.5.1 Provider 节点绑定
时钟 Provider 节点需要定义 #clock-cells 属性,指定输出时钟的数量。对于单输出时钟,#clock-cells = <0>;对于多输出时钟(如 PLL 输出多个分频时钟),#clock-cells = <1>,Consumer 通过索引指定。
/* 固定频率时钟 Provider */
clk_24m: clk-24m {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>;
clock-output-names = "xvclk";
};
/* SoC 内部时钟控制器 Provider */
clkc: clock-controller@ff760000 {
compatible = "rockchip,rk3399-cru";
reg = <0x0 0xff760000 0x0 0x1000>;
#clock-cells = <1>; /* 多输出时钟,通过索引引用 */
#reset-cells = <1>;
};
4.5.2 Consumer 节点绑定
Consumer 节点通过 clocks 属性引用 Provider,并通过 clock-names 为每个时钟指定名称,便于驱动通过 devm_clk_get(dev, name) 获取。
/* V4L2 摄像头传感器节点 */
i2c@ff3d0000 {
#address-cells = <1>;
#size-cells = <0>;
camera_sensor: camera@10 {
compatible = "sony,imx219";
reg = <0x10>;
clocks = <&clk_24m>; /* 引用 Provider */
clock-names = "xvclk"; /* 驱动中使用的名称 */
clock-frequency = <24000000>; /* 期望频率(非标准属性,但常用) */
/* ... 其他属性 ... */
};
};
4.6 V4L2 驱动中的时钟实战要点
在 V4L2 摄像头驱动中,时钟管理直接影响图像采集的稳定性。以下是几个关键实战要点:
- 时钟顺序: 必须先使能时钟,再初始化传感器 I2C 寄存器;关闭时钟必须在传感器进入休眠之后。
- 频率精度: 传感器通常需要精确的时钟频率(如 24MHz ± 1%)。使用
clk_round_rate()检查硬件支持的最佳频率,避免因频率偏差导致图像抖动。 - 异步时钟域: 如果传感器使用外部晶振,而 MIPI CSI 接口使用 SoC 内部 PLL,两者属于不同时钟域。驱动中需确保数据同步,必要时启用硬件握手信号。
- 电源与时钟联动: 某些 SoC 要求先上电再提供时钟。建议在驱动中按“电源 -> 时钟 -> 复位释放”的顺序初始化。
/sys/kernel/debug/clk/clk_summary 查看时钟树状态,确认时钟是否已启用、频率是否正确。这是排查 V4L2 设备无图像输出问题的第一步。
# 查看时钟树摘要
cat /sys/kernel/debug/clk/clk_summary
# 输出示例(部分)
# clock enable_cnt prepare_cnt rate accuracy phase
# ---------------------------------------------------------------------------------------
# xvclk 1 1 24000000 0 0
# pll_sensor 1 1 594000000 0 0
# csi_mclk 1 1 24000000 0 0