尧图精选

i.MX6ULL Platform总线驱动匹配机制详解:从设备树到probe

🕒 发布时间:2026/9/9 11:20:42 📁 来源:尧图网络
做嵌入式Linux驱动开发尤其是从单片机或者RTOS转过来的朋友上手第一个比较懵的概念往往不是字符设备、file_operations这些而是Platform总线。拿i.MX6ULL这颗NXP的Cortex-A7芯片来说你在内核里写一个GPIO驱动、一个I2C控制器驱动几乎绕不开platform_driver和platform_device这一套。我最早接触Platform机制时的困惑很直接我明明在设备树里把硬件描述清楚了驱动里也写了probe函数它俩是怎么“看对眼”的又是靠什么规则完成匹配的后来翻源码、做实验、调试设备树才算把这条线彻底捋顺。这篇文章就围绕i.MX6ULL把Platform设备与驱动匹配这件事从原理到实战拆开讲透适合正在学驱动开发、准备从字符设备进阶到设备模型的朋友。1. 为什么i.MX6ULL的驱动开发绕不开Platform机制1.1 从裸机写法到Linux驱动模型的思维跨越在STM32裸机开发里你要操作一个外设流程基本是“查参考手册→找到寄存器基地址→直接读写”。到了Linux下这套思路在逻辑上依然成立但绝对不能直接这么写。原因是Linux要管理的是整个系统的资源驱动不能想访问哪个地址就访问哪个地址想用哪个中断就用哪个中断必须先向内核“申请”资源。资源怎么描述、怎么申请、驱动怎么知道自己该操作哪一组寄存器这套规范就是Linux设备驱动模型要解决的事。Platform总线也就是platform_bus是这个模型里的核心虚拟总线。说它“虚拟”是因为它不对应任何真实存在的硬件总线不像I2C、SPI、USB那样有物理连接关系。它的作用是充当一个“中间人”把硬件设备信息和驱动代码撮合在一起。在i.MX6ULL平台上绝大多数SoC内部外设——GPIO控制器、UART、ECSPI、I2C、SDIO、LCD控制器等等——都挂在platform_bus下统一管理。1.2 驱动老写法暴露出的三个典型痛点回忆一下很多入门教材里写的“最简驱动”直接register_chrdev申请一个设备号然后用ioremap把某个物理地址映射进来。这种写法在小demo里跑得通但在一个真实项目里会暴露几个痛点第一设备信息硬编码在驱动里。平台上寄存器地址是多少、中断号是多少直接写在驱动代码里。以后换一颗寄存器地址不同的芯片或者同一颗芯片上由于硬件设计改动引脚复用和中断号变了就得改动驱动代码重新编译。第二资源冲突无从管理。如果两个驱动都想去操作同一个物理地址内核没有任何机制去阻止系统崩溃了都不知道是谁干的。第三驱动和设备生命周期无法解耦。Linux里设备可以热插拔驱动可以按需加载如果设备树里某个外设被禁用了驱动却依然去申请它对应的资源就会导致无谓的资源占用甚至启动失败。Platform机制解决的就是这类问题。设备信息由设备树或板级代码描述驱动只负责写“怎么操作”两者通过platform_bus完成匹配生命周期各自管理。1.3 i.MX6ULL平台上典型的Platform设备有哪些先看一组实际例子。i.MX6ULL芯片参考手册里列出的大量外设控制器在内核源码中都是以platform_device的形式存在。外设控制器设备树节点路径示例对应驱动GPIO控制器soc/gpio0209c000drivers/gpio/gpio-mxc.cUART串口soc/serial02020000drivers/tty/serial/imx.cECSPI控制器soc/spi02008000drivers/spi/spi-imx.cSDIO控制器soc/usdhc02190000drivers/mmc/host/sdhci-esdhc-imx.cLCD控制器soc/lcdif021c8000drivers/video/fbdev/mxsfb.c这些驱动不管内部逻辑差异多大入口函数里几乎都注册了一个platform_driverprobe函数里通过platform_get_resource之类的接口拿到寄存器地址、中断号等资源。这也是为什么说想学会i.MX6ULL驱动开发必须先搞懂Platform机制。2. 设备与驱动匹配的完整链路从device和driver说起2.1 设备模型的两个基本对象device与device_driverPlatform机制本质上构建在Linux设备模型之上。模型里有两个基本对象一个是struct device代表一个具体的设备另一个是struct device_driver代表一类驱动。platform_device和platform_driver分别是这两个结构的子类在include/linux/platform_device.h里定义。struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; ... };struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); const struct platform_device_id *id_table; struct device_driver driver; };需要特别注意的是platform_device里的name字段和id字段。一个平台设备可能是设备树节点转换来的这种情况name往往来自设备树节点的compatible属性的第一个值也可能是板级代码里platform_device_register注册的。id字段用于区分同一种设备存在多个实例的情况比如一个SoC有两个I2C控制器id分别是0和1。而platform_driver里真正起作用的是driver子结构里的name和of_match_table。driver.name用于和设备做简单的名字匹配of_match_table用于和设备树里的compatible属性匹配。2.2 核心匹配函数platform_match到底做了什么设备与驱动注册后内核会调用bus_type的match回调函数。对platform_bus来说这个回调就是platform_match定义在drivers/base/platform.c里。源码逻辑并不复杂我结合i.MX6ULL实际场景拆解一下匹配优先级。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);/* 第一优先级设备树compatible属性匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 第二优先级platform_driver id_table匹配 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 第三优先级driver.name与platform_device.name匹配 */ return (strcmp(pdev-name, drv-name) 0);}三种匹配方式不是同时进行的而是严格按照这个顺序。第一种在设备树成为主流后最常用i.MX6ULL官方BSP和主线内核里的驱动绝大多数都靠compatible匹配。第二种id_table是兼容旧平台和某些特殊场景的做法设备树匹配失败才轮得到它。第三种纯名字匹配是最早期的做法野火、正点原子的早期教程里那种在驱动里直接写.name xxx_gpio然后和设备名硬对的方式就是这一种。2.3 设备树节点是如何转成platform_device的这一步很多人容易搞混以为设备树被内核解析后驱动就能直接去读节点属性。实际上设备树里的节点要变成platform_device中间有一个“转换”过程。在i.MX6ULL开机启动流程里内核初始化时会扫描设备树中的所有节点只要节点包含compatible属性并且状态status不是disabled就会为它创建一个platform_device挂到platform_bus上。这个转换过程由内核的init/main.c里do_initcalls阶段触发的of_platform_default_populate_init完成最终调用of_platform_bus_create逐个创建设备。设备树里表示soc总线的根节点下的各个子节点就对应了i.MX6ULL片上外设的platform_device。这里有一个常见误区很多人看到设备树里某个外设节点下还有子节点就以为子节点也都是platform_device。其实不然。如果子节点对应的驱动是I2C client驱动或者SPI device驱动它就不是platform_device而是通过I2C/SPI总线机制注册到对应控制器下的普通设备。of_platform_create_device只处理带compatible属性且兼容“platform”的节点。3. 匹配成功的幕后推手probe函数的触发过程3.1 驱动加载时内核做了什么当你在驱动入口里写platform_driver_register(xxx_driver)内核不是简单地把这个驱动加到一个链表里就完事而是会立即触发一轮“找对象”流程。具体拆开说第一driver_register先把驱动加到platform_bus的驱动链表里。第二bus的for_each_dev逻辑会遍历所有已注册的platform_device逐个调用platform_match去匹配。第三一旦匹配成功紧接着调用device_reprobe或driver_probe_device最终驱动里的probe函数就执行了。反过来还有一种情况设备后注册。虽然多数platform_device在系统启动阶段就注册完了但有些设备依赖的模块比较晚才加载或者设备是动态创建的。这时platform_bus采用同样的逻辑每次有新的device注册进来也会主动去驱动链表里找有没有匹配的driver。匹配成功同样触发probe。这也是为什么你的驱动即使在模块加载完成后才modprobe只要设备树里的设备节点存在probe依然会被调用。3.2 一个贯穿始终的实例i.MX6ULL GPIO设备驱动的probe过程为方便说明假设设备树里有一个GPIO控制器节点gpio5: gpio020ac000 { compatible fsl,imx6ul-gpio, fsl,imx7d-gpio; reg 0x020ac000 0x4000; interrupts GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };驱动注册时platform_match会先去拿driver里的of_match_table和这个节点compatible属性比对发现fsl,imx6ul-gpio完全匹配就返回成功。随后内核调用驱动中对应的probe回调。在gpio-mxc.c驱动里probe大致做这么几件事用platform_get_resource从pdev里取reg资源用devm_ioremap_resource完成物理地址到虚拟地址的映射用platform_get_irq拿中断号最后调用gpiochip_add注册一个gpio_chip。这里要补充的是设备树里reg属性描述的物理地址正是由platform机制代为管理的。如果两个platform_device申请的寄存器区域有重叠内核会在ioremap阶段直接报错。这就是Platform机制相对于“裸写ioremap”的资源管理优势。3.3 probe返回值和资源释放的讲究很多新手在probe里习惯来一句return 0了事但在实际项目中probe失败的情况非常常见返回值非常重要。如果probe返回负数内核认为驱动与该设备匹配失败。这时设备会从“绑定”状态变为“可重试绑定”状态但不会无限重试除非触发unbind/bind事件或驱动重新加载。如果probe需要异步等待某些资源可以用EPROBE_DEFER内核会延后一段时间再次尝试probe。这个机制在多个驱动有依赖关系时极其关键比如I2C控制器驱动没准备好时依附于它的触摸屏驱动就可以返回EPROBE_DEFER等一等。资源释放方面现代内核普遍推荐使用devm_开头的资源管理API。比如devm_ioremap_resource、devm_clk_get、devm_gpiod_get这些API会在设备卸载或probe失败时自动清理资源大幅降低内存泄漏风险。在i.MX6ULL的驱动开发里probe里连续申请多个资源时只要有一个失败用devm管理的前面申请的资源都会被顺带释放不需要手动写一堆goto标签做错误处理。4. 设备树在匹配机制中的具体作用与配置要点4.1 compatible属性的匹配细节compatible是设备树匹配机制的重中之重。这个属性是一个字符串列表例如fsl,imx6ul-gpio, fsl,imx7d-gpio。它的语义是“本设备兼容这些描述规范”排在前面的优先级更高。驱动里的of_match_table定义通常是这种形式static const struct of_device_id imx_gpio_dt_ids[] { { .compatible fsl,imx6ul-gpio }, { .compatible fsl,imx7d-gpio }, { /* sentinel */ } };MODULE_DEVICE_TABLE(of, imx_gpio_dt_ids);platform_match时会把设备树节点里compatible的每一值和of_match_table里的compatible逐一比对只要有任意一个相等就视为匹配成功。这里有一个坑需要注意不要为了偷懒在of_match_table里随便乱填compatible然后设备树里也填一个自定义值。虽然你自己可以配出“匹配成功”的效果但这么做会导致驱动无法被系统里已有的、同样操作该外设的标准驱动匹配上后续UPstream内核升级或移植官方BSP时容易出问题。正确的做法是优先使用芯片原厂在设备树里定义的compatible比如fsl,imx6ul-xxx这种官方字符串。4.2 reg属性和resource的对应关系在设备树节点里reg描述地址和长度会被转换为platform_device中的resource数组。一个节点里可以有多段reg比如有些外设控制器的寄存器区是分开的一个reg用于控制寄存器另一个reg用于数据缓冲区。reg 0x020ac000 0x4000, 0x020b0000 0x4000;在驱动里就要用platform_get_resource(pdev, IORESOURCE_MEM, 0)和platform_get_resource(pdev, IORESOURCE_MEM, 1)分别获取。index参数对应reg在设备树里的顺序从0开始。这个对应关系不搞清楚就会出现驱动里拿到寄存器基地址但读写访问不了、或者分配到了错误的地址区域的诡异问题。排查这类问题时打开/sys/devices/platform目录下对应设备的resource文件里面会直接列出这个platform_device持有的所有资源和驱动里申请到的一对比就明白了。4.3 status属性对匹配结果的影响设备树里的status属性是匹配流程能否启动的那道门槛。如果status disabled那么这个设备节点在转换阶段就不会生成platform_device连匹配的机会都没有。我在i.MX6ULL上调试的时候就遇到过这么一次。客户说UART3驱动加载了但串口就是打不开。查设备里节点的status是okay再看驱动里的probe也执行了最后在dmesg里发现uart3节点被 bootloader 二次修改后status变成了disabled。因为bootloader可能根据硬件拨码开关修改设备树导致内核解析的时候直接把UART3跳过了。所以排查设备匹配不到的问题时第一件事就是确认设备树节点status状态以及在user space下用以下命令确认设备是否真的存在ls /sys/devices/platform/只要看到对应的设备目录说明platform_device已经生成。如果看不到说明设备树解析阶段就没过驱动再好也白搭。5. i.MX6ULL实测一次从零开始编写Platform驱动的全过程5.1 场景设定驱动一个简单的LED为了把匹配机制的整个流程串起来这里用一个实际可编译运行的最小示例说明。场景就设定为i.MX6ULL开发板上一个GPIO控制的LED灯GPIO挂在GPIO5控制器下引脚是IO00。设备树里先定义一个节点/ { led_test { compatible myvendor,led-test; pinctrl-names default; pinctrl-0 pinctrl_gpio_led; led-gpios gpio5 0 GPIO_ACTIVE_HIGH; status okay; }; };注意这个节点是根节点下的自定义叶子节点。只要compatible存在且status为okay它就会被创建成一个platform_device驱动则通过of_match_table来匹配。5.2 驱动侧完整代码与匹配方式选择下面是在模块里注册一个platform_driver的完整流程#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/of_gpio.hstatic struct gpio_desc *led_gpio;static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev;led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, failed to get led gpio\n); return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); dev_info(dev, led probe success, LED on\n); return 0;}static int led_remove(struct platform_device *pdev) { gpiod_set_value(led_gpio, 0); dev_info(pdev-dev, led removed\n); return 0; }static const struct of_device_id led_of_match[] { { .compatible myvendor,led-test }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match);static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name led_test, .of_match_table led_of_match, }, };module_platform_driver(led_driver);MODULE_LICENSE(GPL);driver.name这里写led_test但真正起作用的匹配规则是of_match_table里的compatible。driver.name在无设备树匹配时的作用以及通过sysfs手动bind/unbind时也会用到。这段代码在i.MX6ULL上编译成模块后insmod加载一般会看到“led probe success, LED on”的打印。如果你在开发板上执行modprobe没有任何反应多半是设备树节点和of_match_table里的compatible没对齐或者是节点里的status是disabled。5.3 如何验证匹配确实走的是哪条规则匹配成功后sysfs里会留下完整的“配对”记录这是验证匹配机制的绝佳入口。先查看设备侧ls -l /sys/bus/platform/devices/led_test如果设备树节点名生成的名字不是led_test就会是别的名字。但无论名字是什么这个符号链接最终会指向一个目录里面有一个driver符号链接。再查看驱动侧绑定了哪些设备ls -l /sys/bus/platform/drivers/led_test/这个目录下会出现一个代表已绑定设备的符号链接比如“led_test”指向对应的platform_device目录。假如走的是compatible匹配在/sys/bus/platform/devices/led_test/of_node下还能看到设备树节点的完整属性。如果走的是id_table匹配或者纯name匹配of_node就不一定存在。这套sysfs下的验证方法在i.MX6ULL开发板调试时非常实用比猜测匹配规则靠谱得多。6. 实践中最容易踩的五个坑与排查思路6.1 驱动加载成功但probe没被调用这个坑出现的频率最高原因集中在设备树节点没生成platform_device上。排查顺序建议是第一确认节点存在。执行ls /sys/bus/platform/devices/看有没有对应设备。第二确认status为okay。可以用cat /proc/device-tree/节点路径/status查看。第三确认节点里确实写了compatible并且值和驱动of_match_table一致。空compatible的节点不具备生成platform_device的条件。还有一种不算少见的情况你的设备树节点是某个父节点的子节点但父节点在解析时失败导致子节点也没能成功创建设备。6.2 probe执行两次或匹配到错误的驱动同一个外设节点被两个驱动匹配到probe就可能执行多次或者设备被不期望的驱动接管。导致这个问题的原因通常是of_match_table里compatible写得太宽泛把本不该由自己处理的设备也匹配上了。i.MX6ULL的GPIO驱动of_match_table里同时写了imx6ul和imx7d的compatible这是官方有意为之因为这两颗芯片的GPIO控制器寄存器布局完全兼容。但如果你在自己的驱动里也写一个宽泛的compatible就可能和别人抢设备。好的做法是驱动匹配的compatible尽量具体设备树里如果需要兼容多个平台把最精确的放最前面。6.3 同一设备树节点既有platform驱动又有I2C/SPI驱动挂在上面这个问题在设备树层级嵌套时特别迷惑人。节点本身是I2C控制器platform设备节点下面还有一个触摸屏子节点I2C client设备。你如果误以为触摸屏子节点也会走platform匹配就会在touch driver里写platform_driver却始终匹配不到。正确的认知是I2C控制器这个platform_device先probeprobe过程中注册了I2C adapter。随后I2C核心扫描总线上的子节点把触摸屏子节点创建为i2c_client再走I2C的匹配流程。这个匹配和platform的platform_match毫无关系。6.4 修改设备树后无法生效i.MX6ULL开发板修改设备树后往往需要重新编译dtb文件并放到boot分区。常见错误是修改了设备树源码但内核编译时用的还是旧的dtb于是怎么改都不生效。排查方法很直接启动后在/sys/firmware/devicetree/base/下查节点内容这就是内核实际使用的设备树。如果这里的值和期望不符说明烧进去的dtb没变而不是驱动的问题。6.5 用devm_系列API后还要不要手动释放资源这个问题在入门群里经常被反复问。我的答案很明确probe里用devm_函数申请的资源正常情况下不需要在remove里手动释放。compatible的内核API会在设备生命周期结束时依次释放这些资源。但要避免一种错误习惯一部分资源用devm_一部分不用然后remove里只手写了非devm的释放逻辑devm自己又处理了另外一部分这本身没错更常见的是有人担心没释放干净在remove里又调了一遍释放非devm的部分结果反而double free。实践原则就是全部使用devm_系列remove里只做关闭设备功能这种最后动作不需要操心资源回收。7. 进阶如何通过sysfs手动触发设备与驱动绑定除了自动匹配platform_bus也支持手动绑定和解绑。这在调试驱动或临时切换驱动时非常有用不需要重新编译内核模块和重启系统。手动解绑的用法echo led_test /sys/bus/platform/drivers/led_test/unbind这里的led_test是设备名字不是驱动名。解绑后驱动的remove会被调用设备退回“无驱动”状态。手动绑定的用法echo led_test /sys/bus/platform/drivers/led_test/bind触发绑定后内核会重新执行匹配如果匹配成功probe再次被调用。这个机制我在调试设备抢占或驱动热切换时用得很频繁比反复insmod/rmmod模块要灵活得多。如果驱动中包含了id_table还可以用/sys/bus/platform/drivers/.../new_id动态注册新的匹配项临时把某类设备硬绑定到这个驱动上。这个功能非常强大但也容易破坏设备管理的一致性生产环境中不建议使用调试时倒是可以一试。8. i.MX6ULL开发中的延伸思考从匹配机制到设备模型的理解看完以上内容我想强调的是Platform匹配机制并不是孤立的知识点。它背后是Linux设备模型中的一个通用逻辑任何设备和驱动只要挂在同一条bus上都要经过bus_type的match函数撮合。I2C有i2c_matchSPI有spi_matchPCI有pci_match只不过它们匹配依据的属性和优先级各有不同。一旦熟练掌握了platform_match的三种规则再去看I2C、SPI子系统的设备与驱动匹配代码会发现如出一辙。以I2C为例i2c_adapter是控制器i2c_client是从设备二者在i2c_bus上完成注册和匹配。匹配规则虽然涉及of_match_table和id_table但整体框架和platform几乎一样。所以我始终建议i.MX6ULL的入门开发者不要把时间全花在去背一个个具体驱动的寄存器操作上而要把平台设备模型的“匹配—绑定—probe”这条主线拉通。寄存器操作是“术”设备模型是“道”。道通了换一颗芯片、换一个平台驱动的框架还是一眼就能看明白。对我来说最有效的学习路径是先动手写一个简单的platform驱动用i.MX6ULL开发板验证匹配成功再花时间把platform_match源码逐行读懂最后通过sysfs手动操作bind/unbind加深印象。这套流程走完你对设备树、platform_device、platform_driver三者关系的理解才算真正落地后面再去啃具体外设驱动效率会高很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →