3. 时钟绑定规范:标准时钟绑定属性

时钟绑定,说白了就是告诉内核:这个设备需要哪些时钟,以及怎么找到它们。我刚开始接触设备树时,觉得这玩意儿不就是几个属性嘛,后来踩了坑才发现,这里面的门道还真不少。

3.1 clocks 属性:时钟的“身份证”

clocks 是每个需要时钟的设备节点里最核心的属性。它是一个 phandle + 时钟索引 的列表。每个条目对应一个时钟输入。

举个例子,一个 UART 控制器通常需要两个时钟:一个用于总线访问(PCLK),一个用于波特率生成(UCLK)。在设备树里是这样写的:

uart0: serial@1000a000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x1000a000 0x1000>;
    interrupts = <0 20 4>;
    clocks = <&clkc 15>, <&clkc 16>;
    clock-names = "apb_pclk", "uartclk";
};

这里 &clkc 15 表示引用时钟控制器节点 clkc 的输出索引 15。第二个 &clkc 16 同理。索引号怎么来的?得看时钟控制器的 #clock-cells 属性。

关键点:clocks 属性的顺序很重要。驱动里通常按索引取时钟,如果顺序乱了,驱动拿到的时钟就错了。我见过一个项目,硬件工程师改了时钟树,设备树没同步更新,结果系统跑起来串口乱码,查了两天才发现是时钟索引对不上。

3.2 clock-names 属性:给时钟起个名字

clock-names 是 clocks 的“翻译官”。它把枯燥的索引号变成有意义的字符串。驱动开发时,我更喜欢用名字来获取时钟,而不是硬编码索引。

看上面的例子:

clock-names = "apb_pclk", "uartclk";

驱动里就可以这样用:

clk = devm_clk_get(&pdev->dev, "uartclk");

而不是:

clk = of_clk_get(pdev->dev.of_node, 1);

我个人习惯,只要设备有多个时钟,一定加上 clock-names。为什么?因为代码可读性差太多了。你想想看,三个月后回来看代码,看到 of_clk_get(node, 1),你还记得索引 1 是哪个时钟吗?但看到 "uartclk",一目了然。

小技巧:命名规范建议参考芯片手册里的信号名。比如手册里叫 UCLK,设备树里就用 "uartclk";叫 PCLK,就用 "apb_pclk"。这样硬件工程师和驱动工程师沟通时,不会产生歧义。

3.3 clock-frequency 属性:固定频率的“快捷方式”

有些设备不需要动态调频,频率是固定的。比如一个低速 SPI 控制器,或者一个固定频率的晶振。这时候可以用 clock-frequency 直接指定频率值,单位是 Hz。

spi0: spi@1000b000 {
    compatible = "snps,dw-spi";
    reg = <0x1000b000 0x1000>;
    clock-frequency = <10000000>;  /* 10 MHz */
};

注意,clock-frequency 不是标准时钟绑定属性,它只是一个约定俗成的属性。内核里有些驱动会解析它,有些不会。我建议:能用 clocks 的地方,尽量别用 clock-frequency。后者更适合那些没有时钟控制器的简单设备。

避坑指南:我曾经在一个项目里,把晶振节点的频率写成了 clock-frequency,但驱动里用的是 clocks 属性来获取频率。结果晶振频率始终读不到,系统时钟全乱了。后来才发现,晶振节点应该用 clock-frequency,但消费设备应该用 clocks 引用它。嗯,这里要注意区分“提供者”和“消费者”的角色。

3.4 时钟索引与 phandle:怎么找到正确的时钟

时钟索引(clock index)和 phandle 是设备树里定位时钟的“坐标系统”。

phandle 是节点引用,告诉内核去哪个节点找时钟。比如 &clkc 指向时钟控制器节点。

时钟索引 是时钟控制器内部的输出编号。这个编号由时钟控制器的驱动定义,通常对应硬件上的某个时钟输出引脚。

举个例子,一个 SoC 的时钟控制器可能有 32 个输出:

clkc: clock-controller@1000c000 {
    compatible = "vendor,clkc";
    reg = <0x1000c000 0x1000>;
    #clock-cells = <1>;  /* 一个 cell 表示时钟索引 */
};

这里 #clock-cells = <1> 表示每个时钟引用需要 1 个 cell 来指定索引。所以 <&clkc 15> 就是索引 15 的时钟。

如果 #clock-cells = <0>,表示这个节点只输出一个时钟,不需要索引。比如固定频率晶振:

osc: oscillator {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;
};

引用时直接写 <&osc> 就行,不需要索引。

核心逻辑:#clock-cells 的值决定了 phandle 后面跟几个数字。0 表示不跟,1 表示跟一个索引,2 表示跟两个(比如某些复杂时钟控制器需要域和索引)。这个规则一定要记牢,写错了内核解析会失败。

3.5 实战中的常见问题

我整理了几个实际项目中容易踩的坑:

问题 现象 原因 解决方法
时钟索引写错 驱动 probe 失败,或设备工作异常 设备树里的索引和时钟控制器定义不一致 对照芯片手册,确认每个时钟的索引号
缺少 clock-names 驱动用名字获取时钟时返回 NULL 设备树没提供 clock-names,但驱动用了 devm_clk_get(dev, name) 补上 clock-names,或驱动改用 of_clk_get(node, index)
#clock-cells 不匹配 内核报 "invalid clock specifier" 引用时钟时给的 cell 数量和提供者定义的不一致 检查提供者的 #clock-cells,确保引用时 cell 数量正确
时钟频率单位搞错 设备工作频率不对 clock-frequency 单位是 Hz,有人写成 MHz 确认单位是 Hz,比如 10 MHz 写成 10000000

调试技巧:如果时钟绑定有问题,可以在内核启动参数里加 clk_ignore_unused 和 earlycon,然后看 dmesg 输出。内核会打印 "clk: ..." 相关的调试信息,能帮你定位是哪个时钟没找到。

3.6 知识体系结构图

下面这张图总结了时钟绑定的核心逻辑,我画出来方便你理解:

时钟绑定规范核心结构 设备节点 (Consumer) clocks = <&clkc 15>, <&clkc 16>; clock-names = "apb_pclk", "uartclk"; (可选)clock-frequency phandle 时钟控制器 (Provider) #clock-cells = <1>; 时钟输出索引: 索引 15 → UART PCLK 索引 16 → UART UCLK (固定频率晶振:#clock-cells = <0>) 索引号 驱动 获取 时钟 关键规则总结 1. clocks 属性:phandle + 索引,顺序对应驱动里的时钟数组 2. clock-names 属性:给每个时钟起名字,方便驱动按名获取 3. clock-frequency 属性:固定频率设备的快捷方式,慎用 4. #clock-cells 决定索引数量:0→无索引,1→一个索引,2→两个索引 5. 提供者与消费者:时钟控制器是提供者,外设是消费者

这张图把整个流程串起来了。设备节点通过 clocks 属性引用时钟控制器,时钟控制器通过 #clock-cells 定义索引格式,驱动再根据索引或名字获取对应的时钟。环环相扣,缺一不可。

3.7 写在最后

时钟绑定规范看起来简单,但实际项目中出问题最多的就是这里。我建议你在写设备树时,先确认时钟控制器的 #clock-cells 值,再写消费设备的 clocks 属性。顺序别搞反了,否则排查起来很痛苦。

另外,clock-names 能加就加。这不是强制要求,但绝对是良好习惯。你想想看,一个设备有七八个时钟,没有名字的话,光靠索引谁能记得住?

好了,这一节就到这里。时钟绑定是设备树里最基础也最重要的部分,理解了它,后面的电源管理适配就会轻松很多。