ESC
输入关键词搜索文章标题和内容

Platform驱动框架:设备与驱动解耦的核心机制

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

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字符串匹配

flowchart LR A["设备br/>device_node / platform_device"] -->|"注册"| B["Platform总线<br/>match()"] C["驱动<br/>platform_driver"] -->|"注册"| B B -->|"匹配成功"| D["调用 probe()"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#F3E5F5

设备端有两种来源:设备树(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函数按优先级依次尝试三种匹配方式。

flowchart TB A(["开始匹"]) --> B{"of_match_table<br/>设备树compatible?"} B -->|"匹配成功"| G["调用 probe()"] B -->|"无匹配" | C{"id_table<br/>ID表匹。"} C -->|"匹配成功"| G C -->|"无匹配" | D{"driver.name<br/>名称匹配?"} D -->|"匹配成功"| G D -->|"不匹配 | E["匹配失败<br/>等待新设备注"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#FFF8E1 style D fill:#FFF8E1 style E fill:#FFEBEE style G fill:#E8F5E9

优先级: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);

这种方式的问题:硬件信息硬编码在驱动代码里,换板子就得改代码重新编译。只适合原型验证或无法使用设备树的场景:

来自 linuxros.cn · linuxROS

方式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,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页