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 消费者节点时钟绑定流程图
下图展示了从设备树解析到驱动获取时钟句柄的完整流程:
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>;
};
关键行为:时钟框架在设备枚举阶段(
1. 将
2. 将
3. 驱动在
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 消费者节点时钟关系图
下图展示了消费者节点与时钟提供者之间的绑定关系:
4.9 调试技巧:验证消费者时钟绑定
在系统启动后,可以通过以下方法验证时钟绑定是否正确:
- 查看 /sys/kernel/debug/clk/clk_summary:确认时钟树中对应时钟的
prepare_count和enable_count是否非零。 - 使用
clk_ignore_unused内核参数:临时禁用时钟框架的 unused 时钟关闭逻辑,便于调试时钟使能问题。 - 检查
/proc/device-tree:确认设备树节点中的clocks和clock-names属性已正确展开。 - 添加驱动调试打印:在
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 属性和驱动加载顺序。