4. 硬件适配基础:设备树(Device Tree)概念、语法与U-Boot关系
做嵌入式Linux移植,绕不开设备树。说白点,设备树就是一套描述硬件信息的“数据表格”。它告诉内核:CPU是什么型号、内存多大、外设挂在哪条总线上、中断怎么接。没有它,内核就是个瞎子。
我刚开始接触Linux移植那会儿,还是用老式的板级文件(board-xxx.c)。每个板子都要写一堆C代码来注册设备。后来设备树一普及,整个人都清爽了。嗯,咱们今天就把设备树这层窗户纸捅破。
4.1 设备树是什么?为什么需要它?
设备树(Device Tree,简称DT)是一种描述硬件资源的数据结构。它用树形节点表示硬件拓扑,每个节点包含若干属性(键值对)。
举个例子,一颗SoC内部有CPU、内存控制器、GPIO、UART、I2C等模块。设备树就把这些模块组织成一颗树:根节点是“/”,下面挂“cpus”、“memory”、“soc”等子节点,再往下是具体的外设。
核心思想:硬件描述与内核代码分离。同一份内核镜像,换一块板子只需要换一个.dtb文件,不用重新编译内核。
为什么非要用设备树?我总结三个理由:
- 解耦:内核不再硬编码硬件信息,板级差异全部放到.dts文件中
- 标准化:ARM、RISC-V、x86都在用,社区统一规范
- 可维护:加个新外设,改几行.dts就行,不用动内核源码
我在项目中遇到过一块板子,原厂给的BSP里设备树写错了GPIO中断号。排查了三天,最后发现是.dts里interrupts属性少写了一个cell。从那以后,我拿到新板子第一件事就是对着原理图逐行核对设备树。
4.2 设备树语法基础
设备树源文件(.dts)是给人看的文本文件,经过dtc编译器生成二进制.dtb文件。语法其实不复杂,咱们直接看例子。
4.2.1 基本结构
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
device_type = "cpu";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
};
};
看到没?根节点“/”下面挂cpus、memory、uart0。每个节点用花括号包裹,属性用“key = value;”表示。字符串用双引号,32位整数用尖括号,64位用方括号。
4.2.2 常用属性
| 属性名 | 含义 | 示例 |
|---|---|---|
| compatible | 设备兼容性字符串,驱动匹配依据 | "ti,am335x-uart" |
| reg | 地址/长度对,描述寄存器或内存范围 | <0x44e09000 0x2000> |
| interrupts | 中断号、触发类型 | <0x1c 0x4> |
| status | 设备状态:"okay"、"disabled" | "okay" |
| pinctrl-* | 引脚复用配置 | <&uart0_pins> |
| clocks | 时钟源引用 | <&clk_uart0> |
避坑指南:我曾经在reg属性里写反了地址和长度,导致内核访问外设寄存器时直接挂死。记住:reg = <基址 长度>,长度单位是字节。
4.2.3 引用与覆盖
设备树支持通过标签(label)引用节点,然后在其他地方覆盖属性。这个特性非常实用。
// 在.dtsi中定义
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
// 在.dts中覆盖
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
这样设计的好处是:SoC厂商提供.dtsi文件描述芯片内部资源,板级厂商在.dts里只覆盖需要修改的部分。代码复用率极高。
4.3 设备树与U-Boot的关系
U-Boot和设备树的关系,说白了就是“搬运工”和“说明书”。U-Boot负责把.dtb文件从存储介质加载到内存,然后传递给内核。
4.3.1 传递流程
- 加载:U-Boot从eMMC、SD卡、网络等位置读取.dtb文件到内存
- 修改:U-Boot可以根据环境变量或fdt命令修改设备树(比如调整内存大小、使能/禁用外设)
- 传递:启动内核时,U-Boot把.dtb地址通过寄存器r2(ARM)或a1(RISC-V)传给内核
注意:U-Boot和内核使用的设备树必须匹配。版本不一致可能导致外设无法识别,甚至启动失败。我见过有人拿Linux 4.19的设备树去启动Linux 5.10的内核,结果网卡驱动直接罢工。
4.3.2 U-Boot中的fdt命令
U-Boot提供了一套fdt命令,可以在启动前动态调整设备树。这个功能在调试时特别有用。
# 加载设备树到内存
tftp 0x82000000 myboard.dtb
# 设置fdt地址
fdt addr 0x82000000
# 查看节点
fdt list /soc
# 修改属性
fdt set /soc/mmc@480b4000 status "disabled"
# 添加新节点
fdt mknode /soc mydevice
fdt set /soc/mydevice compatible "my,device"
fdt set /soc/mydevice reg <0x48000000 0x1000>
# 保存修改
fdt boardsetup
我个人习惯在调试新板子时,先用fdt命令临时禁用有问题的外设,等驱动调通了再改.dts源文件。这样不用反复烧写镜像,效率高很多。
4.3.3 设备树在U-Boot中的位置
U-Boot本身也使用设备树。从2016年开始,U-Boot逐步迁移到设备树驱动模型(DM)。现在U-Boot的板级配置、外设驱动都依赖设备树。
U-Boot的设备树和内核的设备树通常是同一份,或者内核设备树的子集。U-Boot只关心启动阶段需要的外设(串口、MMC、网络等),其他外设可以裁剪掉。
关键点:U-Boot编译时会把设备树二进制(.dtb)嵌入到u-boot.bin中,或者单独存放。启动时U-Boot从存储介质加载.dtb,然后传递给内核。内核启动后,U-Boot就完成了它的使命。
4.4 设备树知识体系
下面这张图展示了设备树在嵌入式系统中的核心位置和流转关系。
4.5 实战建议
学设备树,光看语法没用。我建议你拿一块开发板,对着原理图和芯片手册,自己写一份.dts文件。从最简单的串口开始,逐步添加GPIO、I2C、SPI、网卡。每加一个外设,就编译、烧写、验证一次。
遇到问题怎么办?
- 用
dtc -I dtb -O dts反编译.dtb,看看实际生效的设备树长什么样 - 在内核启动参数加
earlycon和console=ttyS0,看设备树解析日志 - 在U-Boot里用
fdt list逐级检查节点属性
踩坑提醒:我曾经在.dts里写错了#address-cells和#size-cells的值,导致reg属性解析出来的地址完全不对。这两个属性决定了reg里每个地址/长度占几个32位单元。ARM64通常用<2>表示64位地址,ARM32用<1>。搞混了,内核读到的寄存器地址就是错的。
设备树说难不难,说简单也不简单。核心就三点:树形结构、键值对属性、引用覆盖。把这三点吃透了,剩下的就是对着手册填参数。嗯,动手写几遍就熟了。