
1. 为什么Linux要搞出Platform这套机制接触i.MX6ULL嵌入式Linux开发的朋友多半都有过裸机开发的经验。裸机写驱动很简单寄存器手册翻到对应外设章节找到物理地址直接操作寄存器就行。但到了Linux下这一套就行不通了。原因有很多Linux要管多个进程不能让用户随便碰物理地址Linux的驱动模型希望把设备硬件信息、驱动逻辑、总线连接方式解耦再一个一个SoC内部有大量没有真正“总线”概念的设备比如GPIO控制器、UART串口、I2C控制器这些内部外设它们既不在PCI总线上也不在USB总线上。这里就引出了Platform机制的核心作用——Platform是一套虚拟总线。Linux设备模型里一个设备要和一个驱动配对必须有总线在中间牵线搭桥。USB设备挂USB总线PCI设备挂PCI总线那SoC内部这些“天生焊死在芯片里”的外设挂哪里总不能让它们无家可归。于是内核定义了platform_bus这条虚拟总线凡是直接挂在CPU地址总线上、没有标准热插拔总线的设备都归它管。i.MX6ULL上几乎所有的外设控制器本质上都是platform设备。这套机制解决了一个特别实际的问题把设备信息和驱动逻辑分开。设备树里描述“我有什么硬件、寄存器在哪、中断号是多少”驱动代码只写“我怎么操作这些硬件、怎么实现功能”。两者只要匹配上内核就自动把资源传给驱动的probe函数驱动拿到资源直接开工。硬件变了改设备树逻辑变了改驱动。互不干扰维护起来特别舒服。对新手来说我第一次在i.MX6ULL上写驱动时最大的困惑就是明明我注册了driver设备树里也有节点为什么probe就是不执行后来才明白我没搞清楚匹配机制的门道。这篇文章就把Platform设备与驱动的匹配机制掰开揉碎讲清楚包括匹配方式有哪些、优先级怎么算、probe函数什么时候被调用、匹配不上怎么排查。不管你是刚入门嵌入式驱动开发还是被设备树折磨过的老手这篇都能帮你把这块的知识拼图补完整。2. Platform设备与驱动的匹配机制拆解2.1 设备端是怎么“登记”的要理解匹配先得搞清楚两边各自长什么样。在i.MX6ULL这种现代嵌入式Linux开发环境下设备信息主要有两种登记方式。方式一是老式的platform_device结构体在代码里手动定义资源再用platform_device_register注册。这种方式在早期的ARM Linux驱动里很常见但现在基本被设备树取代了。方式二是通过设备树在.dts文件里描述硬件节点内核启动时解析设备树自动为每一个带有compatible属性的节点创建platform_device。这是当前的主流做法。以i.MX6ULL的一个GPIO按键为例设备树里会写类似这样的节点key { compatible gpio-keys; pinctrl-names default; pinctrl-0 pinctrl_key; status okay; };内核在启动阶段解析到这里发现compatible是gpio-keys就创建一个platform_device挂在虚拟的platform总线上。这个device身上带着各种资源寄存器地址比如reg属性、中断号interrupts属性、GPIO编号gpio属性等这些就是设备端的“身份信息”。在设备树之前或者某些特殊场景下老式写法长这样static struct resource key_resources[] { [0] { .start GPIO1_IO03, .end GPIO1_IO03, .flags IORESOURCE_IO, }, }; static struct platform_device key_device { .name gpio-key, .id -1, .num_resources ARRAY_SIZE(key_resources), .resource key_resources, }; platform_device_register(key_device);老式写法里没有设备树就用resource结构体手动描述设备的寄存器地址范围、中断号等信息。现在这种写法在面试题里还常见但实际项目中已经很少用了。你只要知道只要有platform_device出现在总线上无论是设备树生成还是代码注册的内核就会尝试为它找driver。2.2 驱动端是怎么“报名”的驱动端的结构是platform_driver驱动开发者的主要工作之一就是填充这个结构体。i.MX6ULL驱动开发入门时最常见的模板长这样static const struct of_device_id gpio_key_of_match[] { { .compatible gpio-keys }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, gpio_key_of_match); static struct platform_driver gpio_key_driver { .probe gpio_key_probe, .remove gpio_key_remove, .driver { .name gpio-key, .of_match_table gpio_key_of_match, }, }; module_platform_driver(gpio_key_driver);这个结构体最核心的成员是driver.name、id_table和of_match_table。这三个字段决定了driver能不能和设备配上对。probe和remove是回调函数匹配成功时内核调用probe驱动在这里完成硬件初始化设备移除或驱动卸载时调用remove做清理工作。module_platform_driver是个便捷宏展开以后就是module_init加上platform_driver_registermodule_exit加上platform_driver_unregister。新手阶段建议先用这个宏等理解了生命周期再手写init和exit也不迟。2.3 匹配的完整流程三种方式与优先级现在我来讲最关键的部分——platform总线是如何执行匹配的。内核里有个函数叫platform_match每次总线上新增设备或者新增驱动总线都会调用它检查是否有配对成功的。匹配顺序是固定的of_match_table匹配driver里通过of_match_table指定了compatible字符串列表内核拿设备树节点的compatible属性逐一比较。比如设备树里compatible gpio-keysdriver的of_match_table里也有gpio-keys那就算匹配上。这是现代设备树模式下最常用的匹配方式也是我最推荐的。id_table匹配如果of_match_table匹配失败或者驱动根本没填of_match_table内核会看driver的id_table。id_table是platform_device_id数组比较的是driver名字和设备name字段。不过这个方式主要用在老式platform_device场景设备树时代用得不多了。name匹配如果id_table也是空的最后内核尝试拿driver.driver.name和设备的名字做比较。同样是比较字符串优先级最低。简单来说匹配就是一个字符串比较的过程。设备树出生就带着compatible属性相当于给自己起了名字、贴了标签驱动这边也列了一份自己认识的标签清单。两边有一致就算“牵手成功”。提示同一个设备可能同时满足多种匹配方式但内核按上面顺序执行先到先得。实际开发中别混用选一种方式写清楚即可。2.4 匹配成功后发生了什么匹配成功不是结束只是开始。之后内核会调用driver的probe函数。probe是驱动开发者的主战场i.MX6ULL下几乎每一个驱动的主要逻辑都在probe里static int gpio_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(dev, failed to get resource\n); return -ENXIO; } /* 以i.MX6ULL的GPIO控制器为例 */ base devm_ioremap(dev, res-start, resource_size(res)); if (!base) { dev_err(dev, failed to ioremap\n); return -ENOMEM; } /* 申请中断、注册字符设备、创建类、创建设备节点 */ ... return 0; }这里你注意devm_ioremap是受设备管理的映射函数内核会跟踪这个ioremap设备注销时自动释放。这类带devm前缀的函数是驱动开发的“懒人福利”能省掉大量错误处理代码。同时值得注意的是probe执行时拿到的pdev参数就是匹配成功的那个platform_device。通过platform_get_resource、device_property_read_u32之类的辅助函数驱动可以拿到寄存器地址、中断号、DMA通道等硬件资源。这套机制的好处在于驱动代码不写死任何地址和中断号全部从设备树动态获取换了板子只需要改dtsdriver不用动。3. i.MX6ULL上从零写一个Platform驱动3.1 环境准备与开发板选型写这篇文章的前提是你手里有一块i.MX6ULL的开发板。市面上的核心板、底板方案有很多NXP官方EVK或者是国内主流开发板都可以。i.MX6ULL是Cortex-A7单核部分型号双核的处理器主频800MHz特点是性价比高、外设丰富非常适合做嵌入式Linux学习的平台。开发环境一般是Ubuntu虚拟机里装交叉编译工具链比如arm-linux-gnueabihf-gcc。烧录和启动用SD卡或者EMMC都行。内核版本建议4.1.15或者更新都无所谓Platform机制在2.6以后就基本定型了各版本差别不大。你只要保证内核源码能编译通过、能启动到文件系统就能跟着实操。3.2 设备树节点的编写思路以驱动一个LED灯为例这是i.MX6ULL驱动开发里最典型的入门项目。假设LED接在GPIO1_IO03上高电平点亮。设备树里新建一个led节点gpioled { compatible mcu-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpios gpio1 3 GPIO_ACTIVE_HIGH; status okay; };注意几个要点。compatible属性是自己定义的比如mcu-led但必须是唯一的不能跟内核已有的驱动冲突。led-gpios是自定义属性名名字可以随便起但GPIO的引用格式是固定的gpio1 3 GPIO_ACTIVE_HIGH表示GPIO1组的第3号引脚高电平有效。pinctrl-0引用了pinmux配置节点这个一定要在iomuxc节点下提前定义好否则引脚复用不对GPIO功能起不来。pinctrl子节点需要在iomuxc节点下补全一般长这样iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这个配置告诉内核把GPIO1_IO03这个引脚复用为GPIO功能同时配置上下拉和驱动能力。宏定义MX6UL_PAD_GPIO1_IO03__GPIO1_IO03在内核的imx6ul-pinfunc.h里已经定义好了直接用即可。0x10b0是pad控制寄存器值不同板子取值略有区别一般参考原厂BSP里的默认值就行。3.3 驱动代码的完整实现对应上面的设备树节点写一个完整的platform驱动。先定义of_match_tablestatic const struct of_device_id led_of_match[] { { .compatible mcu-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match);这段代码的作用是导出一张匹配表。MODULE_DEVICE_TABLE这个宏会生成段信息方便模块化加载时内核自动匹配。即使平时不写它也跑得起来但正规写驱动一定要带上。然后是probe函数实现LED的初始化static int led_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; struct device *dev pdev-dev; enum of_gpio_flags flags; int ret; led_desc.gpio_num of_get_named_gpio_flags(np, led-gpios, 0, flags); if (!gpio_is_valid(led_desc.gpio_num)) { dev_err(dev, failed to get gpio\n); return -EINVAL; } ret devm_gpio_request_one(dev, led_desc.gpio_num, GPIOF_OUT_INIT_LOW, mcu-led); if (ret 0) { dev_err(dev, failed to request gpio %d\n, led_desc.gpio_num); return ret; } led_desc.dev dev; return 0; }这里用了of_get_named_gpio_flags配合设备树获取GPIO号。如果你用的是新版本内核也可以用gpiod_get系列APIgpiod是更现代化的GPIO接口操作抽象更好。但从学习角度gpio_函数族在传统驱动代码里存量巨大看懂很有必要。devm_gpio_request_one的好处仍然是自动释放不用自己在remove里写gpio_free。接着是完整的平台驱动结构static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name mcu-led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL);probe里还可以加一些逻辑创建字符设备、注册miscdevice、创建/sys/class下的类等。LED比较轻量很多人直接用miscdevice省去主设备号分配和类创建的繁琐步骤。如果你要做的驱动比较复杂那还是走传统的cdev class device_create的流程。3.4 编译与验证的心得把驱动编译成模块可以用内核的module编译方式。假设源码放在drivers/led/目录下Makefile写一行obj-m led.o然后在内核源码根目录执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules编译出的led.ko拷贝到开发板文件系统里用insmod加载。如果一切正常dmesg里会看到probe被调用的日志。如果加载后没有任何反应先用cat /sys/kernel/debug/devices_debug或者查看/sys/bus/platform/devices目录确认设备有没有被正确枚举出来。验证驱动正常运行就看probe里有没有报错以及对应功能是否生效。LED驱动的话可以手动echo控制GPIO或者写个测试程序操作设备节点。如果设备节点已经创建比如/dev/mcu_led说明probe执行成功了。4. 匹配不上是常态排查与调试实战4.1 检查设备树解析是否正常i.MX6ULL驱动开发里匹配失败最常见的原因就是设备树没解析成功。排查第一步先确认设备节点有没有被内核创建出来。启动后进入文件系统执行ls /sys/bus/platform/devices/你会看到一堆设备名字比如1e40000.serial、20ac000.adc之类的。前缀的十六进制数字是寄存器地址后面的字符串是设备名。如果你写的dts节点被内核解析了这里会多出来一个对应项。设备树节点的名字一般不会直接显示compatible而是显示节点的name或者unit-name。如果你找不到自己定义的节点说明dts根本没编进去或者解析阶段出了问题。排查dts问题看编译是否成功。在u-boot或者内核启动日志里也能看到设备树相关提示。确认dtb已经烧录到启动分区后还可以在u-boot环境里打印设备树内容fdt print /gpioled这条命令能查看dtb里是否存在这个节点。如果节点存在再看compatible、status、reg这些属性有没有写错。statusdisabled是最坑的情况——节点解析了但设备不会生成匹配自然不会发生。4.2 驱动侧匹配表的细节坑如果设备节点存在驱动也加载了但还是不进probe问题多半出在匹配表上。第一个坑是compatible字符串不一致。设备树里写的是mcu-ledof_match_table里写的是mcu_led肉眼几乎看不出来但内核比较字符串严格区分字符和大小写对不上就匹配失败。我排过很多次这类问题解决办法就是仔细核对或者直接在驱动里打印of_match_table的内容。第二个坑是忘写MODULE_DEVICE_TABLE。如果你的驱动是模块加载方式这个宏不写某些情况下内核可能无法正确生成模块的alias信息导致自动加载失败。手动insmod一般没问题但用udev自动加载时就会出问题。第三个坑在of_match_table没有以空结构体结尾。设备树匹配表和id_table数组都必须有终止项。没有终止项内核遍历时就会越界轻则匹配不到重则内核panic。我见过新手在这里调了一整天最后发现是一个分号的问题。注意在驱动里调试匹配问题最快的方法是加打印。platform_match执行时会遍历所有可能你在probe入口加一条dev_info在remove里也加一条日志一打状态一目了然。如果还是不行还可以用在of_match_table中打印.name字段确认驱动确实遍历到了这个节点。4.3 模块加载顺序与依赖问题还有一个容易忽略的点在某些情况下平台驱动和设备虽然存在但加载顺序不对导致probe暂时没被调用。比如驱动依赖另一个驱动初始化的资源如果被依赖的驱动还没加载probe里的资源申请就会失败返回错误码。这时候平台总线不会自动重试probe除了依赖的驱动加载完成后手动触发重试很多时候我们只能重新insmod一次。还有一种情况驱动编成模块设备树里对应节点的compatible已经被另一个驱动匹配占用了。系统里可能存在两个可以匹配同一设备的驱动内核按驱动注册顺序、或者其他优先级选择另一个先注册的驱动优先拿到了probe机会。如果那个驱动probe失败资源被占用你的驱动也就没戏了。排掉这种问题要么确认加载顺序要么修改compatible字符串让设备只匹配你的驱动。4.4 使用devicetree的调试手段汇总我把日常排查匹配问题的手段整理成一个速查表方便你对照使用排查步骤命令/方法预期结果确认设备节点是否生成ls /sys/bus/platform/devices/能看到对应节点查看设备节点的compatible属性cat /sys/bus/platform/devices/xxx/of_node/compatible与驱动of_match_table一致确认设备是否处于可用状态cat /sys/bus/platform/devices/xxx/status应为okay查看驱动是否注册ls /sys/bus/platform/drivers/能看到驱动目录检查驱动与设备是否已绑定ls -l /sys/bus/platform/drivers/xxx/有设备名软链接则已绑定查看dmesg中的probe日志dmesg | grep -i led能看到probe相关输出在probe入口主动打印dev_info(pdev-dev, probe start\n)日志出现则说明probe已调用这套表我用了很多年绝大多数匹配问题都能快速定位。核心思路就一句话先确认设备存在再确认驱动注册最后核对匹配条件。按这个顺序排查不会像无头苍蝇一样乱撞。4.5 一个真实的bug复盘最后分享一个我实际踩过的坑。有次我在i.MX6ULL上写一个SPI设备的platform驱动设备树里节点写得好好的compatible也一致驱动insmod也不报错但probe就是不进。折腾了半天最后发现问题是这个SPI设备节点写在了spi总线的子节点下面而我又错误地给这个节点写了compatible导致它被注册成了platform设备而不是spi设备总线上根本没有这个设备的“家”自然匹配不上。正确做法是挂在SPI总线下的设备节点应该由spi驱动模型来匹配不应该走platform机制。这个问题的本质是设备挂在哪个总线上就要用哪个总线的驱动模型。挂在i2c总线下的用i2c_driver挂在spi总线下的用spi_driver只有直接挂在SoC内存地址空间上的内部外设才走platform。这件事给我的教训是写platform驱动前先想清楚这个设备到底属于哪条总线。platform不是万能的它只负责那些没有标准总线的设备。理解清楚这一点驱动开发的思路会清晰很多。5. 关于匹配机制的几个扩展思考5.1 新旧接口的取舍与兼容讲完Platform匹配机制我想顺便聊一聊驱动开发中常见的接口混杂问题。这几年内核社区一直在推进设备驱动模型往更抽象的方向演进比如GPIO接口从gpio_request/gpio_set_value往gpiod_get/gpiod_set_value迁移时钟接口也有类似趋势。i.MX6ULL上的老BSP代码很多还在用老接口网上教程也多是老代码。新手跟着老教程学能跑通但总觉得别扭直接上新的gpiod接口又发现老内核版本支持不完整。我的建议是学习阶段两种都看写代码优先用新接口。新接口的好处不仅仅是API更规范更重要的是它对设备树的支持更自然比如gpiod_get(dev, led, GPIOD_OUT_LOW)直接用dev就可以拿到GPIO描述符不需要手动处理GPIO号。如果板子换了个引脚dts改一下就行驱动代码一行不用动。这跟你用of_get_named_gpio_flags拿号再手动request是一个效果但代码量少一半出错面小了。5.2 驱动编译为模块与编入内核的区别还有一个影响匹配行为的因素需要提一嘴驱动是以模块方式加载还是编入内核。编入内核的驱动在设备树解析后、device创建时就会参与匹配也就是说同步匹配设备一存在就尝试匹配而模块方式加载的驱动加载完成后才会去扫描总线上的既有设备属于异步匹配。在i.MX6ULL的实际开发中我一般前期都用模块方式修改驱动后只要重新编译ko拷贝到板子insmod就行不用重新打包内核镜像调试速度快很多。等驱动稳定了再考虑编入内核省去启动后手动加载模块这一步也让系统更接近产品形态。这个流程对匹配机制本身没有影响但调试效率差别巨大值得养成习惯。5.3 从匹配机制延伸到整个驱动模型把Platform匹配机制搞明白之后你会发现Linux整个设备驱动模型就是一套“注册-匹配-回调”的框架。I2C驱动、SPI驱动、USB驱动、PCI驱动套路都是一样的设备侧注册device驱动侧注册driver总线上有match函数匹配成功后调probe。理解了这套抽象你再去看那些体量很大的内核驱动比如网卡驱动、触摸屏驱动、显示控制器驱动会发现虽然probe函数里上千行代码眼花缭乱但骨架永远是取资源、初始化硬件、注册子系统、创建设备文件。匹配机制只是大门推开之后里面的房间才是真正干活的地方。而platform这套机制就是Linux驱动开发入门的第一把钥匙。我当年在i.MX6ULL上调试第一个platform驱动时也是从匹配不上开始的那时候不懂设备树、不懂of_match_table、甚至连/sys/bus/platform目录都不知道去看一眼。后来踩过的坑多了把匹配流程的几个细节搞透了之后换到其它SoC平台、换到其它总线模型都能很快上手。如果你也在驱动开发刚开始的阶段被probe不调用折磨希望这篇文章能帮你少走几个弯路。