在嵌入式Linux开发中,硬件描述一直是个令人头疼的问题。想象一下,你正在为一个基于STM32MP157的开发板编写驱动,突然发现需要修改某个GPIO的配置。传统方式下,你不得不深入内核源码的板级文件迷宫,在arch/arm/mach-xxx目录下寻找对应的硬编码定义——这种体验就像在黑暗房间里摸索电灯开关。
设备树(Device Tree)的出现彻底改变了这一局面。它像一份标准化的硬件"说明书",将硬件配置从内核中抽离出来,用结构化的文本文件(.dts)描述,最终编译成二进制格式(.dtb)供内核使用。这种转变不仅让硬件描述更清晰,还大幅提升了代码的可维护性和可移植性。
在设备树普及之前,ARM架构的Linux内核充斥着大量板级硬编码信息。以i.MX6ULL处理器为例,同一个芯片可能用在几十种不同的开发板上,每种开发板的外设配置都不尽相同。这导致内核源码中出现了大量类似mach-imx6ull-xxx.c的文件,每个文件都包含特定开发板的:
这种架构带来了三个主要问题:
设备树通过声明式描述解决了这些问题。下表展示了两种方式的本质区别:
| 特性 | 传统硬编码方式 | 设备树方式 |
|---|---|---|
| 硬件描述位置 | 内核源码中的C文件 | 独立的.dts文本文件 |
| 修改硬件配置 | 需要重新编译内核 | 只需替换.dtb文件 |
| 代码复用 | 难以复用 | 通过.dtsi头文件实现层次化复用 |
| 可读性 | 需要理解C代码逻辑 | 直观的硬件描述语法 |
| 启动流程 | 内核直接读取硬编码配置 | Bootloader传递.dtb给内核 |
提示:设备树特别适合需要频繁调整硬件配置或支持多款板卡的场景,比如工业控制领域的定制化设备。
一个完整的设备树生态包含以下关键组件:
DTS (Device Tree Source)
人类可读的文本文件,描述硬件拓扑结构。例如:
dts复制/ {
compatible = "st,stm32mp157c-dk2";
model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
memory@c0000000 {
device_type = "memory";
reg = <0xc0000000 0x20000000>;
};
};
DTC (Device Tree Compiler)
将.dts编译为.dtb的工具,通常位于Linux源码的scripts/dtc目录。
DTB (Device Tree Blob)
二进制格式的设备树,由Bootloader加载到内存并传递给内核。
DTSI (Device Tree Include)
类似C语言的头文件,用于存放可复用的公共定义。例如SoC级别的外设配置:
dts复制/* stm32mp157c.dtsi */
&usart1 {
pinctrl-names = "default";
pinctrl-0 = <&usart1_pins_a>;
};
设备树的基本构建块是节点和属性。以下是一个典型节点结构:
dts复制node-name@unit-address {
property1 = value;
property2 = <value1 value2>;
child-node {
/* 子节点定义 */
};
};
常见属性类型说明:
| 属性格式 | 示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "st,stm32"; |
双引号包裹的字符串 |
| 32位整数数组 | reg = <0x40000000 0x1000>; |
尖括号包裹的十六进制数 |
| 二进制数据 | data = [00 01 0a ff]; |
方括号包裹的十六进制字节 |
| 字符串列表 | pinctrl-names = "default", "sleep"; |
逗号分隔的多字符串 |
以STM32MP157开发板为例,你需要:
获取Linux内核源码(建议使用对应版本的稳定分支):
bash复制git clone https://github.com/STMicroelectronics/linux.git -b v5.10-stm32mp
安装设备树编译器:
bash复制sudo apt-get install device-tree-compiler
确认dts文件位置:
code复制linux/arch/arm/boot/dts/
├── stm32mp157c-dk2.dts
├── stm32mp157c.dtsi
└── stm32mp15-pinctrl.dtsi
假设我们要为一个自定义板添加LED控制,步骤如下:
创建基础板级DTS文件(my-board.dts):
dts复制// 包含SoC级定义
#include "stm32mp157c.dtsi"
/ {
model = "My Custom Board";
compatible = "my,board", "st,stm32mp157";
// 内存配置
memory@c0000000 {
device_type = "memory";
reg = <0xc0000000 0x20000000>;
};
// LED节点定义
leds {
compatible = "gpio-leds";
led-red {
label = "red";
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
};
};
};
添加引脚控制定义(参考现有pinctrl文件):
dts复制&gpioa {
led_pins: led-pins {
pins = "PA5";
function = "gpio";
bias-pull-up;
};
};
&leds {
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
};
编译设备树:
bash复制make dtbs DTBS=my-board.dtb
当设备树配置不当时,可能会遇到以下问题:
内核无法启动
reg属性是否正确compatible字符串与驱动匹配外设无法工作
fdtdump工具验证dtb内容:bash复制fdtdump my-board.dtb | less
debug参数查看初始化日志引脚冲突
良好的设备树应该像积木一样可组合。推荐的结构如下:
code复制stm32mp157c.dtsi # SoC级定义
├── stm32mp15-pinctrl.dtsi # 引脚控制
└── my-board.dts # 板级定制
├── my-board-lcd.dtsi # LCD模块
└── my-board-sensors.dtsi # 传感器模块
使用#include指令组织层次结构:
dts复制// my-board.dts
#include "stm32mp157c.dtsi"
#include "my-board-lcd.dtsi"
/* 板级覆盖定义 */
&i2c1 {
touchscreen@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
};
};
设备树节点通过compatible属性匹配驱动。例如以下节点:
dts复制&usart2 {
compatible = "st,stm32h7-uart";
pinctrl-names = "default";
pinctrl-0 = <&usart2_pins>;
status = "okay";
};
对应驱动中需要通过of_match_table声明匹配:
c复制static const struct of_device_id stm32_uart_of_match[] = {
{ .compatible = "st,stm32h7-uart" },
{}
};
MODULE_DEVICE_TABLE(of, stm32_uart_of_match);
查看已加载的设备树
bash复制cat /proc/device-tree/model
提取设备树属性
c复制struct device_node *np = of_find_node_by_path("/leds/led-red");
of_property_read_string(np, "label", &led_name);
覆盖设备树参数
在U-Boot中可动态修改:
code复制setenv fdt_overlays my-custom.dtbo
在实际项目中,设备树的调试往往占用了大量时间。记得每次修改后都要验证:
dtc -I dtb -O dts my-board.dtb)