💡 阅读提示:本文专为物联网工程师打造,从零讲透设备树(Device Tree)的语法、原理、实战修改与调试。读完你将能独立为开发板添加外设、解决驱动加载问题。文末有完整DTS文件和避坑指南。

🚨 开篇:一个“设备树”引发的血案

刚接触Linux驱动开发时,我拿着一个I2C触摸屏,按照内核文档写好驱动,编译进内核,insmod也成功,但就是没反应。折腾了三天,最后一位前辈扫了一眼我的代码,问:“你在设备树里添加节点了吗?”

我一脸懵:设备树是什么树?

后来才知道,ARM Linux从2013年开始全面采用设备树(Device Tree)来描述硬件,取代了以前硬编码的board文件。如果不修改设备树,驱动永远不知道自己要控制的设备在哪里、用什么中断、接在哪个I2C总线上。

设备树就是硬件的“身份证”。它告诉内核:我有这些CPU、这些内存、这些外设,每个外设的中断引脚、时钟频率、寄存器地址分别是什么。

今天,我将从物联网工程师的视角,用最直白的方式带你攻克设备树。读完这篇,你将:

  • 理解设备树的起源和设计哲学

  • 掌握DTS、DTC、DTB的关系

  • 学会编写和修改设备树节点(GPIO、I2C、SPI、中断)

  • 实战:为你的开发板添加一个OLED屏幕驱动

  • 熟练使用设备树调试工具(dtc、procfs、debugfs)

  • 避开95%的新手会踩的坑


一、设备树的“前世今生”

1.1 为什么需要设备树?

在ARM Linux早期,每个板子的硬件信息都写在arch/arm/mach-xxx/board-xxx.c里,比如:

static struct platform_device my_device = {
    .name = "my_driver",
    .id = 0,
    .resource = {
        .start = 0x40000000,
        .end   = 0x400000FF,
        .flags = IORESOURCE_MEM,
    },
};

每增加一个板子,就要新增一个.c文件。内核里积累了成百上千个这样的文件,导致:

  • 内核体积膨胀

  • 一个内核无法同时支持多个板子(需要重新编译)

  • 维护成本极高

2012年,Linus Torvalds发火后,ARM社区决定全面采用设备树——一种源自PowerPC的硬件描述机制。设备树将硬件信息从内核源码中剥离成独立的.dts文本文件,编译成.dtb二进制后由bootloader传递给内核。同一个内核镜像,通过更换不同的dtb,就能适配不同板卡

1.2 设备树的文件家族

扩展名 全称 作用
.dts Device Tree Source 设备树源文件,人类可编辑
.dtsi Device Tree Source Include 包含文件,类似C语言的.h,用于复用公共定义(如SoC的通用外设)
.dtb Device Tree Blob 二进制文件,由DTC编译DTS生成,内核实际解析它
.dtbo Device Tree Blob Overlay 动态覆盖片,用于热插拔设备(如树莓派扩展板)

工具链dtc(Device Tree Compiler)负责编译、反编译、验证。

# 编译 dts 到 dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译 dtb 到 dts(用于查看已有板子的配置)
dtc -I dtb -O dts -o extracted.dts myboard.dtb

二、设备树语法快速入门(物联网工程师视角)

设备树本质上是一棵树,根节点是/,下面有若干子节点,每个节点有属性。

2.1 最简单的设备树示例

/dts-v1/;

/ {
    model = "My IoT Board";
    compatible = "mycompany,myboard";
    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 起始地址0x80000000,大小256MB
    };

    chosen {
        bootargs = "console=ttyS0,115200";
        stdout-path = "serial0:115200n8";
    };

    leds {
        compatible = "gpio-leds";
        led0: led@0 {
            label = "heartbeat";
            gpios = <&gpio1 16 GPIO_ACTIVE_HIGH>;
            default-state = "on";
        };
    };
};

2.2 常用属性详解

属性 含义 示例
compatible 设备兼容性字符串,驱动会用它来匹配 "my_driver", "generic_driver"
reg 寄存器地址和长度,格式是<地址 长度>,个数由#address-cells#size-cells决定 <0x40000000 0x1000>
interrupts 中断号、触发方式 <0 31 1>(SPI中断31,上升沿触发)
interrupt-parent 指定中断控制器 <&gic>
gpios GPIO引脚和有效电平 <&gpio1 16 GPIO_ACTIVE_LOW>
clocks 使用的时钟 <&clk IMX6QDL_CLK_UART_IPG>
status 设备状态,okay表示启用,disabled表示禁用 okay
pinctrl-0pinctrl-names 引脚复用配置 pinctrl-0 = <&uart1_pins>

2.3 节点命名规范

text

节点名@地址
  • 节点名:描述设备类型,如i2cspiled

  • 地址:通常是寄存器的基地址

示例:

  • uart@40000000

  • i2c@30000000

  • led@0(无地址时可为0)

2.4 引用与标签

&符号引用其他节点,用:定义标签。

&i2c1 {           // 引用 SoC 定义好的 i2c1 节点
    status = "okay";
    clock-frequency = <400000>;

    my_device: mydevice@20 {   // 标签为 my_device,地址0x20
        compatible = "my,i2c-sensor";
        reg = <0x20>;
        interrupt-parent = <&gpio2>;
        interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
    };
};

2.5 包含文件

#include "stm32mp157.dtsi"          // SoC 通用定义
#include "stm32mp15xxac-pinctrl.dtsi"

dtsi里定义了默认的外设节点,状态大多是disabled,需要在板级.dts中启用并配置引脚。


三、实战:为开发板添加一个I2C触摸屏

我们有一块搭载I2C触摸屏(FT5x06)的开发板,触摸屏挂在I2C2总线上,地址0x38,中断引脚连接到GPIO1_IO9。

3.1 步骤1:查看原设备树

先反编译或找到开发板原有的.dts,查看I2C2是否已启用。

dtc -I dtb -O dts -o myboard.dts myboard.dtb

或者直接在内核源码中找到对应dts。假设I2C2节点如下:

&i2c2 {
    status = "disabled";   // 默认禁用
    pinctrl-names = "default";
    pinctrl-0 = <&i2c2_pins>;
    clock-frequency = <100000>;
};

3.2 步骤2:在板级dts中启用I2C2并添加触摸屏节点

修改你板子的.dts文件,在根节点下增加:

#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/gpio.h>

/ {
    // ...其他内容
};

&i2c2 {
    status = "okay";              // 启用
    clock-frequency = <400000>;   // 提高到400kHz(如果支持)

    touchscreen@38 {
        compatible = "edt,edt-ft5x06";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
        wakeup-source;
    };
};

3.3 步骤3:配置引脚复用(pinctrl)

如果I2C2和中断引脚还没配置复用,需要在pinctrl节点中增加。通常在&pinctrl&iomuxc下:

&iomuxc {
    pinctrl_i2c2: i2c2grp {
        fsl,pins = <
            MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
            MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
        >;
    };

    pinctrl_touch_irq: touch_irqgrp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x1b0b0   // 中断引脚,输入
            MX6UL_PAD_GPIO1_IO10__GPIO1_IO10 0x1b0b0   // 复位引脚,输出
        >;
    };
};

然后在&i2c2中引用:

&i2c2 {
    pinctrl-0 = <&pinctrl_i2c2>, <&pinctrl_touch_irq>;
    // ...
};

3.4 步骤4:编译设备树

在内核源码根目录下:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

或单独编译你的dtb:

make ARCH=arm myboard.dtb

3.5 步骤5:替换dtb并重启

把新生成的.dtb复制到/boot或烧录到开发板,重启。

3.6 步骤6:验证

系统启动后,查看内核日志:

dmesg | grep ft5x

应该看到类似:

ft5x06 2-0038: ft5x06 touchscreen detected
input: ft5x06 as /devices/platform/soc/2000000.i2c/i2c-2/2-0038/input/input2

检查I2C设备:

cat /sys/bus/i2c/devices/2-0038/name

应该输出ft5x06

检查中断是否注册:

cat /proc/interrupts | grep ft5x

四、调试设备树的必备工具

4.1 在系统运行中查看当前设备树

# 当前使用的完整设备树(二进制格式)
ls -l /sys/firmware/fdt

# 设备树节点呈现为目录结构
ls /sys/firmware/devicetree/base/

# 查看某个节点的 compatible 属性
cat /sys/firmware/devicetree/base/touchscreen@38/compatible

4.2 检查设备树是否被正确解析

# 查看某个设备是否成功创建平台设备
ls /sys/bus/platform/devices/
# 或 for i2c
ls /sys/bus/i2c/devices/

4.3 验证内存地址映射

cat /proc/iomem

4.4 反编译当前使用的dtb

# 将运行中的dtb导出
cp /sys/firmware/fdt /tmp/myboard.dtb
dtc -I dtb -O dts -o /tmp/current.dts /tmp/myboard.dtb

然后查看current.dts确认你的修改是否生效。

4.5 常见错误排查

现象 可能原因 解决方法
驱动probe函数未被调用 compatible不匹配或节点status不为okay 检查compatible字符串,查看dmesg是否有“OF: ... not found”
设备出现在/sys/bus/i2c/devices但驱动没加载 缺少MODULE_DEVICE_TABLE或内核没编译驱动 确认驱动中有of_match_table并已编译进内核
中断不触发 中断号或触发方式错误 查看/proc/interrupts确认中断已注册,用cat /sys/kernel/debug/gpio查看引脚电平
寄存器地址读写出错 reg属性地址或长度错误 检查芯片手册,确认地址范围,查看/proc/iomem是否冲突

五、高级技巧:设备树Overlay动态覆盖

树莓派、BeagleBone等板卡支持设备树Overlay,可以在系统运行时动态加载/卸载设备节点,而无需重新编译整个dtb。

5.1 编写Overlay dts

/dts-v1/;
/plugin/;

&i2c2 {
    #address-cells = <1>;
    #size-cells = <0>;

    my_temp@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};

5.2 编译Overlay

dtc -@ -I dts -O dtb -o my_overlay.dtbo my_overlay.dts

5.3 加载Overlay

# 加载
echo my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/ovl1/path
# 卸载
echo 0 > /sys/kernel/config/device-tree/overlays/ovl1/status

注意:需要内核开启CONFIG_OF_CONFIGFSCONFIG_OF_OVERLAY


六、物联网工程师的避坑指南

❌ 坑1:忘了启用节点

很多SoC的dtsi里默认status = "disabled",必须在板级dts中改为okay

❌ 坑2:中断号和触发方式写错

中断号要查SoC手册,有些是硬件中断号(如GIC SPI),不是Linux中断号。触发方式常用:

  • IRQ_TYPE_EDGE_RISING

  • IRQ_TYPE_EDGE_FALLING

  • IRQ_TYPE_LEVEL_HIGH

  • IRQ_TYPE_LEVEL_LOW

❌ 坑3:引脚复用冲突

两个设备用了同一个引脚(比如I2C和UART共用)。检查pinctrl部分,确保没有重复配置。

❌ 坑4:设备地址错误

I2C设备地址是7位地址(不包含读写位)。查看数据手册确认实际地址,例如0x38。

❌ 坑5:忘记添加时钟或时钟不匹配

某些外设需要时钟才能工作。如果驱动依赖时钟,必须在设备树中提供clocks属性,否则probe会失败。

❌ 坑6:内核配置未开启所需驱动

即使设备树正确,内核也必须编译进对应的驱动(或编译为模块)。检查.configCONFIG_TOUCHSCREEN_FT5X06等。


七、写给物联网工程师的特别建议

  1. 善用厂商提供的dtsi:ST、NXP、全志等厂商会在内核源码中提供SoC的公共dtsi,包含了几乎所有外设的默认节点。你只需要#include它,然后启用并配置引脚即可。

  2. 多做实验:可以在U-Boot阶段手动修改dtb(使用fdt命令),验证修改效果再固化到内核。

  3. 阅读优秀的设备树:从树莓派、BeagleBone等开源硬件的设备树学起,它们规范清晰。

  4. 写驱动时记得添加OF匹配表

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "mycompany,mydevice", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
static struct platform_driver my_driver = {
    .driver = {
        .name = "my_driver",
        .of_match_table = my_driver_of_match,
    },
    .probe = my_probe,
    .remove = my_remove,
};

八、写在最后

设备树是ARM Linux物联网开发的必修课。它并不难,难的是第一次理解它的“思维模式”——把硬件描述从驱动代码中剥离出来。

当你学会修改设备树后,你就不再是“只会写驱动”的工程师,而是能从系统层面看硬件的架构师。

现在,拿起你的开发板,试着写一个自己的设备树节点,点亮一个LED或挂载一个传感器。

设备树,就是物联网工程师的“乐高说明书”。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐