设备树入门:DTS / DTC / DTB 关系与核心作用
作者:黒漂技术佬 | 系列:Linux内核配置与移植
引言
如果你是从单片机(STM32、ESP32)转到嵌入式 Linux 的,第一次看到设备树(Device Tree)可能会觉得这是一门"玄学"——一个文本文件,写着一些奇怪的语法,编译成一个二进制文件,传给内核,硬件就能工作了。没有寄存器地址,没有#define,甚至没有一个main函数,凭啥?本文就从零讲清楚设备树是什么、为什么要有它、以及 DTS / DTC / DTB 三兄弟是怎么配合工作的。
一、为什么需要设备树?
1.1 没有设备树之前——板级代码的噩梦
在设备树出现之前(Linux 2.6 时代),ARM 平台的硬件信息是硬编码在内核arch/arm/mach-xxx/中的。每增加一块新板子,就得写几百上千行板级支持代码:
// arch/arm/mach-xxx/board-xxx.c —— 老时代的做法staticstructresourceuart_resources[]={[0]={.start=0x40002000,// 寄存器基地址写死在代码里.end=0x40002FFF,.flags=IORESOURCE_MEM,},// ... 几十行平台设备定义};staticstructplatform_deviceuart_device={.name="serial8250",.id=0,.num_resources=ARRAY_SIZE(uart_resources),.resource=uart_resources,};这种方式的痛点:
- 内核臃肿:每种板子一套代码,ARM 内核中板级代码一度达到数十万行
- 不通用:换个外设就得改内核、重新编译——哪怕只换了 GPIO 引脚
- 难维护:硬件信息散落在 C 代码中,和驱动逻辑混在一起
Linus 本人对此非常不满,2011 年曾在邮件列表里怒斥 ARM 社区的板级代码是"一坨垃圾"(原文更劲爆)。此后 ARM 社区全面转向设备树。
1.2 设备树的解决思路
核心思想:把硬件描述从内核代码中分离出来。
[老时代] 内核代码 = 驱动逻辑 + 硬件信息 → 改了硬件就得改内核 [设备树时代] 内核代码 = 驱动逻辑 → 不变 DTB文件 = 硬件信息 → 随板子变化设备树本质上是一棵描述硬件的树状数据结构,它告诉内核:
- 有哪些外设(UART、GPIO、I2C、SPI 等)
- 这些外设的寄存器地址是多少
- 用哪个引脚、哪个中断号
- 和哪个驱动绑定
二、DTS / DTC / DTB 三兄弟关系
设备树涉及到三种文件/工具,新手最容易混淆的就是它们之间的关系:
DTS(源文件) DTC(编译器) DTB(二进制) ↓ ↓ ↓ .dts / .dtsi ────→ dtc ──────→ .dtb (文本格式) (编译工具) (二进制) (人读写) (软件) (内核读取)| 名词 | 全称 | 格式 | 谁用 | 说明 |
|---|---|---|---|---|
| DTS | Device Tree Source | 文本 | 开发者编写 | 类似 C 源码,描述硬件配置 |
| DTSI | Device Tree Source Include | 文本 | 被 DTS include | 类似 C 头文件,SoC 级公共描述 |
| DTC | Device Tree Compiler | 可执行工具 | 编译过程自动调用 | 将 DTS/DTSI 编译成 DTB |
| DTB | Device Tree Blob | 二进制 | U-Boot 加载给内核 | 内核运行时解析的实际文件 |
转换命令:
# 从 DTS 编译 DTB(通常 make dtbs 自动完成)scripts/dtc/dtc-Idts-Odtb-ooutput.dtb input.dts# 从 DTB 反编译回 DTS(调试用)scripts/dtc/dtc-Idtb-Odts-ooutput.dts input.dtb三、DTS 文件结构初探
打开一个典型的.dts文件(以versatile-pb.dts为例):
/dts-v1/; #include "versatile-ab.dts" / { model = "ARM Versatile PB"; compatible = "arm,versatile-pb"; memory { reg = <0x00000000 0x08000000>; /* 128MB RAM */ }; chosen { bootargs = "console=ttyAMA0,115200"; }; /* 引用 AB 文件中定义的节点并追加属性 */ &amba { uart@101f1000 { status = "okay"; }; }; };DTS 的树状结构用树形图表达:
/ (根节点) ├── model = "ARM Versatile PB" ├── compatible = "arm,versatile-pb" ├── memory@0 │ └── reg = <0x00000000 0x08000000> ├── chosen │ └── bootargs = "console=ttyAMA0,115200" └── amba (引用节点) └── uart@101f1000 └── status = "okay"DTS 的层叠复用机制(.dts + .dtsi):
SoC级 dtsi (xxx-soc.dtsi) ← 定义芯片所有外设 ↓ #include 板级 dts (xxx-board.dts) ← 使能需要的设备 + 修改引脚.dtsi是硅片厂商提供的,描述芯片本身有哪些硬件资源(不管你是否使用)。.dts是板级开发者写的,决定哪些设备启用、引脚怎么接。
四、设备树编译过程
内核编译时,设备树编译是自动触发的:
makeARCH=armCROSS_COMPILE=arm-linux-gnueabihf- dtbs编译流程:
.dts 源文件 │ ├── C预处理器 (#include / #define) │ ├── dtc 编译器 │ ├── 语法检查 │ ├── 语义检查 (compatible 字符串规范性等) │ └── 输出平坦设备树 (Flattened Device Tree) │ └── .dtb 二进制文件dtc 源码就在内核里:scripts/dtc/目录。执行make scripts会先编译 dtc,然后再用 dtc 编译设备树。
五、设备树如何被内核使用
启动流程中的设备树链路:
U-Boot │ ├── 将 zImage 加载到内存 ├── 将 .dtb 加载到内存 ├── 设置寄存器 r2 = dtb 地址 (ARM32 传参约定) └── 跳转到 zImage 入口 │ ├── 自解压代码 ├── 内核初始化() ├── setup_arch() │ └── 解析 DTB → 生成 platform_device │ ├── 各驱动 probe() │ └── 匹配 compatible 字符串 │ └── 读取 reg/interrupts/gpios 等属性 │ └── 硬件开始工作关键点:
- DTB 在启动时由 Bootloader 传给内核
- 内核解析后生成
platform_device结构,驱动通过compatible字符串匹配 - 这个过程是完全自动的,你不需要写一行 C 代码来描述硬件
六、设备树 vs 传统板级代码对比
| 对比维度 | 传统板级代码 | 设备树 |
|---|---|---|
| 硬件描述位置 | 内核 C 代码中 | 独立的 .dts 文件 |
| 修改后是否需要重编内核 | 是 | 只需要重编 DTB |
| 同一内核支持多板卡 | 需要重新编译 | 换 DTB 即可 |
| 代码可读性 | 差(C和宏混一起) | 好(树状结构直观) |
| 厂商BSP交付方式 | 内核补丁 | .dtsi 文件 |
| 社区认可度 | 已被抛弃 | 当前唯一标准 |
总结
设备树的核心思想就是硬件描述与内核代码解耦。DTS 是你写的硬件说明书(文本),DTC 是翻译官(工具),DTB 是翻译后的精简版说明书(二进制),最终由 Bootloader 交到内核手里。理解了这个三件套的分工,后面学习节点编写就水到渠成了。