1. 从一次probe失败说起:为什么要搞懂Platform匹配机制
做i.MX6ULL驱动开发的人,八成都有过这种经历:设备树节点写得好好的,驱动代码也编译通过了,insmod之后却死活不见probe函数被调用。翻遍内核日志只有一句cold的提示,要么是platform_driver_register之后没有下文,要么是of_device_id表看起来没问题但就是匹配不上。我最早接触Platform总线机制时,就是为了排查这类莫名其妙的问题。
先说结论:Linux设备驱动模型里,设备与驱动的"撮合"工作由总线(bus)完成。USB设备挂在USB总线上,PCI设备挂在PCI总线上,而大量SoC片内设备——比如i.MX6ULL的GPIO控制器、UART、I2C控制器、SPI控制器——它们并不依附于某条物理总线,而是直接挂在内核虚拟的Platform总线上。Platform总线是个没有硬件对应的软件抽象,专门用来管理那些"直接编址在内存空间、由内核直接控制"的设备。
这套机制解决的核心问题是:设备代码与驱动代码解耦。设备信息描述"有什么硬件",驱动代码描述"怎么操作硬件",二者通过匹配规则建立联系。这样同一份驱动可以服务多个设备(通过ID表或设备树compatible字段),同一块板子也可以灵活更换驱动实现。整个匹配过程对上层应用完全透明,但对驱动开发者来说,这是必须啃下的硬骨头。
我自己是在一块以i.MX6ULL为核心板卡上做项目时,被几个FPGA外设和自定义GPIO设备折腾得够呛,才真正把这套匹配机制内部挖了一遍。这篇文章不打算照着内核文档复读,而是把我踩过的坑、验证过的结论、排查流程完整分享出来。如果你正在写i.MX6ULL或类似ARM SoC的平台驱动,或者设备树节点写了却没反应,这篇内容对你会非常有用。
2. Linux设备模型基石:总线、设备与驱动的三角关系
2.1 为什么Linux需要引入Platform总线
理解Platform机制前,先弄清楚Linux设备模型的全貌。内核维护者设计了一套统一的设备模型,核心就是struct bus_type、struct device、struct device_driver这三大数据结构。总线负责维护设备链表和驱动链表,并在两者之间执行匹配逻辑。
对于i.MX6ULL这种嵌入式SoC,绝大部分外设控制器都集成在芯片内部,它们在物理上并不插在某个标准总线上,但依然需要被内核管理。Platform总线就是为这类设备量身定制的虚拟总线。早期内核甚至直接把这类设备叫platform_device,设备树(Device Tree)大规模普及之前,这些设备信息都是在板级文件(arch/arm/mach-xxx)里静态定义的。
你可能会问:既然i.MX6ULL的设备树里已经有了完整的外设描述,为什么还需要Platform机制?因为设备树只是"信息载体",真正让设备工作起来的是驱动。设备树节点描述了硬件地址、中断号、时钟、引脚复用等资源,Platform驱动通过匹配设备树节点获取这些资源,然后完成控制器初始化。设备树负责"告诉内核有什么",Platform驱动负责"让内核用起来"。
2.2 device、driver、bus三者如何协作
把设备模型拆开看,每个总线对象管理两条链表:设备链表存所有注册到该总线上的设备,驱动链表存所有注册到该总线上的驱动。每当有新设备加入或新驱动注册,总线就会触发一次匹配流程。
i.MX6ULL上我们最常见的操作流程是:uboot启动内核后,内核解析设备树,为每个节点创建struct device,并挂到对应总线上——挂在哪条总线,取决于节点的compatible属性在平台驱动目录(drivers/)里的归属。比如compatible = "fsl,imx6ull-uart"会被串口驱动匹配,compatible = "fsl,imx6ull-gpio"会被GPIO驱动匹配。大多数SoC集成控制器驱动的driver结构体都内嵌在struct platform_driver里,由module_platform_driver宏注册。
当驱动注册时,platform_driver_register会执行driver_register,进而触发总线匹配。匹配成功后调用驱动的probe方法;设备移除或驱动卸载时,调用remove方法。这套机制的精髓在于:设备和驱动是完全独立的两个实体,任何一方先注册都可以,另一方到来时总线会主动补一次匹配。
3. 设备侧:从设备树节点到platform_device的完整链路
3.1 设备树是如何变成platform_device的
i.MX6ULL内核启动阶段,start_kernel→setup_arch→unflatten_device_tree会把dtb二进制解析成device_node树,但这些节点还不是设备。真正的设备对象要到init_machine后由of_platform_bus_probe或of_platform_populate创建。
以i.MX6ULL官方内核(4.x/5.x版本)为例,imx6ul_init_machine会调用of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL),遍历设备树根节点下的子节点。对于每个节点,内核创建一个platform_device,并将device_node指针关联到platform_device->dev.of_node。这就是为什么驱动里能用of_property_read_*函数读取设备树属性——因为dev.of_node已经指过来了。
这里有个关键点:并非所有设备树节点都会变成platform_device。只有满足以下条件之一的节点才会被of_platform_populate处理:
- 节点的
compatible属性匹配of_default_bus_match_table中的条目(该表包含simple-bus、simple-mfd、arm,amba-bus等常用总线类型)。 - 节点自身就是某个总线的根节点(比如I2C控制器节点),会先创建平台设备,再递归处理子节点。
- 节点类型(
device_type)为platform或在of_platform_bus_probe的显式处理范围内。
如果一个设备树节点没有被populate成设备,后面自然不会有匹配过程。我之前排查过一个外部中断控制器不工作的问题,最后发现根节点下没有声明compatible = "simple-bus",子节点全部"失踪"了。
3.2 资源解析:platform_get_resource怎么工作
设备树节点转换成platform_device后,设备驱动最常做的事是获取I/O地址、中断号等资源。platform_get_resource函数是从platform_device->resource数组里取数据,而这个数组是在of_device_alloc阶段从设备树节点解析生成的。
具体来说,of_platform_device_create_pdata会调用of_device_add,后者通过of_address_to_resource和of_irq_get把reg属性转成IORESOURCE_MEM类型的资源,把interrupts属性转成IORESOURCE_IRQ类型的资源。一个节点如果有多个reg条目,就会得到多个MEM资源,索引从0开始。
驱动里常见写法是:
struct resource *res; void __iomem *base; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res);这个写法的好处是内存映射由设备模型管理,驱动卸载时自动释放。如果获取失败,devm_ioremap_resource会打印详细错误原因,比手动request_mem_region更友好。我在一块定制板卡上遇到过一次资源获取失败,原因是在设备树里写错了reg地址,和datasheet上的寄存器基址对不上——这种低级错误其实很常见,资源解析失败时第一反应该是查设备树地址。
4. 驱动侧:platform_driver的注册与probe触发条件
4.1 platform_driver结构体到底包含什么
驱动侧的核心数据结构是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 *); 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; bool prevent_deferred_probe; };对i.MX6ULL驱动开发来说,最重要的是probe、driver.name、driver.of_match_table这三个字段。probe是匹配成功后调用的初始化入口;driver.name用于和设备直接匹配(传统方式);of_match_table用于和设备树节点匹配(现代主流方式)。id_table是中间产物,主要用于兼容一些非设备树但有明确id的device,i.MX6ULL开发中很少用。
注册方式通常是用module_platform_driver宏:
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_init和module_exit,分别调用platform_driver_register和platform_driver_unregister,省去手写入口函数的麻烦。但需要注意:如果你在驱动里已经写了module_init/module_exit,再用module_platform_driver就会导致重复定义,编译报错。
4.2 probe函数没有被调用的常见原因
这是驱动开发里最高频的问题。我把排查思路整理成一个清单,按优先级排:
- 驱动没有编入内核镜像:如果驱动编译成
.ko模块,需要确认启动后已经insmod成功。如果编入内核,要确认CONFIG_XXX已经置y。 - 设备树节点被禁用了:检查
status属性,如果为disabled,设备不会生成。改成okay或直接删除该属性。 - 匹配属性不一致:设备树
compatible和驱动of_match_table中compatible字符串必须完全一致,包括厂商前缀。多一个空格都不行。 - 驱动模块未正确注册:
platform_driver_register失败时不会probe。导致注册失败的常见原因是同name冲突、内存不足等。 - 设备树节点没有生成platform_device:根节点下缺
simple-bus,或父节点没有正确展开,设备根本没populate出来。 - deferred probe:
probe返回-EPROBE_DEFER,会进入延迟队列等待依赖资源(如时钟、供电域)就绪。
还有一种隐蔽情况:设备树节点虽然生成了设备,但该设备已经有了对应的驱动且正在使用,此时注册新驱动不会触发probe。内核里每个设备只能绑定一个驱动,所谓"绑定"就是dev->driver指针被赋值为某个驱动。
4.3 延迟探测机制(deferred probe)的作用
i.MX6ULL这类多外设SoC上,设备启动顺序很关键。例如某个传感器驱动依赖I2C控制器,但如果I2C控制器驱动加载在前、传感器驱动加载在后,自然没问题;反过来,如果传感器驱动先注册,I2C控制器还没ready,probe里请求I2C adapter会失败。
Linux解决这个问题的办法就是-EPROBE_DEFER。驱动probe时如果发现依赖资源未就绪,直接返回这个特殊错误码。设备模型不会放弃,而是把设备放入延迟队列,等后续有驱动注册成功时重新触发一次匹配和probe。
使用方式很简单:
static int sensor_probe(struct platform_device *pdev) { struct i2c_adapter *adap; adap = i2c_get_adapter(2); if (!adap) return -EPROBE_DEFER; // 继续初始化... }这里有一个实用技巧:想知道设备为什么一直probe失败,可以在内核启动参数里加deferred_probe_debug,或者启动后查看/sys/kernel/debug/devices_deferred文件,里面列出了所有等待重试的设备及其依赖。
5. 匹配机制深度拆解:of_match_table的优先级与匹配流程
5.1 完整匹配顺序:从name到compatible再到id_table
platform_match是总线匹配的总入口,函数位置在drivers/base/platform.c中。它的匹配顺序如下:
- 使用
of_driver_match_device,优先匹配设备树节点与驱动的of_match_table。 - 如果设备树匹配失败,回退到
platform_match_id,用platform_device的id_entry和驱动id_table逐一比对。 - 如果还是没有匹配结果,执行
strcmp(dev->name, drv->name),比对platform_device名称(通常是设备树节点名去掉unit address)与platform_driver.driver.name。
这里有个重要细节:of_driver_match_device内部还要经历两层匹配。它先遍历驱动的of_match_table中的每个of_device_id,将compatible值和设备树节点的compatible属性逐条比对;然后又检查和设备树节点name、type是否匹配of_device_id中的name和type字段(这两个字段在现代设备树驱动里很少使用)。
5.2 of_device_id的表项结构与匹配优先级
struct of_device_id定义如下:
struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };对i.MX6ULL驱动来说,最常用的是compatible字段。设备树节点的compatible属性可以包含多个字符串,优先级从前往后;驱动的of_match_table则是一个数组,每个条目列出一种兼容的设备。匹配时驱动表项和节点属性任一相符即可。
举个例子:
static const struct of_device_id imx6ull_mydev_of_match[] = { { .compatible = "fsl,imx6ull-mydev", }, { .compatible = "fsl,imx6ul-mydev", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx6ull_mydev_of_match);设备树里可以写:
mydev: mydev@02000000 { compatible = "fsl,imx6ull-mydev", "fsl,imx6ul-mydev"; reg = <0x02000000 0x1000>; interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>; };配置了MODULE_DEVICE_TABLE后,编译模块时modpost会生成modules.ofmap文件,供热插拔和自动加载使用。不过i.MX6ULL这种嵌入式Linux通常直接把驱动编入内核(=y),不依赖modprobe自动加载,所以这块用处不大,但如果你的BSP用initramfs模块加载方式,MODULE_DEVICE_TABLE就很有价值。
5.3 匹配成功后内核做了什么
匹配成功后,总线的probe逻辑被调用:
- 先调用驱动
probe前的准备函数really_probe,完成设备与驱动的绑定,即dev->driver = drv。 - 调用
driver->probe(dev)。对platform驱动来说,最终执行的是platform_drv_probe,它会强制类型转换为struct platform_driver,然后调用我们写的probe函数。
实际上platform_drv_probe里还有一层针对旧式id_table驱动的兼容处理。如果有id_table匹配,会把id_entry传递给驱动;否则传NULL。现在大多用设备树,id_entry基本用不到。
驱动probe成功返回0后,设备进入"已绑定"(bound)状态。此时你在用户态可以查看/sys/bus/platform/devices/下对应设备目录,里面会有driver符号链接,指向驱动就说明绑定成功。这是快速确认驱动是否匹配成功的好方法,比翻dmesg更直观。
6. 一例i.MX6ULL自定义GPIO设备的完整匹配过程
6.1 场景描述与设备树编写
我在一块i.MX6ULL板卡上挂了一颗自定义的IO扩展芯片,挂在片选(chip select)上,类似于通过GPIO模拟的SPI设备(实际是内存映射外设)。为演示Platform匹配机制,假设它被映射到i.MX6ULL的0x0201c000地址区。
设备树节点如下:
&iomuxc { pinctrl_myext: myextgrp { fsl,pins = < MX6UL_PAD_UART1_TX_DATA__GPIO1_IO17 0x17059 MX6UL_PAD_UART1_RX_DATA__GPIO1_IO18 0x17059 >; }; }; myext: myext@0201c000 { compatible = "myvendor,myext", "simple-mfd"; reg = <0x0201c000 0x400>; interrupts = <GIC_SPI 47 IRQ_TYPE_LEVEL_HIGH>; interrupt-parent = <&intc>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_myext>; status = "okay"; };注意我在compatible里加了simple-mfd,这不是必须的,但当一个节点既是设备又是子设备容器时很有用。如果父节点希望子节点被自动填充,最好把它定义为simple-mfd。这个节点会被of_platform_populate识别并生成platform_device。
节点地址用了0x0201c000,这个既能作为reg资源,又让平台设备总线上有唯一标识。实际项目中这个地址要对应你真正的寄存器基址。
6.2 驱动编写与挂载
驱动核心部分如下:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/io.h> #include <linux/interrupt.h> #include <linux/slab.h> #define MYEXT_REG_DATA 0x00 #define MYEXT_REG_CTRL 0x04 struct myext_dev { void __iomem *base; int irq; // 其他私有数据 }; static const struct of_device_id myext_of_match[] = { { .compatible = "myvendor,myext" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myext_of_match); static irqreturn_t myext_isr(int irq, void *dev_id) { struct myext_dev *mdev = dev_id; unsigned int st; st = readl(mdev->base + MYEXT_REG_CTRL); // 中断处理逻辑 pr_info("myext interrupt, ctrl=0x%08x\n", st); return IRQ_HANDLED; } static int myext_probe(struct platform_device *pdev) { struct resource *res; struct myext_dev *mdev; void __iomem *base; int irq; int ret; dev_info(&pdev->dev, "myext probe enter\n"); mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL); if (!mdev) return -ENOMEM; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "failed to get memory resource\n"); return -EINVAL; } base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; mdev->base = base; mdev->irq = irq; ret = devm_request_irq(&pdev->dev, irq, myext_isr, 0, dev_name(&pdev->dev), mdev); if (ret) { dev_err(&pdev->dev, "failed to request irq %d, ret=%d\n", irq, ret); return ret; } platform_set_drvdata(pdev, mdev); dev_info(&pdev->dev, "myext probed successfully, irq=%d\n", irq); return 0; } static int myext_remove(struct platform_device *pdev) { struct myext_dev *mdev = platform_get_drvdata(pdev); // 停止业务逻辑、清理资源(devm管理的会自动释放) dev_info(&pdev->dev, "myext removed\n"); return 0; } static struct platform_driver myext_driver = { .probe = myext_probe, .remove = myext_remove, .driver = { .name = "myext", .of_match_table = myext_of_match, }, }; module_platform_driver(myext_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("i.MX6ULL MyEXT platform driver");这段代码的probe里演示了几个常见操作:获取MEM资源、映射I/O地址、获取中断号、注册中断处理函数。开发中我再三强调:能devm_就devm_,这些接口在驱动卸载或probe失败时会自动释放资源,避免泄漏。
6.3 编译验证与绑定确认
将驱动编入内核(obj-y),重新编译烧录后,启动进入shell,执行:
ls /sys/bus/platform/devices/应能看到myext@0201c000目录。然后查看它的driver链接:
ls -l /sys/bus/platform/devices/myext@0201c000/driver如果结果是.../driver -> ../../../bus/platform/drivers/myext,说明绑定成功。也可以直接看dmesg:
dmesg | grep myext正常输出是:
myext myext@0201c000: myext probe enter myext myext@0201c000: myext probed successfully, irq=47这一步确认远比在代码里加printk更省事。有一次我认为驱动没probe,结果发现编进去的驱动名称打错了,设备树compatible匹配不上,/sys/bus/platform/devices/myext@...下面压根没有driver链接,问题立刻定位。
6.4 两种常见匹配场景的验证手法
场景一:设备树先解析,驱动后编译进内核。这是常态,内核启动时设备树已经生成了platform_device,驱动注册时总线会主动补一次匹配,probe照常执行。
场景二:同一驱动支持多款板卡同类型设备。把多个of_device_id条目放进去:
static const struct of_device_id myext_of_match[] = { { .compatible = "myvendor,myext", }, { .compatible = "myvendor,myext-v2", }, { .compatible = "myvendor,myext-lite", }, { /* sentinel */ } };设备树里写对应的compatible即可。注意不同型号如果寄存器布局有差异,可以利用of_device_id.data字段传递型号特有的配置参数:
static const struct of_device_id myext_of_match[] = { { .compatible = "myvendor,myext", .data = &myext_v1_cfg, }, { .compatible = "myvendor,myext-v2", .data = &myext_v2_cfg, }, { /* sentinel */ } };probe里用of_match_device获取匹配项,再取data:
const struct of_device_id *match = of_match_device(myext_of_match, &pdev->dev); const struct myext_cfg *cfg = match ? match->data : &myext_default_cfg;这种设计的好处是同一份驱动代码灵活支持多个硬件版本,不用为每个版本维护一个驱动。
7. 匹配失败排查实战:从设备树到驱动的完整链路
7.1 排查第一步:确认设备树节点是否生成了platform_device
很多人在probe不触发时直接怀疑驱动代码,这是误区。首先要确认设备树解析结果:
ls /proc/device-tree/myext@0201c000/如果该目录存在,再看里面的compatible文件内容:
cat /proc/device-tree/myext@0201c000/compatible正常输出类似:
myvendor,myext注意compatible字符串在设备树里是以null结尾的,多个字符串时会连在一起,用cat看可能感觉有点怪,可以配合od -c查看:
od -c /proc/device-tree/myext@0201c000/compatible有一次我就是在这里发现compatible值和驱动表里差了大小写,内核匹配字符串是严格区分大小写的,掉坑里很久。
7.2 排查第二步:确认platform_driver是否注册成功
驱动的注册入口在module_init阶段,如果驱动是编入内核的,可以在启动日志里看到类似信息:
[ 1.234567] platform myext@0201c000: Driver myext requests probe deferral但更常见的情况是你自己加的pr_info没有出现,说明module_platform_driver之后到probe之间就没走通。这时先看驱动模块是否加载成功:
lsmod | grep myext如果是编进内核的:
grep myext /sys/bus/platform/drivers/myext/bind、uevent等文件存在,说明驱动注册成功。drivers/myext/目录里如果有设备名,说明已经绑定了设备。
7.3 排查第三步:判断是否deferred probe
如果设备一直没probe但也没报错,最可能原因就是probe返回了-EPROBE_DEFER。查这个状态:
cat /sys/kernel/debug/devices_deferred如果设备在里面,会显示等待原因,比如:
myext@0201c000 waiting for supplier clock这说明它依赖某个时钟还没注册。解决方向是确认时钟树配置、相关控制器驱动是否加载。如果你的内核没开CONFIG_DEBUG_FS,可以用延迟释放版本的dmesg检查:
dmesg | grep -i defer7.4 排查第四步:检查驱动内probe返回值
如果前面都没问题而probe仍然不被调用,那么很可能是驱动内部逻辑问题导致platform_driver_register失败。最典型的错误是同一个name已经被别的驱动占用。platform_driver_register内部调用driver_register,最终执行总线匹配并调用probe,如果注册失败,模块加载就会报错:
myext: probe of myext@0201c000 failed with error -17-EINVAL或-EEXIST这类错误码在设备模型里会打印到内核日志,仔细看dmesg就能定位。
我还遇到过一种少见情况:驱动模块采用自动加载方式,但MODULE_DEVICE_TABLE导出的别名和设备树compatible对不上。查看模块别名:
modinfo myext.ko里面应该有类似alias=of:N*T*Cmyvendor,myext的内容,如果没有,说明MODULE_DEVICE_TABLE没生效,需要检查头文件包含。
7.5 排查第五步:检查pinctrl和时钟资源
probe调用后,如果驱动代码里有时钟、pinctrl相关的初始化,也需要逐一确认。i.MX6ULL这类SoC上,pinctrl配置错误不会阻止probe调用,但可能让外设无法工作,表现为寄存器能读写但功能异常。时钟获取失败则可能导致clk_prepare_enable返回错误,probe里处理不好就fail。这里有个开发经验:probe函数开头就把必要的硬件资源全部获取并检查完,任何一个失败马上返回错误码,配合dev_err输出具体错误资源,这样排查起来效率极高。
8. Platform机制驱动开发中的避坑清单与内核源码解读
8.1 设备树compatible字段的书写规范
设备树compatible字符串有一定规范,通常格式是"厂商,型号",厂商和型号用小写字母、数字、连字符。比如fsl,imx6ull-uart、myvendor,myext。要避免出现大写字母下划线,因为设备树规范要求命名采用小写连字符风格。驱动匹配表里的compatible字符串必须和设备树严格一致,这两个地方的字符串是直接比较的。
比较常见的一个坑是,设备树里写"myvendor,myext-v1",驱动表里写"myvendor,myext-v1"单词一样但编译时字符串尾部可能有不可见字符——特别是从PDF或网页复制代码时,容易带上零宽空格。排查这类问题可以用上面提到的od -c命令看十六进制内容,非常有效。
8.2 同一设备多个compatible时的解析顺序
设备树节点的compatible属性可以写多个字符串,内核匹配时按驱动匹配表的顺序优先还是设备树属性顺序优先?实际上of_driver_match_device遍历的是节点compatible列表,逐个和驱动的of_match_table表项比对——更准确说,是驱动匹配表外层循环,设备属性内层循环,只要驱动表里找到一条和节点任意compatible相同的就匹配成功。
如果你想让一个设备优先绑定某个驱动(多个驱动都声明兼容时),控制权在驱动的注册顺序和驱动表顺序上,但实际开发中这种情况很少,因为Product级系统不允许两个驱动声明同一compatible却做不同的事。反而在调试时可以利用这个特性做"A/B驱动验证":先加载驱动A,再卸载换成驱动B,测试不同实现的表现。
8.3 probe失败与资源释放
驱动probe一旦失败,设备模型会调用相关清理操作。使用devm_系列接口(devm_kzalloc、devm_ioremap_resource、devm_request_irq)时,资源会在设备对象释放时自动回收。如果你在probe中途发生错误分支,直接return相应错误码即可,不必手动释放之前申请的devm_资源,这是设备驱动开发里减少泄漏的关键。
但需要特别注意,platform_get_drvdata在probe里设置的数据,在remove里可以获取。如果probe失败,remove不会被调用,所以最好在probe成功时设置drvdata,失败分支直接跳转返回。
8.4 内核源码关键函数的阅读路线
想深入理解Platform匹配机制,建议按下面的源码文件读,不要盲目乱翻:
drivers/base/platform.c:platform_match、platform_drv_probe、platform_get_resource等核心实现。drivers/base/dd.c:驱动绑定与设备关联的核心逻辑device_attach、driver_probe_device、really_probe。drivers/of/platform.c:of_platform_populate如何从设备树创建设备。drivers/of/device.c:of_driver_match_device、of_device_alloc等解析逻辑。
读代码时重点看背后逻辑,比如platform_match里的of_driver_match_device如果没返回匹配,还会不会走id_table和name匹配——这个顺序内核版本有变化,你的内核具体是什么行为,直接看源码最准。
8.5 内核版本差异带来的陷阱
不同版本的Linux内核在Platform机制上细节有差异。老内核(3.x、早期4.x)里of_match_table匹配失败后会回退到id_table和name匹配;新内核(5.x及以后)在platform_match里也逐渐统一走of匹配优先,但对传统name匹配仍保留兼容。i.MX6ULL官方BSP通常用的4.1.15或4.9内核,网上资料也大多基于这些版本。
另一个影响是设备树的解析方式。早期内核对根节点子节点默认为platform设备,而现在需要通过of_default_bus_match_table确认总线类型。如果你把老BSP的设备树直接搬到新内核,可能有大量节点不会被填充成platform设备。反过来,新版设备树语法(如/ { model = "..."; compatible = "myvendor,myboard"; };)在老内核上也可能出现解析问题。
实际项目里,我建议先确认内核版本,再对照内核源码里的platform.c行为,不要完全依赖网上教程里某个版本的流程描述。
9. 我在i.MX6ULL实战中沉淀的几个习惯
做嵌入式Linux驱动久了,我形成了一套关于Platform驱动的开发习惯,分享几条:
第一,每写一个新外设驱动,先画清设备树节点、驱动匹配表、probe流程这三者的对应关系。不需要画复杂的图,就在注释里写清楚:哪个compatible字段对哪个of_match_table条目,哪个reg条索引对应哪个platform_get_resource参数。这份注释后面排查问题时就是救命稻草。
第二,probe函数一定要做到可重复执行。虽然正常生命周期下probe只调用一次,但热插拔、deferred probe重试都会让probe被反复调用。如果probe里有全局状态的初始化,必须保证重复执行不会出问题。我用过一种方式:probe开头加一个devm_kzalloc的判断位,避免重复初始化。
第三,充分利用设备模型提供的系统信息接口,而不是全靠dmesg。/sys/bus/platform/devices/和/sys/bus/platform/drivers/两个目录底下几乎能看到所有匹配信息,配合find、grep很快就能定位绑定关系。
第四,调试驱动时,优先确认硬件层面是否正常。Platform机制只负责软件层面的匹配,即使probe完美执行,如果GPIO的pinctrl配置不对、时钟没开、供电不稳定,外设照样罢工。所以我在probe函数里有一个固定套路:先获取并映射内存资源,再获取并使能时钟,再配置pinctrl(如果设备树没有自动配置),最后请求中断。每一步失败都直接返回,并在dmesg里打印失败原因和阶段。这个习惯帮我省了大量排错时间。
最后想说,Platform匹配机制本身并不复杂,它的设计哲学是"信息归信息、逻辑归逻辑、平台归平台"。很多初学者觉得设备模型很玄,其实是把精力花在了背函数名和结构体上,缺少对"设备怎么来、驱动怎么挂、总线怎么撮合"这条主线的理解。把主线理顺,再复杂的SoC驱动开发都能举一反三。这套机制背后体现的软件工程思想——解耦、分层、统一抽象——值得任何嵌入式开发者花时间去体会。