
1. 从“硬编码”到“描述文件”设备树的诞生背景如果你是从单片机或者早期的嵌入式Linux开发转过来的肯定对内核源码里那些arch/arm/mach-xxx/目录下的板级文件记忆犹新。那里面充斥着大量的platform_device结构体、GPIO引脚定义、中断号映射代码又长又硬换个板子就得大动干戈。这种“硬编码”的方式让内核源码变得臃肿不堪每支持一块新板子就得在内核里添加一堆针对性的代码内核维护者对此苦不堪言。设备树Device Tree的出现就是为了解决这个“板级细节耦合”的核心痛点。它的核心思想非常清晰将硬件平台的描述信息从内核源码中剥离出来变成一个独立的、结构化的数据文件。你可以把它想象成一份交给内核的“硬件配置清单”。内核启动时不再是去编译好的代码里找硬件信息而是去读取这份清单然后根据清单上的描述动态地创建出对应的设备。这个转变带来的好处是革命性的。对于芯片原厂如瑞芯微、全志等他们只需要维护一个通用的内核然后为不同的开发板哪怕用的是同一颗SoC提供不同的设备树文件.dts或.dtb。对于板卡厂商和开发者定制硬件变得极其简单——你不需要去修改内核源码只需要修改或编写一个新的设备树文件。这极大地提高了内核的通用性和可移植性也使得同一个内核镜像能够通过加载不同的设备树文件来适配不同的硬件平台这正是嵌入式Linux领域“一个内核多种硬件”愿景得以实现的技术基石。2. 设备树文件的结构化解析从源文件到二进制块设备树并不是一个神秘的黑盒它有一套完整的语法和编译流程。理解这个流程是掌握设备树的关键。2.1 核心文件类型.dts, .dtsi, .dtb.dts (Device Tree Source) 设备树源文件。这是人类可读、可编辑的文本文件开发者主要打交道的就是它。它描述了一个具体的硬件平台比如rk3568-evb.dts就描述了瑞芯微RK3568评估板的硬件。.dtsi (Device Tree Source Include) 设备树源包含文件。它类似于C语言中的.h头文件用于被多个.dts文件包含以实现代码复用。通常SoC级别的通用配置比如CPU架构、内存映射、核心外设控制器会定义在.dtsi文件中。例如rk3568.dtsi描述了RK3568这颗芯片的所有共性硬件资源而具体的板子.dts文件则通过#include rk3568.dtsi来引用它并在此基础上添加或覆盖板级特有的配置如具体的GPIO按键、LED、PHY芯片型号等。.dtb (Device Tree Blob) 设备树二进制文件。这是由.dts源文件经过编译器dtc编译后生成的二进制文件。它体积小、格式固定可以直接被Bootloader如U-Boot加载到内存中并传递给Linux内核。内核最终解析的是这个.dtb文件。2.2 设备树语法初窥节点、属性与值设备树语法非常直观它采用树状结构来描述硬件。整棵树由一个个“节点”组成节点里包含“属性”。// 这是一个简单的设备树片段示例 /dts-v1/; / { // 根节点 compatible rockchip,rk3568-evb, rockchip,rk3568; model Rockchip RK3568 Evaluation Board; cpus { // CPU子节点 #address-cells 2; #size-cells 2; cpu0: cpu0 { // CPU0节点带有一个标签‘cpu0’ device_type cpu; compatible arm,cortex-a55; reg 0x0 0x0; enable-method psci; }; }; memory0 { // 内存节点 device_type memory; reg 0x0 0x0 0x0 0x80000000; // 起始地址0大小2GB }; leds { // LED灯节点 compatible gpio-leds; sys_led: led-0 { label sys-led; gpios gpio0 6 GPIO_ACTIVE_HIGH; // 引用GPIO控制器引脚0组6号高电平有效 linux,default-trigger heartbeat; }; }; };节点 用花括号{}定义如/根节点、cpus、leds。节点可以嵌套形成父子关系。属性 节点内的键值对格式为属性名 值;。例如compatible,model,reg。值 可以是字符串如arm,cortex-a55、32位整数数组用尖括号表示如0x0 0x0、字符串列表如rockchip,rk3568-evb, rockchip,rk3568或对另一个节点的引用如gpio0。标签 在节点名前可以加一个标签如cpu0:方便在其他地方通过cpu0来引用这个节点。compatible属性 这是设备树中最重要的属性没有之一。它定义了设备与哪个驱动程序匹配。它的值是一个字符串列表内核驱动程序会声明自己兼容的字符串两者匹配时驱动才会被绑定到这个设备节点上。例如一个LED驱动可能声明兼容gpio-leds那么所有compatible gpio-leds的节点都会被该驱动管理。reg属性 描述设备在父总线地址空间内的寄存器区域。它的值通常是一个或多个地址长度对。#address-cells和#size-cells属性则定义了在子节点的reg属性中用多少个32位整数来表示地址和长度。2.3 编译与传递流程编写 开发者编辑.dts和.dtsi文件。编译 使用设备树编译器dtc将.dts编译成.dtb。dtc -I dts -O dtb -o rk3568-evb.dtb rk3568-evb.dts传递 Bootloader如U-Boot将编译好的.dtb文件加载到内存的特定地址然后在启动内核时通过寄存器如ARM的r2寄存器或特定的启动协议如ARM的ATAGs之后的DTB将这个地址告诉内核。解析 内核启动早期会解析这块内存区域在内存中构建出设备树的结构然后根据节点信息逐一初始化平台设备和设备驱动。3. 内核如何与设备树共舞驱动匹配与资源获取内核拿到设备树二进制块后具体是怎么用的呢这涉及到驱动模型的核心。3.1 平台设备的自动创建在引入设备树之前开发者需要手动编写代码用platform_device_register()来注册一个平台设备。现在这个过程是自动的。内核在解析设备树时对于某些特定类型的节点通常是那些在根节点下有compatible属性但没有status disabled的节点会自动为其生成一个platform_device结构体。这个自动创建的platform_device会包含从设备树节点中提取的关键信息其中最重要的就是compatible字符串列表。这个列表会被放入platform_device的of_match_table相关字段中。3.2 驱动匹配compatible 属性的魔法驱动这边在编写platform_driver时需要定义一个of_device_id数组里面声明这个驱动可以兼容哪些设备。static const struct of_device_id my_led_driver_ids[] { { .compatible mycompany,simple-led }, { .compatible vendor,another-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_driver_ids); static struct platform_driver my_led_driver { .probe my_led_probe, .driver { .name my-simple-led, .of_match_table of_match_ptr(my_led_driver_ids), }, };当内核注册这个驱动时会遍历所有由设备树生成的platform_device将设备的compatible属性与驱动的of_device_id表进行匹配。一旦找到匹配项内核就会调用驱动的.probe()函数并将匹配到的platform_device作为参数传入。3.3 在驱动中获取设备树资源在驱动的.probe()函数里我们如何拿到设备树里定义的硬件资源呢内核提供了一整套以of_为前缀的APIOpen Firmware API。获取基本属性const char *name; name of_get_property(dev-of_node, label, NULL); // 获取 label 属性的字符串值解析GPIO GPIO是最常用的资源之一。struct gpio_desc *led_gpio; led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); // 获取名为 led 的GPIO并初始化为低电平输出 // 这对应设备树中的led-gpios gpio0 6 GPIO_ACTIVE_HIGH; // 注意现代驱动推荐使用 gpiod API 而非旧的 gpio API。解析中断int irq; irq platform_get_irq(pdev, 0); // 获取设备树中定义的第0个中断号 // 这对应设备树中的interrupts GIC_SPI 58 IRQ_TYPE_LEVEL_HIGH;解析寄存器地址regstruct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第0个内存资源 base devm_ioremap_resource(dev, res); // 映射到内核虚拟地址解析其他复杂结构 对于像pinctrl引脚复用、dma等复杂绑定有更专门的API如devm_pinctrl_get_select_default()。注意 使用devm_Managed Device Resource系列API如devm_gpiod_get,devm_ioremap_resource是当前的最佳实践。这些API申请的资源会与设备struct device *的生命周期绑定当设备被卸载或探测失败时资源会自动释放可以有效防止资源泄漏。4. 实战为RK3568添加一个简单的LED设备让我们结合瑞芯微RK3568的平台完成一个从设备树到驱动的小实验。假设我们要在GPIO0_B2即GPIO0组的第10号引脚RK3568的引脚编号方式上控制一个LED。4.1 修改设备树文件首先找到你的板级设备树文件比如arch/arm64/boot/dts/rockchip/rk3568-evb.dts。在根节点/下添加一个leds节点。/ { // ... 其他已有的配置 ... leds { compatible gpio-leds; // 必须用于匹配内核已有的gpio-leds通用驱动 power_led: power-led { label power-led; // 用户空间可通过/sys/class/leds/power-led访问 gpios gpio0 10 GPIO_ACTIVE_HIGH; // 使用GPIO0_B2高电平点亮 linux,default-trigger none; // 默认触发器none表示手动控制 // default-state off; // 默认状态可选 }; }; };这里我们直接使用了内核自带的gpio-leds驱动它是一个通用LED驱动可以通过sysfs接口/sys/class/leds/power-led/控制非常方便。linux,default-trigger可以设置为heartbeat心跳、mmc0SD卡活动等实现自动闪烁。4.2 配置引脚复用Pinctrl在RK3568上一个引脚可能有多种功能复用为GPIO、UART、I2C等。我们需要确保这个引脚被复用为GPIO功能。这通常在pinctrl节点中配置。在设备树文件中找到pinctrl节点添加一个子节点定义我们的LED引脚配置pinctrl { // ... 其他pinctrl配置 ... leds { power_led_pin: power-led-pin { rockchip,pins 0 RK_PB2 RK_FUNC_GPIO pcfg_pull_none; // GPIO0_B2上拉禁用 }; }; };然后在我们的leds节点中引用这个pinctrl配置leds { compatible gpio-leds; pinctrl-names default; pinctrl-0 power_led_pin; // 引用上面定义的pinctrl状态 power_led: power-led { // ... 属性同上 ... }; };4.3 编译与更新设备树在Linux内核源码根目录下使用你的交叉编译工具链make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- dtbs这条命令会编译所有设备树或者指定你的板子make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rockchip/rk3568-evb.dtb将生成的rk3568-evb.dtb文件位于arch/arm64/boot/dts/rockchip/替换掉你的开发板Bootloader加载的设备树文件。具体方法取决于你的启动方式SD卡、eMMC、tftp等。4.4 验证与使用启动开发板后如果配置正确你应该能看到内核日志dmesg | grep leds或dmesg | grep gpio可能会看到类似leds: gpio-leds: power-led的注册成功信息。Sysfs接口ls /sys/class/leds/你应该能看到power-led目录。控制LED# 点亮LED echo 1 /sys/class/leds/power-led/brightness # 熄灭LED echo 0 /sys/class/leds/power-led/brightness # 设置为心跳模式 echo heartbeat /sys/class/leds/power-led/trigger cat /sys/class/leds/power-led/trigger # 查看当前触发器5. 调试设备树当硬件不按预期工作时设备树配置错误是嵌入式Linux启动过程中最常见的问题之一。硬件没反应、驱动没加载第一步就该怀疑设备树。5.1 查看内核解析到的设备树内核在启动时会将解析后的设备树以文件系统的形式挂载到/sys/firmware/devicetree/。这是一个以目录结构呈现的完整设备树。# 查看根节点的属性 ls /sys/firmware/devicetree/base/ cat /sys/firmware/devicetree/base/compatible # 查看我们添加的leds节点 ls /sys/firmware/devicetree/base/leds/ cat /sys/firmware/devicetree/base/leds/power-led/compatible cat /sys/firmware/devicetree/base/leds/power-led/gpios这里的属性值是原始的、未解析的格式比如整数是二进制但对于确认节点和属性是否存在非常有用。5.2 使用 oftest 工具一些工具可以更方便地查看设备树信息。udevadm可以列出所有OFOpen Firmware设备udevadm info -a -p /sys/firmware/devicetree/base/leds/power-led5.3 检查驱动匹配状态驱动是否成功匹配可以通过sysfs查看# 查看已注册的平台设备 ls /sys/devices/platform/ # 查看特定设备的驱动绑定和OF节点信息 ls -l /sys/devices/platform/leds/leds/power-led/of_node/ cat /sys/devices/platform/leds/leds/power-led/of_node/compatible如果设备出现在了/sys/devices/platform/但/sys/class/leds/下没有可能是gpio-leds驱动没有成功绑定需要检查compatible属性是否拼写正确或者驱动是否被编译进内核。5.4 常见踩坑点与排查思路节点被禁用 检查设备树节点是否有status disabled;。内核会忽略被禁用的节点。compatible 字符串不匹配 这是最最常见的问题。仔细核对驱动代码里的of_device_id表和设备树里的compatible字符串必须完全一致包括大小写和逗号。资源获取失败 在驱动.probe函数里对每个资源获取函数如devm_gpiod_get,platform_get_irq的返回值进行严格的错误检查并打印错误日志。经常是GPIO编号错了或者该引脚被其他功能占用pinctrl冲突。Pinctrl配置冲突 一个引脚只能有一种功能。如果你的设备不工作检查这个引脚是否在其他地方比如串口、I2C的节点里也被定义了。使用cat /sys/kernel/debug/pinctrl/pinctrl-handles或cat /sys/kernel/debug/pinctrl/pinctrl-maps可以查看当前的引脚复用状态需要内核开启CONFIG_PINCTRL_DEBUG。时钟或电源管理未开启 有些外设需要额外的时钟或电源域。检查设备树中是否包含了必要的clocks、clock-names、power-domains属性并确保引用的时钟控制器或电源域节点本身是使能的。设备树未正确更新 确保你编译的.dtb文件确实被Bootloader加载并传递给了内核。查看内核启动日志的最开始部分通常会打印出它正在解析的设备树文件地址和大小。也可以检查/proc/device-tree符号链接指向是否正确。设备树的调试是一个需要耐心和细致的过程从内核日志、sysfs信息、驱动代码返回值等多个维度交叉验证是定位问题的关键。