
1. 先把Platform机制是什么说清楚1.1 为什么CPU内置外设需要一条虚拟总线刚开始在i.MX6ULL上做Linux驱动时我其实对Platform有很强的抵触情绪裸机时代我写个LED驱动直接操作GPIO寄存器就完事了到了Linux底下怎么就非得搞什么device、driver、bus这一套绕来绕去的一度觉得是在浪费时间。但真正把Linux的设备模型吃透之后我现在的看法完全不同了。i.MX6ULL这颗Cortex-A7内核芯片上GPIO、UART、SPI、I2C、ECSPI、SDIO这些外设全都挂在SoC内部总线上它们不是像PCI或者USB那样可以热插拔、枚举出真实硬件的总线设备。系统怎么知道有哪些设备中断控制器接了哪些中断源引脚复用成什么功能这些信息在纯软件层面根本看不到必须有人告诉你。在旧内核里这个信息靠arch/arm/mach-xxx目录下大量的board-xxx.c文件写死代码又臭又长换一块板子就要改内核维护成本极高。Linux的做法是把设备、驱动、总线三者抽象成一套模型总线负责匹配设备和驱动设备描述硬件上有什么驱动描述软件上怎么操作。对于挂在CPU内部总线上的这些外设内核就虚拟了一条总线叫platform_bus也就是Imx6ull驱动开发里天天碰到的Platform机制中文常叫平台设备与驱动匹配。所以你可以把platform_bus理解为一条看不见摸不着、但所有CPU内置外设都要走的注册通道它解决的核心问题只有一个让驱动代码和设备描述解耦。1.2 i.MX6ULL上的实际场景从裸机到Linux的思维转换如果你是像我一样从裸机RTOS转过来的最容易卡住的就是设备这个概念。裸机开发时点亮LED的代码无非三步使能GPIO时钟、配置引脚复用为GPIO模式、操作数据寄存器。这没有问题因为你知道电路板上LED接在哪个引脚。但Linux驱动是通用代码同一个驱动文件可能要跑在十几种板子上有的板子LED接GPIO1_IO03有的接GPIO5_IO01你不能把这个信息硬编码在驱动里。Platform机制就是为这个场景准备的板级硬件信息引脚号、时钟频率、中断号、DMA通道等放进设备树驱动里只写操作逻辑通过api去读取设备树里传进来的参数。设备树负责回答板子上有什么、接在哪里驱动负责回答这个硬件怎么用platform_bus在中间做媒人。这个分工一旦想明白Linux驱动开发的整个思维框架就立住了一半。我在i.MX6ULL上做第一款Platform驱动时硬件上只是点了个LED但程序从device、driver到match的完整链路都走了一遍跑通之后回头看整个设备模型的地基算是打牢了后面再写按键、中断、I2C驱动基本就是在这个框架里填肉。2. 匹配机制的三个关键要素2.1 设备树节点compatible就是你的身份证设备树Device Tree是Platform机制里设备的出场凭证。在i.MX6ULL的板级设备树文件通常是imx6ull-14x14-evk.dts里一个典型的设备节点大概长这样led { compatible myvendor,board-led; reg 0x020c406c; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio5 3 GPIO_ACTIVE_LOW; status okay; };兼容属性也就是compatible是匹配的关键身份证。它遵循厂商名设备名的命名约定例如myvendor,board-led这个字符串会同时出现在设备树节点和驱动里匹配时双方对上了内核就认为这个驱动认识这个设备。刚开始我犯过一个低级错误设备树里写的是myvendor,board-led驱动里of_match_table写的是myvendor,board_led一个短横线一个下划线结果驱动probe就是不执行查了半天才在dmesg里发现驱动没匹配上。这种细节一定要小心设备树里的compatible字符串和驱动里的完全一致差一个字符都不行。设备树节点的作用和以前板级文件里的platform_device结构体是一样的只不过从C代码换成了描述性文本。它描述的核心内容包括设备挂在哪个总线上、使用哪些寄存器地址和长度、有哪些中断、引脚的复用关系、GPIO编号等。这些东西最终会被内核的device_node结构体保存下来供驱动解析使用。2.2 platform_driver与of_match_table设备树把设备定义好了接下来就是驱动这边。在驱动代码里核心是一个platform_driver结构体static const struct of_device_id led_of_match[] { { .compatible myvendor,board-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name board-led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver);这里of_match_table就是刚才说的身份证比对库内核会遍历它看有没有和设备树节点compatible值匹配的项。两个都要对应起来设备树那边是我想要谁驱动这边是我能匹配谁一旦相等总线就会调用驱动的probe函数。一个驱动也可以写多个of_device_id条目表示支持多种设备比如static const struct of_device_id led_of_match[] { { .compatible myvendor,board-led }, { .compatible myvendor,board-led-v2 }, { /* sentinel */ } };这样同一份驱动代码可以同时适配V1和V2两个版本的硬件硬件改版时只需要在设备树里换compatible字符串驱动文件一行都不用动。2.3 匹配优先级不止compatible这一条路很多人以为Platform驱动匹配只靠compatible实际上内核的匹配顺序是有一套完整优先级的。在驱动注册或者设备注册的时候内核会按顺序尝试以下几种匹配方式匹配方式说明使用场景设备树of_match_table比较设备节点compatible和驱动表中compatible设备树普及后最常用ACPI匹配比较ACPI表里的HID/CIDx86平台、UEFI环境id_table匹配比较platform_device的name和id_table中的name传统板级文件平台驱动名与设备名匹配比较platform_driver.driver.name和platform_device.name早期platform设备私有无匹配机制设备树节点里带driver_override字段特殊情况强制指定在i.MX6ULL这种嵌入式平台上绝大多数情况走的是第一种方式。等内核的设备树支持完善之后ACPI基本不用管id_table那套则是老平台的产物现在写驱动优先写of_match_table就可以了。理解了优先级你就能解释很多奇怪现象。比如驱动明明注册了设备树里compatible也对但probe没执行那有可能是有另一个驱动先按id_table或设备名抢占了匹配。这种半路杀出个程咬金的case我后续会专门展开说。3. 匹配代码解析与probe触发流程3.1 从内核启动到probe执行的完整链路很多人写过platform驱动但整个匹配的流程细节未必清楚。我画了一条逻辑链路帮助理解梳理完你会发现它其实是层层递进的关系内核启动 - 解压设备树DTB - 解析device_node树 - 遍历节点创建platform_device - 注册到platform_bus - 注册platform_driver - 总线match检查 - probe成功 - 驱动接管设备在i.MX6ULL上设备树的解析发生在内核启动早期由start_kernel调用setup_arch再走到unflatten_device_tree把所有设备树节点展开成struct device_node的树状结构。之后内核会调用of_platform_default_populate_init遍历根节点下所有带compatible属性的节点为它们创建platform_device。我插一句这个过程是存在时序的。如果你把驱动编译成模块.koinsmod加载时驱动才注册这时候设备树早已解析完成platform_device早就挂在总线上了如果你把驱动编进内核built-in那么驱动的注册时机由驱动的initcall层级决定。i.MX6ULL默认的启动流程里绝大部分设备驱动是built-in的系统起来时驱动和设备在总线上相遇并完成匹配。理解了这一点你调试的时候就会知道什么时候该等设备什么时候该等驱动。3.2 一次匹配驱动的executive入口怎么选真正走到匹配环节内核会调用platform_match函数它是platform_bus这个媒婆的看家本领。这个函数匹配逻辑的核心如下基于内核源码逻辑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); /* 用id_table匹配 */ if (pdrv-id_table) if (platform_match_id(pdev, pdrv-id_table) ! NULL) return 1; /* 用设备树匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 用acpi匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 用驱动名和设备名匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }一旦platform_match返回1内核会调用really_probe进而调用driver的probe函数。注意probe函数的参数是struct platform_device指针你可以通过这个指针拿到设备树里的各种资源。如果你是从应用层转到内核开发这里有个特别值得体会的地方以前的裸机程序是我要操作某个寄存器驱动开发是系统告诉我有一个设备你初始化并服务它。控制流反过来了。probe返回0表示成功返回负数表示失败失败时内核可能会尝试下一个驱动或者把设备挂进deferred probe列表等待时机重试。3.3 实战完整写一个i.MX6ULL的platform驱动框架理论说了这么多不落地都是空的。我在i.MX6ULL上写过一个最简但五脏俱全的Platform驱动功能是操控一个LED代码框架分享给大家#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/gpio.h struct led_data { struct gpio_desc *led_gpio; }; static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct led_data *data; data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; /* 从设备树获取GPIO语义化获取不用关心具体引脚号 */ >led { compatible myvendor,board-led; led-gpios gpio5 3 GPIO_ACTIVE_LOW; status okay; };编译的方法和其它内核模块一样写个Makefile放内核源码树下或使用内核模块构建环境obj-m : led_platform.o KDIR : /path/to/linux-imx all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean模块编出来之后如果是模块方式先确保设备树里已经有了那个节点i.MX6ULL上一般改完设备树重新编译DTB烧进去然后在板子上执行insmod led_platform.ko这时候dmesg里如果能看到led probe success说明设备树→platform_device→platform_driver→probe这条路彻底通了。用devm_gpiod_get系列接口而不是老式gpio_request加of_get_named_gpio的好处是资源生命周期全由内核托管probe失败或者remove时不用手动一堆清理操作可以减少很多裸奔内存泄漏的隐患这是我在i.MX6ULL项目里明显体会到的差别。4. 常见问题排查手册probe不执行怎么办4.1 排查清单与定位方法我见过太多初学者卡在驱动编译进内核也注册到总线了但probe就是不被调用其实这类问题95%都能用下面这张清单定位排查项检查方法常见原因设备树节点是否存在/proc/device-tree/或/sys/firmware/devicetree/base/DTB没更新节点没编译进去compatible是否一致对比设备树和of_match_table字符串拼写差异、符号差异设备树status状态查看status属性是否为okaydisabled节点不参与匹配驱动是否注册成功ls /sys/bus/platform/drivers/xxx驱动编译进了别的模块是否被deferred probedmesg搜索deferred依赖的外设还没就绪是否有其它驱动占用查看/sys/bus/platform/devices/驱动名和设备名碰撞排查的时候我习惯按三个步骤走第一步在板子上cat /proc/device-tree/led/compatible确认设备树里字符串到底对不对第二步到/sys/bus/platform/drivers/下面ls看驱动有没有注册进来第三步dmesg搜platform关键词看匹配过程有没有报错。这三板斧下来大部分问题都有了方向。4.2 延迟探测deferred probe是怎么回事真正让人头疼的是deferred probe。我有个很典型的经历在i.MX6ULL上做GPIO按键驱动设备树里设备引用了某个GPIO控制器而Probe里的devm_gpiod_get需要GPIO控制器先完成probe但设备树节点的创建顺序未必保证GPIO控制器在前。这时候驱动程序probe会返回-EPROBE_DEFER告诉内核这次不行但别放弃我等会再试。如果dmesg里看到probe of xxx returned -EPROBE_DEFER这种提示别慌这是内核在排队重试。系统会在所有设备注册完之后对那些返回-EPROBE_DEFER的驱动重新probe。但如果依赖永远不满足比如GPIO控制器驱动根本没编译进内核那设备就会一直卡在deferred状态看起来就像probe永远不被调用。排查deferred probe有一个很实用的命令思路cat /sys/kernel/debug/devices_deferred cat /sys/kernel/debug/device_pm/deferred_probe_pending_list.sh # 或看内核相关debug节点这个文件会列出所有正在等待的设备和原因一眼就能看出是谁在等谁。4.3 查看sysfs和调试信息的实战技巧sysfs是Linux内核设备模型的现实投影调试Platform驱动时非常有用。设备匹配成功后你可以看到ls /sys/bus/platform/devices/ ls /sys/bus/platform/devices/led/ ls /sys/bus/platform/drivers/board-led/后面这条链路可以看到设备和驱动之间建立了链接。我个人调试的时候有一个习惯写驱动时在probe里故意加上dev_info和dev_err打印信息打全一点方便在dmesg里追踪。比如打印pdev-name、of_node的全路径、拿到的GPIO编号等这些信息在调试时能省下大量猜时间。另外强烈建议开启内核的动态调试echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control然后dmesg里就能看到platform_match、really_probe这些核心函数是否被调用以及在哪个环节返回的失败。这个手段我用了很多次比自己瞎改代码重新编译省力太多了。5. 进阶经验与避坑指南5.1 多个同类设备如何匹配同一个驱动实际项目中板子上往往不止一个LED比如电源灯、状态灯、网络灯都是同一颗芯片控制的。这时候你就需要多个设备树节点对应同一个驱动。做法很简单设备树里写多个compatible相同的节点驱动中的of_match_table保持不变内核会为每个节点创建一个platform_device然后逐个匹配同一个platform_driverprobe会被调用多次。但这里有个坑必须注意probe里的devm_gpiod_get如果写死了设备树属性名led多个节点都能用没有问题但如果你用platform_get_resource之类的方式拿到固定的寄存器地址或内存区多个设备之间就可能产生资源冲突。更好的做法是在设备树里用reg属性区分led1 { compatible myvendor,board-led; reg 0x0; led-gpios gpio5 3 GPIO_ACTIVE_LOW; }; led2 { compatible myvendor,board-led; reg 0x1; led-gpios gpio5 4 GPIO_ACTIVE_LOW; };然后在probe里读reg值判断是第几个设备u32 id; struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) id res-start; dev_info(dev, this is led%u\n, id);当然设备树里同一个根节点下不允许有同名子节点所以命名要么用led0、led1要么用led-red、led-green这种方式我推荐前者元素更规整。5.2 设备树修改后一定要reboot这个经验说出来像废话但确实是我最想写进避坑清单的一条。在i.MX6ULL上调试Platform驱动时我发生过两次灵异事件改了设备树重新编译DTB烧到板子上dmesg里的设备还是老样子compatible还是旧值。折腾了很久才发现用的是板卡开发环境里缓存的旧DTB或者u-boot环境变量uimage里指定的设备树路径根本没指向我新烧的位置。后来我给自己定了个规矩每次烧完设备树在板子上第一条命令就是cat /proc/device-tree/led/compatible确认当前生效的设备树就是自己刚改的版本。很多调试问题根源其实是你以为你改的东西已经生效了但实际没有。内核模块也一样insmod之前先rmmod旧模块或者干脆重新启动系统避免模块版本残留、符号冲突带来的干扰。5.3 从Platform机制延伸到其它总线把Platform机制彻底搞懂之后你会发现它其实是理解其它Linux驱动总线的钥匙。i.MX6ULL上还有i2c总线、spi总线、regmap框架它们的匹配逻辑和Platform一脉相承都是device、driver、bus三件套只不过各自的总线类型不同匹配时的id_table和probe参数会有差异。比如I2C设备驱动同样有i2c_device_id、of_match_tableprobe参数是struct i2c_client从设备树获取的也是compatible属性SPI驱动则对应struct spi_device。所以我在带人的时候经常说先把Platform机制啃透再去碰I2C/SPI/regmap一通百通。最后再分享一个小技巧。调试匹配问题时尽量不要用printk裸打养成用dev_dbg/dev_info的习惯日志里自带上层设备和驱动名一眼就知道是谁在打日志。然后把内核日志级别调成8再开动态调试整个匹配过程的每一步都清清楚楚。这是我踩了无数次坑之后养成的习惯对排查probe不执行这类问题特别管用。