
做嵌入式Linux驱动开发的朋友应该都有过类似的经历设备树节点写了一大堆驱动代码也照着模板填好了insmod进去结果probe函数就是纹丝不动。我最早在 i.MX6ULL 上调一个 LED 驱动时就卡过这个坑查了半天才发现compatible字符串两边没对齐。后来把内核里platform_match的实现从头到尾读了一遍才真正弄明白这套匹配机制的优先级和每个细节的设计意图。今天借 i.MX6ULL 这个平台把 Platform 设备与驱动匹配机制从原理到实操完整拆一遍顺便把我踩过的坑和排查套路一起整理出来希望对正在搞驱动开发的朋友有帮助。1. Platform 总线机制解析1.1 为什么是 Platform 而不是传统的物理总线i.MX6ULL 这颗芯片基于 ARM Cortex-A7 内核片内外设非常丰富GPIO、UART、I2C、SPI、CAN、LCD 控制器、eMMC 控制器等等一应俱全。这些外设从芯片内部看都挂在 SoC 内部的 AMBA 总线上和我们在 PC 上熟悉的 PCI、USB 这种“外插式”物理总线完全是两回事。在 Linux 设备模型里像 I2C 设备、SPI 设备、USB 设备都有各自对应的物理总线来挂载设备枚举、驱动匹配都通过真实的链路完成。但 i.MX6ULL 内部的这些外设并没有一条可供 Linux 直接枚举的物理总线它们出厂就焊在芯片里地址固定中断固定不热插拔不动态枚举。这就引出了 Platform 总线平台总线存在的核心价值它不是一条真实的物理总线而是内核为了统一管理这些“挂不到任何物理总线上”的设备专门定义的一条虚拟总线。所有在芯片内部直接集成的外设、内存映射的设备以及使用设备树描述的外部设备统一注册到这条虚拟总线上让设备和驱动可以通过一套标准机制完成匹配。你可以把 Platform 总线理解成物业公司的“业主名册”。芯片内部的外设就像小区的住户每个住户都有固定的房号寄存器地址和联系方式中断号。物业不需要实时监控哪个住户搬走了、哪个搬进来了而是拿着登记表手动配对。Platform 总线的match函数就是那张登记表负责把“设备”和“驱动”对应起来。1.2 设备、驱动、总线三者的关系Linux 设备模型里最核心的三个对象是struct device、struct device_driver和struct bus_type。Platform 机制其实是这套统一模型的一个具体实现实例。在 Platform 语境下三者分别落到了struct platform_device、struct platform_driver和全局唯一的platform_bus_type。其中platform_bus_type在内核的drivers/base/platform.c中定义关键字段如下struct bus_type platform_bus_type { .name platform, .dev_groups platform_dev_groups, .match platform_match, .uevent platform_uevent, .dma_configure platform_dma_configure, };match字段指向platform_match函数这是整个匹配机制的核心入口。每当内核向 Platform 总线注册一个设备device_add或者注册一个驱动driver_register总线都会调用match函数拿着当前要注册的对象去和另一侧的“所有已注册对象”逐一比对直到找到配对或者遍历结束。这里需要理解一个关键逻辑设备先注册还是驱动先注册顺序其实无所谓。内核会先注册的设备保存在总线的设备链表里驱动注册时遍历设备链表找配对反过来如果驱动先注册设备注册时也会遍历驱动链表找配对。platform_bus_type在系统初始化时就已经存在它就像一个公用的“相亲角”谁来了都先到名册上报个到再由match函数负责牵线。2. 设备与驱动匹配的核心原理2.1 platform_match 函数到底做了什么理解了总线的角色接下来看最核心的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. 尝试设备树Device Tree匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 尝试 ACPI 匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试 id_table 匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 4. 尝试 name 匹配 */ return (strcmp(pdev-name, drv-name) 0); }这个函数的执行顺序不是随便排的它反映了内核在不同硬件平台上的适配策略。开发 i.MX6ULL 驱动时我们主要关注第 1 步和第 4 步因为 i.MX6ULL 的 Linux 系统普遍使用设备树来描述硬件资源不涉及 ACPI而id_table更多用于传统板级文件时代或者某些特殊场景。2.2 四种匹配方式逐个拆解先看设备树匹配。of_driver_match_device内部最终会调用of_match_node它要做的事情很纯粹遍历驱动中of_match_table数组里的每一个compatible字符串和设备树节点上compatible属性中的字符串逐一比对。只要发现其中一个字符串完全一致就认为匹配成功。设备树节点上compatible属性的写法一般是这样的myled { compatible mycompany,myled; reg 0x020c406c 0x4; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; };驱动中的of_match_table则定义如下static const struct of_device_id myled_of_match[] { { .compatible mycompany,myled }, { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, myled_of_match);MODULE_DEVICE_TABLE这个宏很重要它会把of_match_table的信息导出到模块文件中。modprobe 加载模块时系统可以据此自动匹配设备并加载对应驱动而不需要手动 insmod。对于模块化编译的驱动漏掉这个宏会导致自动加载失效。ACPI 匹配在 i.MX6ULL 上基本用不到它主要面向 x86 平台。id_table匹配是传统板级文件时代的产物那时没有设备树设备通过platform_device_register在板级文件里注册驱动通过platform_device_id表声明自己支持哪些设备名。匹配到之后platform_match_id会把匹配到的id存入pdev-id_entry这在某些旧驱动的probe里还会被读取。i.MX6ULL 这类新平台直接使用设备树id_table可以留空。最后一种name匹配是最朴素的方式直接比较pdev-name和drv-name两个字符串是否相等。没有设备树、没有 ACPI、也没有id_table时这会作为兜底方案。编写平台驱动时如果driver.name和设备树节点不通过compatible匹配只靠name往往无效——因为设备树注册出的platform_device的name字段通常取自设备树节点名而不是compatible这个细节很容易踩坑。2.3 设备树匹配为什么是 i.MX6ULL 开发的主流i.MX6ULL 的内核版本普遍在 4.x 或 5.x这个阶段设备树早已全面接管了板级硬件描述。NXP 官方的 BSPBoard Support Package和 Yocto 构建系统都要求开发者基于设备树做驱动适配。使用设备树匹配的优势也很明显硬件差异被完全隔离到.dts设备树文件里。同一份驱动可以兼容多个板子只要修改设备树节点的compatible和资源定义即可。驱动源码不再散落着大量#ifdef CONFIG_BOARD_XXX的板级判断代码代码结构清爽很多。设备树还能表达复杂的外设拓扑结构比如多个子节点的父子关系、时钟关系、中断关系这些都是id_table时代很难做到的。在 i.MX6ULL 上开发一个新的外设驱动标准流程基本固定先在设备树中描述硬件资源再写驱动代码定义of_match_table最后通过compatible让二者完成匹配。理解了这个机制驱动开发的整个思路就会非常清晰。3. 手把手实现一个基于 i.MX6ULL 的 Platform 驱动3.1 设备树节点的编写要点以 i.MX6ULL 上一个普通的 GPIO-LED 设备为例设备树节点通常放在根节点下也可以放在某个总线节点下。这里需要注意compatible字符串的取名规范是“厂商名,设备型号”的形式目的是避免不同厂商的同名设备冲突。/ { myled { compatible mycompany,myled; reg 0x020c406c 0x4; /* GPIO1-DR 数据寄存器 */ gpio gpio1 18 GPIO_ACTIVE_HIGH; }; };这里reg属性描述的是物理地址和长度实际项目中更多时候会把寄存器地址统一放到设备树中然后在驱动中启动ioremap完成物理地址到虚拟地址的映射。i.MX6ULL 的 GPIO1 基地址是0x0209C000但这里如果直接用reg传入某个 IO 的配置寄存器地址驱动中获取资源时就需要结合实际情况定义。值得注意的一点是设备树节点必须使能才可能完成匹配。很多板级.dtsi文件里把外设节点默认写成status disabled如果你的板子需要用到某个外设必须在板级.dts文件里重新打开否则设备根本不会注册到 Platform 总线驱动自然无法匹配。3.2 platform_driver 结构体的完整填充驱动部分的代码骨架如下重点看of_match_table和driver.name的写法#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/io.h static int myled_probe(struct platform_device *pdev) { struct resource *res; pr_info(myled probe enter\n); /* 获取 reg 属性描述的内存资源 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get memory resource\n); return -ENODEV; } pr_info(myled phys addr: 0x%llx, size: 0x%llx\n, (unsigned long long)res-start, (unsigned long long)resource_size(res)); return 0; } static int myled_remove(struct platform_device *pdev) { pr_info(myled remove enter\n); return 0; } static const struct of_device_id myled_of_match[] { { .compatible mycompany,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple platform driver for i.MX6ULL);module_platform_driver是一个封装宏展开后等于先定义module_init和module_exit再在初始化函数里调用platform_driver_register在退出函数里调用platform_driver_unregister。这个宏能省掉不少重复代码是新驱动推荐的标准写法。3.3 probe 函数中的资源获取套路probe函数是驱动真正开始工作的起点。匹配成功之后内核会调用probe同时传入匹配到的platform_device指针。拿到这个指针后第一件事通常是获取设备树中声明的硬件资源。platform_get_resource是最常用的获取资源的接口它的第三个参数index表示同类型资源的序号。比如设备树里声明了两组reg第一组通过platform_get_resource(pdev, IORESOURCE_MEM, 0)获取第二组通过platform_get_resource(pdev, IORESOURCE_MEM, 1)获取。中断资源同理platform_get_irq(pdev, 0)获取第一个中断号。更省事的做法是用devm_platform_ioremap_resource把“拿资源”和“ioremap 建立页表映射”一步到位。这个函数内部先调用platform_get_resource拿到IORESOURCE_MEM类型的资源然后调用devm_ioremap_resource完成物理地址到虚拟地址的映射返回值就是驱动可以直接访问的虚拟地址指针。void __iomem *base; base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(base)) { dev_err(pdev-dev, failed to ioremap resource\n); return PTR_ERR(base); }devm_前缀的 API 是设备资源管理机制的一部分。驱动在probe中申请的内存、注册的中断、映射的 IO都由设备模型自动跟踪。当驱动卸载或设备移除时内核自动释放这些资源不需要在remove函数里手动清理。这个机制能避免“probe 到一半失败导致资源泄漏”的经典问题强烈建议新驱动一律使用devm_*接口。3.4 编译、加载与匹配验证驱动源码准备好之后如果用模块方式编译还需要在对应的 Makefile 中添加一行obj-m myled.o然后借助内核源码树编译出myled.ko。在 i.MX6ULL 开发板上insmod myled.ko加载模块后可以通过以下命令确认设备和驱动是否匹配成功# 查看设备是否已经注册到 Platform 总线 ls /sys/bus/platform/devices/ # 查看驱动是否已经注册到 Platform 总线 ls /sys/bus/platform/drivers/myled/ # 查看系统日志中 probe 函数的输出 dmesg | tail -20如果匹配成功/sys/bus/platform/drivers/myled/目录下就会多出一个指向设备目录的符号链接。如果设备树注册正常但驱动没有匹配上这个符号链接不存在dmesg里也看不到 probe 函数的打印。这个验证路径在后续排查问题时非常有效。4. 常见问题与排查技巧实录4.1 probe 函数为什么不执行这个问题在 i.MX6ULL 驱动开发中出现的概率极高而且原因多样每个都能让新手卡上好几天。最常见的原因是compatible字符串不一致。设备树里写的是mycompany,myled驱动里of_match_table写的是mycompany,myled一个字母或逗号之差就会导致匹配失败。我的排查习惯是先在开发板上把设备树挂载到/proc/device-tree的节点读出来然后和驱动源码中的字符串做逐字符对比。其次是设备树节点没有被正确注册。前面提过status disabled的情况另外节点放的位置也很关键。如果你的节点放到了某个总线节点下面但它本质上不是一个挂在真实总线上的设备那就需要确认该总线节点是否使能。还有一种可能设备树编译时语法错误导致节点根本没被解析。开发板上检查节点是否存在的命令如下ls /proc/device-tree/myled/如果目录不存在说明设备树编译生成的 dtb 本身就缺失这个节点需要回到.dts源码排查编译错误和 include 关系。第三类是驱动的MODULE_DEVICE_TABLE缺失导致modprobe不能自动加载驱动。虽然insmod手动加载不受影响但probe依然不执行的情况往往让人困惑。其实手动insmod时模块只是被加载进内核地址空间platform_driver_register已经执行match应该生效。如果这一步没有问题大概率还是compatible或者设备树注册环节出了问题。4.2 platform_get_resource 返回错误的排查方向设备树中reg属性定义了物理地址和长度驱动中用platform_get_resource(pdev, IORESOURCE_MEM, 0)获取。有时候probe进入了但platform_get_resource返回NULL这时需要检查设备树节点是否带有reg属性以及#address-cells和#size-cells的配置是否合理。i.MX6ULL 的根节点通常定义#address-cells 1;和#size-cells 1;表示地址和长度都用 32 位表示。如果你的节点嵌在其他总线节点下外围节点的 cell 设置可能不同导致reg解析出现问题。查看实际解析结果的命令可以定位问题cat /proc/device-tree/myled/reg如果读出的字节数和预期不符就说明设备树 cell 设置有问题。另一个坑是忘记填写reg属性只写了gpio属性驱动却去获取IORESOURCE_MEM自然拿不到。4.3 匹配成功但设备还是无法工作匹配成功只是第一步设备真正可用还要看probe里获取到的资源是否正确以及寄存器地址映射对不对。在 i.M6ULL 上IO 地址能不能访问还受到 iomux 配置的影响。GPIO 要正常工作除了 Platform 驱动本身还需要对应的 pinctrl 子系统完成引脚复用否则写寄存器可能没有预期的电气效果。这类问题排查时常用devmem命令直接读写物理地址检测寄存器层面的表现。比如 GPIO 输出不工作时先用devmem 0x020c406c 32读一下数据寄存器当前状态再用devmem 0x020c406c 32 0x1置位某一位观察实际电平变化。如果devmem操作有效但驱动代码无效问题大概率出在虚拟地址映射或者读改写逻辑上。4.4 利用 sysfs 和 debugfs 做系统化排查Linux 的 sysfs 在调试 Platform 设备匹配问题上提供了非常直观的切入点。/sys/bus/platform/devices/目录下列出了所有注册到 Platform 总线的设备。/sys/bus/platform/drivers/目录下列出了所有注册的 Platform 驱动。借助ls -l命令查看驱动目录下的符号链接就能确认设备与驱动的绑定关系。ls -l /sys/bus/platform/drivers/myled/如果看到类似myled - ../../../devices/soc/.../myled的符号链接说明匹配成功。如果没有符号链接说明驱动注册了但没有任何设备与之配对。这个状态下再配合dmesg搜索platform关键字能获取更详细的错误信息。另一个实用技巧是打开内核的动态调试功能直接跟踪platform_match的执行路径。如果内核开启了CONFIG_DYNAMIC_DEBUG可以在挂载 debugfs 后执行echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control然后再加载驱动dmesg里就会打印platform_match内部每一步的比对结果包括设备树compatible匹配的详细过程。这在应对棘手的匹配问题时能节省大量时间。4.5 一个实战排查案例说一个我在 i.MX6ULL 项目里实际遇到的问题。当时要适配一个 GPIO 按键驱动设备树里写好了节点驱动代码也写好了但probe就是不进入。先查/proc/device-tree/节点确实存在再用ls /sys/bus/platform/devices/查看设备发现设备注册的name字段变成了myled节点名而不是gpio-keys。问题出在设备树节点的compatible值写成了mycompany,gpio-key而驱动of_match_table里的compatible写的是gpio-keys两边不一致。更麻烦的是这个设备树是从 NXP 官方评估板的.dtsi继承下来的官方原版的compatible定义被覆盖过。最后我直接对照官方内核文档把compatible改成gpio-keys驱动立刻正常工作了。这个案例提醒我在承接已有 BSP 的项目时设备树节点的来源一定要搞清楚。很多看似神秘的匹配失败本质都是原始设备树和驱动代码来自不同的软件版本。5. 从 Platform 匹配机制延伸出的设计思路5.1 匹配机制与驱动解耦的逻辑理解匹配机制之后你会发现 Linux 把“设备是什么”“驱动怎么操作设备”这两个问题彻底分开了。设备树只负责说清楚设备在哪里、有哪些资源驱动只负责说清楚自己支持哪些设备、怎么操作这些资源。两边的共同语言就是那一串compatible字符串。这种解耦带来的直接好处是驱动代码里不再需要关心具体的板级差异。芯片换了一个外设、改了一个 GPIO、调整了一个中断只需要改设备树驱动源码一行不用动。i.MX6ULL 平台上经常出现“同一份 BSP 适配多个产品方案”的场景这个机制功不可没。在企业实际的量产项目里驱动工程师经常要维护多个设备树文件对应不同的硬件版本。如果匹配机制设计得不好这几乎是一场灾难但有了设备树和of_match_table的灵活机制新增一款硬件往往只需要在设备树里新增节点驱动里补充一个compatible条目就能搞定。5.2 定时器与中断资源的获取范式除了IORESOURCE_MEM平台设备还经常携带中断资源。i.MX6ULL 的 GPIO 可以配置为中断输入设备树里通过interrupts属性描述中断号和触发类型驱动中使用platform_get_irq获取中断号再调用devm_request_irq注册中断处理函数int irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, failed to get irq\n); return irq; } ret devm_request_irq(pdev-dev, irq, myled_irq_handler, IRQF_TRIGGER_FALLING, myled, NULL); if (ret) { dev_err(pdev-dev, failed to request irq\n); return ret; }这里platform_get_irq的返回值是负数时表示错误码正数才是合法中断号。很多 Linux 驱动老手会习惯性地if (!irq)判断错误但在新的内核版本中已经不允许把 0 当作合法中断判断了正确的排查逻辑应该用if (irq 0)或者if (irq 0)。i.MX6ULL 的 GPIO 中断号从 32 开始0 号中断本身就不会出现但这个编码习惯还是值得养成。5.3 设备树在不同子节点间的资源组织复杂外设经常需要多个子节点协同工作。比如一个 LCD 驱动主节点下不仅有寄存器资源还有时钟、复位、背光、触摸等多个子节点。每个子节点之间通过compatible属性自行匹配各自的驱动父节点驱动负责总体资源分配。这种设计模式在 i.MX6ULL 的官方 BSP 里大量可见lcdif、pxp、csi等模块都是如此。理解 Platform 匹配机制后再看这些内核驱动你会发现它们的结构高度统一设备树描述拓扑驱动各自认领节点互不干扰。这也是为什么建议新手在阅读内核驱动时先看设备树文件再对照驱动源码中的of_match_table两条线索一对应整体结构立刻清晰。6. 写在最后的经验之谈搞明白platform_match的匹配顺序和实现细节之后再回头看 i.MX6ULL 上的驱动开发确实会轻松很多。这里再说一个我个人的习惯每次新板子拿来先不着急写驱动先看设备树里到底注册了哪些设备节点它们都用什么compatible。把这个列表拉出来驱动的工作量就清楚了一半。匹配机制本身很简单但真实项目中能让你崩溃的往往是最基础的东西——拼写错误、大小写错误、设备树父子关系错位、status没有打开。我在排查过十几次类似问题之后养成了一个习惯写驱动前先在设备树节点里放一个printk能识别的自定义属性或者干脆先写一个空probe函数跑通匹配流程再逐步增加资源获取、中断注册等逻辑。这样每次只验证一个环节出问题能第一时间锁定范围。调试驱动的时候记住一个原则先确认设备树有没有解析出正确的节点再确认设备和驱动的绑定情况最后才深入寄存器操作层面的问题。大多数“设备不工作”的故障走到第二步就能找到原因。希望这篇基于 i.MX6ULL 的 Platform 匹配机制拆解能帮你少走一些我当年走过的弯路。