3. 时钟控制器节点:fixed-clock节点、clock-cells属性、时钟频率与时钟索引、时钟提供者与消费者
好,咱们今天聊聊时钟控制器节点。这玩意儿在设备树里,说白了就是给系统提供心跳的。没有它,CPU、外设全都歇菜。我刚开始接触设备树时,就被这些时钟节点绕得晕头转向,后来摸清了门道,发现其实就那么几件事。
3.1 fixed-clock节点:最简单的时钟源
先说说最简单的——fixed-clock。这名字起得挺直白,就是固定频率的时钟源。比如板子上有个24MHz的晶振,你不想搞复杂,直接声明一个固定时钟就行。
我习惯这么写:
clk_24m: clock-24m {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>;
clock-output-names = "clk_24m";
};
这里有几个关键点:
- compatible:必须是"fixed-clock",内核才能识别
- #clock-cells:固定时钟通常设为0,表示它只输出一路时钟
- clock-frequency:频率值,单位是Hz。24MHz就写24000000
- clock-output-names:可选的,给时钟起个名字,调试时方便
嗯,这里要注意:clock-frequency的值不能乱写。我曾经见过有人把24MHz写成24000,结果系统跑起来慢得像蜗牛。你想想看,差了一千倍啊!
3.2 clock-cells属性:时钟索引的奥秘
接下来聊聊#clock-cells。这个属性决定了你的时钟控制器能输出几路时钟。说白了,就是告诉别人:「我这里有N个时钟输出,你要用哪个,得告诉我索引号」。
举个例子:
// 单路时钟输出
clk_single: clock-single {
compatible = "fixed-clock";
#clock-cells = <0>; // 不需要索引
clock-frequency = <100000000>;
};
// 多路时钟输出
clk_multi: clock-multi {
compatible = "some,clock-controller";
#clock-cells = <1>; // 需要1个索引
// 内部有 clk0, clk1, clk2 ...
};
为什么会有这种设计?我遇到过这样一个项目:一个时钟芯片能输出4路不同频率的时钟。如果每路都单独写一个节点,那设备树就太臃肿了。用#clock-cells = <1>,消费者通过索引0、1、2、3就能分别引用不同的时钟输出。
核心原则:#clock-cells的值决定了你在引用时钟时需要提供几个参数。0表示不需要参数,1表示需要一个索引值,2表示需要两个参数(比如索引+标志位)。
3.3 时钟频率与时钟索引
时钟频率这块,我建议你养成好习惯——明确标注单位。虽然设备树里频率默认是Hz,但加个注释总没错:
clk_uart: clock-uart {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <115200>; // 115200 Hz,串口常用波特率
};
至于时钟索引,它就像数组的下标。比如一个时钟控制器有3路输出:
| 索引值 | 时钟名称 | 典型频率 |
|---|---|---|
| 0 | cpu_clk | 1 GHz |
| 1 | bus_clk | 200 MHz |
| 2 | periph_clk | 50 MHz |
消费者引用时,就得带上索引:
&uart0 {
clocks = <&clk_multi 1>; // 引用 bus_clk,索引为1
clock-names = "baud";
};
这里&clk_multi 1中的1就是索引。我刚开始总搞混,以为索引是频率值,后来才明白它只是个编号。
3.4 时钟提供者与消费者
设备树里时钟的架构,说白了就是「生产者-消费者」模型。
- 时钟提供者(Clock Provider):产生时钟信号的节点。比如晶振、PLL、时钟芯片。它们用
#clock-cells声明自己能提供几路时钟。 - 时钟消费者(Clock Consumer):使用时钟信号的节点。比如UART、I2C、SPI控制器。它们用
clocks属性引用提供者。
看个完整的例子:
// 提供者:系统主时钟
clk_main: clock-main {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <500000000>; // 500 MHz
};
// 提供者:外设时钟控制器
clk_periph: clock-periph {
compatible = "vendor,periph-clock";
#clock-cells = <1>;
clocks = <&clk_main>; // 自己也需要一个输入时钟
clock-names = "main";
// 内部产生 100MHz, 50MHz, 25MHz 三路输出
};
// 消费者:UART控制器
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
clocks = <&clk_periph 2>; // 引用索引2,即25MHz
clock-names = "baud";
};
我的经验:写设备树时,先把提供者定义好,再写消费者。就像盖房子,先搭好脚手架,再往上挂东西。我曾经图省事,先写了消费者再补提供者,结果编译报错,查了半天才发现是引用顺序的问题。
3.5 避坑指南
我曾经踩过的坑:
- 频率单位搞错:设备树里频率默认是Hz,但有些IP核的文档写的是MHz。一定要换算清楚。
- 索引从0开始:时钟索引从0开始计数,不是1。我见过有人写
clocks = <&clk 1>想引用第一路时钟,结果引用了第二路。 - #clock-cells不匹配:提供者声明了
#clock-cells = <1>,消费者引用时就必须带索引。少写一个参数,内核直接罢工。 - 固定时钟别乱改频率:
fixed-clock的频率是固定的,运行时不能动态调整。如果你需要变频,得用clk-provider或者专门的时钟驱动。
嗯,时钟控制器节点这块,说白了就是三件事:定义提供者、指定索引、让消费者正确引用。你只要把#clock-cells和clock-frequency搞明白,剩下的就是依葫芦画瓢。我刚开始学的时候,也是反复看内核文档,后来自己动手写了几次,就再也没出过问题。
下一章咱们聊聊复位控制器节点,那个跟时钟是孪生兄弟,很多设计思路是相通的。