i.MX6ULL Linux驱动:Platform总线与设备树匹配机制详解

发布时间:2026/9/7 2:35:25
i.MX6ULL Linux驱动:Platform总线与设备树匹配机制详解 网上聊i.MX6ULL Linux驱动开发的帖子不少但大多一上来就甩代码很少有人把Platform总线这个地基讲透。我自己带过几批做嵌入式的新人发现一个特别典型的现象驱动insmod成功dmesg也没有任何报错但probe函数就是不执行设备节点明明也能在/sys下看到就是死活绑定不上。折腾半天最后发现是对Platform设备与驱动匹配机制的理解出了问题。这篇文章就围绕i.MX6ULL上的Platform机制把设备树、platform_driver、platform_device三者到底怎么对上号这件事讲明白适合刚接触设备树驱动开发的初学者也适合那些调试时经常卡在probe不触发的人。1. 为什么嵌入式驱动离不开Platform总线1.1 从“驱动裸写硬件”到“设备与驱动分家”早期Linux驱动开发有一种很原始的做法在驱动的init函数里直接把寄存器物理地址、中断号、GPIO编号全部硬编码进去。比如我要操作某个外设就在代码里写#define MY_DEV_BASE 0x020C4000 #define MY_IRQ_NUM 32然后ioremap、request_irq一条龙。这种代码在特定板子上能跑但一旦换了板子或者同型号芯片在不同开发板上引脚变了就得回头改驱动源码重新编译。更麻烦的是如果系统里有两颗相同的外设IP但硬件配置不同你几乎没法用一份驱动代码同时管理。Platform机制就是为解决这个问题诞生的。它的核心思想是“设备”和“驱动”分离驱动只描述“我会操作这种硬件”设备只描述“我这里有哪些硬件资源”。两者都注册到系统里由内核去匹配匹配成功就调用驱动的probe把设备资源交给驱动使用。驱动不需要在代码里写死任何物理地址它只需要在probe时从设备结构体里把资源取出来。这个设计对你写驱动的直接影响是你写的platform_driver代码在i.MX6ULL上能用换一块i.MX8MM的板子只要设备树描述得对驱动几乎不用改。这就是解耦的价值。1.2 设备、驱动、总线三者的关系Linux设备模型里有三个核心对象device设备、device_driver驱动、bus总线。可以这么理解设备是插座板上的插座孔驱动是插头总线是那块插座板。设备说“我有这些引脚和功能”驱动说“我能驱动这类插座”而总线的职责就是当任何一个插座孔或插头出现时立刻检查能不能插上。对应到Platform机制platform_device是设备platform_driver是驱动platform_bus_type是它们共同挂载的总线。在i.MX6ULL上系统启动过程中设备树里描述的外设节点会被解析转换成platform_device注册到platform总线而我们的驱动通过platform_driver_register也注册到这条总线上。内核的匹配时机很聪明设备先来也好驱动先来也好只要双方碰面总线就会调用match函数判断是否匹配。bus_type结构体里的match回调就是整个匹配机制的灵魂它对platform总线来说就是platform_match函数。这个函数内部做了什么决定了你的probe能不能被调用。1.3 i.MX6ULL上Platform机制的现实意义i.MX6ULL这颗芯片是NXP的Cortex-A7单核处理器在工业控制、物联网网关、入门级Linux开发板正点原子、野火等上用得非常多。它内部集成了大量外设控制器GPIO、UART、I2C、SPI、ECSPI、LCD控制器、以太网MAC等等。这些外设控制器地址都是固定的内存映射地址不依赖PCIe或USB这类可枚举热插拔总线所以它们天然就是Platform设备。在实际的i.MX6ULL开发中基本流程是先在设备树里描述硬件资源和引脚复用然后写一个platform_driver通过of_match_table里的compatible字符串与设备树节点匹配。匹配成功后内核自动调用probe你在probe里拿资源、注册字符设备、申请中断。如果这个流程没走通后面的一切都无从谈起这就凸显了理解匹配机制的重要性。2. 一次完整的Platform驱动注册以i.MX6ULL点灯为例2.1 设备树侧先把硬件描述清楚以i.MX6ULL开发板上最常见的LED为例。假定LED接在GPIO1_IO03上低电平点亮LED阳极接3.3V阴极经电阻接到GPIO引脚。设备树里需要做两件事配置引脚复用和添加LED设备节点。引脚复用一般在iomuxc节点里添加pinctrl子节点iomuxc { pinctrl_gpioled: gpioledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };然后添加LED设备节点。在imx6ull-14x14-evk.dts这类板级dts的根节点下添加gpioled { compatible fsl,imx6ull-led; pinctrl-names default; pinctrl-0 pinctrl_gpioled; led-gpios gpio1 3 GPIO_ACTIVE_LOW; status okay; };这里最关键的就是compatible属性。设备树节点的compatible相当于给内核看的一张“身份证”驱动端of_match_table里的compatible必须和它完全一致才能匹配上。后面我会专门讲这个字符串有多容易出错。另一个值得注意的点是gpio1在哪里来的。i.MX6ULL的GPIO1控制器本身在imx6ull.dtsi里已经定义好了gpio1: gpio0209c000 { compatible fsl,imx6ull-gpio, fsl,imx6q-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };也就是说我们自定义的LED节点引用了gpio1这个已存在的GPIO控制器节点通过gpio1让设备树知道GPIO控制器在哪里。这里体现的是设备树的分层描述能力也解释了为什么我们自己的LED节点不需要写GPIO控制器的物理地址。2.2 驱动侧of_match_table与probe的准备设备树描述完硬件接下来写驱动。一个标准的最小platform驱动模板长这样#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/gpio/consumer.h static const struct of_device_id led_of_match[] { { .compatible fsl,imx6ull-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static int led_probe(struct platform_device *pdev) { struct gpio_desc *led_gpio; led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio); pr_info(led probe success\n); return 0; } static int led_remove(struct platform_device *pdev) { pr_info(led remove\n); return 0; } static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name gpioled, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name);两个地方要特别解释一下。第一个是of_match_table它指向of_device_id数组数组里每个元素定义一种兼容型号最后必须以空结构体作为哨兵结束。内核遍历这个数组时就是靠这个哨兵来判断边界的少了它会出问题。第二个是MODULE_DEVICE_TABLE宏它会把of_device_id数组导出到模块的别名信息里当设备树中出现对应compatible的节点时udev/mdev可以根据modalias自动加载驱动模块省去手动insmod。module_platform_driver宏本质上是module_init(xxx_init); module_exit(xxx_exit);其中xxx_init调用platform_driver_registerxxx_exit调用platform_driver_unregister。它帮我们省掉了一堆样板代码这是内核推荐的标准写法。2.3 加载测试怎么确认匹配真的发生了交叉编译后把.ko拷贝到板子上insmod加载# insmod gpioled.ko # dmesg | tail [ 1234.567890] led probe success看到这行打印说明匹配和probe都成功了。如果没看到先在/sys/bus/platform/下检查设备与驱动各自的注册情况# ls /sys/bus/platform/devices/ gpioled ... # ls /sys/bus/platform/drivers/gpioled/ bind gpioled module uevent unbind注意drivers/gpioled/目录下有个名字叫gpioled的子目录或链接说明设备已经绑定到了驱动。如果驱动注册了但没绑定设备这里就看不到那个设备条目。这个现象在排查问题时非常关键下面第四章会详细展开。3. 匹配链路的源码级拆解从platform_match谈起3.1 platform_match的五种匹配途径理解匹配机制必须直接看内核源码。在Linux 4.1.15NXP官方BSP和大部分i.MX6ULL开发板内核版本里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); /* 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(pdrv-id_table, pdev) ! NULL; /* fall-back to driver name match */ return (strcmp(pdev-name, drv-name) 0); }匹配是有严格优先级顺序的按下面的顺序依次尝试命中即返回成功匹配方式触发条件适用场景driver_override设备节点设置了driver_override属性用户态强制指定驱动绑定OF匹配设备树节点存在且compatible/type/name命中设备树环境嵌入式主流ACPI匹配ACPI表存在x86/ARM服务器场景ARM板级设备树基本走不到id_table匹配platform_driver设置了id_table传统板级文件/多设备名适配name字符串比较以上均失败时的兜底老代码、简单设备有个细节值得注意id_table匹配和name兜底的顺序。只有在驱动没有设置id_table时才会走到name字符串比较。也就是说如果你的driver.name和设备的pdev-name完全一致但你在driver里设置了id_table且里面没有匹配项那么probe照样不会被调用。这个坑比较隐蔽。3.2 设备树compatible匹配的具体流程OF匹配最终调用的是of_match_device它会遍历设备节点的compatible属性列表对每一个compatible字符串再遍历驱动的of_device_id数组逐个比较。比较的内容包括三个维度of_device_id结构体如下struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };compatible匹配dts节点compatible属性里任意一个字符串等于of_device_id里任意一个compatible即命中。name匹配dts节点的name字段等于of_device_id.name。老式设备树用得多现代dts基本上不靠这个。type匹配dts节点的device_type属性等于of_device_id.type。这个更古老现在几乎不关注。当前i.MX6ULL的设备树开发中基本只靠compatible一条路径。设备树里可以写多个compatible值比如compatible fsl,imx6ull-gpio, fsl,imx6q-gpio;内核依次尝试只要of_match_table里命中任意一个就匹配成功。所以驱动端可以只写一个兼容值设备树端可以列一串这设计的好处是同一驱动可以向后兼容多个芯片型号。还有一个经验compatible字符串里的厂商前缀不要乱省略比如“fsl,imx6ull-led”中的fsl代表NXP飞思卡尔时期遗留的前缀。虽然内核匹配时只是纯字符串比较并没有强制校验前缀但遵守这个命名规范能避免不同厂商的兼容值冲突也方便阅读者一眼看出设备来源。3.3 设备树之外的id_table和name匹配老式非设备树环境下platform_device通常在内核启动时通过板级文件静态注册例如static struct platform_device gpio_led_device { .name gpioled, .id -1, .dev { .platform_data led_pdata, }, };此时没有of_node匹配只能走id_table或name兜底。platform_device_id结构体定义是这样的struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };多个设备名共用一个驱动时用id_table比较方便static const struct platform_device_id led_id_table[] { { gpioled, 0 }, { panel-led, 0 }, { } }; MODULE_DEVICE_TABLE(platform, led_id_table);name兜底则是直接比较pdev-name和drv-name。这里有个和“设备树场景”强相关的坑设备树下platform_device的name字段取自设备树节点的name也就是去掉地址后缀的节点名。比如设备树节点叫gpioled那么pdev-name就是“gpioled”如果节点叫gpioled20a0000那么pdev-name仍然是“gpioled”。所以如果你打算用name兜底来匹配.driver.name要填节点名而不是compatible字符串。曾经见过有人把driver.name写成“fsl,imx6ull-led”然后把of_match_table删掉怎么都匹配不上折腾半天发现pdev-name其实是“gpioled”确实容易懵。4. 实际开发中匹配失败的排查思路4.1 先确认设备与驱动是否“同在总线上”平台驱动不工作第一步不是改代码而是确认设备和驱动的“在场状态”。我自己的调试习惯是依次做这几件事第一确认设备树节点是否被解析成了platform_device# ls /sys/bus/platform/devices/ | grep gpioled gpioled如果这一步就找不到设备说明设备树节点根本没有被实例化常见原因是节点status为disabled或者dts没编译/没生效。第二确认驱动是否注册# ls /sys/bus/platform/drivers/gpioled/第三确认是否绑定。在上面的drivers/gpioled目录下如果出现了设备的软链接比如gpioled - ../../devices/platform/gpioled说明匹配成功且probe已走通。如果没有这个链接说明设备在总线上、驱动也在总线上但匹配环节出了问题。第四直接看设备树解析出来的实际值# cat /sys/bus/platform/devices/gpioled/of_node/compatible fsl,imx6ull-led这个命令能看到内核实际解析出来的compatible列表拿它跟驱动源码里的of_match_table逐字符对比往往一眼就能发现问题。4.2 从设备树到匹配成功的常见坑结合我见到过的案例匹配失败的原因集中在以下几类compatible字符串不一致。这是最高频的问题。dts里写成“fsl,imx6ull-led”驱动里写“fsl,imx6ul-led”少一个x或者dts里字符串末尾带了空格或者编辑器自动把逗号改成了全角逗号。字符串比较是精确匹配一个字符不对就是不行。修改dts后没重新编译或没更新dtb。很多人改了dtsmake完内核忘记单独编译dtb或者编译了dtb但烧录时用的还是旧分区里的文件。可以通过cat /proc/device-tree/gpioled/compatible检查板子上实际生效的值。节点status为disabled。设备树里如果写了status disabled这个节点默认不会被实例化成platform_device。有些SoC的dtsi里默认把外设节点设为disabled板级dts里才改为okay容易忽略。依赖的父设备没起来。如果在probe里获取GPIO、时钟等资源时失败并且返回的不是-EPROBE_DEFER那么这次probe就算失败了。资源依赖问题非常隐蔽后面5.2节细讲。模块没加载或加载失败。检查modprobe有没有因为依赖问题而静默失败dmesg里有没有模块加载的报错。4.3 一个典型的compatible踩坑复盘讲一个我实际带人时遇到的案例。驱动是自己的LED模块交叉编译、insmod都正常dmesg没有任何输出probe就是没跑。当时的排查过程是这样的先看设备# ls /sys/bus/platform/devices/ gpioled设备在。再看驱动# ls /sys/bus/platform/drivers/ gpioled驱动也在。但drivers/gpioled/目录下没有设备链接说明没绑定。接着cat设备树解析出来的compatible# cat /sys/bus/platform/devices/gpioled/of_node/compatible fsl,imx6ull-led然后打开驱动源码of_match_table里写的是static const struct of_device_id led_of_match[] { { .compatible fsl,imx6ul-led }, { } };发现问题了“imx6ull”和“imx6ul”少了一个字母。这种问题之所以难排查是因为Linux在设备树匹配失败时不会打印任何错误信息整个过程安安静静看起来就像驱动代码本身有问题。修正compatible字符串后重新编译加载dmesg立刻出现probe打印。这个案例给的建议是不要凭肉眼对比dmesg和代码直接借助sysfs导出的设备树属性和调用of_match_table来对照效率高得多。也可以在probe里临时加一行pr_info确认它到底有没有被进入再加一个module_init里的打印确认驱动注册本身是成功的这样能快速二分定位问题所在的环节。5. probe之后的资源获取与生命周期5.1 在probe里拿寄存器、中断、GPIO匹配成功后probe被调用这时才真正开始“取资源”。设备树里怎么描述这里就怎么取。三种最常见的资源对应三种API内存映射寄存器设备树里用reg描述驱动里用platform_get_resource加devm_ioremapstruct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; base devm_ioremap(pdev-dev, res-start, resource_size(res)); if (IS_ERR(base)) return PTR_ERR(base);中断设备树里用interrupts描述int irq; irq platform_get_irq(pdev, 0); if (irq 0) return irq; devm_request_irq(pdev-dev, irq, my_isr, 0, my_device, dev);GPIO设备树里用*-gpios属性描述驱动里用devm_gpiod_getstruct gpio_desc *led_gpio; led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);注意devm_gpiod_get的第二个参数是“led”它会自动查找设备树里的led-gpios属性非常方便。设备树里写的led-gpios gpio1 3 GPIO_ACTIVE_LOW驱动里拿到的gpio_desc就已经包含了GPIO控制器、引脚号和有效电平信息。GPIO_ACTIVE_LOW这个标志的意义是调用gpiod_set_value(desc, 1)时内核会在底层自动翻转电平输出低电平点亮LED驱动逻辑不需要关心硬件电平极性这是新式GPIO子系统的优点。5.2 remove与错误处理的正确姿势probe要负责申请资源remove就要负责释放。但大量使用devm_*系列API之后remove里几乎什么都不用做因为devmdevice managed机制会在设备unbind时自动释放资源和打印对应信息。我的习惯是remove里只做用户态可见的清理比如注销字符设备、删除设备节点、关闭电源等。还有一个必须掌握的返回值-EPROBE_DEFER。当probe因为依赖的设备比如GPIO控制器、时钟控制器、复位控制器还没注册而失败时正确做法不是直接返回错误码而是返回-EPROBE_DEFER。内核拿到这个特定返回值会把这个设备放到等待队列等后续有驱动注册时再次尝试probe。比如在i.MX6ULL上如果GPIO控制器驱动没加载完成devm_gpiod_get会返回-EPROBE_DEFER你直接用return PTR_ERR(led_gpio)就会导致设备probe失败且不会重试最后表现为设备一直没法使用。正确的写法是led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (PTR_ERR(led_gpio) -EPROBE_DEFER) return -EPROBE_DEFER; if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);这就是“设备有依赖时告诉内核晚点再试我”的沟通方式。5.3 关于“到底要不要自己写platform驱动”的建议最后说点个人经验。很多初学者会问我写一个纯字符设备驱动没有硬件资源是不是也要套platform我的建议是在i.MX6ULL这种设备树平台上凡是跟实际硬件引脚相关的驱动都按“设备树platform_driver”的套路来但纯软件逻辑模块比如一个内核线程、一个sysfs接口控制某个算法开关没有对应硬件外设那直接用module_init注册就好没必要硬套platform。判断标准很简单你的驱动有没有自己的设备树节点有没有需要从设备树解析的资源如果有就选platform_driver如果没有常规模块即可。设备树里定义的每个外设子节点在系统初始化时都会经过of_platform_populate流程被实例化而根节点下那些compatible命名的独立节点最终也会被注册为platform_device。所以大部分实际外设驱动在i.MX6ULL里天然就应该走platform。还有个小提示不要混淆platform总线和I2C、SPI总线。设备树里挂在i2c1节点下的子设备是i2c_client挂在spi总线下的子设备是spi_device它们不归platform总线管。只有那些直接挂在SoC内存映射空间上的外设控制器或者根节点下独立描述的设备才走platform机制。这一点在阅读dts时务必分清楚否则你会用platform_driver去匹配一个i2c设备节点必然失败。回到匹配机制本身我在实际开发中最大的体会是Linux的驱动模型是一个“先连接、后通信”的框架。设备和驱动通过总线建立连接是第一步probe是连接成功后的第一个回调。连接不成功后面的一切都是空谈。调试platform驱动时先确认sysfs下设备和驱动是否绑定远比反复检查probe内部代码要快的多。这个习惯帮我节省了大量时间也希望对你有所帮助。