3、Platform驱动核心结构:struct platform_driver与struct platform_device详解,probe与remove回调
好,咱们今天来聊聊Platform驱动里最核心的两个结构体——platform_driver和platform_device。说白了,整个驱动框架就是围着这两个家伙转的。你写驱动,本质上就是在填充这两个结构体,然后告诉内核:嘿,我这儿有个驱动,你看着办。
我个人习惯,每次写一个新驱动,都会先把这两个结构体的骨架搭好。就像盖房子,先立柱子,再砌墙。今天我们就来拆开看看,这两根柱子到底长什么样。
3.1 struct platform_driver:驱动的“身份证”
先看驱动侧。每个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);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
这里最关键的,就是probe和remove这两个回调函数。嗯,咱们重点讲它们。
核心要点:probe是驱动的入口,remove是驱动的出口。一个负责“生”,一个负责“死”。
3.1.1 probe回调——驱动的“出生证明”
什么时候会调用probe?当内核发现一个platform_device与你的platform_driver匹配成功时,就会调用你的probe函数。说白了,就是内核说:“嘿,你的设备来了,快去初始化吧!”
我在项目中遇到过好几次,新手写probe时恨不得把所有代码都塞进去。其实probe只应该做三件事:
- 资源获取:从device tree或platform_device中拿到内存地址、中断号、时钟等
- 硬件初始化:复位设备、配置寄存器、启动时钟
- 注册子系统:比如注册字符设备、input设备、net设备等
来看一个我实际项目中用过的probe示例:
static int my_uart_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq, ret;
// 1. 获取IO内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 2. 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 3. 分配并初始化驱动私有数据
// ... 这里省略具体实现
// 4. 注册字符设备
ret = register_chrdev(0, "my_uart", &my_uart_fops);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to register char device\n");
return ret;
}
dev_info(&pdev->dev, "my_uart probed successfully\n");
return 0;
}
我的小技巧:尽量多用devm_系列函数(比如devm_ioremap_resource、devm_kzalloc)。这些函数会在驱动卸载或probe失败时自动帮你释放资源,省得你写一堆错误处理代码。我曾经因为忘记释放中断,导致系统重启后设备无法使用,排查了半天才发现是资源泄漏。
3.1.2 remove回调——驱动的“临终遗言”
remove是probe的逆操作。当驱动被卸载,或者设备被移除时,内核会调用remove。它的任务很简单:把probe里做的事情,一件一件撤销掉。
static int my_uart_remove(struct platform_device *pdev)
{
struct my_uart_data *data = platform_get_drvdata(pdev);
// 1. 注销字符设备
unregister_chrdev(data->major, "my_uart");
// 2. 关闭硬件(比如关时钟、关中断)
// ...
// 3. 释放私有数据(如果用devm_分配,这步可以省略)
// ...
dev_info(&pdev->dev, "my_uart removed\n");
return 0;
}
你想想看,如果probe里申请了5种资源,remove里只释放了4种,那就是一颗定时炸弹。我建议你写probe的时候,就把remove的框架写好,一一对应。
避坑指南:我曾经在remove里忘记调用free_irq,结果驱动卸载后,中断还在触发,系统直接panic。记住:probe里申请了什么,remove里就要释放什么,一个都不能少。
3.2 struct platform_device:设备的“户口本”
设备侧的结构体相对简单一些。它描述了一个具体的硬件设备:它用哪些内存地址?用哪个中断?挂在哪个总线上?
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
/* ... 省略一些不常用的字段 */
};
这里最重要的就是resource数组。它描述了设备需要的所有硬件资源。在设备树时代,这些资源由内核自动解析并填充。但在老式的板级文件(board file)中,你需要手动填写。
3.2.1 资源类型与获取
资源类型用宏定义区分,最常见的有:
| 宏定义 | 含义 | 获取函数 |
|---|---|---|
| IORESOURCE_MEM | 内存映射IO地址 | platform_get_resource(pdev, IORESOURCE_MEM, 0) |
| IORESOURCE_IRQ | 中断号 | platform_get_irq(pdev, 0) |
| IORESOURCE_DMA | DMA通道 | platform_get_resource(pdev, IORESOURCE_DMA, 0) |
| IORESOURCE_IO | IO端口地址 | platform_get_resource(pdev, IORESOURCE_IO, 0) |
嗯,这里要注意:platform_get_irq是专门获取中断的快捷函数,它比platform_get_resource更智能,能处理一些特殊情况。我个人习惯,拿中断一律用platform_get_irq,拿内存用platform_get_resource。
3.3 匹配机制:驱动和设备如何“牵手”
驱动和设备是怎么匹配上的?说白了,就是内核拿着你的platform_driver,去遍历所有platform_device,看谁的name字段能对上。
匹配顺序是这样的:
- 设备树匹配:如果设备树中节点的compatible属性与driver的of_match_table匹配
- ID表匹配:如果platform_driver有id_table,则与platform_device的name比较
- 名字直接匹配:如果以上都不行,直接比较driver的name和device的name
我画了一张图,帮你理清这个流程:
3.4 实战经验:probe/remove的常见陷阱
最后,分享几个我踩过的坑,希望能帮你少走弯路。
陷阱1:probe失败后资源泄漏
如果你在probe中途失败返回,之前申请的资源必须全部释放。用devm_系列函数可以自动处理,但如果你用了request_irq、ioremap等非devm函数,一定要在错误路径上手动释放。
陷阱2:remove被调用时设备还在工作
我曾经在remove里直接释放了DMA缓冲区,但DMA传输还没完成,结果系统直接挂死。正确的做法是:先停止设备的所有活动(关中断、停DMA、等待传输完成),再释放资源。
陷阱3:probe和remove的对称性
我见过一个驱动,probe里申请了3个中断,remove里只释放了2个。这种bug很难查,因为不是每次卸载都会触发。我的习惯是:写probe时,每申请一个资源,就在旁边注释上对应的释放代码。这样写remove时直接抄作业就行。
好了,关于platform_driver和platform_device的核心内容,今天就聊到这儿。这两个结构体是Platform驱动的地基,地基打牢了,上面的房子才稳。下一节我们会深入设备树,看看这些资源是怎么从dts文件里解析出来的。