第4章 设备树(Device Tree)基础
设备树,说白了就是一块「硬件描述板」。它告诉内核:你的板子上有哪些外设、它们怎么连接、地址是多少。我刚开始接触Linux驱动时,总觉得设备树很玄乎,后来发现它其实就是个配置文件——只不过格式特殊了点。
这一章,我们来啃设备树的硬骨头。语法、节点、属性、中断、GPIO,还有那些让人头疼的绑定文档。嗯,我保证讲得接地气。
4.1 设备树语法:dts与dtsi
设备树源文件有两种后缀:.dts 和 .dtsi。前者是板级描述文件,后者是芯片级描述文件。你可以把 .dtsi 想象成C语言的头文件——它定义芯片通用的部分,比如CPU核心、内存控制器、时钟等。而 .dts 则负责具体板子的差异化配置。
我在项目中遇到过一种情况:同一个芯片用在三款不同板子上,如果每块板子都从头写一遍设备树,那简直是灾难。正确的做法是把公共部分抽到 .dtsi 里,板级文件用 #include 包含它,然后只覆盖需要改动的节点。
核心规则:
.dtsi定义芯片级通用资源,不涉及具体板级配置.dts包含.dtsi,并添加板级特有的外设和引脚配置- 同名的节点或属性,后定义的会覆盖先定义的
来看一个简单的例子:
// imx8mm.dtsi(芯片级)
/ {
soc {
uart1: serial@30890000 {
compatible = "fsl,imx8mm-uart", "fsl,imx6q-uart";
reg = <0x0 0x30890000 0x0 0x10000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
status = "disabled";
};
};
};
// my-board.dts(板级)
#include "imx8mm.dtsi"
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
看到没?板级文件用 &uart1 引用芯片级定义的节点,然后只修改 status 和添加 pinctrl。这样既保持了芯片级文件的纯净,又实现了板级定制。
4.2 节点与属性
设备树的每个「外设」都是一个节点。节点可以嵌套,就像文件夹套文件夹。每个节点有属性,属性就是键值对。
节点命名格式:节点名@地址。比如 serial@30890000,serial 是名字,30890000 是基地址。注意,地址不是必须的,但如果有多个同类型设备,加地址可以避免重名。
常用的属性我列一下:
| 属性名 | 含义 | 示例 |
|---|---|---|
compatible |
驱动匹配的关键字 | "fsl,imx8mm-uart" |
reg |
寄存器地址和大小 | <0x30890000 0x10000> |
interrupts |
中断号、触发类型 | <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH> |
status |
设备状态 | "okay" 或 "disabled" |
pinctrl-* |
引脚复用配置 | pinctrl-0 = <&pinctrl_uart1> |
我的小技巧: 写 compatible 时,建议遵循「厂商,型号」的格式。比如 "fsl,imx8mm-uart"。这样驱动匹配时,内核会先尝试精确匹配,不行就回退到通用匹配。我曾经因为写反了顺序,导致驱动死活加载不上,排查了半天才发现是 compatible 写成了 "imx8mm-uart,fsl"。
4.3 中断与GPIO控制器
中断和GPIO是设备树里最容易出坑的地方。我见过太多人把中断号写错,或者GPIO编号搞混。
先看中断。i.MX8M 使用 GIC(通用中断控制器),中断类型分两种:
- SPI(共享外设中断):外设用的中断,多个CPU核都能响应
- PPI(私有外设中断):每个CPU核私有的,比如本地定时器
中断属性的格式:<中断类型 中断号 触发方式>。举个例子:
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
这里 GIC_SPI 是宏定义,值为0。26是中断号。注意,这个中断号是硬件中断号,不是Linux的IRQ号。内核在初始化时会自动映射。
我曾经踩过的坑: 中断号从0开始还是从1开始?不同芯片厂商的习惯不一样。i.MX8M 的数据手册里,中断号通常从0开始,但有些外设的中断号在手册里写的是「32+偏移」。这时候一定要仔细看手册,或者参考官方设备树。我有一回把UART的中断号写成了58,结果应该是26+32=58,但实际GIC_SPI的偏移已经在内核里处理了,直接写26就行。嗯,那次调试花了我一整天。
GPIO控制器的描述稍微复杂一点。i.MX8M 有多个GPIO bank,每个bank有32个引脚。设备树里这样定义:
gpio1: gpio@30200000 {
compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
reg = <0x30200000 0x10000>;
interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
注意两个关键属性:
gpio-controller:声明这是一个GPIO控制器#gpio-cells:指定引用GPIO时需要几个参数。通常为2,第一个是引脚号,第二个是标志(比如高有效还是低有效)
引用GPIO的写法:<&gpio1 15 GPIO_ACTIVE_LOW>。意思是GPIO1组的第15脚,低电平有效。
4.4 常用绑定文档解析
绑定文档(Binding Documentation)是设备树的「说明书」。内核源码的 Documentation/devicetree/bindings/ 目录下,每个外设都有对应的YAML或文本格式的绑定文档。
我个人习惯,写设备树前先翻绑定文档。它告诉你:这个设备需要哪些必须属性、哪些可选属性、属性值的格式是什么。
以I2C控制器为例,绑定文档会说明:
Required properties:
- compatible: 必须包含 "fsl,imx8mm-i2c"
- reg: 寄存器地址和范围
- interrupts: 中断号
- clocks: 时钟句柄
- #address-cells: 应为1
- #size-cells: 应为0
Optional properties:
- clock-frequency: I2C总线频率,默认100000
- pinctrl-*: 引脚配置
你看,有了绑定文档,写设备树就像填表格一样。缺了哪个必填属性,内核启动时就会报错。
我的建议: 别自己瞎猜属性名。去绑定文档里搜一下,或者看看其他板子的设备树是怎么写的。i.MX8M的参考设备树在 arch/arm64/boot/dts/freescale/ 目录下,那里有现成的例子。我刚开始写设备树时,就是一边看参考板子的dts,一边对照绑定文档,慢慢就摸出门道了。
最后说一句,设备树编译工具 dtc 是你的好帮手。写完dts后,用 dtc -I dts -O dtb -o test.dtb test.dts 编译一下,如果有语法错误,它会直接告诉你哪一行有问题。别像我早期那样,写完直接烧到板子上,结果内核panic了才回头检查。
嗯,设备树基础就讲到这里。下一章我们深入设备树的中断映射和引脚复用,那才是真正考验耐心的地方。