1. 设备树基础回顾:什么是设备树、为什么需要设备树、设备树文件结构
各位同学,咱们今天聊聊设备树。说实话,我刚入行那会儿,设备树还是个挺新鲜的东西。那时候做嵌入式,板级文件里全是硬编码的寄存器地址、中断号,改个引脚配置得翻半天代码。后来设备树一普及,整个世界都清爽了。
1.1 什么是设备树?
设备树,英文叫 Device Tree,简称 DT。说白了,它就是一个描述硬件信息的“数据文件”。你想想看,一个嵌入式板子上有 CPU、内存、I2C 控制器、SPI 控制器、GPIO 引脚……这些东西怎么告诉内核?
传统做法是在内核源码里写一堆 C 结构体,把硬件信息硬编码进去。但这样有个问题——换一块板子,就得重新编译内核。设备树把硬件描述从内核代码里剥离出来,变成一份独立的数据文件。内核启动时解析这个文件,就知道自己跑在什么硬件上了。
我在项目中遇到过最典型的场景:同一款 SoC,用在三款不同的产品上。外设引脚分配、内存大小、时钟频率都不一样。如果没有设备树,我得维护三份内核代码。有了设备树,内核镜像完全一样,只换 .dtb 文件就行。嗯,这就是设备树的价值。
1.2 为什么需要设备树?
咱们从几个角度来理解这个问题。
- 解耦硬件与驱动:驱动代码不再关心“我的 I2C 控制器挂在哪个地址”,而是通过设备树节点去匹配。驱动写一次,换个板子只要改设备树。
- 减少内核分支:以前每个板子都要维护 arch/arm/mach-xxx 下的板级文件,代码量巨大。设备树统一了接口,内核主线更容易支持多种硬件。
- 动态配置:有些硬件参数(比如内存大小、MAC 地址)可以在启动时由 bootloader 修改设备树,内核不用重新编译。
- 社区标准:现在 Linux 内核社区已经全面拥抱设备树,新架构(ARM64、RISC-V)基本都靠它。
核心思想:设备树是硬件描述与驱动代码之间的“契约”。驱动通过设备树节点名称和属性来匹配硬件,而不是靠硬编码的地址和中断号。
1.3 设备树文件结构
设备树涉及三种文件格式:.dts、.dtsi、.dtb。我刚开始学的时候也搞混过,咱们一个一个说清楚。
1.3.1 .dts 文件
.dts 是 Device Tree Source 的缩写,也就是设备树源文件。它是文本文件,用人类可读的格式描述硬件。一个典型的 .dts 文件长这样:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyBoard";
compatible = "mycompany,imx6ull-board";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
&uart1 {
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
};
注意看,第一行 /dts-v1/; 是版本声明,表示使用设备树 v1 格式。后面的 #include 可以包含头文件或 .dtsi 文件。根节点 / 下面定义板级信息,比如型号、内存布局。引用其他节点用 &uart1 这种语法,可以覆盖或追加属性。
个人习惯:我写 .dts 文件时,喜欢把板级特有的配置(比如引脚复用、外设使能)放在 .dts 里,把 SoC 通用的部分放在 .dtsi 里。这样复用性最好。
1.3.2 .dtsi 文件
.dtsi 是 Device Tree Source Include 的缩写,相当于设备树的“头文件”。它通常描述 SoC 内部的外设框架,比如 CPU 核心、中断控制器、时钟、GPIO 控制器等。这些信息对同一款 SoC 的所有板子都是通用的。
举个例子,imx6ull.dtsi 里会定义:
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1>;
status = "disabled";
};
这里定义了 UART1 的寄存器基地址、中断号、时钟信息,但默认状态是 disabled。板级 .dts 文件里通过 &uart1 { status = "okay"; }; 来使能它。
我曾经踩过的坑:有一次我在 .dtsi 里把某个外设的 status 写成了 "okay",结果所有用这个 SoC 的板子都默认打开了这个外设。有些板子根本没接这个外设,导致内核启动时访问不存在的硬件,直接挂掉。所以,.dtsi 里尽量保持 status = "disabled",让板级文件去决定是否使能。
1.3.3 .dtb 文件
.dtb 是 Device Tree Blob 的缩写,也就是设备树的二进制文件。它是 .dts 经过编译后的产物,内核启动时直接解析这个二进制数据。
编译命令很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
反过来,你也可以把 .dtb 反编译成 .dts 来查看:
dtc -I dtb -O dts -o myboard.dts myboard.dtb
这个技巧我在调试时经常用。有时候拿到一个现成的 .dtb 文件,想知道它里面定义了哪些硬件,反编译一下一目了然。
1.4 设备树的核心语法速览
咱们快速过一下设备树的基本语法,后面调试时会频繁用到。
| 语法元素 | 说明 | 示例 |
|---|---|---|
| 节点 | 用 node-name@address 表示 |
uart1: serial@02020000 |
| 属性 | key = value; 形式 |
reg = <0x02020000 0x4000>; |
| 标签 | 节点前面的别名,用于引用 | uart1: |
| 引用 | 用 &label 引用节点 |
&uart1 { status = "okay"; }; |
| 包含 | #include "file" |
#include "imx6ull.dtsi" |
| 宏定义 | 配合 #define 使用 |
#define GPIO_ACTIVE_HIGH 0 |
1.5 设备树知识体系结构图
下面这张图总结了设备树的核心知识体系,方便你建立整体认知:
1.6 小结
设备树不是什么高深莫测的东西。它就是一份硬件配置清单,告诉内核:“嘿,我这块板子上有什么外设,它们挂在哪个地址,用哪个中断。” 你只要把这份清单写对,驱动就能自动匹配上。
我个人建议初学者先别急着背语法,而是多看看现成的 .dts 文件。内核源码里 arch/arm/boot/dts/ 目录下有成百上千个例子,随便挑一个跟自己板子相近的,对照着看,很快就能上手。
一个小技巧:调试时如果发现某个外设不工作,先用 dtc -I dtb -O dts 反编译 .dtb,看看设备树里这个节点的 status 是不是 "okay",reg 地址对不对。我遇到过好几次,问题就出在设备树里某个属性写错了。
好了,设备树基础就回顾到这里。记住三个关键词:.dts 是源码,.dtsi 是共用头文件,.dtb 是内核吃的东西。后面咱们会深入调试技巧,到时候这些基础概念会反复用到。