i.MX6ULL Platform设备驱动匹配机制详解:从设备树到probe 开头≥200字做嵌入式Linux驱动开发绕不开一个词platform。尤其当你拿到一块i.MX6ULL核心板翻开内核源码会发现大量驱动文件的probe函数里第一行参数几乎都是struct platform_device *pdev。很多初学者在学到这里时都会感到困惑——我在裸机里操作寄存器明明是直接写地址怎么到了Linux下一个LED驱动要拆成设备、驱动、总线三部分设备树里明明写了compatible my-led驱动里的of_match_table也写了同样的字符串可probe就是不执行到底为什么这不是你一个人遇到的问题而是几乎所有Linux驱动开发者都会经历的一道坎。这篇文章我想以i.MX6ULL为底板完整拆解Platform设备与驱动匹配机制。不只是告诉你“怎么配对”更想讲清楚“为什么内核要设计这一套机制”“设备树节点怎么一步步变成platform_device”“匹配的四种方式分别解决什么问题”以及我在实际调试中踩过的坑。如果你是刚入门嵌入式Linux驱动的开发者或者已经写了几个字符设备驱动但始终没吃透设备模型这篇文章应该能帮你把这块拼图补齐。1. 内容整体设计与思路拆解1.1 从裸机到Linux驱动的思维转变老实说我最早学i.MX6ULL是用的正点原子那套裸机例程直接操作寄存器地址LED一秒闪烁代码直来直去感觉“驱动”也没多难。但是一进入Linux驱动整个人就被绕晕了。我在初始化函数里明明调用了register_chrdev设备节点也手动创建了可一加载就报No such device。后来才明白Linux驱动开发的核心思维不是“操作硬件”而是“描述硬件、匹配硬件、管理硬件”。这里我打一个生活化的比方Linux内核就像一个大型公司的HR系统。裸机开发是“你直接去工位上干活不需要打卡”Linux驱动则要求你先“入职注册”——你的身份、部门、岗位职责都要录入系统。设备端提交简历设备树节点或platform_device驱动端发布岗位JDplatform_driver中的兼容表总线就是HR系统里的匹配算法只有简历和JD对得上内核才会打电话通知你“入职”调用probe。Platform机制解决的正是“外部设备如何挂接到内核统一管理框架”的问题。在Linux设备模型中总线bus、设备device、驱动driver是三个核心对象。总线负责匹配设备和驱动设备代表物理硬件或虚拟硬件驱动则是对硬件的软件抽象。对于I2C设备有i2c总线SPI设备有spi总线USB设备有usb总线那像LED、GPIO、串口这种直接挂在CPU内存总线上的设备该归谁管答案就是platform总线——它不属于任何真实的总线协议而是内核虚构出来的一条“虚拟总线”专门用于管理那些直接挂载在SoC内部总线上的设备。1.2 为什么i.MX6ULL的资料这么多但很多人还是学不会i.MX6ULL是NXP推出的一款Cortex-A7内核、单核主频800MHz的处理器因为价格低、功耗小、资料齐全成为了嵌入式Linux学习者的“街机”。但我在各种技术群里观察到一个现象很多人照着教程把驱动编译进内核insmod也成功但问他“设备树节点是怎么解析的”“compatible匹配的优先级是什么”“如果同时匹配上多个驱动会发生什么”就答不上来了。这个现象背后暴露出的问题是大家把“驱动开发”等同于“写代码”而忽略了“理解框架”。Platform匹配机制恰恰是整个Linux驱动框架的地基。如果你不理解设备树compatible属性如何与驱动of_match_table关联不理解id_table和of_match_table的匹配顺序不理解platform_device的注册时机那你换一块板子、换一个外设依然只会照葫芦画瓢。真正碰到“probe进不去”这种问题就会无从下手。所以我在这篇文章里的思路是先讲清楚platform机制的总体设计——总线、设备、驱动三方如何协作再分别从设备侧设备树节点怎么变成platform_device和驱动侧platform_driver是怎么被注册的两条线拆开看然后重点分析匹配机制的几种路径最后用一个完整的i.MX6ULL LED驱动例子串起整个流程并附上我实际调试中的问题清单。如果你能把这一套逻辑吃透后面学input子系统、RTC驱动、PWM驱动会发现万变不离其宗。2. Platform设备模型拆解总线、设备、驱动三件套2.1 platform_bus_type那条看不见的“总线”在Linux内核源码中platform总线的定义在drivers/base/platform.c文件里核心是一个platform_bus_type结构体。我刚开始看这个文件的时候很困惑所谓“总线”不应该有物理传输信号的电路吗为什么platform总线连个match函数都这么简单下面这个结构体就是platform_bus_type的核心定义不同内核版本略有差异struct bus_type platform_bus_type { .name platform, .dev_groups platform_dev_groups, .match platform_match, .uevent platform_uevent, .pm platform_dev_pm_ops, };注意看它没有read、write、transfer这类方法。因为platform总线本身不负责数据传输它唯一的“职责”就是把设备和驱动拉到一起开个会。每次有新的platform_device或platform_driver注册到内核时总线都会触发一次match扫描看有没有合适的对象可以配对。match回调函数指向platform_match这是整个匹配机制的核心后面会专门展开。你只需要先记住这条逻辑设备或驱动注册时内核会调用bus_type的match函数如果匹配成功就调用驱动的probe函数完成初始化。这个“注册——扫描——匹配——probe”的过程是贯穿整个Linux设备模型的主线。2.2 platform_device硬件资源的信息登记表在Linux设备模型中struct device是一个极其庞大的结构体而platform_device是对它的一个扩展封装。看看include/linux/platform_device.h中的定义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; /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; };面对这个结构体初学者最容易问的问题是device和platform_device到底啥关系其实很简单platform_device是device的“子类”它比device多出来的核心成员是resource资源和id_entry。“资源”指什么就是设备使用的中断号、寄存器物理地址范围、DMA通道等硬件信息。以i.MX6ULL的GPIO控制器为例它的寄存器基地址、中断号等都是通过resource数组来描述。当设备树解析完成后这些信息会被填入platform_device的resources字段驱动在probe里用platform_get_resource或devm_platform_get_and_ioremap_resource来获取。实际操作中i.MX6ULL里很多外设驱动比如I2C控制器、SPI控制器、SDIO控制器本身就是platform_device。举个例子在内核设备树arch/arm/boot/dts/imx6ull.dtsi中usdhc1节点的定义里通常会包含reg 0x02190000 0x4000、interrupts GIC_SPI 22 IRQ_TYPE_LEVEL_HIGH这样的属性解析后就是两个resource条目分别描述寄存器地址和中断号。2.3 platform_driver驱动侧的“岗位JD”与platform_device对应驱动侧的结构体是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; };写驱动时probe和remove是必填的它们是驱动生命周期中最重要的两个函数probe匹配成功后驱动开始“上岗”的入口。在这里完成资源获取、寄存器映射、初始化硬件、注册字符设备或杂项设备等一系列工作。remove驱动卸载或者设备拔出时调用的“下岗”函数负责释放资源、注销设备节点确保不留垃圾。driver这个内嵌结构体是设备模型的核心包装里面记录驱动名、所属总线、of_match_table等信息。这里我强调一下platform_driver.driver.name不是设备匹配的最终依据device_driver里的of_match_table和id_table才是。很多人在加载模块后看到sys/bus/platform/drivers/xxx目录下没有绑定设备就以为是name不匹配这个锅name不背。注册一个platform_driver使用platform_driver_register()函数现在的推荐写法是static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver);注意module_platform_driver这个宏它相当于在module_init中调用platform_driver_register在module_exit中调用platform_driver_unregister省得你手写两个函数。2.4 Linux设备模型的整体联动理解了上面三个对象后整个联动关系就很清晰了。总线的match函数会被以下时机触发向总线注册新的设备时遍历总线上所有驱动找匹配项。向总线注册新的驱动时遍历总线上所有设备找匹配项。设备或驱动的属性发生变化也会触发重新匹配。这就解释了一个现象为什么insmod一个驱动模块后内核日志里几乎同时出现platform xxx.1: Driver my_led requests probe deferral这种提示因为驱动注册时内核立刻扫描了当前总线上所有已注册的设备。如果设备树节点已经解析并生成了platform_device那么当场就能匹配上并调用probe如果匹配还没准备好驱动中返回-EPROBE_DEFER就会等待后续时机重新探测。我在i.MX6ULL上调一个I2C触摸屏驱动时就遇到过这种deferral情况触摸屏的probe需要依赖I2C控制器驱动先准备好但两者注册顺序不确定此时触摸屏驱动返回-EPROBE_DEFER内核把它放到延迟列表里等I2C控制器就绪后再触发一次匹配。这个机制非常优雅但也常常让新手看到probe deferral字样时一头雾水——其实这只是“请稍后再试”不是错误。3. 设备树如何变成Platform Device设备侧深度解析3.1 设备树节点的生命周期在i.MX6ULL这种现代ARM Linux平台上设备树的解析是platform_device生成的核心路径。很多教程直接告诉你“在设备树里加一个节点驱动就能匹配”但没人说清楚中间过程。我把流程拆解如下内核启动早期在setup_arch阶段会调用unflatten_device_tree()将DTB设备树二进制解析成树状结构体device_node。当内核初始化到一定阶段会调用of_platform_default_populate_init()内核版本不同名称有差异遍历根节点下所有带compatible属性的节点。对每个节点内核会调用of_platform_bus_create()递归创建platform_device。每个platform_device通过platform_device_add()注册到platform总线触发总线匹配。这里有个关键知识点不是所有设备树节点都会变成platform_device。如果你为某个节点编写了驱动并要求它绑定平台总线必须先满足“节点位于platform总线的管辖范围”。一般来说根节点下的子节点、或者simple-bus兼容节点下的子节点会被递归展开为platform_device。拿i.MX6ULL的imx6ull.dtsi来说soc节点带有compatible simple-bus所以它下面的ipu1、usdhc1、gpio等节点会依次被创建为platform_device。如果你自己往根节点下加了一个myled节点只要根节点本身是platform总线扫描的起点实际上根的compatible通常为空但子节点会被扫描那么你的节点同样会被转换成platform_device。3.2 compatible属性最关键的“简历关键词”设备树节点的compatible属性是识别设备身份的关键。一个规范设备树节点的样子大概是led_test { compatible myvendor,myled; reg 0x020c406c 0x4; status okay; };在驱动侧of_match_table的写法是static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match);匹配时内核会比较设备树节点的compatible字符串和of_device_id中的compatible是否一致。如果你在设备树写myvendor,myled在of_match_table里写myled那么永远匹配不上。这个坑我自己踩了不止一次。这里再延伸一个知识点MODULE_DEVICE_TABLE(of, my_led_of_match)的作用。很多初学者以为它只是“为了生成模块别名”的技术细节其实它有两层价值对内让modprobe能根据设备树节点的compatible自动加载对应驱动模块。内核会扫描设备树发现不认识的设备时通过MODULE_ALIAS生成“of:NxxxTxxx”这样的别名然后通知用户空间modprobe加载具有相同别名的驱动。对外生成模块的符号信息配合modinfo命令可以看到alias: of:N*T*Cmyvendor,myled。所以在编写设备树和驱动时compatible字符串一定要保持完全一致包括大小写和逗号。我通常的习惯是厂商名,设备名并且全部小写、不用下划线这是设备树社区推荐的做法。3.3 reg与interruptsresource的“翻译官”设备树节点描述了设备的硬件资源驱动probe时需要把它们翻译成resource。拿GPIO控制器节点来说gpio1: gpio0209c000 { compatible fsl,imx6ul-gpio, fsl,imx35-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };当这个节点被解析成platform_device时reg属性会生成第一个resource类型为IORESOURCE_MEM起始地址0x0209c000长度0x4000。interrupts属性会生成一个或多个资源类型为IORESOURCE_IRQ的struct resource。gpio-controller等自定义属性不会自动变成resource它们保留在device_node的properties链表中驱动需要通过of_property_read_u32等API来读取。驱动侧获取资源也有两种风格。老式写法是struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO;现代推荐写法是void __iomem *base; base devm_platform_get_and_ioremap_resource(pdev, 0, res); if (IS_ERR(base)) return PTR_ERR(base);devm_platform_get_and_ioremap_resource是内核提供的一个辅助函数它把platform_get_resource和devm_ioremap_resource封装起来一步到位返回映射后的虚拟地址。devm_前缀的意思是“设备管理资源”在probe失败或remove时自动释放不需要你手动调用iounmap。3.4 不是所有设备树节点都能变成platform_device最后重点提醒一个常见误区。在i.MX6ULL上像I2C外设例如触摸屏ft5426虽然有compatible属性但它在设备树中的位置是挂在i2c总线节点下面的i2c1 { ft5426: touchscreen38 { compatible edt,edt-ft5406; reg 0x38; interrupt-parent gpio1; interrupts 9 IRQ_TYPE_EDGE_FALLING; }; };这种节点不会直接变成platform_device。它是由I2C控制器驱动在probe过程中通过i2c_new_client_device或设备树解析接口创建为i2c_client挂到i2c总线上。它的匹配走的是i2c_driver的id_table或of_match_table跟platform机制无关。很多教程在讲platform匹配时拿I2C设备举例容易让初学者张冠李戴。我建议你每次拿到一个外设时先想清楚它挂在什么总线上如果是CPU内存总线那大概率用platform如果是I2C那就是i2c_client如果是SPI那就是spi_device。总线不同匹配机制有差异虽然核心思想是相通的。4. Platform驱动侧核心环节从注册到probe的完整链路4.1 驱动初始化module_platform_driver背后发生了什么上面我提到了module_platform_driver(my_led_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)); \ } \ module_init(__driver##_init); \ static void __exit __driver##_exit(void) \ { \ __unregister((__driver)); \ } \ module_exit(__driver##_exit);所以当你insmod一个platform驱动模块时实际调用的是platform_driver_register。这个函数内部会做几件事初始化driver结构体将它与platform_bus_type关联。调用driver_register()把驱动加入bus的drivers链表。触发bus的match扫描遍历总线上所有的设备调用platform_match判断是否有匹配项。对匹配成功的设备调用driver_bound()最终执行probe。这一系列动作对用户来说只是insmod一条命令内核却已经跑了不少代码。理解这个顺序对排查“为什么probe没执行”至关重要——首先要看驱动有没有注册成功其次看总线上有没有设备最后看匹配条件是否满足。4.2 probe函数的“上岗准备”你都该做些什么在i.MX6ULL上写一个简单的LED平台驱动probe函数里通常要完成这些步骤static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct led_device *led; struct resource *res; int ret; led devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led-reg_base devm_platform_get_and_ioremap_resource(pdev, 0, res); if (IS_ERR(led-reg_base)) return PTR_ERR(led-reg_base); led-irq platform_get_irq(pdev, 0); if (led-irq 0) return led-irq; led-clk devm_clk_get(dev, NULL); if (IS_ERR(led-clk)) return PTR_ERR(led-clk); platform_set_drvdata(pdev, led); ret misc_register(led-miscdev); if (ret) { dev_err(dev, failed to register misc device\n); return ret; } dev_info(dev, my led probed successfully\n); return 0; }这里面有几个细节值得注意第一devm_kzalloc是设备管理内存分配不用在remove里手动kfree设备分离时自动释放。第二platform_set_drvdata把驱动私有数据绑定到pdev上这样在remove函数里可以通过platform_get_drvdata(pdev)拿回来。第三我用了misc_register注册一个杂项设备对于LED这种简单外设比主设备号管理省事得多。关于资源获取I2C、SPI设备驱动的资源获取方式往往不是platform_get_resource而是从各自的client结构体中取。但platform设备的资源获取路径就是我上面写到的这两条platform_get_resource和platform_get_irq。如果你设备树里定义了多个reg段就可以用platform_get_resource(pdev, IORESOURCE_MEM, 1)来取第二段。4.3 remove函数留得干净下次才能接着用remove函数的职责是清理probe阶段申请的所有东西。用devm_*接口申请的资源可以不管但有些东西还是需要手动释放static int my_led_remove(struct platform_device *pdev) { struct led_device *led platform_get_drvdata(pdev); misc_deregister(led-miscdev); // 如果用了gpio口最好不要在这里释放gpio而是交给devm机制 dev_info(pdev-dev, my led removed\n); return 0; }一个大原则是非devm申请的资源必须手动释放devm申请的资源不用动。如果你混淆了要么内存泄漏要么use-after-free导致内核崩溃。调试时最典型的表现就是反复insmod/rmmod几次后/sys/bus/platform/devices/my_led节点下出现脏数据或者内核报BUG: unable to handle kernel paging request。绝大多数情况下问题出在remove函数里释放了不该释放的东西。5. 匹配机制全解析四种方式与优先级5.1 platform_match一切的入口真正决定设备和驱动配对的是platform_match函数。不同内核版本的实现略有差异但核心逻辑基本一致。我以最近几个版本内核的实现为方便理解做了简化来说明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. 尝试OF设备树匹配最常用 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 尝试ACPI匹配x86等平台 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试id_table匹配 */ if (pdrv-id_table) if (platform_match_id(pdrv-id_table, pdev) ! NULL) return 1; /* 4. 最后尝试name匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }可以看到匹配顺序是OF匹配 → ACPI匹配 → id_table匹配 → name匹配。在i.MX6ULL这种ARM嵌入式平台上ACPI基本用不到那是x86/ARM64服务器场景所以我们重点看OF匹配和id_table匹配。name匹配是老式机制在纯设备树体系里很少作为首选但在某些场景比如用platform_device_register_simple静态创建的设备依然有效。5.2 of_match_table设备树驱动的“一锤定音”of_driver_match_device的实质是比较设备树节点的compatible字符串与of_device_id表中的compatible字符串。它内部实际调用of_match_device取出设备的of_node即设备树节点再依次遍历驱动提供的of_device_id表。在实际开发中我最推荐的写法是static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match);需要特别强调of_device_id数组的最后一个元素必须是空结构体哨兵否则内核遍历数组时会越界。这个问题有时候不会立即暴露但在某些内核配置下会导致随机崩溃很难排查。在i.MX6ULL的imx6ull.dtsi中你可以看到大量类似static const struct of_device_id imx6ull_uart_of_match[] { { .compatible fsl,imx6ul-uart, }, { .compatible fsl,imx7d-uart, }, { /* sentinel */ } };厂商经常让一个驱动兼容多个SoC的同类设备所以在of_match_table里写了多个compatible条目。匹配时只要设备树节点的compatible与其中任意一个相同就算匹配成功。5.3 id_table没有设备树时的Plan B在设备树普及之前平台设备是通过platform_device_register或platform_device_alloc在C代码中静态创建的。这时设备没有of_node只能靠platform_device_id结构体来匹配static const struct platform_device_id my_led_id_table[] { { my_led, (kernel_ulong_t)my_led_data }, { }, }; MODULE_DEVICE_TABLE(platform, my_led_id_table);匹配时内核会比较pdev-name和id_table中的name条目。一旦匹配成功pdev-id_entry会指向对应的id条目驱动就可以用pdev-id_entry-driver_data来区分不同版本的硬件。你可能会有疑问既然有了of_match_table为什么还需要id_table我遇到过一种场景一个驱动既支持设备树描述的硬件又要兼容老的非设备树启动方式。这时驱动里两个表都放probe里先判断dev-of_node是否存在再决定走哪条数据路径。还有一种常见场景同一个驱动要支持多个设备型号且各型号的行为差异较大。你可以通过id_table中的driver_data来传递每个型号的特定参数。在设备树下这个值可以从of_device_id的data字段获取。5.4 name匹配最古老也最容易误解的机制最后是简单的name匹配。它比较pdev-name和drv-driver.name。在设备树体系中pdev-name通常由设备树节点名的name部分派生出来例如节点myled020c406c会派生出name为myled而driver.name是你定义驱动时的字符串。很多驱动不支持设备树匹配或者忘记定义of_match_table此时驱动在设备树环境下可能意外通过name匹配上某些设备。你可能会看到probe被调用但platform_get_resource取不到任何资源因为设备树解析的资源并没有正确关联。我曾经在i.MX6ULL上调一个pwm背光驱动现象是probe莫名其妙执行了两次。查了半天发现驱动里同时定义了of_match_table和driver.name而设备树node name恰好和driver.name相同导致OF匹配和name匹配都成功了。内核虽然只会调用一次probe同一设备同一驱动只会绑定一次但调试日志里两种匹配路径都打出来了很容易让人困惑。所以我的建议是在设备树时代请专注of_match_tabledriver.name只用来在sysfs中创建目录名不要指望它做设备匹配。如果必须支持老式设备再额外写id_table。6. 实战i.MX6ULL平台LED驱动完整流程6.1 准备工作与设备树改造这里我以i.MX6ULL的GPIO1_IO03控制一个LED为例写一个完整的platform设备驱动。为什么用GPIO而不是直接操作寄存器因为GPIO子系统的操作更贴近日常开发而且更能体现platform资源获取的用法。先在设备树中添加节点通常放在根节点下/ { myled { compatible myvendor,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };注意这里我使用了led-gpio自定义属性而不是reg。这是因为GPIO控制器的地址资源由pinctrl子系统管理驱动只需要通过devm_gpiod_get来获取GPIO描述符即可。如果你想演示platform_get_resource可以在节点里加reg 0x020c406c 0x4然后通过devm_platform_get_and_ioremap_resource来映射GPIO方向寄存器和数据寄存器。另外为了pinctrl正常工作还需要在iomuxc节点里定义引脚复用iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };6.2 驱动代码实现一份可直接编译的模块下面是简化但完整的LED平台驱动#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/miscdevice.h #include linux/fs.h struct my_led_dev { struct gpio_desc *led_gpio; }; static int my_led_open(struct inode *inode, struct file *file) { return 0; } static ssize_t my_led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct my_led_dev *led container_of(file-private_data, struct my_led_dev, miscdev); char kbuf[4] {0}; if (count 3) count 3; if (copy_from_user(kbuf, buf, count)) return -EFAULT; if (kbuf[0] 1) gpiod_set_value(led-led_gpio, 1); else if (kbuf[0] 0) gpiod_set_value(led-led_gpio, 0); else return -EINVAL; return count; } static const struct file_operations my_led_fops { .owner THIS_MODULE, .open my_led_open, .write my_led_write, }; static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct my_led_dev *led; int ret; led devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led-led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led-led_gpio)) return PTR_ERR(led-led_gpio); gpiod_set_consumer_name(led-led_gpio, my led); led-miscdev.minor MISC_DYNAMIC_MINOR; led-miscdev.name mled; led-miscdev.fops my_led_fops; led-miscdev.parent dev; ret misc_register(led-miscdev); if (ret) { dev_err(dev, failed to register misc device\n); return ret; } platform_set_drvdata(pdev, led); dev_info(dev, my led driver probed\n); return 0; } static int my_led_remove(struct platform_device *pdev) { struct my_led_dev *led platform_get_drvdata(pdev); misc_deregister(led-miscdev); dev_info(pdev-dev, my led driver removed\n); return 0; } static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform led driver);这段代码中发送echo 1 /dev/mled亮灯echo 0 /dev/mled灭灯。6.3 编译、加载与调试实录在i.MX6ULL上我一般用SDK里的交叉编译工具链编译模块。Makefile大致长这样obj-m : my_led.o KDIR : /path/to/your/kernel/source CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm clean编译好之后拷贝到开发板执行insmod my_led.ko dmesg | tail正常情况下应该能看到platform myled: my led driver probed同时/sys/bus/platform/devices/目录下会出现myled节点/sys/bus/platform/drivers/my_led/目录下会创建myled的符号链接ls -l /sys/bus/platform/devices/myled ls -l /sys/bus/platform/drivers/my_led/看到myled - ../../../devices/platform/myled这样的符号链接说明设备与驱动绑定成功。如果没有这个链接说明匹配失败。6.4 从sysfs验证总线匹配关系我强烈建议你养成一个习惯每次加载或卸载驱动后先去看sysfs里的总线关系而不是急着看dmesg。sysfs是内核设备模型的用户态投影它直接反映了设备、驱动、总线的绑定状态。以下几个检查点在调试时特别有用# 查看设备属性 cat /sys/bus/platform/devices/myled/modalias # 查看驱动是否绑定成功有链接说明成功 ls -l /sys/bus/platform/drivers/my_led/ # 查看设备树节点信息是否正常 ls /sys/firmware/devicetree/base/myled/ cat /sys/firmware/devicetree/base/myled/compatible其中modalias文件在匹配驱动时至关重要。内核通过它生成设备别名用户空间的udev/mdev会根据它自动加载驱动。如果modalias文件不存在或者内容与你驱动的MODULE_ALIAS不一致即使手动insmod成功系统也无法实现自动加载。7. 常见问题与排查技巧实录7.1 probe不进最经典的匹配失败现场这是我在各种技术群被问到最多的问题。驱动insmod成功但probe没执行。排查步骤我总结为一条链按顺序检查驱动是否注册成功执行ls /sys/bus/platform/drivers/my_led/如果目录不存在说明platform_driver_register没成功。设备是否存在执行ls /sys/bus/platform/devices/ | grep myled如果看不到设备节点说明设备树节点解析没生成platform_device。compatible是否一致检查设备树节点的compatible和of_match_table里的字符串注意大小写、逗号、下划线。这一步最容易被忽略。是否被其他驱动抢占了同一设备只能绑定一个驱动。如果你系统中另一个驱动的of_match_table也包含相同的compatible而且先注册成功你的驱动就匹配不上了。我在一次调试中遇到一个特别隐蔽的情况设备树节点的compatible是myvendor,myledof_match_table里也是{ .compatible myvendor,myled }但我在驱动里多写了一个空格myvendor, myled。编译不报错字符串比较却永远不相等。这类问题靠肉眼很难看出来所以我在代码里写测试用例时经常用of_device_is_compatible手动验证。7.2 有多个设备节点时probe会执行几次如果你的of_match_table包含多个compatible而设备树里有多个不同compatible的节点都匹配上了probe会执行多次每次的pdev不同。同样如果of_match_table里只有一个compatible但设备树里有两个节点都使用这个compatibleprobe也会执行两次。这本身不是错误但如果你在probe里用了全局变量保存设备状态就要小心第二次probe覆盖第一次的状态。我建议把设备私有数据都放在struct my_led_dev里用platform_set_drvdata绑定到对应pdev上不要用全局变量。7.3 设备树节点解析成platform_device失败怎么办如果你在设备树里加了节点但/sys/bus/platform/devices/里找不到对应目录先检查节点是否在platform总线的扫描范围内。根节点下的子节点通常没问题但如果节点放在某个i2c节点内部它就会变成i2c_client不会出现在platform devices里。还有一种情况是节点存在语法错误。比如忘了加status okay或者reg属性格式不对。用以下命令验证设备树语法dtc -I dtb -O dts -o output.dts imx6ull.dtb或者在内核启动日志中搜索相关错误信息。i.MX6ULL的u-boot如果设备树加载失败通常会打印Failed to pass device tree to kernel之类的提示。7.4 OF匹配和id_table同时存在时驱动数据怎么选如果一个驱动同时定义了of_match_table和id_tableprobe函数里可以通过以下方式获取匹配数据const struct of_device_id *of_id of_match_device(my_led_of_match, dev); if (of_id) { /* 走设备树路径 */ my_data of_id-data; } else if (pdev-id_entry) { /* 走id_table路径 */ my_data (void *)pdev-id_entry-driver_data; }这个判断逻辑在“一个驱动支持多款硬件”的场合非常常见。比如I2C控制器驱动同时支持imx6ul和imx7d两者的寄存器布局有差异驱动就要根据匹配到的具体型号来选择不同的操作函数集。7.5 不要忽略EPROBE_DEFER的等待机制在复杂系统中驱动A需要依赖驱动B提供的资源如时钟、电源域而B的初始化可能晚于A。如果A的probe在获取资源时失败不要急着返回负数错误码而是返回-EPROBE_DEFER。内核会把A的probe记录在延迟探测列表里等B注册完成后再次调用A的probe。在i.MX6ULL上我实际观察到的合理现象是新的platform_driver注册时很多设备的probe首先会返回-EPROBE_DEFER随后在相关时钟或pinctrl驱动就绪后probe被重新调用一次。如果你在dmesg里看到如下日志platform myled: Driver my_led requests probe deferral platform myled: Retrying from deferred list不要慌这是正常机制。但如果反复出现多次后仍失败就要仔细检查依赖资源的准备工作了。7.6 个人调试经验总结最后分享几个实际操作中的心得希望你少走弯路心得一每次改设备树后不要只insmod模块一定要先确认设备树真的更新了。很多开发板从u-boot加载dtb时会用环境变量指定dtb文件路径如果你改了源码但没重新编译dtb或者编译了但u-boot没加载新的那怎么调都没用。我习惯在启动后执行cat /sys/firmware/devicetree/base/model看MODEL字符串确认设备树版本。心得二在probe函数里多打日志不丢人。我见过很多人从网上抄驱动probe里只有最后一句return 0出问题后完全不知道执行到哪一步。我自己的习惯是资源获取的关键步骤后都跟一句dev_dbg或dev_info调试时开ignore_loglevel或dynamic_debug一眼就能定位到哪一步失败。心得三利用/sys/kernel/debug/devices_deferred查看延迟探测的设备。如果系统启动后有些驱动一直匹配不上这个文件会列出所有处于deferred状态的设备。这是排查依赖关系问题的利器。心得四当你在i.MX6ULL上换用不同内核版本时平台驱动的API可能发生变化。比如老内核里的platform_get_resource(pdev, IORESOURCE_IRQ, 0)新内核推荐platform_get_irq(pdev, 0)。编译报错不要硬改先查内核里对应API的定义。我在从4.1内核移植到5.4内核时光of_get_named_gpio换成devm_gpiod_get就花了不少时间。8. 从Platform机制延伸出去的下一步把platform匹配机制吃透之后你会发现它像一张“血管网”连接着嵌入式Linux驱动的方方面面。任何platform_driver的probe里都能看到这套机制留下的痕迹。再往外走input子系统、RTC子系统、LED子系统、PWM子系统本质上都是在platform_driver.probe的基础上再向各自的子系统框架注册节点。我个人在实际调试中最深的体会是驱动开发中90%的时间不是花在写代码上而是花在确认“内核为什么没有按我预想的方式工作”上。设备树节点、compatible匹配、资源获取、总线扫描任何一个环节出错都会让probe静默失败。而只要你对platform机制有了完整的认识这些问题就能按图索骥地排查而不是靠瞎猜和反复尝试。如果你在i.MX6ULL上照着这篇文章走了一遍建议再做几个小实验加深理解把设备树里的compatible改错看内核会报什么信息去掉MODULE_DEVICE_TABLE宏用modinfo看alias对比差异写第二个驱动故意匹配同一个设备节点看系统如何选择。这些“故意搞坏”的实验往往能帮助你更好地理解系统真正的工作方式。