1. 设备树基础:DT 的起源、作用与基本语法结构,以及时钟子系统在设备树中的角色
1.1 设备树的起源与设计动机
在早期嵌入式 Linux 中,板级硬件信息(内存基址、外设地址、中断号、时钟频率等)被硬编码在 arch/arm/mach-xxx 下的 C 文件中。每更换一个 SoC 或板卡,就必须重新编译内核,导致代码冗余、维护困难。2005 年前后,PowerPC 社区率先引入 Open Firmware Device Tree 概念,随后 ARM Linux 社区在 2011–2012 年大规模采用设备树(Device Tree,DT),将硬件描述从内核源码中剥离,形成“一份 DTS + 通用内核 = 多板支持”的格局。
核心动机: 将硬件拓扑与驱动代码解耦,使同一内核二进制能启动不同板卡,降低 BSP 维护成本。
1.2 设备树的作用
| 作用维度 | 说明 |
|---|---|
| 硬件描述 | 描述 CPU 数量、内存布局、总线拓扑、外设地址、中断、时钟、GPIO、DMA 等资源 |
| 驱动匹配 | 通过 compatible 属性将设备节点与驱动绑定,实现“即插即用”式加载 |
| 资源传递 | 驱动通过 of_ 系列 API 读取设备树中的寄存器地址、中断号、时钟频率等参数 |
| 配置分离 | 板级差异(如外设使能、引脚复用、时钟源选择)放在 DTS 中,内核源码保持通用 |
1.3 基本语法结构
设备树源文件(.dts)采用树形层级结构,每个节点用 node-name@unit-address 标识,节点内包含属性和子节点。常用语法要素如下:
- 根节点:
/ { ... };,整个树的起点。 - 属性:
compatible = "vendor,device";—— 字符串列表,用于驱动匹配。 - 地址资源:
reg = <0x10000000 0x1000>;—— 起始地址 + 长度。 - 中断:
interrupts = <0 31 4>;—— 中断类型、编号、触发方式。 - 时钟:
clocks = <&clk_uart 0>;—— 引用时钟提供者及输出索引。 - 包含文件:
#include "soc.dtsi"—— 复用 SoC 级定义。
// 最小设备树示例
/dts-v1/;
/ {
model = "Example Board";
compatible = "vendor,example-board";
chosen {
stdout-path = &uart0;
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 31 4>;
clocks = <&clk_uart 0>;
status = "okay";
};
clk_uart: clock-controller@20000000 {
compatible = "vendor,uart-clock";
reg = <0x20000000 0x100>;
#clock-cells = <1>;
};
};
1.4 时钟子系统在设备树中的角色
时钟子系统是设备树中最重要的基础设施之一。SoC 内部通常有多个时钟源(晶振、PLL、分频器、门控),外设需要获取正确的时钟频率才能正常工作。设备树通过以下机制描述时钟拓扑:
- 时钟提供者(Clock Provider):用
#clock-cells属性声明输出时钟的数量。例如#clock-cells = <1>表示该控制器有多个输出,通过索引区分。 - 时钟消费者(Clock Consumer):用
clocks属性引用提供者及索引,如clocks = <&clk_uart 0>。 - 时钟名称:
clock-names属性为每个时钟指定语义名称,便于驱动通过devm_clk_get(dev, "baud")获取。 - 时钟频率:
clock-frequency属性用于固定频率时钟(如外部晶振),驱动可直接读取。
调试提示: 时钟节点配置错误是驱动无法 probe 或外设无响应的常见原因。检查
#clock-cells 是否匹配、clocks 引用是否存在循环依赖、clock-frequency 是否与硬件手册一致。
1.5 时钟拓扑关系图
下图展示了一个典型的 SoC 时钟树在设备树中的描述方式:
1.6 时钟节点关键属性速查表
| 属性 | 适用角色 | 说明 | 示例 |
|---|---|---|---|
#clock-cells |
提供者 | 声明输出时钟数量(0 表示单输出,1 表示多输出) | #clock-cells = <1>; |
clock-output-names |
提供者 | 为每个输出时钟指定名称(可选) | clock-output-names = "cpu", "bus"; |
clocks |
消费者 | 引用父时钟提供者及索引 | clocks = <&clk_uart 0>; |
clock-names |
消费者 | 为每个引用时钟指定语义名称 | clock-names = "baud"; |
clock-frequency |
消费者/提供者 | 固定频率时钟直接指定 Hz 值 | clock-frequency = <24000000>; |
assigned-clocks |
消费者 | 在 boot 阶段由内核设置默认时钟父级或频率 | assigned-clocks = <&clk_uart 0>; |
assigned-clock-parents |
消费者 | 指定默认父时钟 | assigned-clock-parents = <&clk_osc>; |
assigned-clock-rates |
消费者 | 指定默认频率 | assigned-clock-rates = <50000000>; |
1.7 时钟子系统在驱动调试中的关键角色
时钟配置错误是嵌入式调试中最隐蔽的问题之一。设备树中的时钟节点直接影响以下驱动行为:
- probe 失败:驱动调用
devm_clk_get()返回-EPROBE_DEFER或-EINVAL,通常因clocks引用错误或提供者未就绪。 - 外设无响应:时钟频率错误导致 UART 波特率偏差、I2C 时序异常、SPI 速率超限。
- 系统挂起/重启:关闭关键时钟(如 DDR 控制器时钟)导致系统崩溃。
- 功耗异常:门控时钟未正确配置,外设时钟在空闲时未关闭。
常见误区: 初学者容易混淆
clock-frequency 和 assigned-clock-rates。前者用于固定频率时钟(如晶振),后者用于可编程时钟控制器,需要配合 assigned-clocks 使用。两者不可互换。