4. 设备树(Device Tree)详解:从语法到实战
设备树,说白了就是一块“硬件描述板”。它告诉内核:你的板子上有哪些外设、它们挂在哪条总线上、中断怎么连、GPIO怎么用。没有它,Linux内核就得靠一堆硬编码的板级文件来认硬件——那维护起来简直是噩梦。
我最早接触设备树是在做一款i.MX6的板子。当时从厂商拿到的BSP里,设备树文件乱成一团,一个dts包含七八个dtsi,层层嵌套。我花了整整两天才理清楚哪个节点是哪个外设。从那以后,我养成了一个习惯:拿到一块新板子,第一件事就是打开它的设备树,从头到尾读一遍。
4.1 设备树文件体系:DTS、DTSI、DTB
设备树源文件有三种形态,它们的关系其实很简单:
| 后缀 | 全称 | 作用 | 谁生成 |
|---|---|---|---|
| .dts | Device Tree Source | 板级设备树源文件,描述整块板子 | 开发者编写 |
| .dtsi | Device Tree Source Include | 公共设备树头文件,被dts包含 | 芯片厂商提供 |
| .dtb | Device Tree Blob | 编译后的二进制文件,内核直接使用 | dtc编译生成 |
你可以把 .dtsi 想象成C语言里的头文件。芯片厂商会把SoC的公共部分——比如CPU核心、中断控制器、时钟、DMA——都放在dtsi里。板级开发者只需要写一个很小的dts,用 #include 把dtsi拉进来,再补充板子特有的外设就行。
核心原则:dtsi管芯片,dts管板子。别把板级特有的东西塞进dtsi,也别在dts里重复定义芯片已有的内容。
编译命令也很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
反编译回来查问题也常用:
dtc -I dtb -O dts -o myboard.dts myboard.dtb
4.2 节点与属性:设备树的“骨架”和“血肉”
设备树的每个硬件单元都是一个节点(node)。节点里用属性(property)来描述它的特征。我习惯把节点看作“硬件模块的身份证”,属性就是身份证上的各项信息。
来看一个典型的节点:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_UART_IPG>,
<&clks IMX6QDL_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
status = "okay";
};
这里有几个关键属性,我一个个说:
- compatible:驱动匹配的关键字。内核通过它找到对应的驱动程序。我建议把兼容列表写得全一点,从具体型号到通用型号,这样内核更容易匹配上。
- reg:寄存器地址和长度。格式是
<起始地址 长度>。如果有多段地址空间,可以写多组。 - interrupts:中断号、触发类型。不同芯片的编码方式不一样,得看芯片手册。
- clocks 和 clock-names:时钟源。names是给驱动用的标签,方便它通过名字找时钟。
- status:设备状态。常用
"okay"或"disabled"。我在调试时经常把某个外设先disabled掉,等驱动调通了再打开。
我的习惯:每个节点都加上 status 属性,即使默认就是 "okay"。这样后续做设备树覆盖时,可以很方便地通过修改 status 来控制设备开关。
4.3 中断与GPIO控制器描述
中断和GPIO是设备树里最容易出坑的地方。我踩过的坑,说出来都是泪。
4.3.1 中断控制器
中断控制器本身也是一个节点,它需要标注 interrupt-controller 属性:
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a9-gic";
reg = <0x00a01000 0x1000>,
<0x00a00100 0x100>;
interrupt-controller;
#interrupt-cells = <3>;
};
#interrupt-cells 告诉内核:这个中断控制器发出的中断描述需要几个cell。GIC(通用中断控制器)通常是3个cell:中断类型(0=SPI,1=PPI)、中断号、触发类型。
外设节点引用中断时,写法如下:
ethernet@2188000 {
interrupts = <0 57 IRQ_TYPE_LEVEL_HIGH>;
interrupt-parent = <&intc>;
};
这里 interrupt-parent 指定了中断路由到哪个控制器。如果整个系统只有一个中断控制器,可以在根节点统一指定,子节点就不用重复写了。
我曾经踩过的坑:有一次我忘了给某个外设节点加 interrupt-parent,而系统里有两个中断控制器。结果内核把中断路由到了错误的控制器上,网卡死活不触发中断。查了两天才发现是这个问题。
4.3.2 GPIO控制器
GPIO控制器的描述套路和中断类似:
gpio1: gpio@0209c000 {
compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
外设引用GPIO时,用 gpios 属性:
leds {
compatible = "gpio-leds";
led-red {
gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
label = "red-led";
};
};
#gpio-cells = <2> 表示每个GPIO描述需要2个cell:第一个是引脚号,第二个是标志位(比如高电平有效还是低电平有效)。
4.4 设备树覆盖(Overlay)
设备树覆盖,说白了就是“运行时打补丁”。你不需要重新编译整个设备树,只需要写一个很小的overlay文件,动态地添加或修改节点。
这个功能在以下场景特别有用:
- 外设模块热插拔:比如你插了一个摄像头模块,overlay可以动态添加I2C设备和GPIO配置。
- FPGA动态重配置:FPGA加载了新逻辑,overlay同步更新设备树。
- 调试阶段快速迭代:不用每次改设备树都重启系统。
一个典型的overlay文件长这样:
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&i2c1>;
__overlay__ {
status = "okay";
temp-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
};
};
编译overlay和普通设备树略有不同:
dtc -I dts -O dtb -@ -o myoverlay.dtbo myoverlay.dts
注意 -@ 选项,它告诉dtc保留符号信息,这样overlay才能找到要覆盖的目标节点。
我的建议:在开发阶段,尽量把板子特有的外设都做成overlay。这样主设备树保持干净,只包含芯片公共部分。调试某个外设时,只需要加载对应的overlay,不影响其他功能。
4.5 设备树核心知识体系
下面这张图是我自己总结的设备树知识框架,帮你快速建立整体认知:
设备树这东西,说难不难,说简单也不简单。关键是多动手、多踩坑。我建议你找一块开发板,把它的设备树从头到尾读一遍,然后试着改几个节点,编译、加载、验证。等你亲手把一块板子的设备树从零搭起来,你就真正掌握了。