嵌入式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 |
| 驱动与硬件耦合度 | 高(驱动直接读取寄存器地址) | 低(驱动通过设备树获取资源) |
| 社区维护成本 | 高(大量重复代码) | 低(硬件描述集中管理) |
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>;
};
};
};
1.3 设备树在 Linux 启动中的作用
Linux 内核启动过程中,设备树扮演着“硬件配置传递者”的角色。从 Bootloader 加载 DTB 到内核解析设备节点,最终驱动匹配硬件,形成完整的设备模型。
1.3.1 启动流程中的设备树传递
- Bootloader 阶段: U-Boot 从存储介质(eMMC、SD 卡、网络)读取 DTB 文件到内存
- 内核入口: Bootloader 将 DTB 物理地址通过寄存器(如 r2)传递给内核
- 早期解析: 内核解压后,
early_init_dt_scan()扫描 DTB 获取内存大小、chosen 节点等 - 设备模型构建:
of_platform_populate()遍历 DTB 创建 platform_device - 驱动匹配: 驱动通过
of_match_table与设备节点的 compatible 属性匹配 - 资源获取: 驱动使用
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 操作 |
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>;
};
};
};
};