1. 设备树基础:DTS 的起源、作用与基本语法结构

1.1 设备树的起源与设计动机

在早期的嵌入式 Linux 系统中,板级硬件信息被硬编码在 arch/arm/mach-xxx 下的 C 语言源文件中。每更换一个外围设备或调整内存映射,就需要重新编译内核。这种“代码即配置”的方式导致内核源码膨胀、维护困难,且无法支持单一内核镜像运行于多个硬件平台。

设备树(Device Tree)正是为解决这一问题而引入的。它起源于 Open Firmware(IEEE 1275)规范,由 PowerPC 架构率先采用,后于 2011 年被 ARM Linux 社区正式采纳为强制性机制。其核心思想是:将硬件描述从内核代码中剥离,以结构化文本形式独立存在,内核在启动时解析该描述,动态构建设备模型。

对比维度 传统板级文件(Board File) 设备树(Device Tree)
硬件描述位置 内核源码 arch/arm/mach-xxx 独立 .dts / .dtsi 文件
内核镜像通用性 每板编译一次,镜像不通用 单镜像 + 独立 DTB,镜像通用
修改硬件配置 需修改 C 代码并重新编译内核 仅修改 DTS 文件,重新编译 DTB
社区支持 ARM 社区已弃用 ARM、RISC-V、x86(ACPI 替代)
关键概念:设备树是“数据”而非“代码”。它不包含执行逻辑,只描述硬件拓扑、地址映射、中断连接、时钟关系等静态信息。内核中的驱动代码通过 of_ 系列 API 读取这些数据。

1.2 设备树文件体系与基本语法

设备树源文件(DTS)采用树形结构,以 /dts-v1/; 开头,使用 node { }; 定义节点,property = value; 定义属性。常见的文件后缀与处理流程如下:

  • .dts — 设备树源文件,描述单板硬件。
  • .dtsi — 设备树包含文件,通常存放 SoC 公共定义,通过 #include 引入。
  • .dtb — 编译后的二进制设备树 Blob,由 Bootloader 加载并传递给内核。
  • .dtbo — 设备树覆盖(Overlay)文件,用于运行时动态修改 DTB。
// 一个简化的设备树示例:包含 CPU、内存、UART 和时钟控制器
/dts-v1/;

#include "soc-common.dtsi"

/ {
    model = "Embedded Learning Board v1.0";
    compatible = "vendor,elb-v1", "vendor,elb";

    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB 内存
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            compatible = "arm,cortex-a7";
            reg = <0>;
            clocks = <&clkc 0>;
        };
    };

    clkc: clock-controller@f8001000 {
        compatible = "vendor,clkctrl";
        reg = <0xf8001000 0x1000>;
        #clock-cells = <1>;
        clocks = <&osc24m>;
        clock-names = "osc";
    };

    uart0: serial@f8006000 {
        compatible = "vendor,uart";
        reg = <0xf8006000 0x100>;
        interrupts = <0 29 4>;
        clocks = <&clkc 12>;
        clock-names = "baud";
    };
};
语法要点:
  • #address-cells 和 #size-cells 定义子节点 reg 属性的地址和长度字段宽度(单位:32 位字)。
  • compatible 是驱动匹配的关键字符串,内核通过它查找对应驱动。
  • clocks = <&clkc 12>; 表示引用 clkc 节点的第 12 号时钟输出。
  • reg 描述设备在总线上的地址范围,格式为 (address, length) 对。

1.3 时钟节点在设备树中的位置与角色

时钟节点是设备树中负责描述时钟源、时钟控制器(Clock Controller)以及设备与时钟之间连接关系的特殊节点。它在整个设备树中处于承上启下的枢纽位置:

  • 根节点附近:固定频率的晶振(如 osc24m)通常直接挂在根节点下,作为最顶层时钟源。
  • SoC 总线区域:时钟控制器节点(如 clock-controller@f8001000)位于 SoC 内部总线地址空间,负责产生和分发各类时钟。
  • 外设节点内部:每个需要时钟的外设(UART、I2C、SPI、Ethernet 等)通过 clocks 属性引用时钟控制器输出的特定时钟。
时钟节点在设备树中的位置与连接关系 / (根节点) osc24m: 24MHz 晶振 clkc: clock-controller uart0: serial i2c0: i2c-controller eth0: ethernet clocks = <&osc24m> clocks = <&clkc 12> clocks = <&clkc 6> clocks = <&clkc 20> 图例说明: ● 绿色节点:固定频率晶振,作为顶层时钟源。 ● 红色节点:时钟控制器,负责频率合成与分发。 ● 紫色节点:外设节点,通过 clocks 属性引用时钟控制器的输出。 ● 橙色连线:时钟信号流向,从晶振到控制器再到外设。

1.4 时钟节点的核心属性与绑定规范

时钟节点遵循 Linux 内核的 Common Clock Framework (CCF) 绑定规范。一个标准的时钟控制器节点通常包含以下属性:

属性名 含义 示例值
#clock-cells 定义时钟输出索引的 cell 数量。0 表示单输出,1 表示多输出。 <1>
clocks 引用该控制器的输入时钟源(如晶振或上级 PLL)。 <&osc24m>
clock-names 输入时钟的名称列表,与 clocks 一一对应。 "osc"
clock-output-names 输出时钟的名称列表,用于调试和日志。 "cpu_clk", "ahb_clk", "uart_clk"
assigned-clocks 在启动时由内核自动配置频率的时钟列表。 <&clkc 0>
assigned-clock-rates 与 assigned-clocks 对应的目标频率值。 <1000000000>
常见配置陷阱:
  • #clock-cells 值错误会导致驱动无法正确解析时钟索引,引发 clk_get 失败。
  • 遗漏 assigned-clock-rates 时,内核可能以默认频率运行,导致外设时序异常。
  • 时钟名称拼写与驱动中 devm_clk_get(dev, "name") 不匹配时,返回 -EPROBE_DEFER 或 NULL。

1.5 时钟节点与性能调优的关联

时钟频率直接影响 CPU 算力、总线带宽和外设吞吐量。在设备树中正确配置时钟参数,是性能调优的第一步:

  • CPU 频率:通过 assigned-clock-rates 设置 CPU 时钟,决定系统算力上限。
  • 总线时钟:AHB/APB 总线频率影响 DMA 传输速率和内存访问延迟。
  • 外设时钟:UART 波特率、I2C 速率、SPI 时钟、以太网 RMII 参考时钟等,均需精确匹配。
  • 动态调频:结合 CPUFreq 框架,设备树可定义 OPP(Operating Performance Points)表,实现按需调频。
时钟配置对系统性能的影响层次 应用层性能表现 CPU 算力 总线带宽 外设吞吐量 CPU 时钟域 AHB/APB 时钟域 外设专用时钟域 ● 设备树中 assigned-clock-rates 直接设定底层时钟域频率。 ● 每个时钟域的频率变化会逐层向上传递,最终影响应用性能。 ● 性能调优的核心:在功耗与性能之间找到每个时钟域的最佳平衡点。

1.6 本章小结