阅读提示本文专为物联网工程师打造从零讲透设备树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 设备树的文件家族扩展名全称作用.dtsDevice Tree Source设备树源文件人类可编辑.dtsiDevice Tree Source Include包含文件类似C语言的.h用于复用公共定义如SoC的通用外设.dtbDevice Tree Blob二进制文件由DTC编译DTS生成内核实际解析它.dtboDevice Tree Blob Overlay动态覆盖片用于热插拔设备如树莓派扩展板工具链dtcDevice 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; memory80000000 { device_type memory; reg 0x80000000 0x10000000; // 起始地址0x80000000大小256MB }; chosen { bootargs consolettyS0,115200; stdout-path serial0:115200n8; }; leds { compatible gpio-leds; led0: led0 { label heartbeat; gpios gpio1 16 GPIO_ACTIVE_HIGH; default-state on; }; }; };2.2 常用属性详解属性含义示例compatible设备兼容性字符串驱动会用它来匹配my_driver, generic_driverreg寄存器地址和长度格式是地址 长度个数由#address-cells和#size-cells决定0x40000000 0x1000interrupts中断号、触发方式0 31 1SPI中断31上升沿触发interrupt-parent指定中断控制器gicgpiosGPIO引脚和有效电平gpio1 16 GPIO_ACTIVE_LOWclocks使用的时钟clk IMX6QDL_CLK_UART_IPGstatus设备状态okay表示启用disabled表示禁用okaypinctrl-0,pinctrl-names引脚复用配置pinctrl-0 uart1_pins2.3 节点命名规范text节点名地址节点名描述设备类型如i2c、spi、led地址通常是寄存器的基地址示例uart40000000i2c30000000led0无地址时可为02.4 引用与标签用符号引用其他节点用:定义标签。i2c1 { // 引用 SoC 定义好的 i2c1 节点 status okay; clock-frequency 400000; my_device: mydevice20 { // 标签为 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.dtsidtsi里定义了默认的外设节点状态大多是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如果支持 touchscreen38 { 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 ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs或单独编译你的dtbmake ARCHarm myboard.dtb3.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/touchscreen38/compatible4.2 检查设备树是否被正确解析# 查看某个设备是否成功创建平台设备 ls /sys/bus/platform/devices/ # 或 for i2c ls /sys/bus/i2c/devices/4.3 验证内存地址映射cat /proc/iomem4.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_temp48 { compatible ti,tmp102; reg 0x48; }; };5.2 编译Overlaydtc - -I dts -O dtb -o my_overlay.dtbo my_overlay.dts5.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_CONFIGFS和CONFIG_OF_OVERLAY。六、物联网工程师的避坑指南❌ 坑1忘了启用节点很多SoC的dtsi里默认status disabled必须在板级dts中改为okay。❌ 坑2中断号和触发方式写错中断号要查SoC手册有些是硬件中断号如GIC SPI不是Linux中断号。触发方式常用IRQ_TYPE_EDGE_RISINGIRQ_TYPE_EDGE_FALLINGIRQ_TYPE_LEVEL_HIGHIRQ_TYPE_LEVEL_LOW❌ 坑3引脚复用冲突两个设备用了同一个引脚比如I2C和UART共用。检查pinctrl部分确保没有重复配置。❌ 坑4设备地址错误I2C设备地址是7位地址不包含读写位。查看数据手册确认实际地址例如0x38。❌ 坑5忘记添加时钟或时钟不匹配某些外设需要时钟才能工作。如果驱动依赖时钟必须在设备树中提供clocks属性否则probe会失败。❌ 坑6内核配置未开启所需驱动即使设备树正确内核也必须编译进对应的驱动或编译为模块。检查.config中CONFIG_TOUCHSCREEN_FT5X06等。七、写给物联网工程师的特别建议善用厂商提供的dtsiST、NXP、全志等厂商会在内核源码中提供SoC的公共dtsi包含了几乎所有外设的默认节点。你只需要#include它然后启用并配置引脚即可。多做实验可以在U-Boot阶段手动修改dtb使用fdt命令验证修改效果再固化到内核。阅读优秀的设备树从树莓派、BeagleBone等开源硬件的设备树学起它们规范清晰。写驱动时记得添加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或挂载一个传感器。设备树就是物联网工程师的“乐高说明书”。
写给物联网人的Linux教程:从小白到精通——一文带你攻克设备驱动开发中的“设备树”难题
阅读提示本文专为物联网工程师打造从零讲透设备树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 设备树的文件家族扩展名全称作用.dtsDevice Tree Source设备树源文件人类可编辑.dtsiDevice Tree Source Include包含文件类似C语言的.h用于复用公共定义如SoC的通用外设.dtbDevice Tree Blob二进制文件由DTC编译DTS生成内核实际解析它.dtboDevice Tree Blob Overlay动态覆盖片用于热插拔设备如树莓派扩展板工具链dtcDevice 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; memory80000000 { device_type memory; reg 0x80000000 0x10000000; // 起始地址0x80000000大小256MB }; chosen { bootargs consolettyS0,115200; stdout-path serial0:115200n8; }; leds { compatible gpio-leds; led0: led0 { label heartbeat; gpios gpio1 16 GPIO_ACTIVE_HIGH; default-state on; }; }; };2.2 常用属性详解属性含义示例compatible设备兼容性字符串驱动会用它来匹配my_driver, generic_driverreg寄存器地址和长度格式是地址 长度个数由#address-cells和#size-cells决定0x40000000 0x1000interrupts中断号、触发方式0 31 1SPI中断31上升沿触发interrupt-parent指定中断控制器gicgpiosGPIO引脚和有效电平gpio1 16 GPIO_ACTIVE_LOWclocks使用的时钟clk IMX6QDL_CLK_UART_IPGstatus设备状态okay表示启用disabled表示禁用okaypinctrl-0,pinctrl-names引脚复用配置pinctrl-0 uart1_pins2.3 节点命名规范text节点名地址节点名描述设备类型如i2c、spi、led地址通常是寄存器的基地址示例uart40000000i2c30000000led0无地址时可为02.4 引用与标签用符号引用其他节点用:定义标签。i2c1 { // 引用 SoC 定义好的 i2c1 节点 status okay; clock-frequency 400000; my_device: mydevice20 { // 标签为 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.dtsidtsi里定义了默认的外设节点状态大多是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如果支持 touchscreen38 { 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 ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs或单独编译你的dtbmake ARCHarm myboard.dtb3.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/touchscreen38/compatible4.2 检查设备树是否被正确解析# 查看某个设备是否成功创建平台设备 ls /sys/bus/platform/devices/ # 或 for i2c ls /sys/bus/i2c/devices/4.3 验证内存地址映射cat /proc/iomem4.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_temp48 { compatible ti,tmp102; reg 0x48; }; };5.2 编译Overlaydtc - -I dts -O dtb -o my_overlay.dtbo my_overlay.dts5.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_CONFIGFS和CONFIG_OF_OVERLAY。六、物联网工程师的避坑指南❌ 坑1忘了启用节点很多SoC的dtsi里默认status disabled必须在板级dts中改为okay。❌ 坑2中断号和触发方式写错中断号要查SoC手册有些是硬件中断号如GIC SPI不是Linux中断号。触发方式常用IRQ_TYPE_EDGE_RISINGIRQ_TYPE_EDGE_FALLINGIRQ_TYPE_LEVEL_HIGHIRQ_TYPE_LEVEL_LOW❌ 坑3引脚复用冲突两个设备用了同一个引脚比如I2C和UART共用。检查pinctrl部分确保没有重复配置。❌ 坑4设备地址错误I2C设备地址是7位地址不包含读写位。查看数据手册确认实际地址例如0x38。❌ 坑5忘记添加时钟或时钟不匹配某些外设需要时钟才能工作。如果驱动依赖时钟必须在设备树中提供clocks属性否则probe会失败。❌ 坑6内核配置未开启所需驱动即使设备树正确内核也必须编译进对应的驱动或编译为模块。检查.config中CONFIG_TOUCHSCREEN_FT5X06等。七、写给物联网工程师的特别建议善用厂商提供的dtsiST、NXP、全志等厂商会在内核源码中提供SoC的公共dtsi包含了几乎所有外设的默认节点。你只需要#include它然后启用并配置引脚即可。多做实验可以在U-Boot阶段手动修改dtb使用fdt命令验证修改效果再固化到内核。阅读优秀的设备树从树莓派、BeagleBone等开源硬件的设备树学起它们规范清晰。写驱动时记得添加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或挂载一个传感器。设备树就是物联网工程师的“乐高说明书”。