4. 设备树(Device Tree)基础
4.1 设备树概述与设计动机
在ARM架构的车载嵌入式系统中,硬件配置千差万别。传统方式将硬件信息硬编码在Linux内核中,导致每更换一个板卡就需要重新编译内核,维护成本极高。设备树(Device Tree)的出现解决了这一问题:它将硬件描述从内核代码中剥离,以结构化数据的形式传递给内核,实现“一次编译,多处运行”。
核心思想: 设备树是一种描述硬件资源的数据结构,由引导加载程序(如U-Boot)传递给内核。内核根据设备树动态识别外设、分配资源、加载驱动,无需重新编译。
4.2 设备树文件体系:dts / dtsi / dtb
设备树涉及三种文件类型,它们之间的关系类似于C语言中的源文件、头文件和编译后的二进制文件。
| 文件类型 | 扩展名 | 说明 | 类比 |
|---|---|---|---|
| 设备树源文件 | .dts |
描述具体板卡的硬件配置,是顶层源文件 | .c 源文件 |
| 设备树包含文件 | .dtsi |
描述SoC公共部分、IP核、通用外设,可被多个.dts包含 |
.h 头文件 |
| 设备树二进制文件 | .dtb |
由.dts编译生成的二进制文件,供内核解析 |
.o / .bin 目标文件 |
编译命令:
反编译命令:
dtc -I dts -O dtb -o myboard.dtb myboard.dts反编译命令:
dtc -I dtb -O dts -o myboard.dts myboard.dtb
典型的文件组织方式如下:
#include "soc.dtsi" // 包含SoC公共定义
#include "pinmux.dtsi" // 包含引脚复用配置
/ {
model = "MyCar Board v1.0";
compatible = "mycompany,mycar-board", "arm,vexpress";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; // 1GB RAM
};
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
};
&uart1 {
status = "okay";
clock-frequency = <24000000>;
};
};
4.3 设备树语法与结构
设备树采用树形结构,以根节点 / 为起点,每个节点代表一个硬件设备或总线,节点内包含属性(键值对)来描述设备特征。
4.3.1 基本语法规则
- 节点定义:
node-name@unit-address { ... };,其中unit-address通常为设备基地址。 - 属性格式:
property-name = <value>;或property-name = "string";。 - 标签(Label): 用于引用节点,如
uart1: serial@101f1000 { ... };,可通过&uart1引用。 - 包含文件: 使用
#include指令引入.dtsi文件。 - 路径引用:
&{/soc/i2c@1000}可直接访问指定路径的节点。
4.3.2 常用标准属性
| 属性名 | 含义 | 示例值 |
|---|---|---|
compatible |
设备兼容性字符串,驱动匹配依据 | "mycompany,my-i2c", "snps,designware-i2c" |
reg |
设备地址空间(基地址 + 长度) | <0x101f1000 0x1000> |
interrupts |
中断号与触发类型 | <0 33 4>(SPI 33, 高电平触发) |
clocks |
时钟句柄引用 | <&clk_uart 0> |
status |
设备状态:okay / disabled |
"okay" |
#address-cells |
地址字段占用的32位字数量 | 1 或 2 |
#size-cells |
长度字段占用的32位字数量 | 1 或 2 |
4.3.3 设备树结构示例
/dts-v1/;
/ {
compatible = "arm,vexpress";
#address-cells = <2>;
#size-cells = <2>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a72";
reg = <0x0>;
device_type = "cpu";
};
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
};
soc {
#address-cells = <2>;
#size-cells = <2>;
ranges;
uart0: serial@1c090000 {
compatible = "snps,dw-apb-uart";
reg = <0x0 0x1c090000 0x0 0x1000>;
interrupts = <0 33 4>;
clocks = <&uart_clk>;
status = "disabled";
};
};
};
4.4 设备树在启动过程中的解析流程
从引导加载程序到内核完成驱动绑定,设备树的解析贯穿整个启动过程。下图展示了核心流程:
4.4.1 详细解析步骤
- U-Boot 加载 DTB: 引导加载程序从存储介质(eMMC、NAND Flash)或网络(TFTP)读取
.dtb文件到内存中。 - 传递 DTB 地址: U-Boot 通过 ARM 的
r2寄存器将 DTB 在内存中的物理地址传递给内核。内核启动时首先读取该寄存器获取 DTB 位置。 - 早期解析(unflatten_device_tree): 内核调用
unflatten_device_tree()将扁平的 DTB 二进制数据解析为内存中的device_node树结构,每个节点对应一个设备。 - 构建 device_node 树: 内核遍历 DTB 的节点和属性,为每个节点分配
struct device_node结构体,建立父子关系,形成完整的设备树。 - 驱动匹配与绑定: 内核遍历
device_node树,根据节点的compatible属性与驱动注册的of_match_table进行匹配。匹配成功后,调用驱动的probe()函数。 - 驱动初始化: 在
probe()函数中,驱动通过of_*API(如of_iomap()、of_get_property()、of_irq_get())读取设备树中的资源信息(寄存器地址、中断号、时钟等),完成设备初始化。
4.4.2 驱动中常用的设备树 API
| API 函数 | 功能 | 使用场景 |
|---|---|---|
of_match_device() |
匹配设备与驱动兼容性表 | probe 入口判断 |
of_iomap() |
将设备寄存器地址映射到虚拟地址 | 访问硬件寄存器 |
of_get_property() |
读取设备树属性值 | 获取自定义配置 |
of_irq_get() |
获取设备中断号 | 注册中断处理函数 |
of_clk_get() |
获取设备时钟句柄 | 时钟使能/频率配置 |
of_platform_populate() |
遍历子节点并创建 platform_device | 总线设备注册 |
4.5 设备树在车载系统中的特殊考量
车载安全关键点: 在车载系统中,设备树配置错误可能导致外设无法工作、中断冲突甚至系统崩溃。务必遵循以下原则:
- 所有外设的
reg地址必须与硬件手册严格一致。 - 中断号需确认是 SPI(共享外设中断)还是 PPI(私有外设中断),并正确填写触发类型。
- 时钟依赖关系必须完整描述,避免驱动 probe 时因时钟未就绪而超时。
- 使用
status = "disabled"禁用未使用的设备,减少内核资源占用。
4.6 设备树调试技巧
- 查看当前设备树: 在 Linux 终端执行
dtc -I fs -O dts /sys/firmware/devicetree/base可导出内核当前使用的设备树。 - 检查设备树节点:
ls /sys/firmware/devicetree/base/可浏览设备树节点目录结构。 - 验证设备匹配: 查看
/sys/bus/platform/devices/下是否有对应设备目录,确认驱动是否成功绑定。 - 内核启动日志: 使用
dmesg | grep -i "dtb\|of\|device tree"过滤设备树相关日志,定位解析错误。
实战建议: 在开发阶段,可在 U-Boot 中通过
fdt set 命令动态修改设备树属性,快速验证硬件配置,无需重新编译内核。例如:fdt set /soc/uart@1c090000 status "okay"