Linux 平台设备(Platform Device)驱动框架是内核中用于管理嵌入式系统或 SoC(System on Chip)中非可发现设备的机制。这些设备不像 PCI 或 USB 设备那样可以通过总线自动枚举,而是通过静态配置(如设备树 Device Tree 或平台数据)来描述,例如内置的 UART、GPIO、I2C 控制器、LED 等。平台设备框架基于设备模型(Device Model),通过"平台总线"(platform bus)模拟一个虚拟总线来统一管理这些设备。该框架简化了驱动开发,适用于 ARM、x86 等架构的嵌入式系统。本文基于 Linux 内核 4.x 及以上版本(兼容 6.x),从框架概述、关键数据结构、注册流程、probe/remove 实现、示例代码以及注意事项等方面进行详解。
1. 框架概述
在 Linux 内核中,设备分为可发现(discoverable,如 PCI)和非可发现(non-discoverable,如 SoC 内置外设)。平台设备框架针对后者,提供了一个虚拟的"平台总线"来处理这些设备:
- 平台总线:一个伪总线(bus_type 为 platform_bus_type),用于挂载平台设备和驱动。
- 设备描述:早期使用平台数据(platform_data)结构体静态定义资源(如 IRQ、内存地址);现代内核推荐使用设备树(Device Tree,.dts 文件)通过 compatible 属性匹配设备和驱动。
- 匹配机制:驱动和设备通过名称(name)或 compatible 字符串匹配。
- 生命周期:设备注册后,内核调用驱动的 probe 函数初始化硬件;卸载时调用 remove。
- 优势:统一设备模型,支持电源管理(PM)、sysfs 接口;缺点:依赖静态配置,不如热插拔总线灵活。
- 适用场景:嵌入式板卡如 Raspberry Pi、BeagleBone 中的 GPIO、SPI 等控制器。
平台设备与普通字符设备驱动的区别:平台设备更注重硬件资源管理(如获取 IO 地址、IRQ),而字符设备焦点在文件操作接口。平台驱动常结合字符/杂项设备提供用户空间访问。
2. 关键数据结构
平台设备框架依赖几个核心结构体,定义在 <linux/platform_device.h> 和 <linux/device.h> 中。
-
struct platform_device :描述平台设备。
struct platform_device { const char *name; // 设备名,用于匹配 int id; // 实例 ID(如 uart0 中的 0),-1 表示自动 struct device dev; // 嵌入的通用设备结构体 u32 num_resources; // 资源数量 struct resource *resource; // 资源数组(内存、IRQ 等) const struct platform_device_id *id_entry; // ID 表匹配 const struct of_device_id *driver_override; // 设备树覆盖 // 其他字段如 driver_data(私有数据) };资源通过 struct resource 定义,包括类型(RESOURCE_IO、RESOURCE_MEM、RESOURCE_IRQ)。
-
struct platform_driver :描述平台驱动。
struct platform_driver { int (*probe)(struct platform_device *); // 初始化回调 int (*remove)(struct platform_device *); // 移除回调 void (*shutdown)(struct platform_device *); // 关机回调(可选) int (*suspend)(struct platform_device *, pm_message_t); // 电源管理(可选) int (*resume)(struct platform_device *); // 恢复回调(可选) struct device_driver driver; // 通用驱动结构体 const struct platform_device_id *id_table; // ID 匹配表 const struct of_device_id *of_match_table; // 设备树 compatible 匹配表 }; -
struct resource :设备资源描述。
struct resource { resource_size_t start; // 起始地址 resource_size_t end; // 结束地址 const char *name; // 资源名 unsigned long flags; // 类型标志(如 IORESOURCE_MEM) struct resource *parent, *sibling, *child; // 树状结构 }; -
匹配表:platform_device_id 用于名称匹配;of_device_id 用于设备树。
3. 注册流程
平台设备的注册分为设备注册和驱动注册。设备通常在板级初始化代码或设备树中定义,驱动作为模块加载。
- 设备注册 (platform_device_register):
- 静态方式:在板级文件(arch/arm/mach-xxx/board.c)中定义并注册。
- 动态方式:使用设备树,内核解析 .dtb 文件自动创建 platform_device。
- 示例:struct platform_device my_pdev = { .name = "mydevice", .id = -1, .resource = my_resources, .num_resources = ARRAY_SIZE(my_resources) }; platform_device_register(&my_pdev);
- 驱动注册 (platform_driver_register):
- 初始化 struct platform_driver。
- 调用 platform_driver_register(&my_driver)。
- 内核匹配后调用 probe。
- 卸载:platform_driver_unregister 和 platform_device_unregister。
对于设备树:
- 在 .dts 中定义节点:mydevice@0 { compatible = "vendor,mydevice"; reg = <0x12340000 0x1000>; interrupts = <10>; };
- 驱动的 of_match_table = { .compatible = "vendor,mydevice" };
4. probe/remove 实现
probe 是驱动的核心,负责硬件初始化;remove 负责清理。
-
probe 示例 :
static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; // 获取资源 res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; // 请求 IRQ request_irq(irq, my_handler, 0, "mydevice", pdev); // 注册字符设备或其他接口 // ... dev_info(&pdev->dev, "Device probed\n"); return 0; } -
remove 示例 :
static int my_remove(struct platform_device *pdev) { // 释放 IRQ、iounmap 等 free_irq(platform_get_irq(pdev, 0), pdev); // 注销接口 dev_info(&pdev->dev, "Device removed\n"); return 0; }使用 devm_ 函数(device managed)简化资源管理,自动在 remove 时释放。
平台驱动常结合 file_operations 提供用户空间接口(如通过 misc_register)。
5. 示例代码:简单平台 LED 驱动
假设控制一个 LED,通过设备树描述。完整驱动代码如下(需包含头文件如 <linux/module.h>、<linux/platform_device.h>、<linux/of.h>、<linux/gpio/consumer.h> 等):
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
struct my_led_data {
struct gpio_desc *gpio;
};
static int my_led_probe(struct platform_device *pdev) {
struct my_led_data *data;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
data->gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->gpio)) return PTR_ERR(data->gpio);
platform_set_drvdata(pdev, data);
dev_info(&pdev->dev, "LED probed\n");
return 0;
}
static int my_led_remove(struct platform_device *pdev) {
dev_info(&pdev->dev, "LED removed\n");
return 0;
}
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "example,my-led" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my-led",
.of_match_table = my_led_of_match,
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
设备树节点示例(.dts):
my-led {
compatible = "example,my-led";
led-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>;
};
编译成 .ko,加载:insmod my_led.ko。测试:通过 sysfs 或自定义接口控制 LED。
6. 注意事项和高级主题
- 资源获取:使用 platform_get_resource、platform_get_irq、devm_ioremap 等函数,避免硬编码地址。
- 电源管理:实现 suspend/resume 支持 PM。
- 错误处理:probe 返回负值表示失败,常用 -ENODEV、-ENOMEM。
- 调试:查看 /sys/bus/platform/devices/ 和 dmesg。使用 of_ 函数解析设备树属性。
- 与其它框架集成:平台驱动常作为后端,与 regmap、pinctrl 等结合。
- 版本差异:内核 5.x+ 强调设备树,弃用平台数据;推荐使用 module_platform_driver 宏简化注册。
- 比较:与字符驱动不同,平台聚焦硬件绑定;与杂项设备相比,平台支持总线模型和资源管理。