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 时钟树在设备树中的描述方式:

设备树时钟拓扑示例 设备树时钟拓扑示例 / (根节点) osc: clock@0 pll: clock-controller@a000 div: clock-controller@b000 uart: serial@1000 i2c: i2c@2000 spi: spi@3000 说明: 晶振 (osc) 提供基准频率 → PLL 倍频 → 分频器 (div) 生成多路时钟 → 外设 (uart/i2c/spi) 通过 clocks 属性引用。 每个节点都包含 compatible、reg、#clock-cells 等属性,驱动通过 of API 解析。

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 使用。两者不可互换。

1.8 本章小结