i.MX6ULL Platform驱动匹配机制详解:从probe不执行到排查实战 如果你在i.MX6ULL上写过Linux驱动大概率遇到过这样的场面设备树节点加了、驱动文件也写了、模块甚至都insmod进去了结果probe函数就跟休眠了一样死活不执行。我最早在调一个GPIO扩展芯片时就被这个问题卡了整整两天最后发现只是compatible字符串里多了一个空格。从那以后我花了不少时间把i.MX6ULL的Platform设备与驱动匹配机制完整梳理了一遍才发现很多困扰新手的问题其实都集中在设备树、of_match_table和platform总线的match回调这几件事上。这篇文章不准备讲空泛的理论我会从一次真实的“probe不执行”现场出发把Platform模型、匹配流程、调试手段以及i.MX6ULL常见外设里的实际用法一层层拆开适合刚入门嵌入式Linux驱动、或者正在被probe问题反复折磨的开发者。1. 从一次“probe不执行”的现场说起Platform匹配问题长什么样1.1 现象描述设备树和驱动都正常就是probe没跑先还原一下我当时的现场。设备树里添加了一个自定义LED节点放在/soc总线上节点的写法大概是这样myled: myled020c4000 { compatible myvendor,myled; reg 0x020c4000 0x1000; status okay; };驱动侧也按照常规套路注册了一个platform_driverof_match_table里写了对应的compatiblestatic const struct of_device_id myled_of_match[] { { .compatible myvendor,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver);然后把模块加载进去dmesg里没有看到任何probe相关打印。更奇怪的是ls /sys/bus/platform/devices/下能看到一个myled.0设备节点设备的地址后缀也是对的但ls -l /sys/bus/platform/devices/myled.0/driver却提示文件不存在。这说明设备已经成功注册到了platform总线上只是没有和我们的驱动建立绑定关系。如果这时候去看驱动的绑定状态ls /sys/bus/platform/drivers/myled/驱动目录里只有模块自身的信息没有任何设备链接。设备和驱动都在同一个公交车站台上却互相看不见这就是典型的匹配失败。1.2 为什么Linux要引入Platform总线来解决“设备与驱动见面”的问题要搞懂这个现象得先理解Linux为什么非要搞一个Platform总线出来。USB设备插上后usb core会去读描述符从中拿到VID、PID然后通过usb bus的match逻辑去找驱动PCI设备也有vendor ID和device ID。这种可以枚举的总线设备是“自己报名字”的。但i.MX6ULL这类SoC内部的集成外设不一样GPIO控制器、UART、I2C控制器、内部定时器它们都挂在芯片内部总线上并没有一个统一的热插拔枚举协议告诉你“我是什么设备”。在早期ARM Linux时代开发者只能在板级文件里逐个platform_device_register手动给每个外设创建一个platform_device。到了设备树时代硬件描述信息统一写在dts里内核启动时根据节点信息自动生成platform_device。平台总线本质上是个虚拟总线它存在的意义就是把“设备描述”和“驱动实现”解耦让双方通过统一的match规则找到对方。用生活化的比喻设备是征婚者驱动是应征者Platform总线就是一个相亲角。设备树相当于征婚者贴出来的信息栏驱动的of_match_table则是对“身份证号”的要求。双方能不能成取决于Platform总线这个“媒人”的匹配规则。理解了这个框架接下来所有问题都围绕一个核心compatible字符串和of_match_table怎么对上。2. 设备与驱动的“身份证”compatible与of_match_table的对应规则2.1 设备树里platform设备节点的最小形态在i.MX6ULL的dts目录里大部分外设控制器节点都放在/soc节点下soc节点本身带有compatible simple-bus内核启动时配合of_platform_default_populate_init会把符合条件的子节点逐个转换为platform_device。一个最小可被platform驱动匹配的设备树节点通常包含这几部分example_led: example-led020c4000 { compatible myvendor,example-led; reg 0x020c4000 0x1000; interrupts GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH; status okay; };compatible是对外声明的硬件型号reg保存寄存器地址段interrupts描述中断资源。这些属性最终会转换成platform_device的resource数组。驱动在probe里用platform_get_resource(pdev, IORESOURCE_MEM, 0)拿地址用platform_get_irq(pdev, 0)拿中断号资源本来就不需要硬编码在驱动里。这也是为什么驱动能在不同板卡间复用管你寄存器地址是0x020c4000还是0x020c5000只要设备树描述正确驱动拿到的资源就是对的。另外要注意status属性。如果写成disabled内核通常不会为这个节点创建platform_device等于直接把这个设备从系统里隐藏了。后面我会再专门展开。2.2 驱动侧of_device_id的三种常见写法驱动侧的匹配信息在platform_driver里。常见的写法有这三种很多初学者容易搞混第一种最常用的of_match_table通过compatible匹配设备树节点static const struct of_device_id example_of_match[] { { .compatible myvendor,example-led }, { .compatible myvendor,example-key }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, example_of_match);一个驱动可以支持多个compatible数组里每一项对应一个硬件型号。哨兵项{ }必须保留否则遍历匹配时会越界。第二种platform_driver.id_table主要用于非设备树环境或者需要按设备名匹配的场景static const struct platform_device_id example_id_table[] { { .name example-led, .driver_data 0 }, { .name example-key, .driver_data 0 }, { } };第三种只设置driver.name不设置of_match_table和id_table。在内核的platform_match逻辑里如果id_table为空驱动名和设备名完全相等也能匹配。但设备树环境下platform_device的name一般由设备节点名生成比如example-led020c4000的设备名可能是example-led所以driver.name example-led也能撞上。不过我不推荐依赖第三种方式。设备树时代compatible才是描述“这是什么硬件”的标准身份driver.name更适合内核自己生成设备名时使用。等你某天改了一个节点名驱动名没同步改probe就悄悄消失了。2.3 最容易踩的坑compatible字符串的模糊匹配并不存在我在文章开头提到那次坑了我两天的就是一个空格。设备树里写的是myvendor,example-ledof_match_table里写的是myvendor,example-led肉眼看上去一模一样但对内核来说就是两个不同的字符串。内核在匹配compatible时使用的就是strcmp必须完全相等才算匹配成功。不存在“前缀匹配”“忽略大小写”“模糊匹配”这些说法。哪怕多一个空格、少一个字母、大小写不一样结果都是匹配失败。我整理过几个新手常犯的写法错误设备树中的compatible驱动of_match_table中的compatible匹配结果myvendor,example-ledmyvendor,example-led成功myvendor,example-ledmyvendor, example-led失败多了空格myvendor,example-ledmyvendor,example_LED失败大小写不同myvendor,example-ledmyvendor,example失败不是前缀匹配myvendor,example-led1myvendor,example-led失败多尾字符执行中如果怀疑设备树没生效可以直接去/proc/device-tree下翻原始二进制属性。比如查找设备节点ls /proc/device-tree/ | grep example cat /proc/device-tree/example-led/compatible | tr \0 \ncompatible属性在设备树里是字符串列表内核里呈现为连续字符串加\0分隔用tr转成换行更好读。确认实际生效的compatible到底是什么再和驱动里的of_device_id逐字符比对。3. 匹配流程全拆解内核优先级、唤醒时机与debug验证3.1 从platform_driver_register到bus_type.match的调用链匹配听起来很科幻其实内核做的就是一件非常朴素的事在驱动注册时遍历总线上所有设备问一遍“你要不要这个驱动”反过来设备注册时也会遍历总线上所有驱动问一遍“你要不要这个设备”。以模块化加载驱动为例调用链大致是module_init - platform_driver_register - __platform_driver_register - driver_register - bus_add_driver - driver_attach - bus_for_each_dev - __driver_attach - driver_match_device - drv-bus-match - platform_matchplatform_match就是这个“媒人”的最终裁决函数。设备侧也有类似路径比如设备树初始化生成platform_device后会触发device_attach反过来扫描已注册驱动。理解这个双向扫描很重要。如果你先加载驱动后设备还没注册那么当设备注册时会再触发一次匹配。如果你先有设备后加载驱动那么驱动注册时会匹配已有设备。两者都不需要你手动干预。3.2 id_table、设备树、ACPI和名称匹配的先后顺序platform_match内部并非只检查compatible一件事它的优先级是固定的。大致逻辑如下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. 设备树compatible匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. ACPI匹配ARM上基本不用 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. id_table按设备名匹配 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 4. 最后退回到driver.name和设备名比较 */ return (strcmp(pdev-name, drv-name) 0); }我经常用一张表来帮同事理解匹配次序匹配方式依据1of_driver_match_device节点compatible vs of_device_id.compatible2acpi_driver_match_deviceACPI表匹配3platform_match_idpdev-name vs id_table[].name4strcmp(pdev-name, drv-name)pdev-name vs driver.name注意第3步的一个隐蔽坑只要pdrv-id_table不为空就不会执行第4步的driver.name兜底。也就是说如果你驱动里同时设置了id_table和driver.name而且id_table里没有这个设备名最终结果就是匹配失败哪怕driver.name和设备名一模一样。反过来如果id_table为空driver.name又恰巧和设备名相等那么即使of_match_table匹配失败整个匹配依然会成功。这也是网上很多人问“为什么我没有of_match_table但probe也能执行”的原因——大概率是driver.name撞上了设备节点名。3.3 用动态debug和sysfs把匹配现场“打回原形”匹配链路看起来清楚但实际验证时我们不太可能为了看一个strcmp结果去重新编译内核。好在Linux提供了两个很轻量的工具dynamic debug和sysfs的bind接口。先挂上debugfs打开platform.c的动态日志mount -t debugfs none /sys/kernel/debug echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control这个操作需要内核开启CONFIG_DYNAMIC_DEBUG。打开后再触发一次绑定然后看dmesg。比如可以先把驱动解绑再重新绑定echo myled.0 /sys/bus/platform/drivers/myled/unbind echo myled.0 /sys/bus/platform/drivers/myled/bind第二次echo如果匹配失败控制台会立刻报错比如cannot bind。通过这个接口我们可以手动强制“相亲”比反复insmod/rmmod模块方便得多。再配合看设备uevent里的MODALIAS信息cat /sys/bus/platform/devices/myled.0/uevent能看到类似MODALIASof:NmyledT(null)Cmyvendor,myled的输出。这段字符串就是设备对外宣告的身份令牌能直接和模块的alias对应。4. i.MX6ULL实战从GPIO按键到I2C控制器Platform如何藏身其中4.1 GPIO按键自己写的platform_driver怎么绑定dts节点单纯讲理论容易忘来一个i.MX6ULL上常见的GPIO按键例子。设备树节点gpio-key { compatible myvendor,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_key; key-gpio gpio1 18 GPIO_ACTIVE_LOW; status okay; };驱动侧的关键部分static int gpio_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *gpio; gpio devm_gpiod_get(dev, key, GPIOD_IN); if (IS_ERR(gpio)) return PTR_ERR(gpio); return 0; } static const struct of_device_id gpio_key_of_match[] { { .compatible myvendor,gpio-key }, { } }; MODULE_DEVICE_TABLE(of, gpio_key_of_match); static struct platform_driver gpio_key_driver { .probe gpio_key_probe, .driver { .name gpio-key, .of_match_table gpio_key_of_match, }, }; module_platform_driver(gpio_key_driver);在probe里调用devm_gpiod_get(dev, key, GPIOD_IN)的时候内核会从pdev-dev指向的设备树节点中读取key-gpio属性。这也是Platform匹配成功带给我们的第一个隐藏红利设备与驱动的绑定让驱动可以通过dev-of_node拿到当前设备的完整设备树上下文。如果没有绑定成功devm_gpiod_get会返回-ENODEV之类错误因为of_node和gpio描述之间无法建立对应关系。4.2 I2C控制器标准子系统下的隐含platform设备很多人学到这里会有一个疑惑I2C设备不是走i2c总线吗为什么讲Platform匹配其实i.MX6ULL的I2C控制器设备本身首先是一个platform_device。设备树中的i2c控制器节点i2c1: i2c021a0000 { compatible fsl,imx6ul-i2c, fsl,imx21-i2c; reg 0x021a0000 0x4000; interrupts GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH; clocks clks IMX6UL_CLK_I2C1; clock-names ipg; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; };这个节点会被转换成platform_device然后由内核里的drivers/i2c/busses/i2c-imx.c驱动匹配对应的compatible就是fsl,imx21-i2c。一旦匹配成功该platform驱动的probe会做两件事初始化I2C控制器硬件调用i2c_add_adapter注册一个i2c adapter。只有这个adapter注册成功后挂在i2c1节点下面的子设备比如加速度传感器、触摸屏才会进入i2c总线自己的匹配流程和对应的i2c_driver进行二次匹配。所以整个链路是两层的第一层I2C控制器节点 vs platform_driver匹配成功后注册I2C适配器。第二层I2C子节点 vs i2c_driver匹配成功后probe传感器驱动。如果I2C控制器的status被改成disabled第一层都不会发生子设备再正常也probe不了。调试时看到i2c子设备报-ENODEV先去查控制器节点是不是被禁用了速度会快很多。4.3 statusdisabled与pinctrl缺配匹配成功但probe失败的另一类问题匹配成功不等于probe一定成功。这是我在后来调试中反复强调的一点。第一类情况是设备树节点statusdisabled内核压根不创建platform_device你在/sys下面都看不到设备自然谈不上匹配。第二类情况更隐蔽设备树节点status是okaycompatible也匹配了platform bus已经把这门“亲事”定了但probe函数因为某个条件不满足返回了负数。常见场景是pinctrl缺失。比如设备树里写了某个外设需要一组引脚但没有给pinctrl-0驱动probe时pinctrl_bind_pins就会失败最终probe返回-19-ENODEV。此时dmesg里会看到类似platform myled.0: Driver myled requests probe deferral或者myled_probe: probe of myled.0 failed with error -19需要注意-EPROBE_DEFER-517不是真正失败而是“依赖的模块还没准备好”。比如你驱动依赖时钟但时钟驱动还没probe完成平台驱动会通过-EPROBE_DEFER告诉内核“以后再试我一次”。内核会在之后继续尝试直到依赖就绪。5. 匹配不上时怎么办一套可复现的完整排查链路5.1 确认设备树节点是否真的被转换成了platform_device匹配失败的排查第一步不是看驱动而是先确认设备到底存不存在。在i.MX6ULL板卡上启动后执行ls /sys/bus/platform/devices/ | grep myled如果能列出类似myled.0的目录说明设备已经注册到platform bus了。接着去/proc/device-tree确认节点属性ls /proc/device-tree/ | grep myled cat /proc/device-tree/myled/compatible | tr \0 \n如果这里看不到节点就要回到设备树本身。常见原因包括节点放在了错误的位置父节点没有simple-bus属性导致未被扫描dtsi文件被宏开关关掉编译时写的dtb根本没有烧录进板子。一个比较彻底的办法是反编译当前生效的设备树dtc -I fs /proc/device-tree -O dts | grep -A 10 myled这样可以排除“代码里看着对实际生效却不是这样”的尴尬情况。5.2 在/sys/bus/platform下反查设备与驱动的双向绑定关系设备存在后下一步看绑定关系。Platform bus在/sys下给了我们双向视角。从设备侧看ls -l /sys/bus/platform/devices/myled.0/driver如果没有这个符号链接说明设备处于无驱动状态。从驱动侧看ls /sys/bus/platform/drivers/myled/如果驱动已注册目录下会有模块信息。如果驱动里已经绑定了某个设备会多出一个指向设备的符号链接。手动强制绑定是很快的判定手段echo myled.0 /sys/bus/platform/drivers/myled/bind如果匹配逻辑有问题这行命令会直接报-ENODEV或No such device。如果设备存在、compatible也匹配绑定成功的同时probe会被执行dmesg里立刻有日志。这个方法可以帮我们区分“设备不存在”和“设备存在但不匹配”。5.3 打开内核日志与ftrace精确看到match回调的执行现场如果怀疑是platform_match内部判定问题可以开ftrace把platform_match的执行过程拉出来看。前提是内核开启了CONFIG_FUNCTION_TRACER。cd /sys/kernel/debug/tracing echo function_graph current_tracer echo platform_match set_ftrace_filter echo 1 tracing_on echo myled.0 /sys/bus/platform/drivers/myled/bind cat trace在trace输出里你能看到platform_match是否被调用函数内部走了哪个分支最终返回值是什么。如果完全看不到platform_match说明驱动注册时设备扫描路径可能没走到问题更可能出现在设备没有注册或模块没有正常加载。ftrace更适合有一定基础的人使用初期可以只靠dynamic debug和bind报错来定位。5.4 区分“没匹配上”和“匹配上但资源申请失败”最后一步是定性到底是没匹配上还是匹配上了但probe失败。这两种场景的日志特征完全不同。没匹配上时dmesg里通常没有任何probe相关错误bind接口会报找不到设备/sys/bus/platform/devices/myled.0/driver一直不存在。匹配上但probe失败时dmesg会打印probe of myled.0 failed with error -XX。常见的失败码错误码含义-19-ENODEV一般内存区域或中断资源不存在-16-EBUSY资源被占用-22-EINVAL参数非法-517-EPROBE_DEFER依赖尚未就绪内核会稍后重试如果是-EPROBE_DEFER还可以看全局延迟probe列表cat /sys/kernel/debug/devices_deferred这个文件会列出当前所有等待依赖的设备方便我们找到谁在等谁。很多i.MX6ULL驱动probe不执行其实不是匹配问题而是GPIO控制器、时钟或pinctrl驱动还没准备好结果被派到了延迟队列。一看到devices_deferred里有自己的设备名心里就有底了。6. 进阶玩法非设备树环境、动态创建设备与模块自动绑定的底层逻辑6.1 不用设备树时如何platform_device_register手动造设备设备树普及之后手动注册platform_device的场景少了很多但理解它仍然很有价值特别是在做模拟设备、板级补丁或者研究内核机制时。在非设备树环境下开发者会自己声明资源和platform_devicestatic struct resource myled_resources[] { [0] DEFINE_RES_MEM(0x020c4000, 0x1000), [1] DEFINE_RES_IRQ(32), }; static struct platform_device myled_device { .name myled, .id -1, .num_resources ARRAY_SIZE(myled_resources), .resource myled_resources, }; static int __init myled_device_init(void) { platform_device_register(myled_device); return 0; }设备名myled就是platform_device的name。设备注册后如果驱动侧的driver.name或id_table里有myled就能匹配。这种方式比设备树更直接也更难维护因为每次硬件地址变化都要重新编译内核。但在某些早期板卡的测试代码里你仍然能看到这种写法。6.2 通过platform_get_resource与私有数据传递硬件信息匹配成功后probe主要就是从设备对象身上拿资源。i.MX6ULL驱动里最常见的三板斧static int myled_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); 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; /* 保存私有数据后续remove和中断回调里用 */ platform_set_drvdata(pdev, my_pointer); return 0; }platform_get_resource背后读的就是设备树节点里的reg属性或手动注册的resource数组。devm_ioremap_resource除了映射地址还会自动做溢出校验比裸ioremap更安全。如果地址段在设备树里没注册它会返回-EINVAL这也是probe失败的常见来源。6.3 modalias、modules.alias与udev自动加载的关系前面提到uevent文件里的MODALIAS这里展开说。Linux设备在注册时会向用户空间发送ueventMODALIAS就是设备对外广播的“可匹配身份”。udev收到后会拿着这串字符去/lib/modules/$(uname -r)/modules.alias里查找对应模块名然后自动modprobe。所以模块想要实现“设备树节点一出现驱动自动加载”需要满足三个条件模块编译进/lib/modules对应的kernel release目录下。驱动里写对了MODULE_DEVICE_TABLE(of, xxx)这样depmod才能生成对应alias。设备树和of_match_table匹配成功。如果模块是手动insmod进去的自动加载机制不起作用但你仍然需要通过MODULE_DEVICE_TABLE把alias生成好。不然将来部署到生产环境用户只放设备树不手动加载模块probe照样不会跑。常用检查命令depmod -a grep myvendor /lib/modules/$(uname -r)/modules.alias能看到类似alias of:N*T*Cmyvendor,myled这样的输出就说明模块autoload链路是通的。6.4 从匹配机制看Linux驱动模型的设计哲学把整个Platform匹配机制透完一遍后我最大的感触是Linux驱动模型本质上是在“设备描述”和“驱动实现”之间加了一个抽象层让两边可以独立演进。设备树管“有什么硬件”驱动管“怎么操作硬件”中间的platform bus负责通过compatible、id_table、name这些标准键值把它们撮合到一起。理解这个哲学再去看i.MX6ULL上的各种驱动会轻松很多。遇到probe不执行先别急着怀疑编译器或内核按设备注册、驱动注册、匹配判定、probe执行的顺序层层排查大多数问题都能在五分钟内定位。我自己现在拿到一块新板卡第一件事就是看设备树里root节点的compatible然后去/sys/bus/platform下逛一圈等养成这个习惯后像“probe不执行”这类问题基本就不再成为拦路虎了。