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属性引用时钟控制器输出的特定时钟。
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)表,实现按需调频。