1. 先聊聊我为什么关注i.MX6ULL这套匹配链路做嵌入式Linux驱动开发的人几乎都绕不过i.MX6ULL这颗芯片。它便宜、资料多、BSP完善很多开发板甚至直接把它当中低端项目的主力芯片用。但真正把它的驱动跑起来你会发现一个很关键的门槛设备树Device Tree下的Platform设备与驱动到底是怎么匹配上的。我最早接触Platform总线时以为驱动只要注册好probe就会自动被调用。结果写了一个demo驱动insmod之后什么反应都没有dmesg里一片安静。后来排查半天发现是compatible字符串和设备树节点对不上。从那时候起我才真正把注意力放到匹配机制上而不是“照着例程抄”。如果你也正在写i.MX6ULL的外设驱动或者想理解Linux设备模型里设备与驱动的结合过程这篇文章就把Platform匹配机制从头到尾拆开讲清楚包括设备端怎么描述、驱动端怎么声明、匹配流程里内核到底做了什么以及我踩过的几个坑。2. 先搞清楚Platform总线的存在意义2.1 总线、设备、驱动三者的关系在很多人的认知里Linux的驱动模型是一个“设备-总线-驱动”的铁三角设备Device描述硬件比如I2C控制器、SPI控制器、GPIO控制器。驱动Driver描述软件逻辑提供操作设备的函数。总线Bus负责把设备和驱动“配对”。对于PCI、USB、I2C、SPI这类硬件总线设备和驱动之间的匹配是由总线自身协议决定的。比如USB设备有vendor ID和product ID驱动声明支持哪个IDUSB核心就能自动识别并绑定。但是有一个问题SoC内部的很多外设并不挂在这样的标准总线上。拿i.MX6ULL来说UART、GPIO、ECSPI、I2C、SDIO这些控制器都是芯片内部集成模块它们直接连在芯片内部总线上没有标准枚举机制也没有可探测的ID。操作系统没法像识别USB设备一样自动识别它们。Platform总线就是为解决这个问题设计的。它不是真实的硬件总线而是内核虚拟出来的一条“平台总线”统一管理SoC内部这些“平台设备”。设备侧注册一个platform_device驱动侧注册一个platform_driverPlatform总线根据匹配规则把两者绑到一起然后调用驱动的probe函数。2.2 i.MX6ULL里哪些设备走Platform路径用一块i.MX6ULL开发板打开设备树源文件你会发现几乎所有外设节点都会有这样一段属性compatible fsl,imx6ull-ecspi, fsl,imx6ul-ecspi;这个字符串就是匹配的钥匙。内核在启动时会扫描设备树中的每个节点为它们创建device。当某个device的父总线被识别为platform总线时就会注册成platform_device。SoC里的gpio、iomuxc、uart、ecspi、i2c这些控制器最终都由platform总线来管理。2.3 为什么适配层如此重要理解了Platform总线的意义你就会明白它本质上是Linux统一设备模型里的一块“粘合剂”。不管底层硬件差异有多大只要走Platform机制驱动开发方式就统一了。我们写驱动时几乎不会直接跟上层应用打交道而是实现一组固定的接口match、probe、remove、shutdown等剩下的绑定、初始化、解绑都由内核帮你完成。这套机制让驱动代码的移植性和可维护性都大大提高。说得再直白一点不会Platform匹配机制你在i.MX6ULL上写的驱动基本跑不起来因为即使最简单的GPIO按键驱动也逃不出设备树节点platform_driver这套流程。3. 设备端与驱动端匹配的两位主角3.1 platform_device到底怎么来要理解匹配先搞清楚设备端是什么状态。在传统非设备树时代开发者在代码里定义一个platform_device结构体再把它注册进内核static struct platform_device my_device { .name my-device, .id -1, .dev { .platform_data my_platform_data, }, }; platform_device_register(my_device);这种方式在内核源码里依然大量存在但现在的i.MX6ULL BSP里设备属性基本上都放进了设备树代码里很少显式注册platform_device。设备树方式下编译生成的dtb在启动时被内核解析每个带compatible属性的节点都会创建对应的platform_device。节点里还可以携带reg、interrupt、clock等资源信息这些都会存到platform_device的resource数组里。驱动在probe时再通过platform_get_resource这类接口把它们取出来。注意设备节点的父节点会影响它注册到哪种总线。如果你的外设节点挂在I2C或SPI控制器的子节点下它注册的就不是platform_device而是i2c_device或spi_device驱动的写法也完全不同。3.2 platform_driver的构成设备端介绍完来看驱动的结构。一个标准的platform_driver大概长这样static const struct of_device_id my_of_match[] { { .compatible fsl,my-device, }, { /* sentinel */ } }; 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);看到这里你应该意识到匹配机制的关键点在driver结构体里.driver.name平台设备的名字在内核内部也用于匹配。.driver.of_match_table指向一组of_device_id数组里面声明该驱动支持的设备树compatible字符串。.probe设备与驱动匹配成功后调用的入口函数。.remove设备移除或驱动卸载时调用负责释放资源。3.3 两种匹配方式的对比我在实际项目中两种方式都踩过。它们各自的用途和限制很不一样匹配方式判断依据适用场景优先级设备树OF匹配compatible属性现代设备树主流方式最高ID表匹配id_table中的name兼容旧设备无设备树场景中驱动name直接匹配platform_device的name特殊自定义场景低内核匹配顺序大致是先查OF匹配表如果没有再查id_table最后回退到驱动名字进行字符串比较。设备树普及后绝大多数情况走第一条路径后面的匹配方式仅作兼容。4. 匹配过程的深层原理与完整链路4.1 匹配的入口platform_match函数设备侧注册platform_device、驱动侧注册platform_driver后平台总线会调用一个名为platform_match的接口用来判断设备与驱动是否匹配。它背后做的事情其实很直白依次尝试多种匹配条件只要有一个成立就返回匹配成功。这个函数在内核源码drivers/base/platform.c里精简后逻辑大致如下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匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试ID表匹配 */ if (pdrv-id_table) if (platform_match_id(pdev, pdrv-id_table) ! NULL) return 1; /* 4. 驱动名与设备名匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }看到这个顺序你就能明白为什么平时调试时最推荐检查compatible属性。尤其是第1步OF匹配不行时后续匹配方式基本指望不上。4.2 OF匹配的底层逻辑OF匹配里的核心函数是of_driver_match_device它最终会检查设备节点的compatible属性是否与of_device_id数组中的某个compatible条目相等。设备树里写mydev: my-device02000000 { compatible fsl,my-device; reg 0x02000000 0x1000; interrupts GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH; };驱动里声明static const struct of_device_id my_of_match[] { { .compatible fsl,my-device, }, { /* sentinel */ } };这样两者就能对上。但有个细节容易踩坑设备树里的compatible允许写多个字符串比如compatible fsl,my-device, fsl,imx6ull-my-device;内核在匹配时会按顺序拿第一个字符串开始跟驱动声明列表比较。驱动里的of_device_id数组也是按顺序遍历。所以如果你在驱动里声明了多个compatible尽量按照设备树里声明的优先级来排否则可能匹配到一个不期望的节点。4.3 匹配成功之后发生了什么匹配成功后内核调用driver-probe也就是我们写的platform_driver里的probe函数。但probe之前还有几个步骤调用bus的probe回调platform_probe。platform_probe里会调用pdrv-probe并把platform_device指针传入。设备驱动模型会对设备进行电源管理等初始化操作。进入probe之后我们需要通过一系列platform API获取硬件资源static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; 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; /* ... */ return 0; }我用过很多次platform_get_resource配合devm_ioremap_resource这套组合。它在设备树方式下会自动从节点的reg属性里取物理地址再申请并映射IO内存省去了手动转换的麻烦。4.4 生命周期管理要注意的细节Linux设备模型对platform设备有一套完整的生命周期管理设备插入、驱动绑定、probe、remove、驱动卸载、设备移除。理解这套生命周期写驱动时才知道资源该在哪释放。probe里申请的资源remove里必须对应释放。好消息是现代内核推荐使用devm_系列函数devm_kzalloc、devm_ioremap_resource、devm_request_irq这些资源在probe失败或设备移除时自动释放。我在初学阶段经常忘记free_irq导致重复请求中断后来全部改成devm接口这类问题基本绝迹。但要注意devm接口管理的是跟dev生命周期绑定的资源不代表你完全不用写清理逻辑。比如你创建了sysfs节点、proc节点、或注册了字符设备这些还是要自己在remove或disconnect里显式删除。5. 基于i.MX6ULL的完整实操从设备树到probe跑通5.1 设备树节点的编写思路我拿一个自定义的“irq-key”设备为例演示完整流程。这个设备模拟一个外部中断按键挂在某个GPIO上。虽然实际用GPIO子系统更合理但为了讲清楚Platform机制就不绕弯了。在i.MX6ULL的设备树里添加/ { my-key-device { compatible my-company,irq-key; pinctrl-names default; pinctrl-0 pinctrl_key; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; status okay; }; };注意两个重点compatible必须与驱动声明一致否则不可能匹配。interrupt-parent要指向实际管脚对应的GPIO控制器interrupts里指定GPIO内部的pin编号和触发类型。i.MX6ULL的GPIO1、GPIO2等各自对应一个中断控制器interrupts里的第一个值指的是该GPIO控制器下的引脚序号不是SoC顶层的中断号。这一点我当初犯过错写成了全局中断号结果probe能进request_irq却失败。5.2 驱动代码的编写与解析驱动完整骨架如下#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/interrupt.h #include linux/slab.h static irqreturn_t my_key_isr(int irq, void *data) { printk(KERN_INFO my-key interrupt triggered\n); return IRQ_HANDLED; } static int my_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int irq; int ret; irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(dev, failed to get irq\n); return irq; } ret devm_request_irq(dev, irq, my_key_isr, IRQF_TRIGGER_FALLING, my-key, NULL); if (ret) { dev_err(dev, failed to request irq\n); return ret; } dev_info(dev, my-key probed, irq%d\n, irq); return 0; } static int my_key_remove(struct platform_device *pdev) { dev_info(pdev-dev, my-key removed\n); return 0; } static const struct of_device_id my_key_of_match[] { { .compatible my-company,irq-key, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_key_of_match); static struct platform_driver my_key_driver { .probe my_key_probe, .remove my_key_remove, .driver { .name my-key, .of_match_table my_key_of_match, }, }; module_platform_driver(my_key_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple platform driver example);写驱动时有个地方值得注意MODULE_DEVICE_TABLE(of, ...)这行看起来不起眼但它会被编译进模块的modinfo信息里。热插拔场景下设备管理工具udev/mdev会读取这个信息来识别驱动支持的设备。虽然在i.MX6ULL嵌入式环境里基本用不到自动加载但这个习惯要保持因为代码如果被拿到其他发行版Linux上缺失这行会导致modprobe无法关联设备。5.3 编译、加载与验证过程在i.MX6ULL的BSP环境里我习惯先把驱动编译成模块.ko方便调试。Makefile用Kbuild方式obj-m : my-key.o KDIR : /path/to/linux-src all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean编译完成后把my-key.ko拷贝到开发板上。加载模块之前先确认设备树已经正确解析出节点。可以在开发板上执行ls /sys/bus/platform/devices/你会看到my-key-device这个名字设备树节点名转换而来。如果没有这个设备说明设备树没编译进去或者dtb没更新需要回去检查设备树配置。然后加载驱动insmod my-key.ko正常情况下dmesg会输出my-key my-key-device: my-key probed, irq...如果没有任何输出大概率是匹配环节出问题接下来就到排查环节。5.4 实战中验证匹配状态加载驱动之后还可以通过sysfs确认绑定关系cat /sys/bus/platform/devices/my-key-device/uevent输出里能看到DRIVERmy-key说明驱动已经绑定成功。如果没有DRIVER字段说明设备并没有找到匹配的驱动。这个方法比我以前dmesg半天管用得多。设备树、驱动、模块三者的状态一目了然。6. 常见匹配问题与排查技巧6.1 probe没有被调用的排查顺序这是我被问得最多的问题。驱动加载成功但probe就是不执行。按我的经验按下面顺序排查基本都能解决第一步确认设备节点存在。先看/sys/bus/platform/devices/下是否有对应设备。没有的话99%是设备树没生效。检查CONFIG_OF是否开启检查dtb是否真正烧录到开发板检查内核启动日志里有无设备树解析报错。第二步确认compatible字符串完全一致。这里“完全一致”不是视觉上差不多就行而是字节序、空格、大小写都要一样。我遇到过设备树里写fsl,imx6ull-ecspi、驱动里写fsl,imx6ul-ecspi导致匹配失败的情况——因为内核里跟i.MX6UL共用BSP两个字符串差一个字母找了大半天。第三步确认模块加载顺序。如果驱动是模块确认是否真的插入了设备之后再加载。Platform总线不是热插拔总线但设备树解析时设备就已经注册了也就是说驱动晚加载也能匹配上这个倒是问题不大。但反过来如果你的设备节点状态是disabled即使驱动匹配成功也不会probe。第四步确认设备树状态属性。设备树节点检查status字段。默认可以不写等效于okay。但如果芯片原厂模板里节点默认disabled你要在板级设备树里主动改为okay否则内核不会为该节点创建设备。我碰到过GPIO、I2C外设节点都是disabled结果所有驱动probe都不跑。第五步确认资源是否冲突。即使匹配成功内存资源被其他设备占用、中断号被占用probe也会返回错误。这时候dmesg会有明确的信息比如request_irq失败、ioremap失败等。这类错误不算匹配失败但现象很像。6.2 dmesg与sysfs联动调试大法一个比较高效的联动调试方法就是把dmesg和sysfs配合起来用。先加载驱动前记录dmesg位置dmesg -c /dev/null insmod my-key.ko dmesg然后立刻去查sysfsls -l /sys/bus/platform/drivers/my-key/如果my-key这个驱动目录下出现了设备符号链接说明绑定成功。没有的话再用cat /sys/bus/platform/devices/my-key-device/uevent确认DRIVER字段。这套组合比单看内核日志更直观。尤其当你面对多个外设驱动时sysfs能快速列出每个设备当前绑定的驱动状态。6.3 我踩过的三个典型坑坑一忘记把设备树节点放在根节点下。Platform设备树节点不一定要放在根节点下但必须保证它最终被解析成一个platform_device。如果节点挂在I2C控制器子节点下内核会试图把它匹配成i2c_client你的platform驱动永远匹配不上。写驱动前先想清楚外设挂在什么总线上。坑二of_match_table里缺少哨兵项。数组结尾的{ /* sentinel */ }不能省内核遍历of_device_id数组时靠NULL结尾判断结束。数组没有哨兵内存遍历不可控匹配行为变成未定义。我一度怀疑是编译器问题最后检查发现就是这个坑。坑三CONFIG_OF设置不当。有些内核配置会把CONFIG_OF关掉这时of_driver_match_device直接返回false全部改走id_table或name匹配。最惨的是compatible对不上name也对不上probe静默失败。排查时打开内核配置文件看看CONFIG_OF是不是y这个检查分分钟但是很多人忽视。7. 特殊情况与进阶理解7.1 同一设备多个驱动时的匹配顺序你可能遇到过这样的情况设备树节点指定的compatible有多个字符串而系统里有多个驱动各自声明了匹配其中的某一个。此时内核按of_device_id数组顺序和设备树compatible顺序双重优先级来决定绑哪个驱动。举个具体例子设备树写compatible fsl,imx6ull-ecspi, fsl,imx6ul-ecspi;驱动A声明匹配第一个字符串驱动B声明匹配第二个。此时驱动A优先如果驱动A没有加载或者probe失败内核不会自动回退去绑定驱动B。设备树compatible列表的排序是有讲究的通常把最具体、最优先的型号放在前面。7.2 驱动defer机制与probe延迟在复杂系统中驱动之间可能存在依赖。比如你的驱动依赖某个电源域、某个时钟或某个GPIO控制器。这些资源可能由另一个platform驱动提供而对方还没probe完成。传统的做法是probe直接返回错误码但这样太粗暴。Linux提供了PROBE_DEFER机制当依赖资源暂时不可用时probe返回-EPROBE_DEFER内核会把这个驱动放到待重试队列等依赖资源注册后再尝试调用probe。在i.MX6ULL的开发中这个机制尤其重要。比如一个I2C外设驱动依赖I2C控制器驱动如果I2C控制器驱动加载慢一拍外设驱动probe就会返回-EPROBE_DEFER等到I2C控制器就绪后自动重试。理解这一点调试的时候就不会把-EPROBE_DEFER当成致命错误去改代码而是查依赖链。7.3 设备树匹配与ACPI匹配的取舍i.MX6ULL这类ARM平台不涉及ACPI那是x86/ARM64服务器的事所以匹配几乎只走OF路径。但代码中platform_match确实会先尝试ACPI这意味着如果你在驱动的of_match_table里没写对可以去看看ACPI是否意外匹配到了什么。嵌入式项目里不太需要关心ACPI但如果你把驱动代码移植到一个既支持设备树又支持ACPI的平台比如某些边缘服务器这条匹配链路就很重要。好在你不需要写两套东西内核的ACPI兼容层会尽量把ACPI信息映射成类似OF的资源结构。8. 从调试到稳定的开发习惯8.1 移植驱动时的建议顺序接手一个别人的i.MX6ULL驱动不要急着编译。我一般按这个顺序来审查先看设备树节点是否存在且使能再看of_match_table里的compatible和设备树是否一致再看probe里用到的资源接口是否与节点属性对应最后看中断号、GPIO号等资源分配是否冲突。这个过程看起来琐碎但能把问题拦截在编译之前。尤其是从其他芯片平台移植过来的驱动寄存器地址、中断号几乎都要改不能只看软件逻辑。8.2 如何利用内核日志快速定位匹配状态内核的驱动模型在连接设备与驱动时会有相应输出不过很多日志级别默认不打印。你可以在内核启动参数里加上dyndbgfile drivers/base/platform.c p打开platform总线核心的动态调试。这样内核会把匹配过程中的关键点打印出来包括匹配了哪个驱动、有没有命中OF表等。实际操作# 在U-Boot启动参数里追加 dyndbgfile drivers/base/platform.c p或者运行时激活echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control这个方法在你不确定内核到底走哪条匹配路径时特别管用。打印信息会直接告诉你匹配到哪个compatible、哪次匹配失败。8.3 从驱动代码层面做防御性设计一个成熟的platform驱动probe里应该做到资源获取失败时逐一回滚、已经分配的资源不遗漏、关键资源未获取时适配-EPROBE_DEFER。我用devm接口居多但在remove里还是会显式写unregister操作防止依赖顺序出问题。代码风格也有讲究。device节点的日志尽量用dev_info、dev_err而不是裸printk。dev_系列接口会输出设备名配合匹配机制能一眼看出日志是哪个设备打出来的。这个习惯在多设备共用一个驱动时不亚于救命稻草。9. 对i.MX6ULL匹配机制的一个落地总结把Platform匹配机制从理论走到代码再走到开发板整个过程折腾了我不少时间。但过了一遍之后再去看内核里复杂的驱动代码思路就清晰了。对初学者最实用的经验是遇到probe不进第一反应别去改驱动代码先去查设备树compatible和status。经验表明绝大多数匹配问题出在设备树这边而不是驱动这边。等你被坑过几次就会发现Linux设备模型其实是一个设计相当优秀的框架——设备、驱动、总线三者解耦得干净利落只要掌握匹配这条主线几乎所有外设驱动都能按同一套思路去解读。如果你也在搞i.MX6ULL建议花一个下午时间把设备树里几个外设节点的compatible与对应驱动源码里的of_match_table对比着看一遍。看完你就会发现Platform匹配机制不过是一层纸捅破了后面全是套路。