在 Linux 驱动开发中,Platform 驱动框架是针对 “无总线设备”(如 SOC 内部集成的定时器、UART、GPIO 控制器等)设计的核心框架,其核心思想是 “设备与驱动分离”,通过内核提供的总线(platform_bus_type)实现设备与驱动的自动匹配,大幅提升驱动的可复用性和系统的可扩展性。以下从框架核心概念、工作流程、关键数据结构、实现步骤及进阶特性展开详述。

一、Platform 框架的核心定位与优势

在理解 Platform 框架前,需先明确其与 “传统总线驱动”(如 PCI、USB)的区别:

  • 传统总线设备:设备通过物理总线(如 PCI 插槽)连接,总线本身提供设备枚举能力(如 PCI 总线可自动扫描总线上的设备)。
  • Platform 设备:多为 SOC 内部外设(如 ARM 芯片的 I2C 控制器、SPI 控制器),无物理总线枚举机制,需手动向内核 “注册” 设备信息。

Platform 框架的核心优势:

  1. 解耦设备与驱动:设备信息(如寄存器地址、中断号)与驱动逻辑分离,同一驱动可适配不同硬件配置的同类型设备。
  2. 统一匹配机制:通过内核提供的 platform_bus 作为 “中间总线”,标准化设备与驱动的匹配流程。
  3. 简化驱动开发:内核封装了总线管理、匹配、资源分配等通用逻辑,开发者只需聚焦设备的具体操作(如寄存器读写、中断处理)。

二、Platform 框架的核心组成

Platform 框架由三部分构成:Platform 设备(Device)、Platform 驱动(Driver)、Platform 总线(Bus),三者的关系如下:

  • Platform 总线:作为 “中介”,维护设备链表和驱动链表,负责触发设备与驱动的匹配。
  • Platform 设备:描述硬件信息(如寄存器基地址、中断号、设备名称),需注册到总线的设备链表。
  • Platform 驱动:包含驱动逻辑(如 probe/remove 函数、中断处理、设备操作接口),注册到总线的驱动链表后,由总线触发匹配。

1. 核心数据结构

Linux 内核通过以下关键结构体定义 Platform 框架的三大组件,均定义在 <linux/platform_device.h> 中:

(1)Platform 设备:struct platform_device

描述硬件设备的核心信息,重点是 “资源(resource)”(如地址、中断、时钟等):

struct platform_device {
    const char      *name;          // 设备名称(用于与驱动匹配)
    int             id;             // 设备编号(同一类型多个设备时区分,如 i2c-0、i2c-1)
    bool            id_auto;        // 自动分配设备编号
    struct device   dev;            // 内嵌的通用设备结构体(所有设备的基类)
    u32             num_resources;  // 资源数量
    struct resource *resource;      // 资源数组(存储设备的硬件资源)
    const struct platform_device_id *id_entry; // 设备 ID 表(用于多设备匹配)
    /* 其他私有成员... */
};

其中,struct resource 是描述硬件资源的关键结构体,定义在 <linux/ioport.h> 中,支持多种资源类型:

struct resource {
    resource_size_t start;  // 资源起始地址(如寄存器基地址、中断号)
    resource_size_t end;    // 资源结束地址(如寄存器末地址)
    const char      *name;  // 资源名称
    unsigned long   flags;  // 资源类型(核心字段)
    struct resource *parent, *sibling, *child; // 资源链表
};

常见 flags 类型:

  • IORESOURCE_MEM:内存映射资源(如 SOC 外设的寄存器地址空间)。
  • IORESOURCE_IRQ:中断资源(如设备的中断号)。
  • IORESOURCE_DMA:DMA 通道资源。
  • IORESOURCE_IO:IO 端口资源(x86 架构常用,ARM 架构少用)。
(2)Platform 驱动: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 state); // 设备休眠时调用
    int (*resume)(struct platform_device *);  // 设备唤醒时调用
    struct device_driver driver;              // 内嵌的通用驱动结构体(所有驱动的基类)
    const struct platform_device_id *id_table; // 驱动支持的设备 ID 表(多设备匹配关键)
    bool prevent_deferred_probe;              // 禁止延迟探测
};
  • probe 函数:驱动的 “入口”,负责初始化硬件(如申请 IO 内存、注册中断、初始化外设)。
  • remove 函数:驱动的 “出口”,负责释放资源(如释放 IO 内存、注销中断、关闭外设)。
  • id_table:驱动支持的设备列表(可匹配多个同类型设备),优先级高于 name 匹配。
(3)Platform 总线:struct bus_type

内核定义的全局总线结构体,名为 platform_bus_type,定义在 drivers/base/platform.c 中,核心是 “匹配函数” 和 “总线属性”:

struct bus_type platform_bus_type = {
    .name       = "platform",  // 总线名称(ls /sys/bus/ 可查看)
    .match      = platform_match, // 设备与驱动的匹配函数(核心)
    .uevent     = platform_uevent, // 生成 uevent 事件(用于用户空间热插拔)
    .pm         = &platform_dev_pm_ops, // 电源管理操作
};
EXPORT_SYMBOL_GPL(platform_bus_type);

其中,platform_match 函数是设备与驱动匹配的核心逻辑,匹配顺序如下(优先级从高到低):

  1. 若驱动和设备均提供 of_match_table(设备树匹配表),则通过设备树的 compatible 属性匹配。
  2. 若驱动提供 id_table 且设备提供 id_entry,则通过 id_table 中的设备名称匹配。
  3. 直接比较设备的 name 与驱动的 driver.name,名称相同则匹配。

三、Platform 驱动的完整工作流程

Platform 框架的工作流程可分为 “设备注册”“驱动注册”“匹配与 probe”“资源释放” 四个阶段,具体如下:

阶段 1:Platform 设备注册

设备需向内核注册硬件信息(如资源、名称),注册方式分为静态注册(内核编译时)和动态注册(运行时),现代 Linux 更推荐基于设备树(Device Tree) 自动生成设备(无需手动注册)。

(1)传统手动注册(静态 / 动态)
  • 静态注册:通过 PLATFORM_DEVICE_DECLARE 宏在编译时定义设备,适用于内核内置设备:

    // 定义资源(如 UART 寄存器地址 0x12340000-0x12340fff,中断号 10)
    static struct resource myuart_resources[] = {
        [0] = {
            .start = 0x12340000,
            .end   = 0x12340fff,
            .flags = IORESOURCE_MEM,
            .name  = "myuart_regs",
        },
        [1] = {
            .start = 10,
            .end   = 10,
            .flags = IORESOURCE_IRQ,
            .name  = "myuart_irq",
        },
    };
    
    // 静态声明 Platform 设备
    PLATFORM_DEVICE_DECLARE(myuart, "myuart", myuart_resources, ARRAY_SIZE(myuart_resources));
    
  • 动态注册:运行时通过 platform_device_register() 函数注册设备,适用于可热插拔设备(少见):

    struct platform_device *pdev;
    pdev = platform_device_alloc("myuart", 0); // 分配设备(name=myuart,id=0)
    if (!pdev) return -ENOMEM;
    
    // 设置设备资源
    platform_device_add_resources(pdev, myuart_resources, ARRAY_SIZE(myuart_resources));
    
    // 注册设备到 platform 总线
    int ret = platform_device_add(pdev);
    if (ret) platform_device_put(pdev); // 注册失败则释放设备
    
(2)基于设备树的自动注册(主流方式)

现代 ARM/Linux 系统中,设备信息通过设备树(.dts) 描述,内核启动时会自动解析设备树中的 compatible 属性,生成对应的 platform_device 并注册到总线。

示例:设备树中定义一个 UART 设备:

dts

/* 设备树节点:描述 myuart 设备 */
myuart: uart@12340000 {
    compatible = "vendor,myuart"; // 兼容属性(用于与驱动的 of_match_table 匹配)
    reg = <0x12340000 0x1000>;    // 内存资源(基地址 0x12340000,长度 0x1000)
    interrupts = <GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH>; // 中断资源(中断号 10,高电平触发)
    clocks = <&clk 24>;           // 时钟资源(可选)
};

内核启动时,会解析该节点:-->解析得到的数据,放到设备结构体platform_device

  1. 根据 reg 生成 IORESOURCE_MEM 类型的资源。
  2. 根据 interrupts 生成 IORESOURCE_IRQ 类型的资源。
  3. 生成 platform_device,其 dev.of_node 指向该设备树节点,name 由内核自动分配。

阶段 2:Platform 驱动注册

驱动通过 platform_driver_register() 函数注册到 platform_bus 的驱动链表,注册时内核会触发总线的 match 函数,检查是否有已注册的设备与之匹配。

示例:注册一个支持上述 UART 设备的驱动:

#include <linux/platform_device.h>
#include <linux/of.h> // 设备树相关头文件

// 设备树匹配表(compatible 属性匹配)
static const struct of_device_id myuart_of_match[] = {
    { .compatible = "vendor,myuart" }, // 与设备树节点的 compatible 一致
    { /* Sentinel */ } // 表结束标记
};
MODULE_DEVICE_TABLE(of, myuart_of_match); // 告知内核该匹配表(用于模块加载时自动匹配)

// probe 函数:设备与驱动匹配成功后调用
static int myuart_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct resource *mem_res, *irq_res;
    void __iomem *base; // 映射后的虚拟地址

    printk(KERN_INFO "myuart probe: device matched!\n");

    // 1. 获取设备资源(从 platform_device 的 resource 数组中)
    mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第 0 个内存资源
    irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);  // 获取第 0 个中断资源
    if (!mem_res || !irq_res) {
        dev_err(dev, "failed to get resources\n");
        return -ENODEV;
    }

    // 2. 申请并映射 IO 内存(将物理地址映射为内核虚拟地址,避免直接操作物理地址)
    base = devm_ioremap_resource(dev, mem_res); // devm_ 前缀:资源自动随设备释放
    if (IS_ERR(base)) return PTR_ERR(base);
    platform_set_drvdata(pdev, base); // 将映射后的地址存入设备私有数据,供后续使用

    // 3. 注册中断(示例:简化版,实际需实现中断处理函数)
    if (devm_request_irq(dev, irq_res->start, myuart_irq_handler, 0, "myuart_irq", pdev)) {
        dev_err(dev, "failed to request irq\n");
        return -EBUSY;
    }

    // 4. 初始化外设(如设置波特率、数据位、停止位等,通过读写映射后的虚拟地址实现)
    writel(0x1234, base + 0x04); // 向寄存器偏移 0x04 写入 0x1234(示例)

    return 0;
}

// remove 函数:设备移除或驱动卸载时调用
static int myuart_remove(struct platform_device *pdev)
{
    void __iomem *base = platform_get_drvdata(pdev); // 获取私有数据(映射地址)
    printk(KERN_INFO "myuart remove: releasing resources\n");

    // 此处无需手动释放 devm_ 申请的资源(内核自动释放)
    // 若使用非 devm_ 函数(如 ioremap、request_irq),需手动释放(如 iounmap、free_irq)

    return 0;
}

// 定义 Platform 驱动结构体
static struct platform_driver myuart_driver = {
    .probe = myuart_probe,
    .remove = myuart_remove,
    .driver = {
        .name = "myuart_driver", // 驱动名称(备用匹配方式)
        .of_match_table = myuart_of_match, // 设备树匹配表(优先匹配)
        .owner = THIS_MODULE,
    },
    // .id_table = ...  // 若支持多设备,可在此定义 id 表
};

// 驱动模块加载函数:注册驱动
static int __init myuart_driver_init(void)
{
    return platform_driver_register(&myuart_driver);
}

// 驱动模块卸载函数:注销驱动
static void __exit myuart_driver_exit(void)
{
    platform_driver_unregister(&myuart_driver);
}

module_init(myuart_driver_init);
module_exit(myuart_driver_exit);
MODULE_LICENSE("GPL"); // 必须声明 GPL 协议,否则内核拒绝加载
MODULE_DESCRIPTION("My UART Platform Driver");
MODULE_AUTHOR("Your Name");

阶段 3:设备与驱动的匹配与 Probe

当驱动注册到 platform_bus 后,内核会执行以下步骤:

  1. 遍历 platform_bus 上的所有已注册设备(platform_device)。
  2. 对每个设备,调用 platform_match 函数,检查驱动是否支持该设备(按前文匹配顺序)。
  3. 若匹配成功,内核调用驱动的 probe 函数,完成设备初始化。
  4. probe 函数执行成功后,设备进入 “就绪” 状态,可通过驱动提供的接口(如字符设备节点)与用户空间交互。

阶段 4:资源释放(Remove/Shutdown)

当发生以下情况时,内核会触发资源释放:

  • 驱动卸载:调用 rmmod 卸载驱动模块时,内核遍历该驱动匹配的所有设备,调用 remove 函数。
  • 设备移除:若设备支持热插拔,移除设备时,内核调用对应驱动的 remove 函数。
  • 系统关机:调用驱动的 shutdown 函数,关闭外设硬件。

remove 函数的核心任务是释放 probe 函数中申请的资源(如 IO 内存、中断、字符设备节点等)。若使用 devm_ 前缀的函数(如 devm_ioremap_resource、devm_request_irq),内核会自动释放资源,无需手动处理;若使用非 devm_ 函数,则需手动释放。

四、关键辅助函数

在 Platform 驱动开发中,内核提供了大量辅助函数,简化资源操作和设备管理,常用函数如下:

函数分类函数原型功能描述
资源获取struct resource *platform_get_resource(struct platform_device *pdev, unsigned int type, unsigned int num)获取设备的指定类型、指定序号的资源(如内存、中断)
资源获取(设备树)struct resource *platform_get_resource_byname(struct platform_device *pdev, unsigned int type, const char *name)按资源名称获取资源(适用于多资源场景)
IO 内存映射void __iomem *devm_ioremap_resource(struct device *dev, const struct resource *res)申请并映射 IO 内存(devm_ 自动释放)
中断申请int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id)申请中断(devm_ 自动释放)
私有数据管理void platform_set_drvdata(struct platform_device *pdev, void *data)向设备存入私有数据(如映射后的地址)
私有数据管理void *platform_get_drvdata(struct platform_device *pdev)从设备获取私有数据
设备树匹配const struct of_device_id *of_match_device(const struct of_device_id *matches, const struct device *dev)手动检查设备是否与设备树匹配表匹配

五、Platform 框架的进阶特性

1. 多设备匹配(id_table)

若驱动需支持多个同类型但名称不同的设备,可通过 id_table 实现。例如,一个 UART 驱动支持 “vendor,myuart-v1” 和 “vendor,myuart-v2” 两个设备:

// 定义设备 ID 表
static const struct platform_device_id myuart_id_table[] = {
    { "myuart-v1", 0 }, // 设备名称 1,私有数据 0
    { "myuart-v2", 1 }, // 设备名称 2,私有数据 1
    { /* Sentinel */ }
};
MODULE_DEVICE_TABLE(platform, myuart_id_table); // 告知内核该 ID 表

// 驱动结构体中指定 id_table
static struct platform_driver myuart_driver = {
    .probe = myuart_probe,
    .remove = myuart_remove,
    .id_table = myuart_id_table, // 多设备匹配表
    .driver = {
        .name = "myuart-drv",
        .of_match_table = myuart_of_match,
    },
};

匹配时,内核会优先使用 id_table 中的名称与设备的 name 匹配,匹配成功后,pdev->id_entry 会指向匹配的 id_table 项,probe 函数可通过 pdev->id_entry->driver_data 获取私有数据,区分不同设备版本。

2. 电源管理

Platform 框架内置电源管理支持,通过 struct platform_driver 中的 suspend 和 resume 函数实现设备的休眠与唤醒:

// 设备休眠时调用:保存设备状态(如寄存器值),关闭硬件
static int myuart_suspend(struct platform_device *pdev, pm_message_t state)
{
    void __iomem *base = platform_get_drvdata(pdev);
    u32 reg_val = readl(base + 0x04); // 保存寄存器值
    platform_set_drvdata(pdev, (void *)reg_val); // 暂存状态
    writel(0x0000, base + 0x04); // 关闭设备
    return 0;
}

// 设备唤醒时调用:恢复设备状态
static int myuart_resume(struct platform_device *pdev)
{
    void __iomem *base = platform_get_drvdata(pdev);
    u32 reg_val = (u32)base; // 恢复之前保存的寄存器值
    writel(reg_val, base + 0x04); // 恢复设备状态
    return 0;
}

// 驱动结构体中注册电源管理函数
static struct platform_driver myuart_driver = {
    .suspend = myuart_suspend,
    .resume = myuart_resume,
    /* 其他成员... */
};

3. 延迟探测(Deferred Probe)

若设备依赖的资源(如时钟、电源)尚未准备就绪(如驱动加载时,时钟控制器驱动尚未加载),probe 函数可返回 -EPROBE_DEFER,告知内核 “暂时无法初始化设备,稍后重试”。内核会将设备加入延迟探测队列,待依赖资源就绪后,重新触发 probe 函数。

示例:

static int myuart_probe(struct platform_device *pdev)
{
    struct clk *clk;

    // 获取时钟资源(若时钟未准备好,clk_get 会失败)
    clk = devm_clk_get(&pdev->dev, "uart_clk");
    if (IS_ERR(clk)) {
        if (PTR_ERR(clk) == -EPROBE_DEFER)
            return -EPROBE_DEFER; // 延迟探测
        return PTR_ERR(clk);
    }

    /* 其他初始化逻辑... */
    return 0;
}

六、总结

Platform 驱动框架是 Linux 内核中针对 “无总线设备” 的标准化解决方案,其核心是 “设备与驱动分离”,通过 platform_bus 实现统一匹配。开发者在实际开发中,需重点关注:

  1. 设备信息描述:通过设备树或手动注册,提供准确的硬件资源(地址、中断、时钟)。
  2. 驱动逻辑实现:聚焦 probe/remove 函数,完成资源申请、硬件初始化与释放。
  3. 匹配机制选择:优先使用设备树 compatible 属性匹配,其次使用 id_table 或 name 匹配。

掌握 Platform 框架,是理解 Linux 其他驱动框架(如 AMBA、PCIe)的基础,也是嵌入式 Linux 驱动开发的核心技能之一。

更多推荐