嵌入式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总线提供三级匹配策略,按优先级从高到低依次尝试:
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总线,无需手动编码。
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总线匹配流程
7.2 probe函数执行流程
八、常见问题解决
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策略。