ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式Linux设备树:从硬件描述到驱动匹配的完整解析

嵌入式Linux设备树:从硬件描述到驱动匹配的完整解析

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: cpu@0 { // CPU0节点,带有一个标签‘cpu0’ device_type = "cpu"; compatible = "arm,cortex-a55"; reg = <0x0 0x0>; enable-method = "psci"; }; }; memory@0 { // 内存节点 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"; }; }; };
  • 节点: 用花括号{}定义,如/(根节点)、cpusleds。节点可以嵌套,形成父子关系。
  • 属性: 节点内的键值对,格式为属性名 = 值;。例如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 编译与传递流程

  1. 编写: 开发者编辑.dts.dtsi文件。
  2. 编译: 使用设备树编译器dtc.dts编译成.dtb
    dtc -I dts -O dtb -o rk3568-evb.dtb rk3568-evb.dts
  3. 传递: Bootloader(如U-Boot)将编译好的.dtb文件加载到内存的特定地址,然后在启动内核时,通过寄存器(如ARM的r2寄存器)或特定的启动协议(如ARM的ATAGs之后的DTB)将这个地址告诉内核。
  4. 解析: 内核启动早期,会解析这块内存区域,在内存中构建出设备树的结构,然后根据节点信息逐一初始化平台设备和设备驱动。

3. 内核如何与设备树共舞:驱动匹配与资源获取

内核拿到设备树二进制块后,具体是怎么用的呢?这涉及到驱动模型的核心。

3.1 平台设备的自动创建

在引入设备树之前,开发者需要手动编写代码,用platform_device_register()来注册一个平台设备。现在,这个过程是自动的。内核在解析设备树时,对于某些特定类型的节点(通常是那些在根节点下,有compatible属性但没有status = "disabled"的节点),会自动为其生成一个platform_device结构体。

这个自动创建的platform_device会包含从设备树节点中提取的关键信息,其中最重要的就是compatible字符串列表。这个列表会被放入platform_deviceof_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_为前缀的API(Open 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>;
  • 解析寄存器地址(reg)

    struct 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"(心跳)、"mmc0"(SD卡活动)等,实现自动闪烁。

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 编译与更新设备树

  1. 在Linux内核源码根目录下,使用你的交叉编译工具链:
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
    这条命令会编译所有设备树,或者指定你的板子:
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip/rk3568-evb.dtb
  2. 将生成的rk3568-evb.dtb文件(位于arch/arm64/boot/dts/rockchip/)替换掉你的开发板Bootloader加载的设备树文件。具体方法取决于你的启动方式(SD卡、eMMC、tftp等)。

4.4 验证与使用

启动开发板后,如果配置正确,你应该能看到:

  1. 内核日志dmesg | grep ledsdmesg | grep gpio,可能会看到类似leds: gpio-leds: power-led的注册成功信息。
  2. Sysfs接口ls /sys/class/leds/,你应该能看到power-led目录。
  3. 控制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可以列出所有OF(Open Firmware)设备:

udevadm info -a -p /sys/firmware/devicetree/base/leds/power-led

5.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 常见踩坑点与排查思路

  1. 节点被禁用: 检查设备树节点是否有status = "disabled";。内核会忽略被禁用的节点。
  2. compatible 字符串不匹配: 这是最最常见的问题。仔细核对驱动代码里的of_device_id表和设备树里的compatible字符串,必须完全一致,包括大小写和逗号。
  3. 资源获取失败: 在驱动.probe函数里,对每个资源获取函数(如devm_gpiod_get,platform_get_irq)的返回值进行严格的错误检查,并打印错误日志。经常是GPIO编号错了,或者该引脚被其他功能占用(pinctrl冲突)。
  4. Pinctrl配置冲突: 一个引脚只能有一种功能。如果你的设备不工作,检查这个引脚是否在其他地方(比如串口、I2C的节点里)也被定义了。使用cat /sys/kernel/debug/pinctrl/pinctrl-handlescat /sys/kernel/debug/pinctrl/pinctrl-maps可以查看当前的引脚复用状态(需要内核开启CONFIG_PINCTRL_DEBUG)。
  5. 时钟或电源管理未开启: 有些外设需要额外的时钟或电源域。检查设备树中是否包含了必要的clocksclock-namespower-domains属性,并确保引用的时钟控制器或电源域节点本身是使能的。
  6. 设备树未正确更新: 确保你编译的.dtb文件确实被Bootloader加载并传递给了内核。查看内核启动日志的最开始部分,通常会打印出它正在解析的设备树文件地址和大小。也可以检查/proc/device-tree符号链接指向是否正确。

设备树的调试是一个需要耐心和细致的过程,从内核日志、sysfs信息、驱动代码返回值等多个维度交叉验证,是定位问题的关键。

返回列表