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:传递启动参数,不描述硬件
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",系统在运行时切换。
4.4 知识结构图
下面这张图把设备树、引脚复用、外设驱动的关系串起来了:
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 H3:第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地址写错:外设基地址写错了,驱动访问时直接挂掉。对照数据手册确认。
设备树这东西,多写几次就熟了。刚开始觉得繁琐,但习惯了之后,你会发现它比原来那种硬编码的方式灵活太多了。