1. 课程导论:什么是 Platform 驱动?为什么需要设备树?课程目标与学习路径

1.1 从“字符设备”到“Platform 设备”的演进

在嵌入式 Linux 开发中,驱动程序是连接硬件与操作系统的桥梁。初学者往往从 字符设备驱动 入手,通过 register_chrdev 注册设备号、实现 file_operations 来操作硬件。然而,这种模式在 SoC(片上系统)时代遇到了瓶颈:

  • 硬件不挂在传统总线上:SoC 内部集成的 UART、I2C、SPI、DMA、GPIO 等控制器,并不像 PCI 或 USB 设备那样具有可枚举的总线协议。
  • 资源描述硬编码:驱动中直接写死中断号、内存地址、时钟频率,导致代码与特定板级硬件强耦合,更换硬件平台时必须修改驱动源码。
  • 无法动态匹配:系统启动时,内核无法自动识别这些“片上外设”,需要手动注册设备信息。
核心痛点:传统字符驱动模型缺乏“设备-驱动”分离机制,导致代码复用性差、维护成本高、硬件变更困难。

为了解决上述问题,Linux 内核引入了 Platform 驱动模型(也称为“平台驱动”或“伪总线驱动”)。它将 SoC 内部集成的外设抽象为 Platform 设备,并通过一条虚拟的 Platform 总线 实现设备与驱动的自动匹配。

一句话理解:Platform 驱动是 Linux 内核为“非可枚举总线设备”提供的一套标准驱动框架,让片上外设也能像 PCI/USB 设备一样实现“设备-驱动”分离。
传统字符驱动 vs Platform 驱动模型 传统字符驱动(硬编码) 驱动代码 硬件资源 直接引用 中断号、地址、时钟写死在代码中 更换硬件 → 必须修改驱动源码 Platform 驱动模型(分离) Platform 驱动 Platform 设备 匹配 驱动只关心操作逻辑,不关心具体资源 资源由设备树或板级文件提供

1.2 为什么需要设备树?

在 Linux 早期(2.6 内核时代),Platform 设备的描述是通过 板级文件(board file) 中的 C 语言结构体完成的。例如在 arch/arm/mach-xxx 目录下,每个开发板都有一个 board-xxx.c 文件,里面充斥着 platform_device 的静态定义:

static struct resource uart_resources[] = {
    {
        .start = 0x10000000,
        .end   = 0x10000FFF,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = IRQ_UART0,
        .end   = IRQ_UART0,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device uart_device = {
    .name = "my-uart",
    .id   = 0,
    .num_resources = ARRAY_SIZE(uart_resources),
    .resource = uart_resources,
};

这种方式的弊端随着 ARM 生态的爆发式增长而暴露:

问题 描述 影响
代码膨胀 每个开发板都需要独立的 board-xxx.c 文件 arch/arm 目录下包含数百个板级文件,内核体积臃肿
硬件变更困难 修改硬件配置(如更换 UART 引脚)需要重新编译内核 开发效率低,无法实现“一次编译,多处运行”
碎片化严重 不同厂商的板级文件风格各异,难以统一维护 社区维护成本急剧上升,Linus Torvalds 曾多次批评
无法支持多平台 一个内核镜像只能支持一种开发板 与通用 Linux 发行版理念背道而驰
转折点:2011 年,Linux 3.2 内核开始强制要求 ARM 平台使用设备树(Device Tree),板级文件被逐步淘汰。设备树成为描述硬件资源的“标准语言”。

设备树(Device Tree) 是一种描述硬件拓扑结构的数据结构,它以 .dts(Device Tree Source)文件的形式存在,通过 dtc 编译器生成 .dtb(Device Tree Blob)二进制文件,在系统启动时由 Bootloader 传递给内核。设备树的核心价值在于:

  • 硬件描述与内核代码分离:驱动不再包含硬件资源信息,只负责操作逻辑。
  • 支持多平台:同一份内核镜像,搭配不同的 .dtb 文件即可适配不同硬件。
  • 标准化接口:设备树提供统一的 compatible 属性匹配机制,驱动通过 of_match_table 与设备树节点建立联系。
设备树在系统启动中的角色 .dts 源文件 DTC 编译器 .dtb 二进制 Bootloader 编译 生成 加载 Linux 内核解析 DTB → 创建 Platform 设备 传递 设备树将硬件描述从内核源码中剥离,实现“硬件配置”与“驱动代码”的彻底解耦 同一份内核镜像 + 不同的 .dtb = 支持不同的硬件平台

1.3 Platform 驱动与设备树的匹配机制

当内核启动并解析设备树后,会为每个设备树节点创建一个 Platform 设备。随后,Platform 总线开始遍历已注册的 Platform 驱动,通过 匹配机制 将驱动与设备绑定。匹配的核心依据是设备树节点的 compatible 属性与驱动中 of_match_table 的对应关系。

匹配流程可以概括为以下步骤:

  1. 设备注册:内核解析 DTB,为每个设备树节点创建 platform_device 结构体。
  2. 驱动注册:驱动模块加载时,调用 platform_driver_register 注册 platform_driver。
  3. 总线匹配:Platform 总线调用 platform_match 函数,依次尝试多种匹配方式。
  4. 驱动绑定:匹配成功后,调用驱动的 probe 函数,驱动开始初始化硬件。
匹配方式 匹配依据 优先级 典型场景
OF 匹配 设备树 compatible 属性 vs 驱动 of_match_table 最高 现代 ARM/ RISC-V 平台,设备树是主流
ACPI 匹配 ACPI 表 HID/CID vs 驱动 acpi_match_table 高 x86 平台,如 Intel/AMD SoC
名称匹配 设备 name 字段 vs 驱动 id_table 中的名称 中 传统板级文件或简单设备
ID 匹配 设备 name 字段 vs 驱动 driver.name 低 兼容旧版驱动,不推荐在新代码中使用
实战要点:在设备树时代,OF 匹配 是最常用且推荐的方式。驱动开发者只需在 of_match_table 中声明支持的 compatible 字符串,设备树开发者确保节点属性正确,两者即可自动匹配。
Platform 驱动与设备树匹配流程 设备树侧(Device Tree) DTB 被内核解析 创建 platform_device 设备加入 Platform 总线 compatible = "vendor,device" 驱动侧(Driver) 驱动模块加载 注册 platform_driver 驱动加入 Platform 总线 of_match_table 声明 compatible Platform 总线匹配 platform_match() 匹配成功 → 调用 probe() 函数

1.4 课程目标与学习路径

本课程旨在帮助嵌入式开发者系统掌握 Platform 驱动与设备树匹配机制,从原理到实战,构建完整的知识体系。课程分为以下四个阶段:

阶段 章节范围 核心目标 实战产出
基础篇 第 1-6 章 理解 Platform 驱动模型、设备树语法、匹配原理 手写第一个 Platform 驱动 + 设备树节点
进阶篇 第 7-14 章 掌握设备树资源获取 API、中断、DMA、时钟等复杂资源 实现一个完整的 UART/GPIO 控制器驱动
实战篇 第 15-24 章 多设备树文件、overlay、pinmux、电源管理等高级主题 在 QEMU/真实开发板上验证完整驱动栈
调试与优化篇 第 25-30 章 动态调试、trace、性能分析、兼容性处理 解决实际项目中的匹配失败、资源冲突问题
学习建议:本课程强调“动手实践”。每章都配有可运行的代码示例和设备树文件,建议在 Linux 开发环境(Ubuntu + QEMU 或真实 ARM 开发板)上同步操作。遇到匹配问题时,优先检查 /sys/bus/platform/devices/ 和 /sys/bus/platform/drivers/ 下的文件,这是最直观的调试入口。

通过本课程的学习,你将能够:

  • 独立编写符合规范的 Platform 驱动,并正确编写对应的设备树节点。
  • 深入理解 platform_match 的四种匹配机制及其优先级。
  • 熟练使用 of_ 系列 API 从设备树中获取内存、中断、GPIO、时钟等资源。
  • 掌握设备树 overlay 动态加载技术,实现硬件功能的运行时切换。
  • 具备分析和解决实际项目中驱动匹配失败、资源冲突等问题的能力。
预备知识:建议读者具备 C 语言基础、Linux 基本操作(文件系统、命令行)、以及字符设备驱动的入门知识。如果对 file_operations 和 module_init 等概念还不熟悉,建议先阅读 Linux 字符设备驱动的入门教程。