4. 时钟消费者节点配置:外设节点如何引用时钟,使用clocks与clock-names属性绑定时钟源

在设备树中,外设节点作为时钟的消费者(Consumer),通过标准属性声明其使用的时钟源。正确配置 clocks 和 clock-names 属性是驱动获取时钟句柄、实现时钟使能/频率调整的前提。本节详细讲解消费者节点的绑定规范、属性语法及常见错误。

4.1 消费者节点绑定规范

设备树规范(ePAPR / Devicetree Specification)要求所有时钟消费者节点遵循以下规则:

  • clocks:必选属性,类型为 <phandle> 列表,每个元素指向一个时钟提供者节点及其输出索引。
  • clock-names:可选属性,类型为 <string> 列表,为每个时钟指定一个逻辑名称,便于驱动按名查找。
  • clock-frequency:可选属性,用于直接指定外设期望的运行频率(当不需要动态时钟配置时使用)。
  • assigned-clocks / assigned-clock-parents / assigned-clock-rates:用于在系统启动阶段由时钟框架自动配置时钟源和频率。
最佳实践:即使外设只使用一个时钟,也建议显式提供 clock-names,这能显著提升驱动代码的可读性和可移植性。

4.2 clocks 属性语法详解

clocks 属性是一个 <u32> 数组,每两个 <u32> 值构成一个时钟说明符(Clock Specifier):

clocks = <CLOCK_PROVIDER_PHANDLE CLOCK_INDEX>,
         <CLOCK_PROVIDER_PHANDLE CLOCK_INDEX>,
         ...;
字段 说明 示例值
CLOCK_PROVIDER_PHANDLE 时钟提供者节点的 phandle(引用标签) &clkc、&osc24m
CLOCK_INDEX 时钟提供者内部的输出索引(从 0 开始) 0、3

典型示例:

/* 时钟提供者节点 */
clkc: clock-controller@ff760000 {
    compatible = "rockchip,rk3399-cru";
    reg = <0x0 0xff760000 0x0 0x1000>;
    #clock-cells = <1>;   /* 需要 1 个索引参数 */
};

/* 消费者外设节点:UART2 */
uart2: serial@ff180000 {
    compatible = "rockchip,rk3399-uart", "snps,dw-apb-uart";
    reg = <0x0 0xff180000 0x0 0x100>;
    interrupts = <GIC_SPI 113 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clkc SCLK_UART2>, <&clkc PCLK_UART2>;
    clock-names = "baudclk", "apb_pclk";
};

上述代码中,SCLK_UART2 和 PCLK_UART2 是时钟提供者头文件中定义的宏,展开后为整数索引。

4.3 clock-names 属性与驱动查找机制

clock-names 为每个时钟提供一个语义化名称,驱动通过 devm_clk_get() 或 of_clk_get_by_name() 按名获取时钟句柄:

/* 驱动侧获取时钟 */
struct clk *baudclk = devm_clk_get(dev, "baudclk");
struct clk *apb_pclk = devm_clk_get(dev, "apb_pclk");

if (IS_ERR(baudclk)) {
    dev_err(dev, "failed to get baudclk\n");
    return PTR_ERR(baudclk);
}

名称与索引的对应关系:

clocks 列表顺序 clock-names 对应值 驱动查找函数
第 1 个时钟 "baudclk" devm_clk_get(dev, "baudclk")
第 2 个时钟 "apb_pclk" devm_clk_get(dev, "apb_pclk")
常见陷阱:如果 clock-names 缺失,驱动只能通过 of_clk_get(dev, index) 按索引获取时钟。索引顺序必须与 clocks 属性中的顺序严格一致。一旦中间插入或删除时钟,所有索引都会偏移,极易导致驱动获取到错误的时钟句柄。

4.4 消费者节点时钟绑定流程图

下图展示了从设备树解析到驱动获取时钟句柄的完整流程:

消费者节点时钟绑定与驱动获取流程 设备树消费者节点 clocks = <&clkc 42>, <&clkc 67> clock-names = "baudclk", "apb_pclk" 内核 OF 时钟子系统解析 驱动调用 devm_clk_get() 按 clock-names 匹配索引 按 clocks 索引获取句柄 说明:驱动优先按名查找(clock-names),若未定义则按索引顺序匹配 clocks 列表

4.5 多时钟源与复合时钟配置

复杂外设(如 USB 控制器、GPU、ISP)通常需要多个时钟源协同工作。以下是一个典型的多时钟消费者节点示例:

usb_host0_ehci: usb@fe380000 {
    compatible = "generic-ehci";
    reg = <0x0 0xfe380000 0x0 0x20000>;
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clkc HCLK_HOST0>,
             <&clkc HCLK_HOST0_ARB>,
             <&clkc PCLK_HOST0>;
    clock-names = "hclk_host0",
                  "hclk_host0_arb",
                  "pclk_host0";
    phys = <&u2phy0>;
    phy-names = "usb2-phy";
    status = "disabled";
};

时钟角色说明:

时钟名称 功能角色 驱动使用场景
hclk_host0 主机控制器主时钟(AHB 总线时钟) 使能控制器核心逻辑
hclk_host0_arb 仲裁器时钟 控制 USB 事务仲裁
pclk_host0 APB 外设总线时钟 寄存器访问时钟

4.6 assigned-clocks 属性:启动时自动配置

当外设需要在系统启动阶段就确定时钟源或频率时,使用 assigned-clocks 系列属性:

i2c1: i2c@ff3d0000 {
    compatible = "rockchip,rk3399-i2c";
    reg = <0x0 0xff3d0000 0x0 0x1000>;
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clkc SCLK_I2C1>, <&clkc PCLK_I2C1>;
    clock-names = "i2c", "pclk";
    assigned-clocks = <&clkc SCLK_I2C1>;
    assigned-clock-parents = <&clkc PLL_APLL>;
    assigned-clock-rates = <200000000>;
    #address-cells = <1>;
    #size-cells = <0>;
};
关键行为:时钟框架在设备枚举阶段(of_clk_init() 之后)自动执行以下操作:
1. 将 SCLK_I2C1 的父时钟设置为 PLL_APLL。
2. 将 SCLK_I2C1 的输出频率设置为 200 MHz。
3. 驱动在 probe() 中只需 clk_prepare_enable(),无需额外配置频率。

4.7 常见错误与排查清单

错误现象 可能原因 排查方法
devm_clk_get() 返回 -EPROBE_DEFER 时钟提供者尚未初始化 检查提供者节点 status 是否为 "okay";确认提供者驱动已编译
devm_clk_get() 返回 -EINVAL clock-names 名称拼写错误或 clocks 索引越界 核对设备树中 clock-names 字符串与驱动代码中的名称是否完全一致(区分大小写)
clk_set_rate() 失败 时钟提供者不支持该频率或未设置 assigned-clock-rates 使用 clk_round_rate() 查询支持的频率范围
外设时钟不工作 驱动未调用 clk_prepare_enable() 在驱动 probe() 中检查是否对每个获取的时钟都执行了使能操作
设备树编译报错 clocks 中 phandle 引用错误或 #clock-cells 不匹配 检查提供者节点的 #clock-cells 值;确认宏定义已正确包含头文件

4.8 消费者节点时钟关系图

下图展示了消费者节点与时钟提供者之间的绑定关系:

消费者节点与时钟提供者绑定关系 时钟提供者 clkc: clock-controller #clock-cells = <1> clk_out0 (SCLK) clk_out1 (PCLK) 消费者外设 uart2: serial@ff180000 clock-names = "baudclk","apb_pclk" 索引 0 索引 1 "baudclk" "apb_pclk" 绑定规则总结 1. clocks 属性中的 phandle 指向提供者节点,索引对应提供者的输出时钟编号。 2. clock-names 为每个时钟提供语义化名称,驱动按名获取,避免索引耦合。 3. 提供者的 #clock-cells 决定每个时钟说明符需要几个参数(通常为 0 或 1)。 4. assigned-clocks 系列属性在启动阶段自动配置时钟源和频率,减少驱动初始化负担。 5. 驱动必须对每个获取的时钟调用 clk_prepare_enable(),否则外设无法正常工作。

4.9 调试技巧:验证消费者时钟绑定

在系统启动后,可以通过以下方法验证时钟绑定是否正确:

  1. 查看 /sys/kernel/debug/clk/clk_summary:确认时钟树中对应时钟的 prepare_count 和 enable_count 是否非零。
  2. 使用 clk_ignore_unused 内核参数:临时禁用时钟框架的 unused 时钟关闭逻辑,便于调试时钟使能问题。
  3. 检查 /proc/device-tree:确认设备树节点中的 clocks 和 clock-names 属性已正确展开。
  4. 添加驱动调试打印:在 probe() 中打印获取到的时钟句柄和频率:
baudclk = devm_clk_get(dev, "baudclk");
if (!IS_ERR(baudclk)) {
    dev_info(dev, "baudclk rate: %lu Hz\n", clk_get_rate(baudclk));
}
重要提醒:在调试阶段,务必确认时钟提供者节点已正确初始化。如果提供者驱动返回 -EPROBE_DEFER,消费者驱动也会被延迟探测。此时应优先排查提供者节点的 status 属性和驱动加载顺序。