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";
        };
    };
};

看到没?每个节点都有名字,后面可以跟@地址。属性就是键值对,字符串用双引号,数字用尖括号。

我的习惯:写.dts文件时,我会先画一张硬件框图,把CPU、内存、串口、SPI、I2C、GPIO这些模块标清楚。然后照着框图写设备树,不容易漏东西。

节点之间的关系就是父子关系。比如上面的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()就能拿到这个地址。

我曾经踩过的坑:有一次我把reg的长度写小了,驱动只能访问到部分寄存器,结果串口收发数据总丢包。查了两天才发现是地址范围没给够。所以,配reg时一定要对照芯片手册,把外设的完整地址段都覆盖到。

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就是电气配置,包括上下拉、驱动能力、速度等。

我的经验:电气属性值不要随便抄。同一个管脚,做输入和做输出,配置值可能不一样。我习惯先看芯片手册的IOMUX章节,找到对应管脚的电气配置表,再填值。

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描述符,然后操作。

注意:GPIO号不要和Pinmux冲突。同一个管脚,既配成UART功能,又配成GPIO功能,那板子就乱套了。我见过有人把调试串口的TX管脚配成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
Overlay的典型应用场景:
  • Modbus网关插入不同的扩展板(RS485、CAN、DI/DO)
  • 动态切换管脚功能(比如同一个管脚,有时做GPIO,有时做PWM)
  • 调试阶段快速验证硬件配置

我在一个项目中,用Overlay实现了“热插拔”扩展模块。用户插上RS485板,系统自动加载对应的Overlay;拔掉就卸载。用户体验很好,但底层调试确实费了不少功夫。

4.5 本章知识结构

下面这张图,是我梳理的设备树知识体系。你可以把它当成一个“地图”,学完本章后对照着检查自己掌握了多少。

设备树知识结构图 设备树 (Device Tree) DTS语法与结构 Reg / Interrupt GPIO & Pinmux Overlay 机制 节点/属性 compatible 地址映射 中断号 pinctrl gpios .dtbo编译 configfs加载 💡 核心思路:设备树是硬件描述语言,Overlay是动态补丁机制 掌握这四块内容,Modbus网关的硬件适配工作就能游刃有余

设备树这东西,初看觉得繁琐,用多了就会发现它真的很优雅。尤其是Overlay机制,让Modbus网关这种需要灵活扩展的设备,硬件适配变得异常简单。