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_IO | I/O 端口 |
IORESOURCE_DMA | DMA 通道 |
四、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" 节点被忽略 |
关联笔记
- PWM驱动开发 — PWM 子系统 probe 函数实战
- GPIO-Hog机制详解 — GPIO 资源的特殊占用方式
- 平台设备驱动模型 — 本文
- Linux内核移植(iMX6ULL) — DTS 编写与编译
延伸阅读
掌握 Platform 模型后,可通过 LED 和蜂鸣器两个小型驱动验证完整流程。
- 返回嵌入式 Linux 学习路线,确认本文在完整知识结构中的位置。
- LED Platform 驱动开发
- Beep Platform 驱动开发(miscdevice 框架)




