[TOC]
drivers/base/platform.c 平台设备模型(Platform Device Model) 描述SoC内部设备
历史与背景
这项技术是为了解决什么特定问题而诞生的?
drivers/base/platform.c
文件实现了Linux内核中的“平台设备模型”(Platform Device Model)。这项技术是为了解决嵌入式系统(特别是基于ARM等SoC的设备)中如何描述和管理那些不通过标准总线(如PCI, USB, I2C, SPI)枚举,而是直接由系统集成商(SoC厂商)通过内存映射地址或其他固定方式连接到CPU总线上的设备的问题。
在没有平台设备模型之前,这些SoC内部的设备(如GPIO控制器、UART控制器、时钟控制器、中断控制器、DMA控制器等)的驱动程序通常是零散地被编写的,并且管理方式非常底层和不统一。开发者需要手动解析设备树(Device Tree)或其他平台数据,获取设备的基地址、长度、中断号等信息,然后手动映射内存、申请中断,并将这些信息传递给驱动。这种方式存在以下问题:
- 代码冗余:每种类型的设备驱动都需要重复实现获取和解析设备信息的代码。
- 缺乏统一性:驱动程序没有一个通用的接口来描述自己支持的设备,也无法通过设备模型进行统一管理。
- 不易于管理:对这些设备的生命周期管理(如probe, remove, suspend, resume)缺乏统一的框架。
平台设备模型通过引入 struct platform_device
和 struct platform_driver
,以及一个总线类型(”platform” bus),提供了一个标准化的接口来描述这些设备,并将它们纳入到Linux统一的设备模型和驱动核心框架中进行管理。
它的发展经历了哪些重要的里程碑或版本迭代?
平台设备模型的发展与Linux在嵌入式领域的应用和设备树(Device Tree)的普及密切相关。
- 早期(Linux 2.5/2.6):随着Linux内核向嵌入式领域扩展,需要一种方式来描述SoC上的设备。最初可能是一些基于ACPI或主板特定描述文件的方法,但对于ARM等平台,更通用的方式是基于设备树。
- 设备树(Device Tree)的普及:在ARM架构中,设备树(DT)成为描述硬件配置的标准方式。DT通过
.dts
(Device Tree Source)文件描述了硬件拓扑、设备属性、资源(内存基地址、中断号、GPIO等)。 - 平台设备模型与DT的结合:平台设备模型成为解析DT中描述的“平台设备”信息,并将其转换为内核可以识别和管理的
struct platform_device
的关键机制。DT核心会解析DT节点,为每个符合条件的节点创建一个struct platform_device
实例。 - 统一驱动模型集成:平台设备模型本身是Linux统一设备模型的一部分。它通过为
struct platform_driver
定义标准的.probe
,.remove
,.suspend
,.resume
等回调函数,并将其注册到platform_bus_type
,使得平台设备能够与驱动核心的其他部分无缝集成,享受通用驱动管理带来的好处。 - ACPI支持:在x86等使用ACPI的平台上,ACPI表也能被解析,从中提取平台设备信息,创建
struct platform_device
。
目前该技术的社区活跃度和主流应用情况如何?
平台设备模型是Linux内核中一个非常成熟、稳定且极其活跃的核心子系统。它几乎是所有SoC(如ARM Cortex-A系列、RISC-V等)内部设备驱动(如CPU内部的UART、SPI控制器、I2C控制器、GPIO控制器、定时器、中断控制器、显示控制器、内存控制器等)的基础。任何为嵌入式系统编写的驱动,只要其设备信息是通过设备树或ACPI描述的,几乎都会使用平台设备模型来注册和管理。
核心原理与设计
它的核心工作原理是什么?
平台设备模型的核心是将硬件设备的描述(通常来自设备树或ACPI)转换为内核可管理的“平台设备”和“平台驱动”配对。
设备树(或ACPI)解析:
- 当内核启动并初始化设备树(或ACPI)时,DT/ACPI解析器会遍历硬件描述。
- 对于那些被描述为“平台设备”的节点(通常通过
compatible
属性来识别,如"vendor,soc-uart"
),解析器会创建一个struct platform_device
实例。 - 这个
struct platform_device
会包含该设备的所有信息,如:- 设备名(通常来自DT节点名或
compatible
属性)。 - 资源列表(Memory regions, I/O ports, IRQs, GPIOs等)。
- 平台数据(
void *platform_data
,存储特定于该设备的、驱动需要的数据)。 - I/O资源描述。
- 设备名(通常来自DT节点名或
平台设备注册:
- DT/ACPI解析完成后,会将所有创建的
struct platform_device
实例注册到内核的平台总线(platform_bus_type
)上。 - 注册过程会触发驱动核心(
drivers/base/driver.c
)尝试将该设备与已注册的、支持它的platform_driver
进行匹配。
- DT/ACPI解析完成后,会将所有创建的
平台驱动注册:
- 驱动开发者定义一个
struct platform_driver
结构体,其中包含:- 驱动名。
- 匹配函数(
.match
)或compatible
字符串数组(在DT环境中,平台总线会根据DT节点的compatible
属性自动匹配)。 - 标准的驱动回调函数:
.probe
(初始化),.remove
(卸载),.suspend
(休眠),.resume
(唤醒) 等。
- 驱动开发者通过
platform_driver_register()
将其驱动注册到内核。
- 驱动开发者定义一个
设备-驱动绑定:
- 当一个
platform_device
被注册到平台总线,并且系统中有与之匹配的platform_driver
存在时,驱动核心会调用该驱动的.probe()
函数。 - 在
.probe()
函数中,驱动可以安全地访问设备的所有资源(如通过platform_get_resource()
获取内存地址、中断号等),并进行初始化。
- 当一个
生命周期管理:
- 驱动的
.remove()
函数会在设备被移除(如DT节点被移除,或执行driver_unregister()
的逆操作)时被调用。 .suspend()
和.resume()
回调函数会在系统执行休眠和唤醒时被内核电源管理子系统调用。
- 驱动的
它的主要优势体现在哪些方面?
- 标准化:为SoC内部设备提供了一个统一的描述和管理框架,无论其具体硬件功能是什么。
- 解耦:将硬件描述(DT/ACPI)与驱动逻辑完全分离。驱动程序不需要关心硬件是如何被具体描述的,只需要知道如何解析平台设备提供的资源。
- 自动化匹配:通过
compatible
字符串等机制,实现驱动与设备的自动匹配,无需手动干预。 - 集成到通用设备模型:平台设备和驱动可以无缝地接入Linux的设备模型,享受设备模型提供的所有通用服务,如sysfs接口、电源管理、设备资源管理(devres)等。
- 简化驱动开发:驱动开发者只需要实现特定于硬件的逻辑,而资源的获取、设备的匹配、生命周期管理等通用工作由平台层和驱动核心完成。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
drivers/base/platform.c
作为平台设备模型的核心,其设计非常健壮。其“劣势”更多体现在它的设计哲学限制上:
- 仅适用于特定类型的设备:平台设备模型主要用于描述那些不通过标准总线枚举的、直接连接到CPU总线上的设备。它不适用于通过PCIe、USB、I2C、SPI等标准总线发现的设备(这些设备有各自的总线驱动模型)。
- 依赖于固件描述:平台设备信息(资源、匹配信息)通常需要从设备树或ACPI等硬件描述语言中解析而来。如果硬件描述不完整或不正确,那么平台设备及其驱动的初始化就会失败。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
平台设备模型是处理SoC内部集成硬件设备驱动的标准和唯一解决方案。
- 场景一:ARM SoC的UART控制器
一个ARM Cortex-A SoC包含几个内置的UART控制器。在设备树源文件(.dts
)中,会为每个UART控制器节点定义其内存基地址、中断号、时钟源以及compatible
属性(如"vendor,soc-uart-v1"
)。内核启动时,DT解析器会为每个UART创建一个struct platform_device
。然后,drivers/uart/uart_ 8250_native.c
或其他UART驱动的platform_driver
会根据compatible
字符串进行匹配,调用其.probe()
函数来初始化UART硬件。 - 场景二:GPIO控制器驱动
SoC集成的GPIO控制器,其操作寄存器和中断通常是直接映射到CPU地址空间的。设备树中会描述GPIO控制器的资源(内存映射的寄存器地址、中断号)以及它所能控制的GPIO引脚的配置信息。内核的DT驱动会为GPIO控制器创建一个struct platform_device
,然后drivers/gpio/gpio- 6button.c
或相关GPIO库驱动的platform_driver
会匹配并注册,以便内核的其他部分可以通过GPIO子系统来访问这些引脚。 - 场景三:CPU内部定时器和中断控制器
系统时钟、高精度定时器(HPET)、高级可编程中断控制器(APIC)等核心硬件,通常也通过平台设备模型来描述和驱动。
是否有不推荐使用该技术的场景?为什么?
- 总线枚举设备:对于通过PCIe总线发现的网卡、USB控制器,或通过USB总线发现的USB设备,它们有各自更适合的驱动模型(PCI驱动模型、USB驱动模型)。虽然理论上也可以将它们抽象成平台设备,但这会失去总线特有的信息(如PCI Vendor ID/Device ID),并且增加了不必要的复杂性。
- 纯软件虚拟设备:对于完全由软件模拟产生的虚拟设备(例如,在
drivers/base/faux.c
中介绍的Faux Bus所管理的设备),使用平台设备模型是“滥用”,因为它提供了远超所需的匹配和资源管理功能。
对比分析
请将其 与 其他相似技术 进行详细对比。
平台设备模型的主要对比对象是其他Linux内核中的总线驱动模型,特别是PCI和USB,以及与硬件描述相关的设备树。
特性 | Platform Device Model (platform.c ) |
PCI Device Model (drivers/pci/* ) |
USB Device Model (drivers/usb/* ) |
Device Tree (DT) |
---|---|---|---|---|
设计用途 | 描述SoC内部、非标准总线枚举的设备,通常由DT/ACPI描述。 | 描述通过PCI总线连接的标准外设。 | 描述通过USB总线连接的标准外设。 | 描述硬件的拓扑结构、资源分配和设备属性,作为所有总线模型的输入。 |
硬件发现 | 间接:通过DT/ACPI解析,非总线枚举。 | 直接:PCI总线进行枚举,识别Vendor/Device ID。 | 直接:USB总线进行枚举,识别Vendor/Product ID和Class。 | 提供描述:DT文件定义了硬件,内核根据DT创建设备。 |
驱动匹配 | 间接:DT/ACPI的compatible 字符串、设备名。 |
直接:PCI Vendor/Device ID、Subsystem ID、Class Code。 | 直接:USB Vendor/Product ID、Class/SubClass/Protocol。 | 首要信息:DT的compatible 字符串是设备识别的关键。 |
核心实体 | struct platform_device , struct platform_driver |
struct pci_dev , struct pci_driver |
struct usb_device , struct usb_driver |
struct device_node , struct property |
资源获取 | platform_get_resource() , platform_get_irq() , devm_platform_get_resource() |
pci_request_regions() , pci_ioremap_bar() , pci_enable_msi() |
usb_get_intf() , usb_alloc_coherent() |
由DT节点中的reg , interrupts , gpios , clocks 等属性描述,并由相应总线驱动解析。 |
生命周期管理 | 集成到统一设备模型,由driver.c 协调Probe/Remove/Suspend/Resume。 |
集成到统一设备模型,有专门的PCI电源管理。 | 集成到统一设备模型,有专门的USB电源管理。 | 提供硬件描述,但不直接管理驱动生命周期,由总线驱动负责。 |
platform_bus: 代表/sys/devices/platform的根设备
这个结构体定义了一个虚拟的根设备。它本身不对应任何具体的硬件,其唯一的作用是在sysfs
文件系统中创建一个名为/sys/devices/platform
的目录,作为所有平台设备的父容器,将它们组织在一起。
1 | /* |
platform_dev_group: 定义平台设备在sysfs中的属性文件组
这些结构体和宏定义了当一个平台设备被注册后,在其sysfs
目录中应该出现哪些属性文件。is_visible
回调函数还可以根据具体设备的情况动态决定某个文件是否应该显示。
1 | /* |
platform_dev_pm_ops: 定义平台总线的电源管理回调函数
这个结构体定义了平台设备默认的电源管理操作。当系统进入休眠或设备进入运行时挂起状态时,内核会调用这些函数。
1 | /* |
这组函数共同定义了平台总线(Platform Bus)的核心行为,它们作为platform_bus_type
结构体的回调函数,构成了Linux内核驱动模型中设备与驱动交互的完整生命周期。对于STM32H750这样的嵌入式系统,平台总线是管理其所有片上外设(如SPI、I2C、UART、DMA等)的基础。这些回调函数确保了当一个在设备树中定义的设备被注册时,内核能够正确地为其找到并加载驱动、配置所需资源(时钟、DMA、电源)、并在需要时安全地移除它。
platform_match: 匹配平台设备与平台驱动
此函数是平台总线的核心匹配逻辑。每当一个新设备或新驱动被注册到平台总线时,内核就会调用此函数来判断它们是否“匹配”。只有匹配成功,后续的probe
(探测)过程才会被触发。
1 | /** |
platform_uevent: 为平台设备生成uevent事件
当平台设备被注册或移除时,此函数被调用以生成一个uevent
。用户空间的udev
守护进程会监听这些事件,并根据事件内容(主要是MODALIAS
)自动加载或卸载对应的驱动程序内核模块(.ko
文件)。
1 | /* |
platform_probe: 探测并初始化平台设备
在platform_match
成功后,内核驱动核心会调用此函数。它作为总线层和具体驱动之间的桥梁,负责执行一些通用的准备工作(如配置时钟和电源域),然后调用具体驱动程序提供的probe
函数,以完成硬件的最终初始化。
1 | /* |
platform_remove, platform_shutdown: 移除和关闭平台设备
这两个函数分别在设备被移除(例如,驱动模块被卸载)或系统关机时被调用。它们负责调用具体驱动中的对应函数,以执行反初始化和清理工作。
1 | static void platform_remove(struct device *_dev) |
platform_dma_configure, platform_dma_cleanup: 配置和清理DMA
这两个函数是为设备配置DMA(直接内存访问)的关键钩子。对于STM32H750上的高性能外设,DMA是必不可少的。platform_dma_configure
在设备探测期间被调用,负责解析设备树中的DMA相关属性,并配置好DMA通道和IOMMU(如果存在)。
1 | static int platform_dma_configure(struct device *dev) |
platform_bus_type: 定义平台总线的核心行为和属性
这是平台总线模型中最重要的结构体。它定义了总线的所有行为,如如何匹配设备和驱动,如何在匹配成功后执行探测(probe),以及总线在sysfs
中的呈现方式。
1 | /* |
platform_bus_init: 注册平台总线和根设备
这个函数在内核启动过程中被调用,它执行了将平台总线注册到内核驱动核心的实际操作。__init
属性告诉编译器将此函数放在特殊的初始化段中,在启动完成后,这段代码所占用的内存会被释放,这对于内存有限的嵌入式系统来说非常重要。
1 | void __weak __init early_platform_cleanup(void) { } |
platform_driver_register 向内核注册一个平台驱动程序
此代码定义了 Linux 内核中的平台总线类型 (platform_bus_type
) 并提供了平台驱动注册函数 (platform_driver_register
)。其核心作用是为非自发现(non-discoverable)的、基于内存地址映射的硬件设备, 提供一套标准的驱动注册与设备绑定机制。
原理与作用: 平台总线——管理非自发现设备
什么是总线 (Bus)? 在Linux驱动模型中, “总线”是一种软件概念, 用于连接设备和驱动。对于像USB或PCI这样的物理总线, 设备可以自发现 (self-discovering)。当你插入一个USB设备时, 它会主动向主机报告自己的ID, 内核根据这个ID去查找并加载匹配的驱动。
什么是平台总线 (Platform Bus)? 在像STM32H750这样的嵌入式系统中, 大部分外设 (如UART、I2C、SPI、定时器、GPIO控制器) 都不是自发现的。它们“固定地”存在于芯片的特定物理地址上。内核无法主动去扫描并识别它们。这些设备就被称为平台设备 (platform devices)。平台总线就是一个虚拟的、不存在于物理硬件上的总线, 专门用来管理这些平台设备和它们的驱动。
平台总线 (
platform_bus_type
) 的定义:platform_bus_type
是一个const struct bus_type
类型的全局实例。它作为一个软件实体, 定义了平台总线的所有行为规则, 其关键成员为函数指针:.name
: 字符串 “platform”, 作为该总线在sysfs
文件系统中的标识 (/sys/bus/platform
)。.match
: 指向platform_match
函数。当一个新设备或新驱动注册到平台总线时, 内核的驱动核心 (driver core) 会调用此函数, 以判断该设备与驱动之间是否存在对应关系。匹配逻辑通常是比较设备 (来自设备树) 的compatible
属性与驱动支持的compatible
字符串列表。.probe
: 指向platform_probe
函数。当.match
函数返回成功后, 驱动核心会调用此函数,platform_probe
再进一步调用驱动程序自身实现的.probe
函数, 以执行硬件的初始化。- 其他函数指针定义了总线在移除设备、关机、电源管理和DMA配置等不同阶段的行为。
1 | /* |
platform_get_irq_optional
和 platform_get_irq
: 获取平台设备的中断号
这两个函数是Linux内核平台设备驱动模型中用于从硬件描述(主要是设备树)中获取中断请求(IRQ)号的标准API。它们是驱动程序将其自身与硬件中断连接起来的桥梁。
platform_get_irq_optional
是核心的、”安静”的工作函数, 而platform_get_irq
则是它的一层”健谈”的封装。
platform_get_irq_optional
: 以”可选”方式获取中断号
此函数的核心原理是提供一个统一的、跨多种固件接口(Device Tree, ACPI, 旧式平台数据)的、健壮的中断号发现机制。它会按照预设的优先级顺序尝试多种方法来解析中断信息。对于STM32H750这样的、完全依赖设备树(Device Tree)的嵌入式系统, 其主要执行路径如下:
- 设备树优先: 函数首先检查设备是否有一个设备树节点 (
is_of_node
)。如果是(STM32平台总是如此), 它会立即调用of_irq_get
。这个函数会直接解析设备树节点中的interrupts
或interrupts-extended
属性, 并将其翻译成一个Linux内核可以使用的IRQ号。 - 处理依赖:
of_irq_get
是一个强大的函数, 如果它发现中断控制器驱动尚未就绪, 它不会立即失败, 而是会返回-EPROBE_DEFER
。platform_get_irq_optional
会忠实地将这个返回值传递上去, 从而触发内核的延迟探测机制, 保证了正确的驱动加载顺序。 - 旧式平台数据回退: 如果从设备树获取失败(例如, 设备树文件不规范), 它会回退到
platform_get_resource
。这是一种较旧的机制, 用于从驱动程序硬编码或由板级文件提供的”平台资源”数组中查找类型为IORESOURCE_IRQ
的资源。 - 设置触发类型: 如果通过
platform_get_resource
找到了中断, 它还会检查资源标志位, 并使用irqd_set_trigger_type
来配置中断的触发方式(如上升沿、下降沿、高电平或低电平触发)。 - 最终检查: 在返回之前, 它会进行一个重要的安全检查:
WARN(!ret, "0 is an invalid IRQ number\n")
。在Linux中, IRQ 0通常被认为是无效或保留的。如果函数最终计算出的IRQ号为0, 它会打印一个内核警告并返回-EINVAL
, 防止驱动程序使用一个无效的中断。
“Optional”(可选)的含义在于, 如果所有方法都找不到中断, 它只会”安静地”返回一个错误码(-ENXIO
), 而不会在内核日志中打印错误信息。这对于那些中断是可选功能(例如, 一个可以通过轮询或中断模式工作的驱动)的设备非常有用。
1 | /** |
platform_get_irq
: 以”必需”方式获取中断号
此函数是一个简单但非常实用的封装。它的原理是在platform_get_irq_optional
的基础上, 增加了一个自动的错误处理和日志记录层。
当一个驱动程序需要的中断是其正常运行所必需的时, 应该使用这个函数。如果platform_get_irq_optional
返回错误, platform_get_irq
会使用dev_err_probe
函数:
- 在内核日志中打印一条标准的、格式化的错误信息, 其中包含了设备名和请求失败的中断索引号。这对于调试非常有帮助。
dev_err_probe
能智能地处理-EPROBE_DEFER
错误码。在这种情况下, 它会返回-EPROBE_DEFER
本身而不会打印错误消息, 因为这是一个正常的、可恢复的”依赖未就绪”状态, 而不是一个真正的错误。
1 | /** |
平台设备资源获取与映射辅助函数
此代码片段提供了一组在Linux平台设备驱动模型中广泛使用的、至关重要的辅助函数。它们的核心作用是为平台驱动程序(platform driver)提供一个标准的、便捷的、且高度安全的方式, 来获取其对应的硬件设备(platform device)所拥有的资源(特别是内存映射的I/O寄存器地址), 并将其映射到内核的虚拟地址空间中。
这套API的原理是将资源查找和ioremap
操作紧密地封装在一起, 并利用devm
框架实现全自动的生命周期管理, 从而极大地简化了驱动程序的编写并提高了其健壮性。
platform_get_resource
: 这是最基础的查找函数。平台设备(struct platform_device
)在被内核实例化时, 会从设备树(Device Tree)中读取其reg
属性, 并将这些信息(物理起始地址, 大小)预先填充到一个名为resource
的数组中。此函数的作用就是让驱动程序能够通过类型和索引号, 在这个预填充的数组中找到一个特定的资源。devm_platform_..._ioremap_resource...
: 这一系列函数是更高层的封装。它们首先在内部调用platform_get_resource
或platform_get_resource_byname
来找到目标资源, 然后立即将该资源传递给devm_ioremap_resource
函数(我们之前分析过)。devm_ioremap_resource
会负责请求内存区域、执行ioremap
映射、并将所有这些操作注册到devm
框架中。这意味着驱动程序的开发者在probe
函数中只需调用这一个函数, 就可以完成资源获取和映射, 并且完全无需在错误处理路径或驱动的remove
函数中手动编写iounmap
或release_mem_region
的代码。内核设备模型会在驱动卸载时自动完成所有清理工作。
platform_get_resource
: 获取一个平台设备资源
1 | /** |
devm
框架下的封装函数
1 |
|