嵌入式Linux驱动移植:从3 x到5 x内核升级 - 第4节
4.1 设备树(DTS)基础回顾
设备树(Device Tree,DT)是一种描述硬件资源的数据结构,由一系列节点(node)和属性(property)组成。在嵌入式Linux中,设备树源文件(.dts)经过设备树编译器(DTC)编译为二进制设备树(.dtb),由Bootloader传递给内核。内核通过解析设备树来发现平台设备、内存映射、中断、时钟、GPIO等硬件资源,从而替代了传统3.x内核中大量的板级文件(board-*.c)。
.dtb文件即可。
4.1.1 设备树语法速览
一个典型的设备树节点结构如下:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
leds {
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
led@2 {
label = "beaglebone:green:heartbeat";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
| 语法元素 | 说明 | 示例 |
|---|---|---|
| 节点 | 以 node-name@unit-address 命名,描述一个硬件设备或总线 | i2c@44e0b000 |
| 属性 | 键值对,值可以是字符串、32位整数数组、字节串等 | compatible = "ti,am335x-i2c"; |
| reg | 描述设备地址空间,格式为 <address length> | reg = <0x44e0b000 0x1000>; |
| interrupts | 描述中断号、触发类型 | interrupts = <56 2>; |
| pinctrl | 引脚复用控制,引用已定义的pinmux节点 | pinctrl-0 = <&i2c0_pins>; |
| status | 设备状态:okay、disabled、fail | status = "okay"; |
arch/arm/mach-omap2/board-am335xevm.c中,使用platform_device和i2c_board_info结构体静态注册。5.x内核完全依赖设备树,不再需要这些板级文件。
4.1.2 设备树节点关系SVG图
下图展示了一个简化的设备树层级结构,从根节点到具体外设的路径关系:
4.2 3.x内核板级文件 vs 5.x内核设备树
在Linux 3.x时代,ARM架构的嵌入式平台普遍采用板级文件(board-*.c)来静态注册硬件设备和资源。每个开发板都需要在arch/arm/mach-xxx/目录下维护一个独立的board-xxx.c文件,其中包含大量的platform_device、i2c_board_info、spi_board_info结构体定义。这种方式导致内核代码冗余、维护困难,且无法支持多平台通用内核镜像。
4.2.1 核心差异对比表
| 对比维度 | 3.x 板级文件 | 5.x 设备树 |
|---|---|---|
| 硬件描述位置 | arch/arm/mach-xxx/board-xxx.c | arch/arm/boot/dts/xxx.dts |
| 设备注册方式 | platform_device_register() 静态注册 | 内核解析 DTB 后动态创建 platform_device |
| 资源定义 | struct resource 数组,手动填写地址、中断 | reg、interrupts 属性自动解析 |
| 引脚复用 | pinmux_register() 或板级初始化函数 | pinctrl-* 属性引用 pinmux 节点 |
| 时钟/电源管理 | clk_get() + 板级时钟初始化 | clocks 属性 + 通用时钟框架 |
| 多板支持 | 每个板子一个 board-*.c,编译时选择 | 同一内核镜像,启动时选择 DTB |
| 驱动匹配 | platform_driver 通过 id_table 或名称匹配 | of_match_table 通过 compatible 匹配 |
| 维护成本 | 高,新增板子需大量重复代码 | 低,只需编写 DTS 文件 |
omap_gpio_base)来获取硬件信息。迁移到5.x设备树后,必须改为通过device_node和of_* API读取设备树属性,否则驱动将无法正确获取资源。
4.2.2 板级文件到设备树的迁移流程SVG图
下图展示了从3.x板级文件到5.x设备树的典型迁移路径:
4.3 设备树绑定文档的规范化
设备树绑定文档(Device Tree Binding Documentation)是描述设备树节点属性含义、数据类型、必选/可选字段的规范文档。在3.x内核中,绑定文档分散在Documentation/devicetree/bindings/目录下,格式不统一,部分绑定甚至缺失。5.x内核社区对绑定文档进行了严格的规范化,引入了YAML格式的绑定描述,并配合DT schema工具进行自动化校验。
4.3.1 绑定文档的演进对比
| 特性 | 3.x 绑定文档 | 5.x 绑定文档 |
|---|---|---|
| 格式 | 纯文本(.txt),自由格式描述 | YAML(.yaml),结构化定义 |
| 校验方式 | 人工审查,无自动化工具 | dt_binding_check + dtbs_check 自动化校验 |
| 属性定义 | 文字描述,可能遗漏必选字段 | required 明确列出必选属性,properties 定义类型 |
| 示例 | 有时包含DTS片段,但无语法检查 | 示例DTS片段会被编译校验 |
| 维护者 | 通常由驱动作者维护 | 绑定文档与驱动分离,由设备树维护者审核 |
4.3.2 YAML绑定文档示例
以下是一个规范的YAML绑定文档示例,描述一个I2C EEPROM设备:
# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/eeprom/at24.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
title: AT24 compatible I2C EEPROM
maintainers:
- Bartosz Golaszewski <bgolaszewski@baylibre.com>
properties:
compatible:
enum:
- atmel,at24c01
- atmel,at24c02
- atmel,at24c04
- atmel,at24c08
- atmel,at24c16
reg:
maxItems: 1
description: I2C slave address
pagesize:
$ref: /schemas/types.yaml#/definitions/uint32
description: Page size in bytes for write operations
read-only:
type: boolean
description: Mark the EEPROM as read-only
required:
- compatible
- reg
additionalProperties: false
examples:
- |
eeprom@50 {
compatible = "atmel,at24c02";
reg = <0x50>;
pagesize = <8>;
};
dtbs_check目标会自动校验所有.dts文件是否符合对应的YAML绑定,提前发现属性缺失、类型错误、值越界等问题,极大降低了运行时因设备树错误导致的驱动加载失败。
4.3.3 绑定文档规范化流程SVG图
下图展示了从传统文本绑定到YAML规范化绑定的工作流:
4.4 驱动移植中的设备树适配要点
在将3.x内核驱动移植到5.x内核时,设备树相关的适配工作通常包括以下几个关键步骤:
- 移除板级依赖:删除驱动中对
arch/arm/mach-xxx/中全局变量或函数的直接调用,改为通过device_node获取资源。 - 添加of_match_table:在
platform_driver中增加of_match_table,并定义compatible字符串列表。 - 替换资源获取API:将
platform_get_resource()替换为of_iomap()、of_property_read_u32()等设备树API。 - 处理中断:使用
irq_of_parse_and_map()替代platform_get_irq()(后者在设备树环境下仍然可用,但推荐使用of_*系列)。 - 引脚控制:确保DTS中定义了
pinctrl-*属性,驱动通过pinctrl子系统自动配置引脚复用。 - 时钟与电源:使用
clk_get()配合设备树中的clocks属性,避免硬编码时钟名称。
| 3.x 板级API | 5.x 设备树API | 说明 |
|---|---|---|
platform_get_resource(pdev, IORESOURCE_MEM, 0) | of_iomap(dev->of_node, 0) | 获取内存基地址并映射 |
platform_get_irq(pdev, 0) | irq_of_parse_and_map(dev->of_node, 0) | 解析中断号 |
clk_get(&pdev->dev, "uart_clk") | of_clk_get(dev->of_node, 0) | 从设备树获取时钟 |
gpio_request() + 板级GPIO号 | of_get_named_gpio() + devm_gpio_request() | 通过GPIO属性名获取引脚 |
4.4.1 驱动匹配方式变更示例
3.x内核中,驱动通常使用platform_driver的id_table进行匹配:
/* 3.x 风格:基于id_table匹配 */
static struct platform_device_id my_driver_ids[] = {
{ "my_device", 0 },
{ }
};
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
},
.id_table = my_driver_ids,
};
在5.x内核中,推荐使用of_match_table进行设备树匹配:
/* 5.x 风格:基于of_match_table匹配 */
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-device", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
id_table和of_match_table。内核会优先使用of_match_table进行匹配。但在纯设备树平台(如5.x的ARM64、RISC-V)上,id_table将不会被使用,因此建议完全迁移到of_match_table。
4.5 本章小结
设备树是嵌入式Linux从3.x到5.x内核升级中最核心的变革之一。本章回顾了设备树的基础语法,对比了3.x板级文件与5.x设备树在硬件描述、资源注册、驱动匹配等方面的差异,并介绍了设备树绑定文档从自由文本到YAML规范化的演进。掌握设备树的编写与绑定规范,是成功完成驱动移植的前提。在实际移植工作中,建议优先为硬件平台编写完整的DTS文件,并利用dtbs_check工具确保设备树语法和属性定义的正确性。