4. 设备树基础:设备树语法入门、T113设备树文件结构、GPIO与引脚复用配置

设备树(Device Tree)这东西,刚接触Linux驱动的人总觉得它很玄乎。说白了,它就是一套用来描述硬件信息的“配置文件”。以前内核里塞满了各种板级文件,改个引脚都得重新编译内核,麻烦得很。设备树把硬件描述和内核代码解耦了,你改硬件配置,改dts文件就行,不用动C代码。

我在T113项目上吃过设备树的亏。有一次点不亮LCD,查了两天才发现是引脚复用配置冲突了——同一个引脚被两个外设同时声明。从那以后,我对设备树的每个节点都格外小心。

4.1 设备树语法入门

设备树的源文件后缀是.dts,编译后的二进制是.dtb。内核启动时,Bootloader会把dtb传给内核,内核解析后就知道板上接了哪些外设。

先看一个最简单的节点:

/dts-v1/;

/ {
    model = "全志T113开发板";
    compatible = "allwinner,t113-s3", "allwinner,sun8i";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

    memory {
        device_type = "memory";
        reg = <0x40000000 0x20000000>;
    };
};

几个关键点:

  • 根节点:/ 是设备树的根,所有设备都在它下面
  • compatible:匹配字符串,内核通过它找到对应的驱动
  • reg:地址和大小,格式是 <地址 大小>
  • chosen:传递启动参数,不描述硬件
我的习惯:写compatible时,把具体型号放前面,通用型号放后面。比如"allwinner,t113-s3"在前,"allwinner,sun8i"在后。这样驱动匹配时优先匹配具体型号,更精确。

4.2 T113设备树文件结构

T113的官方BSP里,设备树文件分了好几层。我第一次看的时候也懵,后来理清楚了:

文件 作用 说明
sun8i-t113-s3.dtsi SoC级描述 CPU、中断控制器、时钟、所有外设控制器
sun8i-t113-s3-xxx.dts 板级描述 具体开发板的外设、引脚、内存大小
sun8i-t113-s3-xxx.dtb 编译产物 内核实际加载的二进制文件

.dtsi 是头文件,用 #include 包含。SoC级文件定义所有硬件资源,板级文件只声明“这个板用了哪些”。

举个例子,T113的UART控制器在.dtsi里是这样定义的:

uart0: serial@02500000 {
    compatible = "allwinner,sun8i-uart";
    reg = <0x02500000 0x400>;
    interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&ccu CLK_BUS_UART0>;
    resets = <&ccu RST_BUS_UART0>;
    status = "disabled";
};

注意 status = "disabled"。SoC级文件默认把所有外设都关掉,板级文件里需要用的才打开:

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
    status = "okay";
};

关键理解:&uart0 是引用标签,不是重新定义。它把uart0节点的status从"disabled"改成"okay",并加上引脚配置。这种“引用+覆盖”的写法是设备树的精髓。

4.3 GPIO与引脚复用配置

引脚复用是嵌入式开发里最容易出问题的地方。T113的引脚功能很多,一个引脚可能是UART的TX,也可能是I2C的SCL,还可能是GPIO。配置错了,外设就罢工。

在设备树里,引脚复用通过 pinctrl 子系统配置。先看一个例子:

&pio {
    uart0_pins: uart0-pins {
        pins = "PB8", "PB9";
        function = "uart0";
        bias-pull-up;
    };

    uart1_pins: uart1-pins {
        pins = "PG6", "PG7";
        function = "uart1";
    };
};

这里 &pio 是T113的引脚控制器节点。每个引脚组定义:

  • pins:哪些引脚,格式是"PxN",比如PB8就是Port B第8脚
  • function:复用功能,比如"uart0"、"i2c1"、"emac"
  • bias-pull-up/down:上下拉,可选
  • drive-strength:驱动能力,单位mA

然后外设节点引用这个引脚组:

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
    status = "okay";
};

pinctrl-names 可以定义多种状态,比如"default"和"sleep",系统在运行时切换。

我曾经踩过的坑:两个外设用了同一个引脚,但没报错,只是其中一个不工作。比如UART1和SPI1都用了PG6/PG7,编译不报错,但UART1就是没数据。排查方法:检查所有引脚组,确保每个引脚只出现在一个外设的pinctrl-0里。

4.4 知识结构图

下面这张图把设备树、引脚复用、外设驱动的关系串起来了:

T113设备树与引脚复用关系图 T113设备树与引脚复用关系 sun8i-t113.dtsi 板级 .dts 编译 → .dtb 内核解析设备树 pinctrl子系统 配置引脚复用功能 外设驱动 UART/I2C/SPI/EMAC... T113 SoC 引脚 PB8/PB9 → UART0 | PG6/PG7 → UART1 关键:每个引脚只能被一个外设使用,pinctrl负责配置复用功能,驱动通过设备树获取引脚配置 排查冲突时,检查所有外设的pinctrl-0引用,确保引脚不重复

4.5 实战:添加一个GPIO按键

光说不练假把式。我们给T113板子加一个GPIO按键,用设备树配置。假设按键接在PH3引脚,按下为低电平。

第一步,在板级dts里添加按键节点:

/ {
    gpio-keys {
        compatible = "gpio-keys";
        
        key-power {
            label = "Power Key";
            linux,code = <KEY_POWER>;
            gpios = <&pio PH 3 GPIO_ACTIVE_LOW>;
            wakeup-source;
        };
    };
};

这里 gpios = <&pio PH 3 GPIO_ACTIVE_LOW> 的意思是:

  • &pio:引用引脚控制器
  • PH:Port H
  • 3:第3脚
  • GPIO_ACTIVE_LOW:低电平有效

第二步,检查PH3有没有被其他外设占用。去.dtsi里搜一下"PH3",确保没有其他节点引用它。

第三步,编译设备树:

make dtbs

把生成的.dtb替换到boot分区,重启。内核的gpio-keys驱动会自动注册这个按键,按一下就能在/dev/input/eventX里看到事件。

调试技巧:如果按键不工作,先确认设备树有没有被正确加载。在/sys/firmware/devicetree/base/下查看节点是否存在。或者用dtc -I fs -O dts /sys/firmware/devicetree/base反编译当前设备树,看看你的节点在不在。

4.6 避坑指南

最后总结几个我实际遇到过的设备树问题:

  • 引脚冲突:同一个引脚被两个外设引用,编译不报错,运行时只有一个能工作。排查方法:手动检查所有pinctrl节点。
  • status忘记改:SoC级默认是"disabled",板级忘了改成"okay",外设不工作。我犯过这种低级错误。
  • compatible不匹配:驱动和设备树的compatible字符串必须完全一致,大小写、标点都不能错。
  • reg地址写错:外设基地址写错了,驱动访问时直接挂掉。对照数据手册确认。

设备树这东西,多写几次就熟了。刚开始觉得繁琐,但习惯了之后,你会发现它比原来那种硬编码的方式灵活太多了。