1. 设备树基础概念:什么是设备树、为什么需要设备树、设备树与ACPI的区别
大家好,我是老李。做嵌入式Linux开发这么多年,我见过太多工程师在设备树这块栽跟头。今天咱们就来聊聊设备树最基础的东西。别小看这些概念,理解透了,后面移植起来才顺手。
1.1 什么是设备树
设备树,英文叫Device Tree,说白了就是一个描述硬件信息的文本文件。它用树形结构把CPU、内存、外设这些硬件资源组织起来。嗯,你可以把它想象成一张硬件配置清单。
举个例子,一个简单的设备树节点长这样:
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
leds {
pinctrl-names = "default";
pinctrl-0 = <&user_led_pins>;
led@2 {
label = "beaglebone:green:heartbeat";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
};
看到没?根节点"/"下面挂了memory节点和leds节点。每个节点都有属性,比如compatible、reg这些。内核启动时,就靠这些信息来知道板子上有什么硬件。
核心要点:设备树是描述硬件的结构化数据,不是代码。它告诉内核"我有什么硬件",而不是"怎么驱动硬件"。
1.2 为什么需要设备树
这个问题我经常被问到。其实原因很简单——历史教训。
在设备树出现之前,Linux内核是怎么处理不同板子的?每个板子都要写一个board文件,里面全是平台设备注册代码。我当年做ARM开发时,内核源码里充斥着arch/arm/mach-xxx/board-xxx.c这样的文件。一个芯片厂商出个新板子,就得加一个新文件。内核维护者都快疯了。
为什么会这样?因为ARM架构不像x86那样有标准化的硬件发现机制。x86有PCIe枚举、有ACPI,操作系统可以主动扫描硬件。但ARM不行,每个SoC的外设地址、中断号都不一样,没法自动发现。
设备树解决了这个问题:
- 硬件描述和内核代码分离:换板子只需要改.dts文件,不用改内核代码
- 减少内核源码膨胀:不再需要成百上千的board文件
- 支持多平台统一:ARM、PowerPC、RISC-V都用同一套机制
- 启动参数灵活:内存大小、时钟频率这些可以在设备树里配
我的经验:我在一个项目里遇到过,客户换了颗DDR颗粒,内存从512MB变成1GB。要是以前,得改内核代码重新编译。有了设备树,我直接在.dts里把reg属性改一下,5分钟搞定。这就是设备树的价值。
1.3 设备树与ACPI的区别
很多从x86转过来的朋友会问:设备树和ACPI到底啥区别?
咱们直接看对比:
| 对比项 | 设备树 (DT) | ACPI |
|---|---|---|
| 起源 | Open Firmware,用于嵌入式 | Intel/Microsoft/Toshiba,用于PC |
| 格式 | 文本文件(.dts),编译成二进制(.dtb) | AML字节码,存储在BIOS/UEFI中 |
| 运行时 | 启动时一次性解析,之后固定 | 运行时可以动态修改(热插拔、电源管理) |
| 硬件发现 | 静态描述,需要提前知道硬件配置 | 动态枚举,支持PCIe/USB等总线扫描 |
| 电源管理 | 简单,主要通过pinctrl和clock控制 | 复杂,支持C-states、P-states、热管理 |
| 适用场景 | 嵌入式、IoT、ARM/RISC-V | x86 PC、服务器、笔记本 |
说白了,ACPI是个"活"的规范,它能在系统运行时动态管理硬件。比如你插个USB设备,ACPI能自动处理电源和配置。设备树是个"死"的配置文件,启动时读一次就完事了。
注意:别以为设备树比ACPI差。它们设计目标不同。嵌入式系统硬件固定,不需要动态管理,设备树轻量高效。你想想看,一个MCU上跑Linux,哪来的BIOS给你存ACPI表?
我记得有个项目,客户非要在ARM板子上跑ACPI。折腾了两周,最后发现ACPI需要UEFI支持,而他们的bootloader连UEFI是啥都不知道。最后还是老老实实用设备树。
1.4 设备树文件体系
实际项目中,设备树文件分三层:
- .dtsi:SoC级描述文件,芯片厂商提供。比如中断控制器、时钟、GPIO这些通用外设。
- .dts:板级描述文件,板卡厂商写。继承.dtsi,加上板子特有的外设。
- .dtb:编译后的二进制文件,bootloader加载给内核。
举个例子,BeagleBone Black的文件结构:
am33xx.dtsi # TI AM335x SoC通用描述
am335x-bone.dtsi # BeagleBone通用板级描述
am335x-boneblack.dts # BeagleBone Black具体配置
编译命令也很简单:
# 把.dts编译成.dtb
dtc -I dts -O dtb -o am335x-boneblack.dtb am335x-boneblack.dts
# 反编译查看
dtc -I dtb -O dts -o am335x-boneblack.dts am335x-boneblack.dtb
小技巧:我习惯在调试时用dtc的反编译功能。有时候设备树加载不对,反编译看看实际解析成啥样,问题一目了然。
1.5 设备树在启动流程中的位置
搞懂设备树,还得知道它在系统启动时怎么工作的。大致流程:
- Bootloader(如U-Boot)从存储介质加载.dtb文件到内存
- Bootloader把.dtb地址传给内核(通过r2寄存器或设备树地址参数)
- 内核启动早期,解压设备树,建立内存映射
- 内核遍历设备树节点,为每个compatible匹配的节点创建platform_device
- 驱动probe时,从设备树节点读取reg、interrupts等资源信息
嗯,这里要注意:设备树解析是在内核启动早期完成的,比init进程启动还早。所以如果设备树写错了,你连内核日志都看不到,直接死机。
我曾经犯过一个低级错误——把reg属性里的地址写成了十进制。内核解析时按十六进制处理,结果外设地址全错了。查了两天才发现,从那以后我写设备树地址都加0x前缀,养成习惯了。
好了,这一章就到这里。设备树的概念是基础,后面我们会一步步深入,从语法到实战移植,把这块彻底搞透。