Linux Platform 设备驱动模型 文章封面

Linux Platform 设备驱动模型

Linux 驱动开发的基础框架,几乎所有片上外设驱动(GPIO、UART、SPI、I2C、PWM 等)都基于此模型。
本文覆盖:核心概念 → 数据结构 → 匹配机制 → 资源获取 → 完整代码示例 → 面试要点。


一、为什么需要平台设备驱动模型

Linux 2.4 之前,驱动程序把寄存器地址、中断号等硬件信息硬编码在驱动源码中,导致:

  • 同一外设换块板子就要改驱动源码重新编译
  • 硬件信息散落在各处,无法统一管理

Linux 2.6 引入平台设备驱动模型(Platform Device/Driver Model),核心思想是:

硬件描述(DTS)  ←→  内核匹配  ←→  驱动程序
     分离                                分离
  "板子有什么"            "外设怎么工作"

设备(硬件信息) 和 驱动(控制逻辑) 彻底分离。驱动无需关心自己跑在哪块板子上,板子信息全部由设备树(DTS)描述。


二、三大核心概念

2.1 platform_bus(平台总线)

平台总线是一条虚拟总线,内核在启动时由 platform_bus_init() 自动创建,用户代码无需也无法注册它。

/sys/bus/platform/
├── devices/     ← 所有平台设备在此注册
└── drivers/     ← 所有平台驱动在此注册

平台总线维护两张链表(devices / drivers),并在两者发生变化时自动触发匹配(match)。

2.2 platform_device(平台设备)

平台设备代表一个具体的硬件外设。有两种来源:

来源场景
设备树(DTS)→ 内核解析后自动注册现代嵌入式开发(主流)
platform_device_register() 手动注册无设备树的老板子

2.3 platform_driver(平台驱动)

平台驱动是驱动工程师编写的模块,通过 platform_driver_register() 注册到平台总线,等待与设备匹配。

三者关系:

设备树(.dts)
     │  内核解析
     ▼
platform_device   ──── platform_bus ────  platform_driver
(硬件描述)              (虚拟总线,              (驱动逻辑,
                          负责匹配)               工程师编写)
                              │
                         match 成功
                              ▼
                         调用 probe()


三、关键数据结构

3.1 struct platform_device

/* include/linux/platform_device.h */
struct platform_device {
    const char          *name;          /* 设备名称,与 driver.name 匹配 */
    int                  id;            /* 设备 ID,多个同类设备用于区分 */
    struct device        dev;           /* 内嵌的通用设备结构(含 of_node) */
    u32                  num_resources; /* 资源数量 */
    struct resource     *resource;      /* 资源数组(寄存器地址、中断号等) */
    const struct platform_device_id *id_entry; /* 匹配到的 ID 条目 */
};

dev.of_node 指向 DTS 中对应的设备节点,驱动通过它可以读取自定义属性。

3.2 struct platform_driver

/* include/linux/platform_device.h */
struct platform_driver {
    int  (*probe) (struct platform_device *);   /* 设备匹配成功后调用 */
    int  (*remove)(struct platform_device *);   /* 设备移除时调用 */
    void (*shutdown)(struct platform_device *); /* 系统关机时调用 */
    struct device_driver driver;                /* 内嵌通用驱动结构 */
    const struct platform_device_id *id_table;  /* 设备 ID 表(非 DTS 匹配用) */
};

/* driver 字段的关键成员 */
struct device_driver {
    const char              *name;          /* 驱动名称 */
    const struct of_device_id *of_match_table; /* DTS compatible 匹配表 */
    struct module           *owner;
    /* ... */
};

3.3 struct resource

平台设备资源的通用表示:

/* include/linux/ioport.h */
struct resource {
    resource_size_t start;  /* 资源起始地址(物理地址或中断号) */
    resource_size_t end;    /* 资源结束地址 */
    const char     *name;
    unsigned long   flags;  /* 资源类型标志 */
};

flags 值含义
IORESOURCE_MEM内存映射寄存器(物理地址)
IORESOURCE_IRQ中断号
IORESOURCE_IOI/O 端口
IORESOURCE_DMADMA 通道

四、DTS 设备描述

驱动开发的第一步:在 DTS 中描述硬件。

/* arch/arm/boot/dts/imx6ull-alpha.dts */
&ecspi3 {
    /* ... */
};

/* 假设在根节点下添加一个自定义设备 */
myled: led@0 {
    compatible = "mycompany,myled";        /* 匹配字符串,与驱动中 of_match_table 对应 */
    reg = <0x020AC000 0x4000>;             /* 寄存器基址 + 大小 */
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>;  /* 中断号 + 触发方式 */
    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; /* 自定义属性:GPIO */
    status = "okay";                       /* "okay" 生效,"disabled" 禁用 */
};

关键属性说明:

属性作用
compatible驱动匹配的核心依据,格式为 "厂商,设备型号"
reg寄存器物理地址和大小,被解析为 IORESOURCE_MEM 资源
interrupts中断信息,被解析为 IORESOURCE_IRQ 资源
status"okay" 才会被内核注册为 platform_device

五、匹配机制

设备注册或驱动注册时,平台总线调用 platform_match() 尝试匹配,优先级从高到低:

/* drivers/base/platform.c */
static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* 1. driver_override:强制绑定(最高优先级,调试用) */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* 2. of_match_table:DTS compatible 字符串匹配(主流路径) */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* 3. ACPI 匹配(x86 平台) */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* 4. id_table:平台设备 ID 表匹配 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* 5. 设备名与驱动名直接比较(最低优先级) */
    return (strcmp(pdev->name, drv->name) == 0);
}

嵌入式开发最常用的是第 2 步:驱动的 of_match_table 中的 compatible 字符串与 DTS 节点的 compatible 属性逐一比较,匹配则触发 probe()。


六、驱动注册 API

6.1 注册与注销

/* 注册平台驱动 */
int platform_driver_register(struct platform_driver *drv);

/* 注销平台驱动 */
void platform_driver_unregister(struct platform_driver *drv);

6.2 module_platform_driver 宏(推荐写法)

以前需要手动写 module_init + module_exit + 注册/注销调用,内核提供了简化宏:

/* 等价于手动写 module_init/module_exit + register/unregister */
module_platform_driver(myled_driver);

展开后相当于:

static int __init myled_driver_init(void)
{
    return platform_driver_register(&myled_driver);
}
module_init(myled_driver_init);

static void __exit myled_driver_exit(void)
{
    platform_driver_unregister(&myled_driver);
}
module_exit(myled_driver_exit);


七、probe 函数中的资源获取

匹配成功后内核调用 probe(),驱动在这里获取硬件资源并完成初始化。

7.1 获取寄存器地址并映射

static int myled_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem    *base;

    /* 获取第 0 个 IORESOURCE_MEM 资源(来自 DTS 的 reg 属性) */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "no mem resourcen");
        return -ENODEV;
    }

    /* 申请并映射寄存器(devm_ 前缀:设备移除时自动释放) */
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 现在可以直接用 readl/writel 访问寄存器 */
    writel(0x1, base + REG_OFFSET);
    /* ... */
}

7.2 获取中断号

    /* 获取第 0 个中断资源(来自 DTS 的 interrupts 属性) */
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "no irq resourcen");
        return irq;
    }

    /* 申请中断(devm_ 前缀:自动释放) */
    ret = devm_request_irq(&pdev->dev, irq, myled_irq_handler,
                           IRQF_TRIGGER_RISING, "myled", priv);
    if (ret)
        return ret;

7.3 读取 DTS 自定义属性

    struct device_node *np = pdev->dev.of_node;
    u32 val;

    /* 读取整型属性 */
    of_property_read_u32(np, "some-property", &val);

    /* 获取 GPIO(来自 DTS 的 led-gpio 属性) */
    int gpio = of_get_named_gpio(np, "led-gpio", 0);


八、devm_* 托管资源

devm_* 是设备托管资源(Device Managed Resource)系列函数,与普通申请函数的区别:

普通函数devm_ 函数差异
ioremap()devm_ioremap_resource()probe 失败或 remove 时自动 iounmap
request_irq()devm_request_irq()自动 free_irq
kzalloc()devm_kzalloc()自动 kfree
gpio_request()devm_gpio_request()自动 gpio_free

核心原则:使用 devm_* 后,remove 函数里通常不需要手动释放资源,内核在设备移除时按申请的逆序自动释放,避免资源泄漏。

⚠️ 注意:devm_* 的释放时机是设备(struct device)引用计数归零时,而不是模块卸载时。绝大多数情况下两者一致,但在有设备引用计数延迟的场景需注意。


九、完整驱动示例

以一个 LED 驱动为例,展示平台驱动的完整写法:

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

/* 私有数据结构 */
struct myled_priv {
    void __iomem *base;
    int           gpio;
    int           irq;
};

/* ① DTS compatible 匹配表 */
static const struct of_device_id myled_of_match[] = {
    { .compatible = "mycompany,myled" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, myled_of_match);

/* ② probe:设备匹配后调用 */
static int myled_probe(struct platform_device *pdev)
{
    struct myled_priv *priv;
    struct resource   *res;
    int                ret;

    dev_info(&pdev->dev, "myled proben");

    /* 分配私有数据(devm:设备移除时自动 kfree) */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 获取并映射寄存器 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    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;

    /* 读取 DTS 中自定义的 GPIO 属性 */
    priv->gpio = of_get_named_gpio(pdev->dev.of_node, "led-gpio", 0);
    if (!gpio_is_valid(priv->gpio)) {
        dev_err(&pdev->dev, "invalid led-gpion");
        return -ENODEV;
    }

    ret = devm_gpio_request_one(&pdev->dev, priv->gpio,
                                GPIOF_OUT_INIT_LOW, "myled");
    if (ret)
        return ret;

    /* 将私有数据挂到设备上,remove 时取回 */
    platform_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "myled probe success, base=%pn", priv->base);
    return 0;
}

/* ③ remove:设备移除时调用(devm_* 资源无需手动释放) */
static int myled_remove(struct platform_device *pdev)
{
    struct myled_priv *priv = platform_get_drvdata(pdev);

    dev_info(&pdev->dev, "myled removen");
    /* gpio、ioremap 等 devm_ 资源会自动释放 */
    return 0;
}

/* ④ platform_driver 结构体 */
static struct platform_driver myled_driver = {
    .probe  = myled_probe,
    .remove = myled_remove,
    .driver = {
        .name           = "myled",
        .of_match_table = myled_of_match,
        .owner          = THIS_MODULE,
    },
};

/* ⑤ 用宏替代手写 module_init/module_exit */
module_platform_driver(myled_driver);

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example LED Platform Driver");
MODULE_LICENSE("GPL");


十、完整启动流程梳理

内核启动
   │
   ├─ platform_bus_init()          ← 创建虚拟平台总线(/sys/bus/platform)
   │
   ├─ of_platform_populate()       ← 遍历 DTS,为每个 status="okay" 的节点
   │                                  创建 platform_device 并注册到平台总线
   │
   ├─ 驱动模块加载(insmod / 内置)
   │       │
   │       └─ platform_driver_register()
   │               │
   │               └─ 遍历已有 platform_device,调用 platform_match()
   │                       │
   │                       └─ compatible 匹配成功
   │                               │
   │                               └─ really_probe() → driver->probe()
   │                                       │
   │                                       └─ 获取资源、初始化硬件、注册字符设备等
   │
   └─ 驱动正常工作


十一、高频面试题

问题关键答案
平台设备驱动模型解决了什么问题?将硬件描述(DTS)与驱动逻辑分离,提高可移植性
platform_bus 是谁注册的?内核启动时由 platform_bus_init() 自动注册,用户不操作
compatible 如何匹配?驱动的 of_match_table 与 DTS 节点的 compatible 字符串逐一比较
probe 函数什么时候被调用?设备和驱动通过 match 匹配成功后,由内核调用 really_probe() 触发
如何在 probe 里获取寄存器地址?platform_get_resource() 取 struct resource,再 devm_ioremap_resource() 映射
devm_ 系列函数的作用?托管资源,设备移除时自动释放,避免在 remove 里手动 free
platform_set_drvdata / platform_get_drvdata 的作用?probe 里保存私有数据指针,remove 和其他回调里取出
module_platform_driver 宏做了什么?展开为 module_init + module_exit + platform_driver_register/unregister
of_match_table 里为什么要有空条目结尾?内核遍历时以 compatible == NULL 为结束标志(sentinel pattern)
DTS 节点 status 属性的作用?"okay" 才会被注册为 platform_device;"disabled" 节点被忽略

关联笔记

延伸阅读

掌握 Platform 模型后,可通过 LED 和蜂鸣器两个小型驱动验证完整流程。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部