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

嵌入式Linux Platform驱动框架:从设备分离到自动匹配

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

嵌入式Linux Platform驱动框架:从设备分离到自动匹配

Platform驱动框架把硬件信息和驱动逻辑拆开,platform_device描述"有什么硬件",platform_driver描述"怎么操作",总线负责自动匹配。本文讲清楚分离机制、三级匹配策略、设备树定义、probe/remove实现。

一、原理简析

在Linux内核2.6之前,外设驱动普遍采用"硬件信息硬编码"的方式——寄存器地址、中断号直接写死在驱动代码里。换一块板子就得改驱动,可移植性几乎为零。

Platform驱动框架做的事情很简单——设备与驱动分离:platform_device描述"有什么硬件"(地址、中断、时钟),platform_driver描述"怎么操作硬件"(probe、remove、suspend)。两者通过Platform总线(platform_bus_type)自动匹配,匹配成功后内核调用驱动的probe函数完成初始化。

这种分离带来三个直接好处:驱动代码与板级信息解耦,同一驱动可适配不同硬件;设备树替代硬编码,改板只需改DTS;内核统一管理设备生命周期,资源释放更安全。

二、核心数据结构

2.1 platform_device

struct platform_device {
    const char              *name;       // 设备名称,用于name匹配
    int                      id;         // 设备实例编号(-1表示唯一)
    struct device            dev;        // 内嵌device结构体
    u32                      num_resources;
    struct resource         *resource;   // 资源数组(内存/中断/DMA)
    const struct platform_device_id *id_entry;
};

关键字段说明:

字段 用途 示例
name 与驱动的driver.name匹配 "uart0"
id 同名设备的实例编号 0, 1, -1(唯一)
resource 硬件资源描述数组 寄存器地址、中断号
dev.of_node 设备树节点指针 内核自动填充

2.2 platform_driver

struct platform_driver {
    int  (*probe)(struct platform_device *pdev);
    int  (*remove)(struct platform_device *pdev);
    void (*shutdown)(struct platform_device *pdev);
    struct device_driver  driver;
    const struct platform_device_id *id_table;
};

其中driver子结构体包含匹配所需的关键字段:

struct device_driver {
    const char             *name;           // 用于name匹配
    const struct of_device_id *of_match_table; // 设备树匹配表
    // ...
};

2.3 resource结构体

struct resource {
    resource_size_t  start;   // 资源起始
    resource_size_t  end;     // 资源结束
    const char      *name;
    unsigned long    flags;   // IORESOURCE_MEM / IORESOURCE_IRQ
};

三、匹配机制详解

Platform总线提供三级匹配策略,按优先级从高到低依次尝试:

flowchart TB A(["设备注册到Platform总线"]) --> B{"of_match_table<br/>是否匹配?"} B -->|"是"| F(["匹配成功<br/>调用probe"]) B -->|"否"| C{"id_table<br/>是否匹配?"} C -->|"是"| F C -->|"否"| D{"driver.name<br/>== device.name?"} D -->|"是"| F D -->|"否"| E(["匹配失败<br/>等待后续注册"])

3.1 of_match_table(最高优先级)

设备树匹配,通过compatible字符串实现。驱动中定义匹配表:

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "vendor,my-device-v1" },
    { .compatible = "vendor,my-device-v2" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);

匹配规则:内核遍历of_match_table数组,将每个.compatible字符串与设备树节点中的compatible属性逐个比较,第一个完全一致的即为匹配成功。MODULE_DEVICE_TABLE宏将该表导出到模块信息,支持热插拔时自动加载驱动模块。

3.2 id_table(次优先级)

传统ID匹配,支持一个驱动对应多个设备名称:

static const struct platform_device_id my_ids[] = {
    { .name = "my-device-a" },
    { .name = "my-device-b" },
    { /* sentinel */ }
};

3.3 name匹配(最低优先级)

直接比较platform_device.name与platform_driver.driver.name,仅支持1:1匹配。

实践建议:新项目一律使用of_match_table,它是设备树体系的主要匹配方式。id_table和name匹配属于遗留接口,仅在无设备树的老平台上使用。

四、设备定义方式

4.1 设备树方式(推荐)

my_device: my-device@10000000 {
    compatible = "vendor,my-device";
    reg = <0x10000000 0x1000>;           // 第0个MEM资源
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;  // 第0个IRQ
    clocks = <&clk_uart0>;
    clock-names = "apb";
    status = "okay";
};

interrupts三元组含义(以GIC为例):

Cell 含义 取值说明
第1个 中断类型 GIC_SPI(0)=共享外设中断, GIC_PPI(1)=私有外设中断
第2个 中断号 SPI范围[0-987], PPI范围[0-15]
第3个 触发标志 IRQ_TYPE_LEVEL_HIGH(4)=高电平, IRQ_TYPE_EDGE_RISING(1)=上升沿

内核启动时自动解析DTS,为每个节点创建platform_device并注册到Platform总线,无需手动编码。

来自 linuxros.cn · linuxROS

4.2 代码静态定义(遗留方式)

static struct resource my_resources[] = {
    [0] = {
        .start = 0x10000000,
        .end   = 0x10000000 + 0x1000 - 1,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 50,
        .end   = 50,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device my_device = {
    .name           = "my-device",
    .id             = -1,
    .num_resources  = ARRAY_SIZE(my_resources),
    .resource       = my_resources,
    .dev = {
        .release = my_device_release,
    },
};

/* 模块初始化时注册 */
platform_device_register(&my_device);

注意:代码定义方式需要手动管理platform_device的注册和注销,且dev.release回调必须实现,否则内核会告警。新项目应优先使用设备树方式。

五、驱动实现

5.1 完整驱动示例

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>

struct my_dev_priv {
    void __iomem    *base;
    int              irq;
    struct clk      *clk;
};

static irqreturn_t my_isr(int irq, void *dev_id)
{
    struct my_dev_priv *priv = dev_id;
    // 读取中断状态并处理
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_dev_priv *priv;
    int ret;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 方式一:获取内存资源并映射(两步法) */
    /* struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
     * priv->base = devm_ioremap_resource(dev, res);
     */

    /* 方式二:一步完成(推荐,内核5.x起广泛使用) */
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 获取中断号 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;  /* 可能返回-EPROBE_DEFER */

    /* 获取时钟 */
    priv->clk = devm_clk_get(dev, NULL);
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);

    ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;

    ret = devm_request_irq(dev, priv->irq, my_isr, 0,
                           dev_name(dev), priv);
    if (ret) {
        clk_disable_unprepare(priv->clk);
        return ret;
    }

    platform_set_drvdata(pdev, priv);
    dev_info(dev, "probe success\n");
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_dev_priv *priv = platform_get_drvdata(pdev);

    /* devm_*分配的资源由内核自动释放,无需手动free */
    clk_disable_unprepare(priv->clk);
    dev_info(&pdev->dev, "remove success\n");
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name           = "my-device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);

MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("Platform Driver Example");
MODULE_LICENSE("GPL");

5.2 关键API说明

API 功能 返回值
devm_platform_ioremap_resource(pdev, idx) 获取第idx个MEM资源并映射 成功返回虚拟地址,失败返回PTR_ERR
devm_ioremap_resource(dev, res) 根据resource指针映射 同上
platform_get_irq(pdev, idx) 获取第idx个中断号 成功返回Linux IRQ号(>0),失败返回负数错误码
devm_clk_get(dev, id) 获取时钟 成功返回clk指针,失败返回PTR_ERR
devm_kzalloc(dev, size, flags) 分配设备绑定内存 成功返回指针,失败返回NULL

重要:devm_ioremap_resource()已替代废弃的devm_request_and_ioremap()。更推荐使用devm_platform_ioremap_resource(),它把platform_get_resource+devm_ioremap_resource两步合成一步,内核社区正在全面迁移(2025年起大量patch合入主线)。

5.3 module_platform_driver宏展开

#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                  platform_driver_unregister)

#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
    return __register(&(__driver), ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
    __unregister(&(__driver), ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);

展开后等价于:

static int __init my_driver_init(void)
{
    return platform_driver_register(&my_driver);
}
module_init(my_driver_init);

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_driver);
}
module_exit(my_driver_exit);

一行宏替代八行样板代码,是内核驱动的标准写法。

六、对比表格

对比维度 Platform驱动框架 传统字符设备驱动
硬件信息 设备树/资源数组描述,与驱动解耦 硬编码在驱动中
匹配方式 三级自动匹配(OF/ID/Name) 手动绑定主设备号
可移植性 改DTS即可适配新板卡 换板必须改驱动代码
资源管理 devm_*自动释放 手动申请/释放,易泄漏
热插拔 支持deferred probe 不支持
代码复用 一个驱动适配多型号设备 一板一驱动
电源管理 内置suspend/resume回调 需自行实现

七、核心流程图

7.1 Platform总线匹配流程

flowchart TB A(["platform_device<br/>注册到总线"]) --> B["遍历已注册的<br/>platform_driver"] B --> C{"of_match_table<br/>compatible匹配?"} C -->|"匹配"| G(["匹配成功"]) C -->|"不匹配"| D{"id_table<br/>name匹配?"} D -->|"匹配"| G D -->|"不匹配"| E{"driver.name<br/>== device.name?"} E -->|"匹配"| G E -->|"不匹配"| F(["加入未匹配列表<br/>等待新驱动注册"]) G --> H["调用driver.probe()"]

7.2 probe函数执行流程

flowchart TB A(["probe被调用"]) --> B["devm_kzalloc<br/>分配私有数据"] B --> C["devm_platform_ioremap_resource<br/>获取并映射寄存器"] C --> D{"映射成功?"} D -->|"失败"| E(["返回PTR_ERR"]) D -->|"成功"| F["platform_get_irq<br/>获取中断号"] F --> G{"irq < 0?"} G -->|"是 -EPROBE_DEFER"| H(["延迟探测<br/>稍后重试"]) G -->|"是 其他错误"| I(["返回错误码"]) G -->|"否"| J["devm_clk_get<br/>获取时钟"] J --> K["devm_request_irq<br/>注册中断处理"] K --> L{"注册成功?"} L -->|"失败"| M(["释放已获取资源<br/>返回错误码"]) L -->|"成功"| N(["probe完成<br/>设备可用"])

八、常见问题解决

Q1:probe函数没有被调用?

检查三个环节:设备树中compatible字符串是否与驱动of_match_table完全一致(注意大小写和逗号);设备节点status是否为"okay";内核配置是否启用了CONFIG_OF。用dmesg | grep my-device查看是否有匹配失败的日志。

Q2:platform_get_irq返回-EPROBE_DEFER?

这不是错误,而是内核的延迟探测机制。当依赖的中断控制器驱动尚未加载时,platform_get_irq返回-EPROBE_DEFER,内核会在依赖就绪后自动重新调用probe。驱动代码应直接return irq将错误码上交,不要替换为其他错误码,否则延迟探测会失效。

Q3:devm_ioremap_resource返回错误指针?

常见原因:资源类型不是IORESOURCE_MEM;物理地址范围与已注册设备冲突;设备树中reg属性缺失或格式错误。用IS_ERR()检测返回值,用PTR_ERR()获取错误码,用dev_err_probe()打印日志(它会自动抑制-EPROBE_DEFER的冗余告警)。

Q4:remove后内核报"Device does not have a release() function"?

代码静态定义platform_device时,必须实现dev.release回调,哪怕函数体为空。设备树方式创建的设备由内核自动管理,无需处理。

九、总结

Platform驱动框架靠设备与驱动分离、三级自动匹配、devm资源管理,解决了传统驱动硬编码、难移植、易泄漏的问题:

  • 分离:platform_device描述硬件,platform_driver实现逻辑,总线负责匹配
  • 匹配优先级:of_match_table > id_table > name,新项目统一用设备树匹配
  • 资源获取:devm_platform_ioremap_resource一步到位,platform_get_irq注意-EPROBE_DEFER
  • 驱动注册:module_platform_driver一行宏搞定,无需手写init/exit
  • 设备树中断:GIC三元组<类型 中断号 触发标志>,内核自动转换为Linux IRQ号

下期讲Platform驱动的高级话题:DMA-BUF集成、regmap框架、以及多设备实例的probe策略。

版权声明

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