基于i.MX6ULL深度解析Linux Platform设备驱动匹配机制

发布时间:2026/9/9 8:56:17
基于i.MX6ULL深度解析Linux Platform设备驱动匹配机制 最近在调试一块i.MX6ULL的开发板涉及到一个外接传感器芯片的驱动移植。整个过程里最让我有感触的倒不是中断处理或者寄存器配置反而是很多人觉得“太基础”的Platform设备和驱动的匹配机制。基础归基础一旦真机调试时驱动进不了probe或者莫名其妙probe了两次你就知道这玩意儿有多折腾人了。这篇就把我实际踩坑、看源码、做实验的过程整理出来算是对Platform机制的一次彻底复盘。核心关键词就三个——i.MX6ULL、Linux驱动开发、Platform匹配机制适合刚入门嵌入式Linux驱动或者已经在写驱动但总觉得“设备树和platform之间有点朦胧”的朋友。1. 为什么内核要造一条无法被物理命中的Platform虚拟总线先聊一个很多人忽略的问题Linux里明明有PCI、USB、I2C、SPI这些真实存在的物理总线为什么还要搞出一条”Platform Bus”这种纯软件抽象的东西我在刚学驱动的时候也有这个困惑。后来被一个老工程师一句话点醒“你的设备根本没挂在任何一条标准总线上那内核怎么知道该用哪个驱动去匹配它”这句话戳到了本质。在真正的嵌入式SoC上比如i.MX6ULL大量设备GPIO控制器、UART、SPI控制器、I2C控制器、看门狗、定时器……都是直接集成在芯片内部的它们没有PCI那样标准化的枚举机制也没有USB那样的热插拔发现流程。这些设备本质上就是“挂”在CPU的内存地址空间上以一串寄存器的方式存在。如果在内核里为每一个SoC都单独建一条总线去管理它们工作量会爆炸。于是内核的维护者做了一个决定设计一个虚拟总线专门给这些“没有归属的设备”提供一个统一的挂载点。因为这种设备在整个Linux的历史版本里最初大量出现在板级文件board file中硬件资源又极度依赖于具体平台platform所以就叫它platform bus挂在它上面的设备就叫platform device对应的驱动就是platform driver。这里面有个容易混淆的点千万不要把platform理解为某种硬件接口。它就是一条“软件总线”负责把以下这几种设备收集起来统一管理直接集成在SoC内部、没有独立物理总线可依附的外设在板级文件现在基本是设备树里手动注册的那些“虚拟设备”需要共享系统资源的特殊设备。在i.MX6ULL上绝大多数片内外设的驱动最终都会挂到Platform机制上。包括你写GPIO子系统的底层驱动、I2C控制器的驱动、中断控制器GIC的驱动统统都在platform总线的统辖范围内。理解了这一点你自然也就明白了为什么在内核目录下那么多驱动的probe函数签名都是static int xxx_probe(struct platform_device *pdev)。2. 匹配机制的本质驱动与设备是怎样“互相找到对方”的搞清楚了platform总线的定位后重点就是匹配机制。不管是设备还是驱动只要注册到platform总线上总线就会启动一次“配对”。配对成功的核心依据是两边各自暴露出来的关键信息是否对得上。先看驱动侧一个常规的platform_driver结构如下static const struct of_device_id my_sensor_of_match[] { { .compatible mycompany,my-sensor, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static struct platform_driver my_sensor_driver { .probe my_sensor_probe, .remove my_sensor_remove, .driver { .name my_sensor, .of_match_table my_sensor_of_match, }, }; module_platform_driver(my_sensor_driver);这里信息量很大归纳起来驱动向总线“宣告”自己能匹配的设备时主要有这么几个线索第一driver.name一个简单的字符串名称第二of_match_table指向一个of_device_id数组里面装的是设备树匹配用的compatible字符串第三如果走的是老式板级文件方案还会有id_table这个在设备树时代用得越来越少了但老代码里依然能看到。再看设备侧在设备树时代一个设备节点比如i.MX6ULL上的I2C控制器在设备树里长这样i2c1 { clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; my_sensor48 { compatible mycompany,my-sensor; reg 0x48; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; }; };内核在启动时解析设备树会为每一个节点创建一个struct platform_device前提是节点没有被框架自动识别为I2C客户端等专用设备节点的compatible属性就被转换成struct platform_device里的dev.of_node-data等结构供匹配时查阅。总线的匹配逻辑核心代码在drivers/base/platform.c里的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. 尝试ACPI匹配ARM环境不常用但x86等有ACPI的平台上会最先执行 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 2. 尝试设备树匹配这是i.MX6ULL上最主流的路径 */ if (of_driver_match_device(dev, drv)) return 1; /* 3. 尝试ID表匹配对应platform_driver-id_table */ if (pdrv-id_table) if (platform_match_id(pdrv-id_table, pdev) ! NULL) return 1; /* 4. 退而求其次比较驱动名和设备名 */ return (strcmp(pdev-name, drv-name) 0); }从这段代码你能看出来Linux内核非常讲究稳妥不放过任何一次可能配对成功的机会ACPI不行就上设备树设备树不行再试ID表最后还能退回到最原始的字符串名字匹配。这也是为什么有时候你写驱动时明明什么都没设置只给driver.name赋值了却依然能匹配上——因为内核兜底机制在起作用。不过在i.MX6ULL上实际99%的场景走的是第2条路径——设备树匹配。所以compatible字符串写对没有直接决定了你的驱动能不能被唤醒。3. 设备树匹配的微观拆解compatible字符串的命中过程设备树匹配这条路径值得单独拿出来讲。因为很多新手在设备树和驱动两头检查了半天觉得都没写错但probe就是不执行最后发现是compatible字符串不匹配这种很细小的原因。沿着of_driver_match_device()往下追函数会走到of_match_device()然后调用of_match_node()。真正干活的是在drivers/of/base.c里其核心是比较驱动声明的of_device_id数组和设备的of_node里的compatible列表。有一点非常关键设备树节点里的compatible是一个字符串数组可以有多个值匹配时会依次去比对优先级从前到后。打个比方设备节点里这样写compatible mycompany,my-sensor-v2, mycompany,my-sensor;这意思是首选第一个字符串第一个字符串决定主兼容性第二个是后备兼容性。通常前面的代表具体版本型号后面的代表兼容的老型号或家族系列。驱动侧则可以这样static const struct of_device_id my_sensor_of_match[] { { .compatible mycompany,my-sensor, }, { .compatible mycompany,my-sensor-v2, }, { /* sentinel */ } };只要设备节点compatible数组中的任意一个字符串与驱动的of_match_table中任何一项的compatible完全一致就算匹配成功。这里的“完全一致”四个字是重点我吃过亏。我当时写的驱动里of_match_table里面填的是mycompany,my_sensor而设备树节点里写的是mycompany,my-sensor1就差一个数字“1”内核自然是一脸茫然。这种低级错误在调试期间很难一眼发现因为你在设备树和驱动里分别看都会觉得自己没写错。解决方案也简单就是仔细比对字符串的每一个字符甚至连大小写都不要放过。另外有个细节值得注意of_device_id里的.data字段可以用来携带自定义数据比如匹配到之后给驱动传递某种枚举值或配置参数。我在实际项目中就用它来区分同一系列传感器的不同型号匹配成功后直接从.data里取值作为初始化参数的一部分。类似这样static const struct of_device_id my_sensor_of_match[] { { .compatible mycompany,my-sensor-a, .data sensor_config_a }, { .compatible mycompany,my-sensor-b, .data sensor_config_b }, { /* sentinel */ } };在probe函数里通过of_device_get_match_data(pdev-dev)取回这个指针。这么做的好处是当你在同一个板子上需要兼容几个型号相近的传感器时不需要在probe里用一堆if-else去判断compatible代码会干净很多。还有一种情况是驱动里同时设置了of_match_table和driver.name但是compatible匹配成功之后你会发现name字段似乎没什么存在感。这是正常的因为匹配是“或”的逻辑只要一种方式成功配对就算成立。但是一旦probe执行了驱动就与设备绑定了后面driver.name更多是用于系统信息展示和绑定关系审计。4. 匹配成功后不等于万事大吉probe失败与绑定关系的那些坑匹配成功之后内核会调用驱动里的probe函数。此时整个项目可以说走过了最险的一关但离真正正常运行还差得远。probe里如果返回错误之前的匹配就白费了总线的状态会回滚。这里有个经典问题驱动probe返回-EPROBE_DEFER该怎么理解。字面意思是“probe推迟请稍后重试”。这个机制在依赖关系比较复杂的系统里极其重要i.MX6ULL上尤为常见。举个例子你的传感器驱动需要依赖GPIO中断控制器、I2C控制器和pinctrl子系统。如果这些资源对应驱动还没准备好probe函数在获取GPIO时会失败。如果简单地返回-ENODEV内核就认为这个设备不需要这个驱动可能永远不再尝试绑定。而返回-EPROBE_DEFER内核会把这个设备放到等待队列里后续只要有新的驱动注册到系统就会重新触发一次匹配和probe尝试。我在调试一个音频编解码器驱动时遇到过类似问题编解码器驱动的匹配对象本身很简单但它依赖的I2C控制器驱动稍微晚了一点注册结果probe获取不到I2C适配器。最初我直接返回-EIO导致驱动绑定失败。后来把返回值改成-EPROBE_DEFER系统在I2C控制器驱动注册完成后自动再次触发probe问题迎刃而解。所以如果你在日志里看到类似这样的输出platform 1a40000.i2c: Driver my_sensor requests probe deferral别慌这是内核在按部就班地处理依赖顺序等依赖就绪后会自己重试。再来看一个绑定关系的细节匹配成功probe执行成功驱动和设备就牢牢绑定了。此时你在/sys/bus/platform/下能看到两个对应目录/sys/bus/platform/devices/1a40000.i2c /sys/bus/platform/drivers/my_sensor前者是设备后者是驱动。它们之间会形成一种双向链接的关系。如果设备树里status disabled那么设备节点根本不会生成platform_device就算compatible写得再对也没用。相反如果status okay节点生成了设备而驱动没能匹配上那在设备目录下就不会出现driver链接。这个目录结构是排查驱动是否匹配成功的利器。我在实践中会这样子检查# 检查设备是否生成 ls -l /sys/bus/platform/devices/ # 检查设备是否绑定了驱动 ls -l /sys/bus/platform/devices/1a40000.i2c/driver # 检查驱动是否绑定了设备 ls -l /sys/bus/platform/drivers/my_sensor/假如/sys/bus/platform/devices/1a40000.i2c/下面没有driver这个软链接说明设备没有被任何驱动接管。再检查驱动目录是否存在如果驱动目录都不存在可能是驱动模块没有加载成功。这种从sysfs一层层倒推的排查方法比打印pr_info管用得多能直接定位到是设备没生成还是驱动没注册还是匹配没成功。5. 从0到1编写一个Platform驱动并用设备树验证的完整流程光说不练假把式。下面以i.MX6ULL平台上一颗虚拟传感器为例完整走一遍从驱动代码到设备树配置、再到实际验证的流程。这个例子的硬件资源假设很简化传感器挂在一根独立的GPIO中断引脚上需要读取一个寄存器来获得数据。实际上手时你可以把它替换成真实项目中的外设。驱动代码用了一个最简单的模块框架核心是probe函数。这里特别注意在设备树时代资源获取强烈建议使用device property接口因为这套接口对ACPI和设备树都不挑剔后续代码若迁移到别的平台也能复用。下面是驱动程序骨架我做了详细注释#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/interrupt.h #include linux/gpio/consumer.h #include linux/err.h static const struct of_device_id my_sensor_of_match[] { { .compatible mycompany,my-sensor, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static irqreturn_t my_sensor_isr(int irq, void *data) { /* 中断处理数据读取逻辑等 */ return IRQ_HANDLED; } static int my_sensor_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *irq_gpio; int irq; int ret; dev_info(dev, my_sensor probe enter\n); /* 推荐使用devm_gpiod_get获取GPIO描述符设备树中对应irq-gpios属性 */ irq_gpio devm_gpiod_get(dev, irq, GPIOD_IN); if (IS_ERR(irq_gpio)) { return PTR_ERR(irq_gpio); } irq gpiod_to_irq(irq_gpio); if (irq 0) { return irq; } ret devm_request_irq(dev, irq, my_sensor_isr, IRQF_TRIGGER_FALLING, my_sensor, NULL); if (ret) { return ret; } dev_info(dev, my_sensor probe success\n); return 0; } static int my_sensor_remove(struct platform_device *pdev) { /* 不需要手动释放devm管理的资源 */ return 0; } static struct platform_driver my_sensor_driver { .probe my_sensor_probe, .remove my_sensor_remove, .driver { .name my_sensor, .of_match_table my_sensor_of_match, }, }; module_platform_driver(my_sensor_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A demo platform sensor driver for i.MX6ULL);对应地在设备树里你需要添加一个节点这里以挂在某个GPIO组下为例实际引脚的pinctrl配置需要结合你的原理图my_sensor { compatible mycompany,my-sensor; pinctrl-names default; pinctrl-0 pinctrl_sensor_int; interrupt-gpios gpio1 18 GPIO_ACTIVE_LOW; status okay; };devm_gpiod_get在设备树中解析的就是interrupt-gpios这个属性注意它去掉-gpios后缀后变成irq所以gpiod_get的第二个参数是irq。这套命名规则也是新手经常弄错的点——设备树属性和GPIO API参数之间是存在对应关系的。如果设备树里写的是irq-gpios那么API参数就是irq如果设备树里写的是enable-gpiosAPI参数就是enable。规则是去掉“-gpios”后缀剩余部分作为API的con_id。编译并加载模块后可以做一些实际验证# 观察驱动是否注册成功 ls /sys/bus/platform/drivers/my_sensor/ # 观察设备是否生成设备树节点名就是设备名 ls /sys/bus/platform/devices/my_sensor/ # 检查绑定关系如果显示driver - ../../bus/platform/drivers/my_sensor 就说明绑定成功 ls -l /sys/bus/platform/devices/my_sensor/driver # 查看内核日志确认probe执行 dmesg | grep my_sensor正常流程中模块加载后你会看到probe的输出/sys/bus/platform/devices/my_sensor/driver软链接也会生成。如果probe没有执行优先检查compatible字符串和status状态。这里我想强调一下模块加载会触发总线上所有未绑定设备的匹配设备树节点如果一直没有被匹配而模块已加载那无非两种情况——设备树节点没生成设备或者compatible对不上。这两点检查清楚问题基本迎刃而解。6. 我踩过的真实坑匹配机制引发的三个经典调试案例光讲原理不够贴三个真实案例都是我在这块i.MX6ULL板卡上实际遇到的问题。这些案例能帮你把抽象机制与实际现象对上号。6.1 compatible写错导致probe毫无反应第一个案例最典型。当时给一个温湿度传感器sht30写驱动of_match_table里写的compatible是sensirion,sht30设备树节点里写的却是sensirion,sht3x。模块加载之后dmesg里静悄悄的什么输出都没有probe根本没进去。一度怀疑是不是设备树没生效甚至反复检查是否忘了编译设备树。后来把设备树的compatible字段用dtc工具反编译出来逐一比对才发现是自己手滑写错了一个字符。这个坑给到的教训是碰到probe不执行先别急着去翻代码逻辑第一步应该做字符串比对。尤其注意of_device_id数组最后要有哨兵项{ /* sentinel */ }我之前有一次就是忘了加导致内核遍历of_match_table时越界行为诡异。6.2 GPIO请求与中断触发电平不匹配导致probe卡死第二个案例是说probe其实进去了但是卡在了devm_gpiod_get返回值上。设备树里GPIO属性写的是GPIO_ACTIVE_HIGH代码里却用GPIOD_IN去请求。表面上看没什么问题但在链路里GPIO子系统会做物理电平与逻辑电平的转换。进一步排查发现中断触发方式也与实际硬件不匹配。中断引脚默认接了上拉硬件设计上触发方式是低电平或下降沿但设备树里却写成了上升沿触发。设备确实绑定了驱动中断也申请成功了就是触发不了。这种bug是硬件的“阴间”设计GPIO功能、上下拉、触发沿三者必须同时匹配。排查方法是把中断服务函数里加上计数器用按键或者示波器人为制造一次触发脉冲看handler是否执行如果不执行就去翻原理图重新确认硬件连接。6.3 status disabled导致的设备隐身第三个案例比较隐蔽。板子有两个功能完全一样的I2C控制器我在设备树中把某个外设节点挂在i2c2下但调试时发现设备始终无法匹配。看设备树文件节点明明存在的compatible也一模一样。折腾了挺久才发现是i2c2节点本身的status被设置成了disabled。这种写法在官方开发板设备树里非常常见——某些接口在出厂时未引出默认被禁用需要用户在使用时手动改为okay。如果你在设备树里看到某个外设节点或它所依附的父节点状态是disabled哪怕它的子节点内容再正确整个分支都不会产生platform_device。这就解释了为什么设备树明明有节点但/sys/bus/platform/devices下却看不到对应目录。修改方法就是在你的板级设备树文件一般以你的板卡名命名比如imx6ull-myboard.dts中将对应节点覆盖为i2c2 { status okay; };。6.4 驱动与设备的“身份错位”模块加载顺序的坑最后一个案例可以说刷新了我的认知。我那段时间把传感器驱动编译成内核模块并设置为开机自动加载。但驱动模块加载时设备树节点已经生成了platform_device理论上匹配就该发生。然而实际发现当模块加载时如果同一总线上有多个设备都使用同一个of_match_table内核会按顺序一次性尝试绑定所有未绑定的设备。这时候如果你的probe函数对设备类型判断不够严格可能不同设备就都跑进去了。最典型的场景两个设备节点都写成了同一个厂商前缀的compatible但一个功能是传感器另一个是执行器。驱动of_match_table为了兼容两者把它们都加了进去。结果probe函数没有区分当前到底匹配的是哪个设备也就是没有用of_device_get_match_data()去拿差异化配置导致执行器初始化时加载了一套传感器的配置设备行为完全混乱。解决方案就是在probe里通过of_device_get_match_data()获取匹配项携带的设备特定数据或者用platform_get_resource()取资源时根据节点名与寄存器地址进一步区分。不要总指望内核只挑一个设备来匹配要自己做好设备身份的判断。7. 在实际项目中用好Platform匹配机制的一些落地建议最后以现阶段我对这套机制的理解给你一些实际建议。这些建议不是从书本上抄来的是逐个坑踩出来的。设备树compatible的命名一定要遵守规范。厂商前缀加型号名称比如mycompany,my-sensor。不要学网上某些教程用sensor,my-sensor这种未注册前缀。前缀命名最好去内核文档Documentation/devicetree/bindings/vendor-prefixes.yaml里查一下如果有官方认可前缀就直接用避免后续做设备树检查时一堆warning。能用devm家族函数就绝不手动管理资源。devm_request_irq、devm_gpiod_get、devm_ioremap、devm_platform_ioremap_resource这些函数在probe失败或remove时都会自动释放资源。我早期用手动申请资源写过一个驱动probe到一半发现后续申请失败直接返回错误结果忘了释放前面申请的中断号和GPIO内核直接报资源泄漏。后来全部改成devm系列代码简洁可读性也提高了一个档次。resource的获取方式要统一。在设备树时代寄存器地址和中断号不要再用platform_get_resource(pdev, IORESOURCE_MEM, 0)或者platform_get_irq(pdev, 0)以外的奇技淫巧。这套API稳定且直观。如果想更省心可以直接用devm_platform_ioremap_resource(pdev, 0)它在内部已经帮你处理了resource和ioremap两步能少写不少代码。匹配成功后绑定关系会直接影响驱动的卸载顺序。如果你在一个驱动里同时匹配了多个设备比如一个i2c控制器和一个外设子设备那么remove函数的执行顺序和匹配顺序是相反的。这种栈式卸载顺序在多数情况下是安全的但如果你在remove里做了某些全局状态清理或者多个设备共享了一个全局变量那么顺序错乱就会带来问题。我的建议是尽量减少驱动里对全局共享状态的使用每个设备实例化状态最好都挂在struct device的私有数据结构中。关注内核版本带来的行为差异。老内核比如3.x和现代内核5.x/6.x在platform匹配上基本逻辑没有大变但细节上有些差别。例如设备的platform_match在早期版本里没有ACPI匹配这一步但5.x以后加入了。如果你在i.MX6ULL上用的是比较新的BSP内核某些行为可能与本篇源码路径略有差异这并不奇怪以实际内核源码为准即可。调试时善用sysfs和devicetree工具。配套的调试组合是/sys/bus/platform/下的设备与驱动目录、dmesg输出、设备树反编译工具dtc。把这套三件套用熟几乎所有匹配问题都能快速定位。如果遇到设备树节点不存在还可以用/proc/device-tree最终确认内核解析后的设备树内容这一步比从源文件盲猜要靠谱得多。写驱动这件事很多时候难的不是代码本身而是对机制的理解和对工具链的熟练度。Platform匹配机制是嵌入式Linux驱动开发里绕不开的一个环节在i.MX6ULL平台上几乎天天都要跟它打交道。希望通过这篇详细的拆解你能把这套机制的脉络彻底理清楚后续在调试时少走我在这些坑上走过的弯路。尤其当你面对自己第一块设备树配置无从下手时记得回到匹配函数里一个个环节去排除——这比盲目试错高效得多。