1. 设备树基础概念与历史背景
1.1 设备树的诞生背景
在嵌入式Linux开发早期(内核3.x版本之前),所有硬件配置信息都直接硬编码在内核源码中。具体来说,这些信息存放在arch/arm/mach-xxx目录下的板级文件中。这种设计导致了一个严重问题:每推出一款新的开发板,都需要修改内核代码并重新编译内核。
这种方式的弊端显而易见:
- 内核源码变得越来越臃肿
- 不同厂商的板级文件混杂在一起难以维护
- 每次硬件变更都需要重新编译整个内核
- 内核开发者需要处理大量重复但略有差异的板级代码
1.2 设备树的解决方案
设备树(Device Tree,简称DT)的出现完美解决了这些问题。它的核心思想是将硬件描述信息从内核代码中剥离出来,用一种结构化的文本格式(DTS)独立维护。这种分离带来了几个关键优势:
- 内核与硬件解耦:内核只需包含通用驱动程序,不再需要为每块板子定制代码
- 配置灵活:同一套内核可以搭配不同的设备树文件,适配多种硬件平台
- 维护简便:硬件描述以文本形式存在,修改后只需重新编译设备树,无需动内核
- 标准化:所有ARM平台使用统一的描述方式,降低了学习成本
提示:设备树最初由Open Firmware提出,后被Linux内核采纳并成为ARM平台的事实标准。现在不仅是ARM,包括龙芯、RISC-V等架构也都全面采用了设备树机制。
1.3 设备树的基本组成
一个完整的设备树系统包含以下几个关键部分:
- DTS(Device Tree Source):人类可读的源文件,扩展名为.dts
- DTSI(Device Tree Source Include):类似C语言的头文件,用于公共部分的复用
- DTC(Device Tree Compiler):将DTS编译为二进制DTB的工具
- DTB(Device Tree Blob):二进制格式的设备树文件,由Bootloader加载并传递给内核
- OF(Open Firmware)API:内核中用于解析和操作设备树的接口
2. 设备树工作全流程解析
2.1 设备树生命周期全景图
设备树从编写到最终被内核使用的完整流程可以分为四个关键阶段:
- 编写阶段:工程师根据硬件实际情况编写.dts和.dtsi文件
- 编译阶段:使用DTC工具将文本格式的.dts编译为二进制.dtb
- 加载阶段:Bootloader(通常是U-Boot)将.dtb加载到内存并传递给内核
- 解析阶段:内核启动时解析设备树,构建内部数据结构供驱动使用
2.2 编写阶段详解
设备树源文件使用一种类似JSON的层级结构来描述硬件。一个典型的.dts文件结构如下:
dts复制/dts-v1/; // 指定设备树版本
#include "soc-base.dtsi" // 包含SoC公共定义
/ { // 根节点
model = "My Board Name"; // 板子名称
compatible = "vendor,board-name"; // 兼容性标识
memory@80000000 { // 内存节点
device_type = "memory";
reg = <0x80000000 0x20000000>; // 起始地址和大小
};
cpus { // CPU节点
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0>;
};
};
};
2.2.1 节点命名规则
设备树中的每个节点都遵循name@unit-address的命名规则:
name:描述节点功能,如i2c、gpio、uart等unit-address:通常是该设备寄存器空间的基地址
注意:
unit-address必须与节点中reg属性的第一个地址值保持一致,这是设备树的重要验证规则之一。
2.3 编译阶段详解
DTC编译器将人类可读的.dts文件转换为机器可读的.dtb文件。编译命令如下:
bash复制dtc -I dts -O dtb -o output.dtb input.dts
几个有用的编译选项:
-@:生成符号表,支持动态插件-W:开启额外警告检查-H:指定phandle的存储格式
排错技巧:当遇到设备树问题时,可以使用反编译命令查看实际生成的设备树内容:
bash复制dtc -I dtb -O dts -o debug.dts output.dtb
2.4 加载阶段详解
Bootloader(通常是U-Boot)负责将DTB文件加载到内存并传递给内核。典型的U-Boot命令序列:
bash复制# 设置DTB加载地址
setenv fdt_addr_r 0x08300000
# 从存储设备加载DTB
load mmc 0:1 ${fdt_addr_r} board.dtb
# 调整DTB大小(可选)
fdt addr ${fdt_addr_r}
fdt resize
# 启动内核并传递DTB
bootz ${kernel_addr_r} - ${fdt_addr_r}
关键点:
- DTB加载地址需要与内核配置匹配
- 确保DTB大小不超过预留内存区域
- 某些平台可能需要先应用设备树覆盖(DTBO)
2.5 内核解析阶段
内核在启动早期会解析设备树,主要过程包括:
- 解压DTB(如果使用压缩格式)
- 将DTB转换为
struct device_node的内部表示 - 根据
compatible属性匹配驱动程序 - 调用驱动程序的probe函数初始化设备
内核提供了完整的OF API供驱动程序访问设备树信息,常用的包括:
of_find_node_by_path():通过路径查找节点of_property_read_xxx():读取各种类型的属性of_get_named_gpio():获取GPIO编号of_irq_get():获取中断号
3. 设备树核心语法精讲
3.1 节点结构与属性
设备树的基本构建块是节点(Node),每个节点可以包含子节点和属性(Property)。一个完整的节点示例如下:
dts复制i2c1: i2c@40000000 {
compatible = "vendor,i2c-controller";
reg = <0x40000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
interrupts = <15 IRQ_TYPE_LEVEL_HIGH>;
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
};
};
3.1.1 常用属性解析
-
compatible:最重要的属性,用于驱动匹配
- 格式:"manufacturer,model"
- 可以有多个值,按优先级排列
-
reg:描述设备寄存器地址范围
- 格式:
<地址1 长度1 [地址2 长度2 ...]> - 具体解释依赖父节点的#address-cells和#size-cells
- 格式:
-
interrupts:描述中断信息
- 格式依赖中断控制器定义
- 通常包含中断号和触发方式
-
status:设备状态控制
- "okay":启用设备
- "disabled":禁用设备
- "reserved":保留给其他用途
3.2 标签与引用机制
设备树支持标签(Label)和引用(Reference)机制,提高了可维护性:
dts复制// 定义标签
i2c1: i2c@40000000 {
...
};
// 通过&引用
&i2c1 {
status = "okay";
};
实现原理:
- 标签在编译时会被转换为phandle(一个唯一的32位整数)
- 引用处会被替换为对应的phandle
- 内核通过phandle建立节点间的关联关系
3.3 地址与大小表示
设备树使用#address-cells和#size-cells来定义地址和长度的编码方式:
dts复制/ {
#address-cells = <2>; // 地址用2个32位数表示
#size-cells = <1>; // 大小用1个32位数表示
external-bus {
#address-cells = <1>;
#size-cells = <1>;
ethernet@0,0 {
reg = <0 0 0x1000>; // 父节点有2个address-cells
};
};
};
常见配置:
- 对于内存映射设备:通常
#address-cells = <1>,#size-cells = <1> - 对于无地址空间的设备(如I2C设备):
#address-cells = <1>,#size-cells = <0> - 64位系统:可能需要
#address-cells = <2>
4. 驱动与设备树的交互
4.1 compatible匹配机制
驱动通过of_device_id结构体声明自己支持的设备:
c复制static const struct of_device_id my_driver_ids[] = {
{ .compatible = "vendor,device-model" },
{ /* 哨兵元素 */ }
};
MODULE_DEVICE_TABLE(of, my_driver_ids);
内核启动时会遍历设备树中的所有节点,将节点的compatible属性与驱动注册的of_device_id表进行匹配。匹配成功后,内核会:
- 分配并初始化设备结构体
- 调用驱动的probe函数
- 将设备树节点绑定到设备
4.2 OF API详解
驱动中常用的设备树操作API包括:
4.2.1 基本属性读取
c复制// 读取u32属性
int of_property_read_u32(const struct device_node *np,
const char *propname, u32 *out_value);
// 读取字符串属性
const char *of_property_read_string(struct device_node *np,
const char *propname);
4.2.2 寄存器操作
c复制// 映射寄存器区域
void __iomem *of_iomap(struct device_node *np, int index);
// 获取寄存器地址和大小
int of_address_to_resource(struct device_node *np, int index,
struct resource *r);
4.2.3 中断处理
c复制// 获取中断号
int of_irq_get(struct device_node *np, int index);
// 解析中断标志
int of_irq_parse_one(struct device_node *np, int index,
struct of_phandle_args *out_irq);
4.2.4 GPIO操作
c复制// 获取GPIO编号
int of_get_named_gpio(struct device_node *np,
const char *propname, int index);
// 获取GPIO标志
int of_get_named_gpio_flags(struct device_node *np,
const char *propname, int index,
enum of_gpio_flags *flags);
4.3 典型驱动初始化流程
一个完整的设备树感知驱动通常遵循以下初始化流程:
c复制static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct resource *res;
void __iomem *base;
int irq, ret;
// 1. 获取寄存器资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 2. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 3. 获取GPIO
int reset_gpio = of_get_named_gpio(np, "reset-gpio", 0);
// 4. 获取时钟
struct clk *clk = devm_clk_get(&pdev->dev, "core");
if (IS_ERR(clk))
return PTR_ERR(clk);
// 5. 其他初始化...
return 0;
}
5. 设备树调试与排错指南
5.1 常见问题分类
根据实际项目经验,设备树相关的问题主要分为以下几类:
- 驱动未加载:probe函数没有被调用
- 资源获取失败:寄存器、中断、时钟等获取错误
- 硬件不工作:配置正确但硬件无响应
- 系统崩溃:设备树配置导致内核panic
5.2 系统级调试方法
5.2.1 确认DTB已加载
bash复制dmesg | grep -i "dtb\|device tree"
# 正常输出示例:
# [ 0.000000] OF: fdt: Machine model: My Board Name
5.2.2 检查设备树节点
bash复制# 查看所有设备树节点
ls /proc/device-tree/
# 查看特定节点属性
cat /proc/device-tree/i2c@40000000/compatible
5.2.3 反编译DTB
bash复制# 从运行中的系统获取DTB(如果支持)
cat /sys/firmware/fdt > running.dtb
dtc -I dtb -O dts running.dtb -o debug.dts
5.3 典型问题解决方案
5.3.1 Probe函数未调用
排查步骤:
- 确认DTB已正确加载
- 检查节点是否存在于/proc/device-tree/
- 确认compatible字符串完全匹配(包括大小写和标点)
- 检查驱动是否编译进内核或正确加载
常见错误:
dts复制// 错误:多了空格
compatible = "vendor, device";
// 错误:大小写不一致
compatible = "Vendor,Device";
// 正确
compatible = "vendor,device";
5.3.2 寄存器映射失败
排查步骤:
- 检查reg属性格式是否正确
- 确认父节点的#address-cells和#size-cells设置合理
- 验证物理地址是否正确
- 检查是否有其他驱动已经占用了该地址区域
示例调试:
c复制// 在驱动中添加调试打印
dev_info(&pdev->dev, "reg: %pr\n", res);
5.3.3 中断无法触发
排查步骤:
- 确认中断号是否正确获取
- 检查中断触发类型设置
- 验证中断控制器配置
- 使用示波器或逻辑分析仪确认硬件信号
关键检查点:
dts复制interrupts = <15 IRQ_TYPE_LEVEL_HIGH>; // 中断号+触发类型
interrupt-parent = <&intc>; // 指定中断控制器
5.4 高级调试技巧
5.4.1 设备树覆盖调试
对于复杂问题,可以动态修改设备树进行测试:
bash复制# 编译叠加层
dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
# 应用叠加层
mkdir /config/device-tree/overlays/my-overlay
cat overlay.dtbo > /config/device-tree/overlays/my-overlay/dtbo
5.4.2 内核调试选项
启用以下内核配置有助于设备树调试:
- CONFIG_DEBUG_DRIVER
- CONFIG_OF_DEBUG
- CONFIG_DYNAMIC_DEBUG
5.4.3 设备树可视化工具
bash复制# 生成PDF视图
dtc -I dts -O dot -o graph.dot input.dts
dot -Tpdf graph.dot -o output.pdf
6. 实战案例:RK3568 I2C设备添加
6.1 硬件环境说明
以Rockchip RK3568平台为例,添加一个I2C接口的温度传感器TMP75:
- I2C控制器:i2c1,地址0xfe5a0000
- 温度传感器:I2C地址0x48
- 中断:GPIO3_A5,低电平触发
6.2 设备树修改
编辑板级DTS文件(通常是arch/arm64/boot/dts/rockchip/rk3568-evb.dts):
dts复制&i2c1 {
status = "okay";
clock-frequency = <400000>; // 400kHz
tmp75@48 {
compatible = "ti,tmp75";
reg = <0x48>;
interrupt-parent = <&gpio3>;
interrupts = <5 IRQ_TYPE_LEVEL_LOW>;
ti,therm-limit = <80>; // 80°C报警阈值
};
};
6.3 驱动适配
确保内核已配置TMP75驱动:
bash复制make menuconfig
# Device Drivers -> Hardware Monitoring support -> Texas Instruments TMP75
或作为模块编译:
bash复制CONFIG_SENSORS_TMP75=m
6.4 验证步骤
- 编译并烧录新DTB
- 启动系统后检查I2C总线:
bash复制i2cdetect -y 1 # 扫描I2C1总线上的设备 - 验证传感器注册:
bash复制ls /sys/class/hwmon/hwmon*/name cat /sys/class/hwmon/hwmon0/temp1_input - 检查中断:
bash复制cat /proc/interrupts | grep tmp75
6.5 常见问题处理
问题1:传感器未出现在I2C总线上
- 检查I2C控制器是否已启用(status = "okay")
- 验证物理连接和电源
- 用示波器检查I2C信号
问题2:温度读数不正确
- 检查传感器配置寄存器
- 验证参考电压
- 检查是否有其他设备占用同一I2C地址
问题3:中断不触发
- 确认GPIO复用配置正确
- 检查中断触发类型设置
- 验证GPIO方向配置
7. 设备树最佳实践
7.1 代码组织建议
-
分层设计:
- SoC级定义放在.dtsi中
- 板级定制放在.dts中
- 设备特定配置放在覆盖层或板级文件中
-
合理使用include:
dts复制#include "soc-base.dtsi" #include "display.dtsi" -
模块化设计:
- 将相关设备分组管理
- 为可选项使用status属性
7.2 版本控制策略
-
DTS与硬件版本对应:
- 为每个硬件版本维护独立的DTS文件
- 使用兼容性字符串区分版本
-
变更记录:
- 在文件头添加修改历史
- 为重大变更添加注释说明
7.3 性能优化技巧
-
减少设备树大小:
- 移除未使用的节点
- 合并相同属性的节点
-
启动时间优化:
- 将关键设备放在前面
- 使用设备树插件延迟加载非关键设备
7.4 兼容性设计
-
向后兼容:
- 保留旧版兼容性字符串
- 使用新属性扩展功能
-
向前兼容:
- 避免依赖未标准化的属性
- 为未来扩展预留空间
8. 进阶主题与未来发展
8.1 设备树与ACPI比较
| 特性 | 设备树(DT) | ACPI |
|---|---|---|
| 适用平台 | 嵌入式系统 | x86服务器/PC |
| 描述方式 | 静态描述 | 动态AML字节码 |
| 复杂度 | 相对简单 | 非常复杂 |
| 运行时修改 | 有限支持(覆盖层) | 完全支持 |
| 工具链 | DTC | ASL编译器 |
8.2 设备树覆盖层技术
设备树覆盖层(Overlay)允许在运行时动态修改设备树,适用于:
- 插件式硬件模块
- 开发调试阶段快速迭代
- 系统配置动态调整
典型实现方式:
bash复制# 加载覆盖层
echo overlay.dtbo > /sys/kernel/config/device-tree/overlays/path/load
# 卸载覆盖层
echo 0 > /sys/kernel/config/device-tree/overlays/path/status
8.3 设备树标准化进展
设备树规范正在不断演进,主要方向包括:
- Schema验证:使用JSON Schema验证DTS文件
bash复制
dt-validate input.dts schema.yaml - 高级绑定:更丰富的元数据描述
- 安全扩展:设备树签名与验证
8.4 设备树在非ARM平台的应用
虽然设备树起源于ARM平台,但现在已扩展到:
- RISC-V:几乎所有实现都采用设备树
- MIPS:部分现代实现支持
- PowerPC:与Open Firmware传统结合
- x86:有限场景下使用
9. 开发工具链推荐
9.1 编辑器支持
-
VS Code:
- Device Tree插件提供语法高亮
- 代码片段自动完成
-
Vim/Emacs:
- dts语法高亮脚本
- ctags支持
9.2 图形化工具
- dtviz:设备树可视化工具
bash复制
dtviz input.dts -o output.pdf - Eclipse插件:集成DTC编译和调试
9.3 调试工具
- fdtdump:快速查看DTB结构
bash复制
fdtdump board.dtb - dtc diff:比较两个DTB文件的差异
bash复制
dtc-diff old.dtb new.dtb
9.4 自动化测试
- dt-unit-test:设备树单元测试框架
- CI集成:将DTC编译作为构建流程的一部分
10. 个人经验与心得分享
在实际项目中应用设备树多年,我总结了以下宝贵经验:
-
版本控制至关重要:
- 将DTS与硬件版本严格对应
- 每次硬件变更都提交对应的DTS修改
-
注释是好朋友:
dts复制/* * 这个配置针对RevB硬件版本 * RevA使用reg = <0x0 0x1000> * 修改者:张三 2023-05-01 */ reg = <0x0 0x2000>; -
测试策略:
- 新DTS先在模拟器上测试
- 使用设备树覆盖层进行增量测试
- 保留已知良好的DTB备份
-
性能考量:
- 大型设备树会影响启动时间
- 考虑将非关键设备移到覆盖层
-
团队协作:
- 建立DTS编写规范
- 进行代码审查
- 维护公共dtsi库
-
调试技巧:
- 遇到问题时首先反编译DTB
- 使用
/proc/device-tree进行运行时检查 - 逐步简化设备树定位问题
-
未来展望:
- 关注设备树Schema验证发展
- 评估设备树覆盖层在生产环境的应用
- 跟踪RISC-V等新架构的设备树实践
设备树作为嵌入式Linux的核心技术,掌握它不仅能够解决当下的开发问题,更能为未来的技术演进做好准备。希望这篇指南能帮助你少走弯路,在嵌入式开发的道路上更加得心应手。