i.MX6ULL platform驱动匹配机制详解与probe不调用排查思路
发布时间:2026/9/6 10:12:19 作者:尧图编辑部 阅读量:1,286

前几天帮同事调i.MX6ULL的驱动现象非常典型模块加载了日志里却没有probe函数的打印。设备树里compatible属性的值和驱动of_match_table里的字符串肉眼看上去一模一样就是匹配不上。最后我在/sys/bus/platform目录下翻了半天才发现设备树节点compatible值的末尾多了一个不可见的空格。这类问题根子都在对Platform设备与驱动匹配机制的理解还停留在“大概知道”的层面。本文就以i.MX6ULL平台为例把platform总线的匹配机制、代码路径、实操套路和排错思路完整串一遍。如果你正准备入门i.MX6ULL或者其他Cortex-A系列芯片的Linux驱动开发或者写过platform驱动但老是栽在匹配环节这篇应该能帮你省不少时间。1. 为什么i.MX6ULL上的外设多半绕不开platform机制1.1 从硬件控制器的角度理解platform设备的来源i.MX6ULL是一颗Cortex-A7内核的处理器但芯片内部集成了大量外设控制器比如UART、I2C、SPI(ECSPI)、SDIO、GPIO、PWM、ADC、LCDIF、ENET等。这些控制器从软件角度看本质就是一段寄存器地址区域加上若干中断号但在Linux设备模型里它们需要被抽象成设备节点挂到某一类总线上。问题在于这些控制器不是PCI设备也无法枚举它们的位置和资源在硬件设计时就固定了。Linux内核为这类“挂不到真实总线上的片上外设”设计了一条虚拟总线就是platform总线。i.MX6ULL上几乎每一个内部外设控制器在内核启动后都会注册成一个platform_device。设计上通常分两步第一步是描述硬件传统方式是在arch/arm/mach-imx/目录下的板级文件里手动填充platform_device结构体并调用platform_device_register这种方式在内核3.x时代很常见第二步是现在主流的设备树方式内核在启动过程中通过of_platform_default_populate等函数解析设备树把节点自动转换成platform_device。设备树之所以能成为主流核心原因是解决了硬件描述与驱动代码耦合的问题更换板卡硬件时不用重新编译内核只改设备树文件。在i.MX6ULL的官方BSP里默认就是设备树方式。你在设备树里写一个节点内核启动后就可以在/sys/bus/platform/devices/目录下看到对应的设备目录。1.2 platform设备与platform驱动的注册时机刚学驱动的人很容易有一个困惑驱动和设备到底谁先注册probe函数什么时候调用这里的关键是理解Linux设备模型的事件驱动机制。当驱动注册时总线会遍历所有已经注册的设备逐个调用匹配函数寻找合适的设备找到就立刻调用驱动的probe反过来当设备注册时总线也会遍历所有已经注册的驱动找到匹配项就触发probe。所以注册顺序本身不影响最终是否能匹配上只影响probe触发的早晚。如果两个都注册完成后还没匹配上那就是匹配条件本身出了问题这也是调试时的基本判断方向。在i.MX6ULL的启动流程里设备树解析生成platform_device的动作发生得比较早通常在kernel_init之前。而驱动模块如果选择编译为模块(.ko)则是在系统启动后由modprobe或insmod加载。操作系统会先存在设备再补充驱动如果驱动编译进内核(zImage)则设备和驱动的注册顺序就不一定了但各自注册时都会去扫描对方所以probe依然会正常触发。1.3 platform总线在sysfs中的组织结构/sys/bus/platform/目录下有两个关键子目录devices/和drivers/。devices目录下面是所有platform设备的软链接drivers目录下面是所有platform驱动的软链接。当驱动和设备匹配成功后设备目录下会多出一个driver链接指向对应的驱动目录同时驱动目录下也会出现设备的链接。这个双向链接是判断绑定关系最直观的依据。我在调试时几乎离不开这个目录。设备有没有注册、驱动有没有加载、绑定关系是否建立ls一下马上就知道。这篇文章后续的排错环节很多操作也是围绕这个目录展开的。2. platform_match的执行顺序驱动和设备到底怎么“对上眼”2.1 内核源码里platform_match的完整逻辑platform_driver和platform_device的匹配入口是platform_match函数定义在drivers/base/platform.c中。以Linux 5.x/6.x内核为例核心逻辑如下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); /* When driver_override is set, only bind to the matching driver */ if (pdev-driver_override) return !strcmp(pdev-driver_override, drv-name); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* fall-back to driver name match */ return (strcmp(pdev-name, drv-name) 0); }这五步的优先级是硬编码的从上到下依次执行命中即返回。理解这个顺序对调试很多匹配问题非常有帮助。2.2 五种匹配方式的适用场景先看driver_override这是留给调试和特殊场景用的“后门”。如果设备节点的driver_override属性被设置那么只和这个字段指定的驱动名进行字符串比较其他匹配方式全部跳过。我一般用它来强制绑定某个驱动或者在设备树不便于修改的板子上暂时切换驱动后面排错章节会详细演示。接着是of_driver_match_device这是i.MX6ULL设备树平台最常用的匹配路径。它做的事可以理解为取出device_node的compatible属性和驱动的of_device_id数组中每个成员的compatible字段逐一比较只要有任何一个字符串相等就返回匹配。注意compatible比较要求完全相等包括厂商前缀和大小写一个字符不对都不行。然后是acpi_driver_match_devicex86平台和部分ARM服务器平台会走ACPI路径i.MX6ULL这种典型嵌入式平台几乎不用但代码逻辑存在不影响什么。再然后是platform_match_id这是给没有设备树的老式驱动用的。驱动可以定义一个platform_device_id数组static const struct platform_device_id mybeep_id_table[] { { mybeep, 0 }, { } };platform_match_id会拿设备的name字段和id_table里的name做比较。那什么时候会走上这条路呢最常见的就是没有设备树、设备通过platform_device_register注册的场景设备名字就是在platform_device结构体里指定的那个字符串。最后一步是退化匹配直接把pdev-name和drv-driver.name做字符串比较。这种写法在很老的驱动里能看到比如static struct platform_driver mybeep_driver { .driver { .name mybeep, }, .probe mybeep_probe, .remove mybeep_remove, };如果设备树里的节点没有compatible属性且设备节点名为mybeep驱动名也叫mybeep走这一步就能匹配上。但这属于“祖传写法”在新代码里不推荐依赖它。一方面它太隐晦可读性差另一方面设备树节点的name字段通常包含总线前缀或单元地址和驱动名不一定对得上。规范做法是使用of_match_table或id_table。2.3 设备树compatible与of_match_table的对应逻辑static const struct of_device_id mybeep_of_match[] { { .compatible myvendor,mybeep, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mybeep_of_match);关于MODULE_DEVICE_TABLE很多人以为它只是给用户态工具看的其实它还有一层重要含义它会在编译时生成模块别名把of_device_id里的compatible字符串编码进.modinfo段。这样modprobe在加载模块时可以通过内核发送的uevent事件自动匹配设备。驱动编译为模块后用modinfo检查能看到类似aliasof:NTCmyvendor,mybeep的信息有这个信息才说明模块别名生成正确。我在写platform驱动时的习惯是只要面对的是设备树平台of_match_table一定写id_table可以不写name退化匹配基本不考虑。这是最清晰、最不容易出错的路径。3. 在i.MX6ULL上实操从设备树到probe触发的完整过程3.1 一个最简单的beep硬件设备与设备树描述以一块i.MX6ULL开发板上的蜂鸣器为例。蜂鸣器的控制引脚通常接到某个GPIO上比如GPIO5_IO01具体引脚以你板子原理图为准。硬件上无非是给GPIO输出高电平就响输出低电平就停。设备树里我习惯这样描述/ { mybeep { compatible myvendor,mybeep; pinctrl-names default; pinctrl-0 pinctrl_mybeep; mybeep-gpios gpio5 1 GPIO_ACTIVE_HIGH; status okay; }; };引脚复用配置放在iomuxc节点下iomuxc { pinctrl_mybeep: mybeepgrp { fsl,pins MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x17059 ; }; };MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01这个宏由SDK提供位于imx6ull-pinfunc.h头文件具体引脚对应的宏名以你的BSP为准。0x17059是引脚配置值包含了上下拉、驱动能力、速度等设置。这里不展开讲怎么算直接用SDK推荐的配置值就可以。3.2 platform驱动代码骨架与加载后的sysfs变化驱动的代码结构如下#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h static int mybeep_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *desc; desc devm_gpiod_get(dev, mybeep, GPIOD_OUT_LOW); if (IS_ERR(desc)) { dev_err(dev, failed to get mybeep gpio: %ld\n, PTR_ERR(desc)); return PTR_ERR(desc); } dev_info(dev, mybeep probe ok, beep on\n); gpiod_set_value(desc, 1); return 0; } static void mybeep_remove(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, mybeep removed\n); } static const struct of_device_id mybeep_of_match[] { { .compatible myvendor,mybeep, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mybeep_of_match); static struct platform_driver mybeep_driver { .probe mybeep_probe, .remove mybeep_remove, .driver { .name mybeep, .of_match_table mybeep_of_match, }, }; module_platform_driver(mybeep_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(i.MX6ULL beep platform driver);注意编译镜像时设备树源文件(dts)要加入编译列表如果手动用fdtdump或dtc工具查看dtb确认节点确实存在再加载驱动模块。模块加载命令insmod mybeep.ko加载后先看dmesgdmesg | tail -20正常情况下会看到“mybeep probe ok”。再检查sysfsls -l /sys/bus/platform/devices/mybeep/driver lrwxrwxrwx 1 root root 0 Jan 1 00:00 /sys/bus/platform/devices/mybeep/driver - ../../../bus/platform/drivers/mybeep这个尾部箭头指向mybeep驱动说明设备与驱动已经绑定probe成功执行。3.3 驱动、设备和probe的对应关系我见过不少初学者在probe里写了一堆初始化但完全没搞懂probe为什么能拿到pdev参数。platform_driver的probe回调签名为int (*probe)(struct platform_device *)这个参数就是匹配上的那个platform_device。probe里通过pdev-dev拿到struct device指针之后就可以用devm系列API、device_property系列API访问设备树属性、GPIO、中断等资源。probe的本质是“驱动被允许操作设备”的入口不是驱动模块加载入口。驱动的模块加载入口其实是由module_platform_driver宏展开时的module_init函数完成的它负责调用platform_driver_register最终在总线匹配成功后触发probe。理解这个层次排查问题思路就会清晰很多。4. probe没调用怎么排查我在i.MX6ULL上的完整debug链路4.1 一次典型的匹配失败复现假设驱动加载后dmesg没有probe日志先从最基础的确认开始。第一步确认设备树节点是否被内核解析ls /proc/device-tree/mybeep/ cat /proc/device-tree/mybeep/compatible/proc/device-tree是设备树在内存中的展开形式节点存在就说明设备树里确实有这个节点而且dtb已经正确加载。cat compatible时建议用xxd或od查看十六进制因为compatible值在设备树里是字符串数组用cat可能看不到结尾空格或不可见字符。我当初遇到的那个多一个空格的坑就是靠xxd查出来的xxd /proc/device-tree/mybeep/compatible 00000000: 6d79 7665 6e64 6f72 2c6d 7962 6565 700a myvendor,mybeep.看到末尾的0x0a了吗当时我们设备树里字符串后面多了一个换行符。compatible的字符串比较是逐字节进行的一个多余字符就会导致匹配失败。如果cat显示正常、情况又非常诡异一定要用xxd确认原始字节。4.2 确认platform_device与platform_driver两端的注册情况设备树节点解析成功后内核还未必生成了platform_device。用下面的命令看设备端ls /sys/bus/platform/devices/如果mybeep目录不存在说明设备没有注册成功问题出在设备树解析阶段常见原因包括节点语法错误、status属性为disabled、父节点状态不对。如果目录存在再查看驱动注册情况ls /sys/bus/platform/drivers/mybeep/如果这个目录不存在说明驱动模块没有注册成功常见原因包括platform_driver结构体初始化错误、module_platform_driver宏使用不当、模块加载失败。可以先用modprobe或insmod看返回信息再用dmesg查模块加载阶段是否有报错。如果两边都存在却没有绑定链接就要看驱动目录下的uevent或者设备的ueventcat /sys/bus/platform/drivers/mybeep/uevent cat /sys/bus/platform/devices/mybeep/ueventuevent文件会显示模块名和MODALIAS信息。如果驱动的MODALIAS里有of:N...T...Cmyvendor,mybeep设备的MODALIAS也是Cmyvendor,mybeep那匹配理论上应该成立。这里经常出现的问题是大小写不一致、compatible缺少厂商前缀、或者of_match_table结尾忘了写sentinel空条目。4.3 对比compatible字符串、检查of_match_table的常见错误of_match_table的检查重点有三个。第一of_device_id数组必须以空结构体结束也就是sentinel否则内核遍历数组时会越界或漏匹配。第二compatible字符串必须完整按惯例包含“厂商名,设备名”两部分比如“myvendor,mybeep”不少人在设备树里少写了厂商前缀驱动里写了两个字符串当然不相等。第三驱动结构体中of_match_table字段是否真的赋值给了.driver的成员而不是赋给了platform_driver的顶层字段。有时候代码写成了static struct platform_driver mybeep_driver { .of_match_table mybeep_of_match, ... };这是错的of_match_table必须放在.driver子结构体里。这个问题编译不会报错但驱动加载后平台总线完全不知道匹配表的存在只能退回去走name匹配自然匹配不上。4.4 用driver_override强制绑定与手动bind/unbind验证为了快速验证“到底是不是匹配逻辑的问题”可以手动触发绑定。设备已经有了驱动也注册了直接写sysfs# 先解除可能的旧绑定 echo mybeep /sys/bus/platform/drivers/mybeep/unbind 2/dev/null # 强制指定驱动 echo my_beep /sys/bus/platform/devices/mybeep/driver_override echo mybeep /sys/bus/platform/drivers/my_beep/bind注意driver_override里写的是驱动名即.driver.name的值bind里写的是设备名。如果这样手动绑定时probe能执行说明驱动本身没问题问题一定出在自动匹配机制的某个环节。如果手动绑定也失败驱动代码本身要回炉检查重点看probe里有没有返回错误码。还要强调一种容易误导的现象probe函数被调用了但设备状态仍然显示not bound。这时要区分probe根本没执行和probe执行后返回错误。第一种对应匹配失败第二种对应初始化失败。初始化失败时dmesg里通常有probe函数的dev_err输出sysfs下设备的driver链接也会消失。这种情况需要用driver_override加上echo bind的方式复现观察内核打印的具体strace或错误码。4.5 一张表总结probe没调用的常见原因现象可能原因验证方法/proc/device-tree下无节点设备树未编译/节点被裁剪/status为disabled检查dts编译列表、dtb内容、status属性/sys/bus/platform/devices下无设备节点存在但未生成platform_device检查节点语法、父节点状态、of_platform_create/sys/bus/platform/drivers下无驱动驱动模块加载失败/驱动未注册dmesg查看模块加载日志两边都存在但无driver链接compatible不匹配/of_match_table错误xxd对比compatible字符串检查of_match_table的sentinel和字段位置probe执行但设备报错驱动初始化失败或资源获取失败查看dmesg中probe内错误日志检查GPIO等资源是否冲突5. 容易被忽略的细节module_platform_driver宏、remove回调与资源管理习惯5.1 module_platform_driver宏到底展开了什么module_platform_driver是一个宏不是函数。它把驱动的注册和注销包装成标准的模块入口函数static int __init mybeep_driver_init(void) { return platform_driver_register(mybeep_driver); } module_init(mybeep_driver_init); static void __exit mybeep_driver_exit(void) { platform_driver_unregister(mybeep_driver); } module_exit(mybeep_driver_exit);使用这个宏的好处是省去手写入口函数避免注册和注销逻辑不对称。但这也带来一个理解上的盲区很多新人以为probe函数是模块加载入口实际上模块加载入口是宏展开出来的mybeep_driver_init它做了teamplate注册工作后立即返回。platform_driver_register内部会同步扫描总线上的设备如果找到匹配项调用匹配逻辑后再调用probe。手动载入驱动有时会遇到probe在注册期间就同步执行的情况。如果probe里做了耗时操作modprobe命令就会卡住一会儿这不是系统异常而是probe同步调用的表现。知道了这一点就不会误以为系统死机了。5.2 remove回调签名变化与不同内核版本的兼容处理i.MX6ULL的老BSP比如NXP官方4.1.15和5.4内核platform_driver的remove回调签名是static int mybeep_remove(struct platform_device *pdev)但在Linux 6.1之后内核社区把remove的返回值改成了void引入了一个过渡阶段新代码用remove_new成员保存void返回类型的回调。比如static void mybeep_remove(struct platform_device *pdev) { ... } static struct platform_driver mybeep_driver { .probe mybeep_probe, .remove_new mybeep_remove, ... };如果你在6.1以上内核里直接写.remove mybeep_remove(旧式int返回版本)编译会收到warning甚至报错。如果你的驱动需要同时兼容老内核和新内核可以使用内核提供的宏或者条件编译处理。这不是i.MX6ULL专有问题但很多用老SDK的工程师升级内核时都会撞上。5.3 资源管理为什么推荐devm_platform_ioremap_resource与devm_gpiod_get在probe里获取硬件资源时我强烈建议使用devm系列API这类API把资源的申请和释放绑定到了struct device的生命周期。驱动卸载时probe里用devm_获取的资源会自动释放不需要在remove里逐个手动释放。少了release步骤不仅代码简洁还减少了内存泄漏和资源泄漏的风险。对于寄存器地址映射老式写法是res platform_get_resource(pdev, IORESOURCE_MEM, 0); regs devm_ioremap_resource(pdev-dev, res);或者直接一步到位regs devm_platform_ioremap_resource(pdev, 0);devm_platform_ioremap_resource内部会做platform_get_resource、devm_request_mem_region、devm_ioremap三件事返回映射后的虚拟地址。如果资源不存在或已被占用返回ERR_PTR错误。GPIO的获取同理用devm_gpiod_get系列。设备树属性名mybeep-gpios会被转换成con_id“mybeep”这正好对应我们设备树里的写法。devm_gpiod_get返回struct gpio_desc指针后续可以使用gpiod_set_value、gpiod_get_value等操作函数。5.4 关于“什么情况下才应该写platform驱动”的思考最后聊一个偏设计的话题。这个细节是我带新人时总被问到的是不是驱动都必须写成platform驱动并没有这个要求。platform驱动适合描述“挂在CPU总线上的外设控制器”这类实体硬件。如果设备树里能用一个物理节点描述它probe后需要访问寄存器或GPIO等硬件资源就适合platform驱动。但如果只是一个纯软件逻辑比如一个内核线程配合procfs提供调试接口没有对应的实体硬件那写成platform驱动更多是套了一套框架反而显得绕。在i.MX6ULL上像beep这种简单GPIO输出设备其实还可以考虑用内核自带的led-class框架或者pwm-leds驱动不需要自己写platform驱动。自己写platform驱动更适合那些需要访问私有寄存器、处理特定中断、做硬件状态管理的设备。选型时先把内核现成子系统找一遍能复用就复用这才是驱动开发的省力之道。我在实际调试中养成了一个习惯遇到匹配问题先看/sys/bus/platform下设备与驱动两端是否存在再对比/proc/device-tree里的compatible原始字节最后才去翻源码。这套流程跑下来大部分platform驱动匹配问题都能定位到根因。希望这篇文章能帮你把i.MX6ULL上的platform机制这块拼图补完整。