
第一次给i.MX6ULL写驱动的时候我盯着设备树里的一堆节点想不明白这个叫“平台总线”的东西到底挂在哪条硬件总线上为什么驱动里填了一个compatible字符串probe函数就会被自动调用后来把Platform设备和驱动的匹配机制彻底搞懂了之前很多“玄学”才真正消失。这篇文章就是围绕i.MX6ULL上的Platform设备与驱动匹配机制展开的适合正在学Linux驱动开发、尤其是用i.MX6ULL这样的ARM SoC做嵌入式项目的开发者。我会从内核为什么要弄一条虚拟总线讲起再把设备树、platform_device、platform_driver三者如何“对上眼”的过程拆开最后结合i.MX6ULL的板级环境给出一套可以照着跑的最小示例和排查套路。1. Platform总线为什么存在没有物理总线也要有统一的设备管理1.1 i.MX6ULL的片上外设并不“挂在”任何物理总线上i.MX6ULL是NXP基于Cortex-A7内核设计的嵌入式处理器芯片内部集成了UART、I2C、SPI、GPIO、SDIO、以太网MAC等大量外设控制器。这些外设有一个共同特点它们的寄存器就映射在芯片的某个内存地址上CPU直接通过地址访问不存在一条像USB、PCIe那样可以枚举设备的物理总线。那问题来了Linux的设备模型要求任何一个设备都必须归属于某条总线因为bus_type上挂着device和driver两个链表内核要靠“设备加入总线时遍历驱动、驱动注册时遍历设备”这套逻辑来完成配对。没有总线归属设备生命周期管理、电源管理、sysfs属性导出、自动加载驱动这些机制全都无从谈起。所以内核做了一个非常务实的决定凭空造一条platform总线。它不对应任何真实硬件只是一个软件概念专门收纳那些“直接挂在CPU地址空间上、无法被标准物理总线枚举”的设备。i.MX6ULL内部几乎所有的片上外设控制器在Linux眼里都是platform_device。1.2 从设备树节点到platform_device的转化时机在i.MX6ULL上设备树dtb由U-Boot加载进内存内核启动早期会把FDT格式的二进制数据解析成设备树结构然后调用of_platform_default_populate_init等机制把设备树里符合条件的节点一个个转换成platform_device挂到platform总线上。哪些节点会被转化最核心的条件是这个节点带有compatible属性。比如imx6ull.dtsi里gpio控制器节点是这样定义的gpio5: gpio020ac000 { compatible fsl,imx6ul-gpio, fsl,imx6ull-gpio; reg 0x020ac000 0x4000; interrupts GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH; ... };节点里有compatible内核就认为这是一个“平台设备”把它变成platform_device注册到platform总线。这里请记住设备树里节点的名字五花八门不重要compatible才是驱动和设备匹配的关键身份证。当然并不是所有compatible节点都会变成platform_device。如果节点挂在i2c总线下它会被i2c控制器驱动识别为i2c_client挂在spi总线下会变成spi_device。节点能不能展开成platform_device还取决于父节点的总线类型和compatible是否包含“simple-bus”之类的标识。这一点后面排查问题时会专门说。1.3 platform_device和platform_driver各自的角色platform_device描述“硬件有什么”比如寄存器地址、中断号、DMA通道、时钟等资源这些信息在设备树里写清楚内核帮助生成resource结构platform_driver描述“软件怎么操作这个硬件”比如probe函数、remove函数、电源管理回调。这两个角色经过platform总线的match撮合一旦配对成功内核就会调用platform_driver的probe函数驱动开发者的主要工作就是在这个函数里初始化硬件、注册字符设备、建立中断处理等。probe被调用说明驱动和设备已经“领证”之后开发者的代码才开始真正执行。那么配对的具体规则是什么这是整个机制里最核心的部分。2. 匹配机制拆解compatible、id_table、driver.name三层逻辑2.1 bus_type-match是被调用的总入口platform总线的类型定义在drivers/base/platform.c里其中match函数是platform_match。当发生两种情况时这个函数会被调用一个platform_device注册到platform总线时内核会遍历总线上所有platform_driver逐个调用platform_match看有没有驱动愿意认领这个设备。一个platform_driver注册到platform总线时内核会遍历总线上所有platform_device逐个调用platform_match看有没有设备可以被这个驱动接管。理解了触发时机你就明白为什么probe总会在“设备注册”或“驱动注册”之后的某个时刻被调用。如果你只写了一个驱动并insmod加载但设备树里没有对应设备probe永远不会执行反过来设备树上写了节点但内核里没有注册匹配的驱动设备就一直在总线上“无人认领”。2.2 匹配顺序先看设备树再看ID表最后比名字platform_match函数的匹配逻辑大致按下面顺序执行优先使用设备树匹配如果设备有of_node并且驱动的of_match_table不为空就遍历of_match_table里的每一条of_device_id把它的compatible字符串和设备树节点compatible属性里的字符串逐一比较任何一个相等就算匹配成功。然后尝试ACPI匹配在嵌入式ARM设备上基本不会走到i.MX6ULL也用不到。如果上面都没有成功再看驱动的id_table调platform_match_id函数拿platform_device的name字段去和id_table中每条id_entry的name字段比较。如果连id_table都没有那就直接比较platform_driver.driver.name和platform_device的name是否相同。注意第1步和第3步的关系即使设备树节点存在只要of_match_table里没有对应项内核并不会立即放弃还会继续尝试id_table和名字匹配。但在实际设备树开发中强烈建议直接走第1步因为这是语义最清晰、最不易出错的方式。我把三种匹配方式整理一下方便对比匹配方式关键字段设备树环境推荐度说明DT compatible匹配驱动.of_match_table[].compatible 与 设备节点compatible最推荐直观、稳定、标准做法id_table name匹配驱动.id_table[].name 与 platform_device.name少用设备树生成的device name不好控制容易踩坑直接比较driver.name驱动.driver.name 与 platform_device.name不推荐设备树环境下几乎不可靠2.3 compatible里的“厂商,型号”为什么必须完全一致设备树里常见这种写法compatible myvendor,mydevice;驱动侧对应的of_device_id数组static const struct of_device_id mydevice_of_match[] { { .compatible myvendor,mydevice, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydevice_of_match);compatible字符串的比较是纯字符串比较不能有多余空格不能大小写不一致逗号必须是英文半角逗号。很多初学者在设备树里写的是myvendor, mydevice逗号后面多了一个空格驱动里写的是myvendor,mydevice结果死活匹配不上查半天发现是一个空格的问题。另外设备树节点的compatible可以写多个值比如compatible myvendor,mydevice, vendor,generic-device;匹配时内核会先用第一个值去遍历of_match_table没找到再用第二个值。这种写法在Linux里叫“板级兼容芯片级兼容”目的是让同系列硬件可以共用驱动。实际开发中驱动侧只要在其中任何一个值上命中即可。2.4 一次完整匹配的“时间线”以i.MX6ULL启动过程为例一次成功的匹配大致是这样的U-Boot加载设备树dtb传入内核。内核解压设备树构建of_node节点树。of_platform_default_populate等流程把带compatible属性的节点转成platform_device注册到platform总线。platform总线触发match此时对应的platform_driver可能还没注册匹配暂时失败设备进入“待认领”状态。驱动代码执行platform_driver_register或module_platform_driver宏展开后的注册函数向platform总线注册platform_driver。platform总线再次触发match逐一遍历总线上尚未绑定驱动的设备。设备树compatible和驱动of_match_table命中。内核调用驱动的probe函数传入platform_device指针驱动开发者拿到设备资源开始初始化硬件。如果驱动是编译进内核的第5步会在内核启动更早阶段就完成第3步生成设备时直接匹配成功probe几乎和设备注册同时发生所以你看到的现象就是“设备树节点写着写着probe就跑了”。3. i.MX6ULL上从零写一个Platform驱动动设备树、驱动框架与验证3.1 硬件环境与最小目标在i.MX6ULL的板子上验证这套机制最简单的方式不是直接去驱动UART或GPIO控制器而是先加一个自定义的虚拟设备节点跑通“设备树生成设备、驱动匹配、probe打印”的最小链路。这个最小链路一旦成立后面不管换什么外设本质流程都是一样的。我用的平台是正点原子或野火这类常见的i.MX6ULL开发板内核版本4.x/5.x都可以设备树文件对应arch/arm/boot/dts/imx6ull-14x14-evk.dts。如果你用的是其他板子找到自己的板级dts文件即可。3.2 在设备树上添加自定义节点在板级dts的根节点/内部添加一个子节点/ { mydevice { compatible myvendor,mydevice; reg 0x020c406c 0x4; status okay; }; };这里reg随便填了一个i.MX6ULL的寄存器地址空间只是为了演示资源获取。实际驱动里probe函数可以用platform_get_resource拿到这段region。修改完dts后重新编译。如果环境里单独编译设备树make imx6ull-14x14-evk.dtb然后确认新的dtb被U-Boot加载。很多老手在这翻车改了dts但U-Boot加载的还是旧dtb启动后设备树根本没变驱动自然匹配不上。3.3 驱动的完整代码框架驱动代码的结构其实很固定核心就四件事定义of_device_id、定义platform_driver、实现probe/remove、注册模块。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/io.h static int mydevice_probe(struct platform_device *pdev) { struct resource *res; dev_info(pdev-dev, mydevice probe success\n); res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) { dev_info(pdev-dev, reg phys addr: 0x%llx, size: %lld\n, (unsigned long long)res-start, (unsigned long long)resource_size(res)); } return 0; } static int mydevice_remove(struct platform_device *pdev) { dev_info(pdev-dev, mydevice remove\n); return 0; } static const struct of_device_id mydevice_of_match[] { { .compatible myvendor,mydevice, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static struct platform_driver mydevice_driver { .probe mydevice_probe, .remove mydevice_remove, .driver { .name mydevice, .of_match_table mydevice_of_match, }, }; module_platform_driver(mydevice_driver); MODULE_LICENSE(GPL);代码里最关键的就是MODULE_DEVICE_TABLE(of, mydevice_of_match)。这一步的作用是导出of_device_id表让内核模块加载时通过modinfo能看到这个驱动支持的compatible列表。如果不写很多情况下of_match_table虽然在模块内可见但在某些工具链配置下会出现“驱动加载了但匹配不上”的诡异问题。of_match_table mydevice_of_match把表挂到驱动上platform_match在第一步就会拿到这张表。module_platform_driver是一个便捷宏展开后相当于module_init(platform_driver_register(mydevice_driver)); module_exit(platform_driver_unregister(mydevice_driver));它同时承担注册和注销工作比自己写module_init/module_exit更不容易漏。3.4 编译、加载与验证把驱动编译成模块make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- M/path/to/driver/dir modules把生成的.ko拷贝到板子上插入模块insmod mydevice.ko立刻去看dmesgdmesg | tail -20正常应该看到mydevice mydevice.0: mydevice probe success mydevice mydevice.0: reg phys addr: 0x20c406c, size: 4“mydevice.0”这个带编号的名字是内核给platform_device自动生成的后缀0一般是同名设备序号。在sysfs里也能看到绑定关系。设备侧ls -l /sys/bus/platform/devices/mydevice.0/如果驱动已经绑定里面会有一个driver符号链接指向驱动的sysfs目录。驱动侧ls /sys/bus/platform/drivers/mydevice/正常会有mydevice.0这个条目表示该设备已经被这个驱动接管。如果目录下什么都没有说明驱动注册了但没有设备匹配或者设备注册了但驱动还没匹配上。3.5 probe里还能做什么匹配成功后开发者可以在probe里做很多事用platform_get_resource获取reg和irq资源用of_property_read_u32读取设备树自定义属性用ioremap映射寄存器地址用devm_request_irq注册中断用misc_register或cdev注册设备节点。probe函数成功返回0设备和驱动才算正式绑定返回负值内核会认为probe失败绑定关系解除之后除非驱动或设备状态变化否则不会自动重试。我个人建议第一次验证时probe里只做dev_info输出确认机制通了再逐步加代码。这样定位问题时可以明确区分“是匹配没成功还是probe里面某一步失败”排查成本低很多。4. 匹配失败的排查链路问题、原因与sysfs诊断方法4.1 设备存在但就是没driver先从设备树侧找原因我在项目里见过最多的匹配失败案例不是驱动代码写错了而是设备树侧就没生成预期的platform_device。遇到这种情况先按下面顺序排查。第一步确认设备树真的被加载了。在板子上执行cat /proc/device-tree/mydevice/compatible如果设备树解析成功这个文件存在内容是myvendor,mydevice。如果文件不存在要么节点没写进dts要么U-Boot加载的dtb不是新编译的。第二步确认节点被展开成platform_devicels /sys/bus/platform/devices/ | grep mydevice没有输出说明内核压根没把它变成platform_device。常见原因有两个节点没有compatible属性或者status disabled被of_platform跳过。节点挂在某个非platform总线下比如i2c节点或spi节点下的子节点它们会被对应总线框架接管不会生成platform_device。举个例子如果把自定义节点放到i2c1 { ... }下面它就会被i2c子系统当作地址设备等待探测而不是成为platform_device。这在初学时非常容易踩坑明明写了compatible却总匹配不上platform驱动一查发现节点位置不对。第三步确认设备树节点里的compatible和驱动of_match_table里的字符串完全一致。为了保险可以反编译dtb再查一遍dtc -I dtb -O dts -o out.dts imx6ull-14x14-evk.dtb grep -n myvendor,mydevice out.dts这一步能排除“代码里看着是对的、实际编译进dtb的是旧版本”这种隐藏问题。4.2 设备树正常驱动也加载了但probe没执行如果/sys/bus/platform/devices/下面能看到设备名/sys/bus/platform/drivers/下面也能看到驱动名两者就是没绑定就要看匹配环节。先检查驱动是不是真的注册到了platform总线。有可能ko文件insmod了但module_init没执行成功。看dmesg里有没有加载报错比如符号缺失、内存不足、probe函数初始化失败之类。不过module_platform_driver方式下驱动注册基本不会失败失败大多出在匹配逻辑上。is_match的关键在of_match_table。检查点of_device_id数组最后有没有{ /* sentinel */ }空结构体。内核遍历表依赖这个哨兵项缺了它遍历可能出现未定义行为匹配结果不可控。表里compatible字段是否和设备树完全一致。建议在驱动里对of_match_table里的字符串做一次“肉眼比对字节数比对”空格、大小写、逗号位置都要看。确认驱动编译时of_match_table确实被编译进目标文件。可以modinfo mydevice.ko看有没有alias: of:N...T...C...类似信息这个是MODULE_DEVICE_TABLE导出的效果。如果完全没有alias说明表没生效。还有一个容易被忽略的点即使of_match_table匹配失败只要驱动id_table里有匹配项或者driver.name与platform_device.name一致platform_match依然可能返回true。但在设备树环境下platform_device的name来自内核of_device_make_bus_id生成的设备名一般形如“mydevice.0”很难预先猜到所以靠名字匹配属于碰运气开发时不要依赖它。4.3 用sysfs手动绑定来验证驱动本身没问题如果设备存在、驱动存在、匹配又查不出问题可以用sysfs的bind/unbind接口手动触发绑定和解除绑定。先把设备从当前驱动上解绑如果没有绑定可以忽略echo mydevice.0 /sys/bus/platform/drivers/mydevice/unbind然后手动绑定echo mydevice.0 /sys/bus/platform/drivers/mydevice/bind每执行一条命令立刻看dmesg。如果bind成功probe被调用如果bind报错错误信息会直接告诉你失败原因比如设备忙、probe返回值错误等。这个技巧在调试驱动时非常高效比反复insmod/rmmod省事得多。驱动卸载不想让probe再介入时也可以用unbind先把设备摘下来再rmmod驱动避免remove与probe交叉调用的混乱。4.4 probe为何返回了错误设备和驱动解除绑定probe函数返回非0值会被内核视为probe失败设备和驱动之间的绑定关系被撤销。常见几种情况寄存器资源获取失败platform_get_resource返回NULL处理时直接return -ENXIO。ioremap失败地址空间无效或内存不足。中断申请失败devm_request_irq返回非0。某些初始化子步骤失败比如时钟使能失败、GPIO请求失败。排查此类问题最简单的办法就是在probe里每一步都加上dev_err或dev_info打印逐步缩小范围。等到驱动逻辑稳定后再把这些调试打印移除或转为dev_dbg。还有一个容易被忽略的点devm_系列API申请的资源在probe失败时会被自动释放这是好事避免忘记资源回收导致的内存泄漏但反过来如果你在probe失败后还继续访问已经释放的资源就会踩到野指针调试起来非常痛苦。4.5 模块加载与设备树展开的优先级问题在设备树驱动开发中还有一个经典困惑驱动被编译成模块时设备树里的设备可能在系统启动时就已经注册但驱动模块还没加载。操作系统会先把这个设备挂起在总线上等待驱动不会因为“暂时没有驱动”就把它删除。所以在i.MX6ULL上只要驱动模块最终能被加载probe就会在insmod之后被触发。这个“先有设备、后有驱动”的动态匹配能力正是Linux设备模型相比传统查询式初始化的优势所在。反过来如果驱动先注册、设备后生成比如热插拔设备platform_match同样会在设备注册时把驱动找出来。理解这一点就不会纠结probe到底在哪个瞬间执行的问题。4.6 一张排查顺序表把上面这些经验整理成一张排查顺序表按序号执行基本能覆盖绝大多数匹配失败场景序号检查项方法/命令失败说明1dtb是否加载了新配置cat /proc/device-tree/mydevice/compatible文件不存在说明节点没进设备树2设备是否生成ls /sys/bus/platform/devices/ | grep mydevice没有设备说明节点没被展开3驱动是否注册ls /sys/bus/platform/drivers/ | grep mydevice没有驱动说明module_init失败4compatible是否一致检查dts和of_match_table字符串多空格、大小写、逗号都会导致失败5是否已经绑定ls -l /sys/bus/platform/devices/mydevice.0/driver无链接说明绑定未建立6手动绑定测试echo mydevice.0 .../bindbind报错会给出直接原因7probe执行结果在probe内加dev_info打印定位失败发生在哪一步4.7 其他值得留意的坑位除了上面说的还有几个在特定条件下才会遇到但遇到了就非常难查的情况。一个是设备树节点存在但父节点的compatible包含simple-bus与否会影响子节点是否被展开成platform_device。如果你的自定义节点直接挂在根节点下根节点本身会被特殊处理所以一般没问题但如果挂在某个外设控制器节点下需要确认那个控制器的驱动是否已经把子节点注册为platform_device或者总线的class类型是否符合预期。另一个是设备树里status okay的坑。如果你在dtsi里定义了一个节点板级dts里通过xxx覆盖它但覆盖时忘了写status okay或者写成disabled设备就不会生效。这个事在调试I2C、SPI外设时经常出现Platform设备节点同理。还有MODULE_DEVICE_TABLE和of_match_table的关系。很多人以为of_match_table填了数组就行忘了导出alias。虽然大部分情况下内核照样能匹配但如果你使用了自动加载机制比如udev、mdev在设备出现时自动modprobe驱动alias缺失就可能导致驱动不自动加载看起来像匹配失败。嵌入式系统里如果手动insmod这个问题不容易暴露但如果做了modprobe自动加载就要留意。5. 最后说一点个人经验这套Platform匹配机制第一次接触时很容易被各种术语绕晕platform_device、platform_driver、of_match_table、MODULE_DEVICE_TABLE……但换个角度看它就是“设备树提供数据、驱动提供逻辑、平台总线负责撮合”的三方协作。设备是“甲方”把硬件资源写在dts里驱动是“乙方”把操作逻辑写在C代码里总线就是“中介”通过compatible这个暗号让双方相认。暗号对上了probe就是签约仪式。想通这一层再去学i2c_driver、spi_driver你会发现骨架几乎一样只是总线实现细节不同。把Platform这个模型吃透Linux驱动开发就已经迈过最重要的一道门槛。补充一个调试小技巧在驱动里临时把probe函数里的dev_info全改成dev_err这样日志等级更高不容易被dmesg级别过滤掉。还有在调试阶段建议把printk开关打开通过/proc/sys/kernel/printk临时调整日志级别能少走不少弯路。等机制跑通再把调试信息清理干净。