4、设备树(Device Tree)详解:DTS语法与结构、Reg/Interrupt属性、GPIO与Pinmux绑定、Overlay机制
设备树,说白了就是一套描述硬件信息的“配置文件”。
我刚开始接触嵌入式Linux时,总觉得这玩意儿有点多余——硬件信息写死在代码里不香吗?直到我接手一个项目,同一块板子要换三款不同型号的传感器,每次都得重新编译内核……那滋味,谁试谁知道。
设备树把硬件描述和内核代码解耦了。你改个外设,改改.dts文件就行,不用动内核源码。对于Modbus网关这种需要对接各种现场总线的设备来说,这特性简直是救命稻草。
4.1 DTS语法与结构——先看懂再说
设备树源文件(.dts)的语法其实很直观。我习惯把它理解成一棵“树”,根节点就是你的板子,子节点就是各种外设。
/dts-v1/;
/ {
model = "Modbus Gateway Board v1.0";
compatible = "vendor,modbus-gw", "vendor,imx6ull";
chosen {
bootargs = "console=ttymxc0,115200";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB RAM */
};
leds {
compatible = "gpio-leds";
status-led {
label = "status";
gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
看到没?每个节点都有名字,后面可以跟@地址。属性就是键值对,字符串用双引号,数字用尖括号。
节点之间的关系就是父子关系。比如上面的leds是根节点的孩子,status-led是leds的孩子。驱动加载时,内核会沿着这棵树往下找,匹配compatible属性。
4.2 Reg与Interrupt属性——地址和中断怎么配
这两个属性是设备树里最常用的,也是新手最容易配错的地方。
4.2.1 reg属性
reg描述的是设备的地址空间。格式是:reg = <地址1 长度1 地址2 长度2 ...>。
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_IPG>,
<&clks IMX6UL_CLK_UART1_SERIAL>;
clock-names = "ipg", "per";
status = "okay";
};
这里0x02020000是UART1的基地址,0x4000是地址范围。驱动里通过platform_get_resource()就能拿到这个地址。
4.2.2 interrupts属性
中断属性稍微复杂一点。它通常包含三个部分:中断控制器类型、中断号、触发方式。
| 字段 | 说明 | 常见取值 |
|---|---|---|
| 中断类型 | GIC_SPI(共享外设中断)或 GIC_PPI(私有外设中断) | 0 或 1 |
| 中断号 | 硬件中断号,从芯片手册查 | 0~255 |
| 触发方式 | 电平触发或边沿触发 | IRQ_TYPE_LEVEL_HIGH / IRQ_TYPE_EDGE_RISING |
驱动里用platform_get_irq()获取中断号,然后注册中断处理函数。嗯,这里要注意:中断号不要搞错,否则驱动根本收不到中断。
4.3 GPIO与Pinmux绑定——管脚复用是门学问
Modbus网关通常要用到多个串口、GPIO控制继电器、SPI接LCD……管脚复用配置不好,板子就废了。
4.3.1 Pinmux配置
Pinmux(管脚复用)在设备树里通过pinctrl子系统配置。我一般这样写:
&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
>;
};
};
每个管脚配置包含三部分:管脚名、功能选择、电气属性。那个0x1b0b1就是电气配置,包括上下拉、驱动能力、速度等。
4.3.2 GPIO绑定
GPIO在设备树里用gpios属性引用。比如控制一个继电器:
relay {
compatible = "gpio-leds"; /* 也可以用自定义驱动 */
relay-ctrl {
label = "relay1";
gpios = <&gpio5 7 GPIO_ACTIVE_HIGH>;
};
};
这里&gpio5 7表示GPIO5组的第7号引脚。GPIO_ACTIVE_HIGH表示高电平有效。驱动里通过gpiod_get()获取GPIO描述符,然后操作。
4.4 Overlay机制——动态修改设备树
Overlay是设备树的“补丁”机制。你想想看,如果Modbus网关要支持多种扩展模块,总不能每种组合都编译一个完整的设备树吧?Overlay就是干这个的。
它的原理很简单:在基础设备树(.dtb)上,叠加一个或多个Overlay片段(.dtbo),动态添加或修改节点。
// rs485-overlay.dts
/dts-v1/;
/plugin/;
&uart2 {
status = "okay";
pinctrl-0 = <&pinctrl_uart2_rs485>;
linux,rs485-enabled-at-boot-time;
};
编译成.dtbo文件后,通过configfs接口加载:
# 挂载configfs
mount -t configfs configfs /sys/kernel/config
# 创建Overlay
mkdir /sys/kernel/config/device-tree/overlays/rs485
# 写入Overlay数据
cat rs485.dtbo > /sys/kernel/config/device-tree/overlays/rs485/dtbo
加载成功后,uart2节点就被激活了,而且配成了RS485模式。想卸载?直接删目录就行:
rmdir /sys/kernel/config/device-tree/overlays/rs485
- Modbus网关插入不同的扩展板(RS485、CAN、DI/DO)
- 动态切换管脚功能(比如同一个管脚,有时做GPIO,有时做PWM)
- 调试阶段快速验证硬件配置
我在一个项目中,用Overlay实现了“热插拔”扩展模块。用户插上RS485板,系统自动加载对应的Overlay;拔掉就卸载。用户体验很好,但底层调试确实费了不少功夫。
4.5 本章知识结构
下面这张图,是我梳理的设备树知识体系。你可以把它当成一个“地图”,学完本章后对照着检查自己掌握了多少。
设备树这东西,初看觉得繁琐,用多了就会发现它真的很优雅。尤其是Overlay机制,让Modbus网关这种需要灵活扩展的设备,硬件适配变得异常简单。