Platform驱动框架:设备与驱动解耦的核心机制
写ARM上的驱动,十有八九绕不开Platform框架。USB设备插上去内核自动发现,PCI设备扫描总线就能枚举,但SoC内部的GPIO控制器、I2C适配器、定时器这些——它们焊死在芯片里,没有标准总线帮它"自报家门"。Platform总线就是干这事的:一条虚拟总线,把"设备长什么样"。驱动怎么操作"拆开,靠名字或设备树compatible字符串把两边撮合到一起。本文基于Linux 6.8内核,从为什么需要Platform框架讲起,覆盖核心数据结构、匹配机制、设备定义方式、完整驱动示例和资源获取API。
一、为什么需要Platform框架
PCI设备有配置空间,USB设备有描述符,内核扫描总线就能知道"这有个设备,型号是XXX"。但ARM SoC上的内部外设没有这种自动发现机制——GPIO控制器就挂在AHB总线上,I2C适配器就在某个固定地址,内核不扫就不知道它们存在。
Platform总线是一条虚拟总线(struct bus_type platform_bus_type),不对应任何物理硬件。它的核心价值:把设备描述和驱动实现解耦。设备端只管"我有什么资源",驱动端只管"我怎么操作",两边通过名称或compatible字符串匹配
设备端有两种来源:设备树(device_node解析为platform_device)或代码手动注册。驱动端注册platform_driver后,总线核心遍历已注册设备,调用match函数检查是否匹配。匹配成功则调用驱动的probe函数。
二、核心数据结构
Platform框架围绕两个核心结构体展开:platform_device描述设备,platform_driver描述驱动。
2.1 platform_device
// include/linux/platform_device.h
struct platform_device {
const char *name; // 设备名称,匹配依据之一
int id; // 设备实例编号,-1表示唯一
struct device dev; // 内嵌device结构建 u32 num_resources; // 资源数量
struct resource *resource; // 资源数组
};
关键字段说明。
| 字段 | 说明 |
|:-----|:-----|
| name | 设备名称,与驱动的name或id_table匹配 |
| id | 同名设备的实例编号,如uart0的id=0,uart1的id=1。1表示只有一个实例 |
| dev | 内嵌的struct device,包含of_node(设备树节点)、platform_data、dma_mask|
| resource | 设备占用的内存区域和中断号数量 |
| num_resources | rresource数组的长度 |
2.2 platform_driver
// include/linux/platform_device.h
struct platform_driver {
int (*probe)(struct platform_device *pdev);
void (*remove)(struct platform_device *pdev);
struct device_driver driver; // 内嵌driver结构建 const struct platform_device_id *id_table; // id匹配置};
其中内嵌的struct device_driver包含匹配关键字段。
struct device_driver {
const char *name; // 驱动名称
const struct of_device_id *of_match_table; // 设备树匹配表
// ...
};
2.3 resource
// include/linux/ioport.h
struct resource {
resource_size_t start; // 资源起始地址或中断号
resource_size_t end; // 资源结束地址
const char *name; // 资源名称
unsigned long flags; // 资源类型标志
};
常用flags。
| 标志 | 值 | 含义 |
|:-----|:---|:-----|
| IORESOURCE_MEM | 0x00000200 | 内存映射区域 |
| IORESOURCE_IRQ | 0x00000400 | 中断号 |
设备树中的reg属性解析为IORESOURCE_MEM,interrupts属性解析为IORESOURCE_IRQ。
三、匹配机制详。
Platform总线的match函数按优先级依次尝试三种匹配方式。
优先级:of_match > id_table > name
第一种:of_match_table(设备树匹配,优先级最高)
驱动定义of_device_id数组,通过compatible字符串与设备树节点匹配:
// 驱动侧定义匹配表
static const struct of_device_id led_of_match[] = {
{ .compatible = "myboard,led-gpio" },
{ .compatible = "myboard,led-pwm" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
// 绑定到driver
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my-led",
.of_match_table = led_of_match,
},
};
MODULE_DEVICE_TABLE(of, led_of_match)这个宏不能省。它把匹配表导出到模块的.modinfo段,modprobe据此自动加载对应驱动。没有这行,modprobe不知道该在设备树出现某个compatible时加载哪个ko。
第二种:id_table(ID表匹配)
适用于不用设备树的场景:
static const struct platform_device_id led_ids[] = {
{ .name = "my-led-gpio" },
{ .name = "my-led-pwm" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(platform, led_ids);
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.id_table = led_ids,
.driver = {
.name = "my-led",
},
};
第三种:driver.name(名称匹配,优先级最低)
最简单的匹配方式,驱动的driver.name与设备的name字符串完全相同时匹配。不推荐新代码使用,仅用于兼容旧驱动。
四、设备定义方。
方式1:设备树(推荐)
在DTS中定义设备节点,内核启动时解析为platform_device。
// arch/arm/boot/dts/myboard.dts
/ {
leds {
compatible = "myboard,led-gpio";
reg = <0x1000 0x100>; // 寄存器基地址0x1000,长度0x100
interrupts = <0 42 4>; // 中断号42
led-gpio = <&gpio1 5 0>; // GPIO引脚
label = "status-led";
};
};
内核启动流程:DTB 。解析device_node 。匹配of_match_table 。创建platform_device 。调用probe。
这是当前主流方式。设备树把硬件描述从内核代码中剥离出来,换板子只换DTS,驱动代码不用改。
方式2:代码注。
不用设备树时,在代码中手动创建并注册platform_device。
// 定义资源
static struct resource led_resources[] = {
[0] = {
.start = 0x1000,
.end = 0x10FF,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 42,
.end = 42,
.flags = IORESOURCE_IRQ,
},
};
// 定义设备
static struct platform_device led_device = {
.name = "my-led",
.id = -1,
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
};
// 注册设备
platform_device_register(&led_device);
// 卸载时注销
platform_device_unregister(&led_device);
这种方式的问题:硬件信息硬编码在驱动代码里,换板子就得改代码重新编译。只适合原型验证或无法使用设备树的场景:
方式3:module_platform_driver。
这个宏不是定义设备的方式,而是简化驱动注册的语法糖:
// 展开销module_platform_driver(led_driver);
// 展开后等价于
static int __init led_driver_init(void)
{
return platform_driver_register(&led_driver);
}
module_init(led_driver_init);
static void __exit led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_exit(led_driver_exit);
绝大多数Platform驱动都用这个宏,省掉init/exit的模板代码。如果你的驱动需要在注册前做额外初始化,就不要用这个宏,手动写init/exit。
五、驱动实现完整示例
下面是一个基于设备树的LED Platform驱动,覆盖probe资源获取、寄存器映射、中断申请、字符设备注册,以及remove的逆序释放。
设备树节。
/ {
my_led {
compatible = "myboard,led-gpio";
reg = <0x30200000 0x1000>;
interrupts = <0 65 4>;
led-default-state = "off";
};
};
完整驱动代码
// led_platform.c - 基于设备树的LED Platform驱动(Linux 6.8。#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/mod_devicetable.h>
#define LED_CTRL_REG 0x00
#define LED_STATE_REG 0x04
struct led_priv {
void __iomem *base; // 映射后的寄存器基地址
int irq; // 中断。 struct cdev cdev; // 字符设备
dev_t devt; // 设备。 struct device *dev; // device指针,用于devm
struct class *cls; // 设备。};
static irqreturn_t led_irq_handler(int irq, void *dev_id)
{
struct led_priv *priv = dev_id;
u32 state = readl(priv->base + LED_STATE_REG);
dev_info(priv->dev, "irq triggered, state=%u\n", state);
return IRQ_HANDLED;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct led_priv *priv = filp->private_data;
char kbuf[4] = {0};
if (count > sizeof(kbuf))
count = sizeof(kbuf);
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
/* 。亮灯,写0灭灯 */
writel(kbuf[0] == '1' ? 1 : 0, priv->base + LED_CTRL_REG);
return count;
}
static int led_open(struct inode *inode, struct file *filp)
{
struct led_priv *priv = container_of(inode->i_cdev,
struct led_priv, cdev);
filp->private_data = priv;
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
static int led_probe(struct platform_device *pdev)
{
struct led_priv *priv;
struct resource *res;
int ret;
dev_info(&pdev->dev, "probe enter\n");
/* 分配私有数据 */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = &pdev->dev;
/* 获取MEM资源并映*/
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
/* 获取中断*/
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
/* 申请中断 */
ret = devm_request_irq(&pdev->dev, priv->irq, led_irq_handler,
IRQF_TRIGGER_RISING, "led-irq", priv);
if (ret)
return ret;
/* 注册字符设备 */
ret = alloc_chrdev_region(&priv->devt, 0, 1, "my-led");
if (ret)
return ret;
cdev_init(&priv->cdev, &led_fops);
priv->cdev.owner = THIS_MODULE;
ret = cdev_add(&priv->cdev, priv->devt, 1);
if (ret)
goto err_chrdev;
/* 创建设备节点 /dev/my-led */
priv->cls = class_create("my-led");
if (IS_ERR(priv->cls)) {
ret = PTR_ERR(priv->cls);
goto err_cdev;
}
/* 6.8: device_create参数调整,class_create只需一个参数*/
if (IS_ERR(device_create(priv->cls, &pdev->dev, priv->devt,
NULL, "my-led"))) {
ret = -ENOMEM;
goto err_class;
}
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "probe success, major=%u\n", MAJOR(priv->devt));
return 0;
err_class:
class_destroy(priv->cls);
err_cdev:
cdev_del(&priv->cdev);
err_chrdev:
unregister_chrdev_region(priv->devt, 1);
return ret;
}
static void led_remove(struct platform_device *pdev)
{
struct led_priv *priv = platform_get_drvdata(pdev);
/* 逆序释放 */
device_destroy(priv->cls, priv->devt);
class_destroy(priv->cls);
cdev_del(&priv->cdev);
unregister_chrdev_region(priv->devt, 1);
/* devm资源自动释放,无需手动iounmap/free_irq */
dev_info(&pdev->dev, "remove done\n");
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "myboard,led-gpio" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my-led",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("LED Platform Driver Demo");
devm资源管理
上面代码大量使用devm_前缀的API。devm。device-managed"的缩写,核心思路径资源绑定到device的生命周期,设备移除时自动释。
| 手动API | devm版本 | 释放时机 |
|:--------|:---------|:---------|
| kmalloc / kfree | devm_kzalloc | remove时自动kfree |
| ioremap / iounmap | devm_ioremap_resource | remove时自动iounmap |
| request_irq / free_irq | devm_request_irq | remove时自动free_irq |
用devm的好处:probe失败时不用逐个goto跳转释放,remove函数不用写一堆清理代码。坏处:如果资源的生命周期短于设备(比如运行时动态申请释放),就不能用devm。
六、资源获取API
6.1 platform_get_resource
获取指定类型的资源,按索引顺序查找:
struct resource *res;
/* 获取第一个MEM资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
/* 获取第二个IRQ资源 */
res = platform_get_resource(pdev, IORESOURCE_IRQ, 1);
参数:(pdev, 资源类型, 索引)。索引从0开始,同类型资源按DTS中定义顺序编号。返回NULL表示没找到。
6.2 platform_get_irq
专门获取中断号,比platform_get_resource + IORESOURCE_IRQ更常用:
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq; // 返回负数错误码,不是-1
6.8内核注意事项。
- 返回0或正数表示中断号,负数表示错误码
- 常见错误码:-ENOENT(设备树无interrupts属性)、-EINVAL(属性格式错误)
- 不要用irq < 0以外的判断方式,旧代码里irq == -ENXIO的写法已过时
- platform_get_irq_optional用于中断可选的场景,找不到时返回0而不是错误码
6.3 devm_ioremap_resource
映射寄存器物理地址到内核虚拟地址。
void __iomem *base;
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
这个函数做了三件事:检查资源是否已被占用(__request_mem_region)、映射(ioremap)、返回虚拟地址。如果资源被别的驱动占了,返回EINVAL。
不要用裸ioremap。devm_ioremap_resource自带资源冲突检查,裸ioremap没有,两个驱动映射同一块寄存器不会报错,调试起来是灾难。
6.4 of_property_read_*
从设备树节点读取自定义属性:
struct device_node *np = pdev->dev.of_node;
u32 width;
const char *label;
/* 读取u32属*/
ret = of_property_read_u32(np, "led-width", &width);
if (ret)
width = 8; // 读取失败用默认。
/* 读取字符串属*/
ret = of_property_read_string(np, "label", &label);
if (ret)
label = "unknown";
/* 读取u32数组 */
u32 pins[4];
int count = of_property_count_u32_elems(np, "led-pins");
if (count > 0 && count <= 4)
of_property_read_u32_array(np, "led-pins", pins, count);
| API | 用途 |
|---|---|
of_property_read_u32 |
读单个u32 |
of_property_read_u32_array |
读u32数组 |
of_property_read_string |
读字符串 |
of_property_read_bool |
判断布尔属性是否存在 |
of_property_count_u32_elems |
获取u32数组的元素个数 |
这些函数读取失败时返回负数错误码(通常是-EINVAL或-ENODATA),不是返回0。驱动中通常对返回值做判断,失败则用默认值
七、Platform vs 传统字符设备
| 对比 | Platform驱动 | 传统字符设备 |
|---|---|---|
| 注册方式 | module_platform_driver注册到总线 |
register_chrdev直接注册 |
| 设备发现 | 总线自动匹配,probe被调用 | 手动mknod或class_create创建节点 |
| 资源管理 | resource结构体描述,platform_get_*获取 |
硬编码寄存器地址和中断号 |
| 热插拔 | 支持设备树动态覆盖和overlay | 不支持 |
| 电源管理 | 内置runtime PM和系统休眠回调 | 需手动实现 |
| 设备树集成 | 原生支持compatible匹配 | 不支持 |
| 代码复用 | 同一驱动匹配不同compatible支持多板 | 换板子改代码 |
| 适用场景 | SoC内部外设(GPIO/I2C/SPI/Timer) | 纯软件设备、虚拟设备 |
一句话总结有硬件资源的用Platform,纯软件逻辑的用字符设备*。实际项目中Platform驱动内部通常会注册字符设备(如上面示例),两者不是互斥的。
八、常见问题
Q1:probe不被调用。
驱动insmod成功但probe没执行,最常见两个原因。
1. compatible不匹配:设备树里的compatible字符串和驱动of_match_table里的不完全一致,差一个字符都不行。用dmesg | grep -i "of"查看匹配日志。2. 设备树未编译进内存*:修改了DTS但没重新编译DTB,或编译了没更新到boot分区。确认/sys/firmware/devicetree/base/下能看到对应节点。
排查步骤。
# 检查设备树节点是否存在
ls /sys/firmware/devicetree/base/my_led/
# 检查compatible属性
cat /sys/firmware/devicetree/base/my_led/compatible
# 查看总线上的设备和驱动ls /sys/bus/platform/devices/ | grep led
ls /sys/bus/platform/drivers/ | grep led
# 查看匹配日志
dmesg | grep -i "my-led\|myboard,led"
Q2:platform_get_irq返回-ENOENT。
设备树节点没有定义interrupts属性,或者索引越界。检查DTS。
my_led {
compatible = "myboard,led-gpio";
/* 缺少这行就会返回-ENOENT */
interrupts = <0 65 4>;
};
如果中断是可选的,用platform_get_irq_optional代替。
Q3:devm_ioremap_resource返回EINVAL。
两种可能。
1. 资源已被其他驱动占用:另一个驱动先devm_ioremap_resource了同一块地址区间。检查/proc/iomem看谁占了这块区域。2. **resource的start/end不合理:设备树reg属性写错了,start > end。
# 查看内存资源占用
cat /proc/iomem | grep 30200000
九、总结
Platform框架速查表:
| 项目 | 要点 |
|---|---|
| 核心思想 | 设备描述与驱动实现解耦,虚拟总线匹配 |
| 匹配优先级 | of_match_table > id_table > driver.name |
| 设备定义 | 设备树(推荐)或 代码注册 |
| 驱动注册 | module_platform_driver |
| 资源获取 | platform_get_resource / platform_get_irq |
| 寄存器映射 | devm_ioremap_resource(不要用裸ioremap) |
| 属性读取 | of_property_read_u32/string/bool |
| 资源管理 | 优先用devm系列,probe失败和remove时自动释放 |
| 必须导出 | MODULE_DEVICE_TABLE(of, ...) 否则modprobe无法自动加载 |
本文首发于linuxros.cn,转载请注明出处。