嵌入式Linux驱动:设备树覆盖与驱动移植实战 - 第1节

1.1 设备树的起源与设计动机

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,最早源于 Open Firmware(IEEE 1275)规范。在嵌入式 Linux 早期,内核通过大量 board-*.c 文件硬编码板级信息,导致每次更换 SoC 或外设都需要重新编译内核。设备树的引入将硬件描述与内核代码分离,实现了“同一内核镜像支持多款硬件”的目标。

核心动机: 解决 ARM Linux 社区长期存在的“板级文件泛滥”问题,将硬件配置从 C 代码迁移到结构化文本中。

1.1.1 设备树 vs 传统板级文件

对比维度 传统板级文件(board-*.c) 设备树(DTS/DTB)
硬件描述位置 内核源码 C 文件 独立 .dts 文本文件
修改硬件支持 需修改内核源码并重新编译 仅修改 .dts 文件,重新编译 DTB
内核镜像通用性 每款板卡需单独编译 同一镜像可加载不同 DTB
驱动与硬件耦合度 高(驱动直接读取寄存器地址) 低(驱动通过设备树获取资源)
社区维护成本 高(大量重复代码) 低(硬件描述集中管理)
设备树在嵌入式 Linux 中的定位 传统板级文件方式 board-xxx.c 硬编码 寄存器地址、中断号、GPIO ↓ 每次改硬件 → 改内核源码 ↓ 重新编译内核镜像 ↓ 烧录整机 ❌ 维护成本高 设备树方式 myboard.dts 文本描述 寄存器地址、中断号、GPIO ↓ 改硬件 → 改 .dts 文件 ↓ 仅编译 DTB ↓ 内核加载新 DTB ✅ 硬件与内核解耦

1.2 设备树语法结构

设备树源文件(.dts)采用树形层级结构,以 /dts-v1/; 开头标识版本。每个设备树由根节点 / 和若干子节点组成,节点内包含属性(property)描述硬件特征。

1.2.1 基本语法规则

  • 节点定义: node-name@unit-address,其中 unit-address 通常为寄存器基址
  • 属性格式: property-name = value; 或 property-name;(空属性)
  • 标签(label): 节点前可加 label:,用于引用或覆盖
  • 包含文件: #include "soc.dtsi" 引入 SoC 公共定义
  • 注释: 支持 C 风格 /* */ 和 C++ 风格 //
实践建议: 编写 .dts 文件时,优先使用 .dtsi 文件存放 SoC 级通用描述,板级 .dts 只做差异覆盖,提高复用性。

1.2.2 节点与属性详解

语法元素 示例 说明
根节点 / { ... }; 设备树顶层容器,所有设备挂载其下
子节点 uart0: serial@10000000 { ... }; 标签 uart0,节点名 serial,地址 0x10000000
compatible 属性 compatible = "vendor,device"; 驱动匹配的关键字符串,优先级从高到低
reg 属性 reg = <0x10000000 0x1000>; 寄存器地址和长度,支持多段
interrupts 属性 interrupts = <0 31 4>; 中断号、触发类型等
status 属性 status = "okay"; 或 "disabled" 控制设备是否启用
// 一个完整的设备树片段示例
/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "Embedded Linux Board";
    compatible = "vendor,myboard", "vendor,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB RAM
    };

    uart1: serial@02020000 {
        compatible = "vendor,imx6ull-uart";
        reg = <0x02020000 0x4000>;
        interrupts = <0 26 4>;
        status = "okay";
    };

    gpio-leds {
        compatible = "gpio-leds";
        led-red {
            label = "red-led";
            gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
        };
    };
};
设备树节点层级结构 / (根节点) chosen memory@80000000 uart1: serial@... gpio-leds led-red 节点属性示例 compatible = "vendor,myboard"; // 驱动匹配字符串 reg = <0x80000000 0x20000000>; // 地址 + 长度 status = "okay"; // 设备启用状态

1.3 设备树在 Linux 启动中的作用

Linux 内核启动过程中,设备树扮演着“硬件配置传递者”的角色。从 Bootloader 加载 DTB 到内核解析设备节点,最终驱动匹配硬件,形成完整的设备模型。

1.3.1 启动流程中的设备树传递

  1. Bootloader 阶段: U-Boot 从存储介质(eMMC、SD 卡、网络)读取 DTB 文件到内存
  2. 内核入口: Bootloader 将 DTB 物理地址通过寄存器(如 r2)传递给内核
  3. 早期解析: 内核解压后,early_init_dt_scan() 扫描 DTB 获取内存大小、chosen 节点等
  4. 设备模型构建: of_platform_populate() 遍历 DTB 创建 platform_device
  5. 驱动匹配: 驱动通过 of_match_table 与设备节点的 compatible 属性匹配
  6. 资源获取: 驱动使用 of_* API 读取 reg、interrupts、GPIO 等资源
常见陷阱: DTB 地址传递错误是启动失败的常见原因。确保 Bootloader 中 fdt_addr_r 与内核期望的地址一致,且 DTB 大小不超过预留内存区域。

1.3.2 关键内核 API 与设备树交互

API 函数 作用 使用阶段
of_find_node_by_path() 通过路径查找节点 驱动 probe
of_property_read_u32() 读取 32 位整型属性 资源解析
of_iomap() 将 reg 地址映射到虚拟空间 寄存器访问
irq_of_parse_and_map() 解析中断并映射 中断注册
of_get_named_gpio() 获取 GPIO 编号 GPIO 操作
设备树在 Linux 启动中的角色 Bootloader 加载 DTB 到内存 内核入口 解析 DTB 头部 早期扫描 内存/chosen 节点 设备模型 创建 platform_device 驱动匹配阶段 of_match_table → compatible 资源获取 of_iomap / irq_of_parse_and_map 关键:DTB 是硬件与驱动之间的“契约文档” 修改硬件时只需更新 DTB,无需重新编译内核

1.4 设备树覆盖(Device Tree Overlay)简介

设备树覆盖(DTO)允许在不修改主 DTB 的前提下,动态添加或修改设备节点。这对于 FPGA 动态加载、外设热插拔、模块化硬件扩展等场景至关重要。覆盖文件(.dtso)语法与 .dts 一致,但使用 /plugin/; 标识,并通过 target 或 target-path 指定挂载位置。

覆盖典型应用: 树莓派 HAT 自动识别、FPGA 动态重配置、扩展板设备自动加载。
// 设备树覆盖示例:添加一个 I2C 温度传感器
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &i2c1;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;

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