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

发布时间:2026/9/6 11:36:23
Linux Platform总线与设备驱动匹配机制详解:以i.MX6ULL为例 1. 为什么你的第一个驱动在i.MX6ULL上总是不工作先从一个非常典型的“翻车”现场说起。很多人拿到i.MX6ULL开发板照着网上的教程写了一个字符设备驱动register_chrdev、class_create、device_create三件套统统写完insmod之后也看到了设备节点但是read、write就是不对或者probe函数压根就没被调用。排错半天最后发现问题根本不是代码逻辑而是驱动和设备在总线上压根没“配上对”。这个现象背后藏着的正是Linux驱动模型里最基础也最核心的机制——Platform总线。在i.MX6ULL这种嵌入式平台上几乎所有外设驱动GPIO、UART、I2C、SPI、以太网、LCD控制器都是挂在这一套机制上的。不理解它你写的驱动就只能靠“玄学”工作换一个设备树配置就全线崩盘理解了它看任何厂家的BSP代码都会顺畅很多。这篇文章我就围绕Platform设备与驱动的匹配机制展开从总线模型、设备树节点与platform_device的转化、driver与device的匹配顺序到i.MX6ULL上常见的匹配失败案例完整地把这条链路拆开讲一遍。内容偏实战适合已经能独立编写helloworld字符驱动、但对Linux设备模型只有一个模糊概念的开发者也适合准备面试、想系统梳理设备模型知识点的朋友。提示这篇文章不涉及具体的点灯代码也不讲read/write怎么实现。它讲的是“驱动和设备是怎么找到彼此的”——这件事搞不清楚后面写再多功能代码都是空中楼阁。在正式开始之前我先提一个问题带着问题往下读效果会好很多为什么同样的一个platform_driver注册函数换一块板子、改一个设备树节点probe函数就不被调用了驱动代码一行没改问题出在了哪里答案就在匹配机制里。2. Platform总线的定位它到底解决什么问题2.1 不是所有设备都能“即插即拔”先想一个场景PC机上USB鼠标插上去系统立刻识别拔下来系统立刻注销。这种设备属于“热插拔总线”上的设备典型的如USB、PCI、SDIO。它们的特点是设备device可以在系统运行期间动态出现和消失所以内核必须提供一套机制让设备到来时能找到对应的驱动设备移除时能通知驱动释放资源。但嵌入式平台上的大部分设备完全不是这个玩法。i.MX6ULL芯片内部的UART控制器、GPIO控制器、SDIO控制器、LCD控制器从芯片出厂那天起就焊死在某个内存地址上中断号也是固定的板上某个GPIO引脚接了LED、某个I2C总线上挂着触摸屏——这些东西通电就在断电就消失不存在运行中插拔的概念。那么问题来了Linux的设备模型是围绕总线bus组织的一个设备必须挂在哪条总线上驱动才能和设备匹配。对于I2C设备、SPI设备它们有自己的总线i2c_bus_type、spi_bus_type没问题。但对于那些直接挂在CPU地址总线上的“片上外设”它们属于哪条总线总得找一条总线把它们管起来。Platform总线就是为这个需求而生的。它是一条虚拟总线把芯片自带的、不可热插拔的、直接映射在内存地址空间上的设备统一挂载起来让device和driver能够在同一条总线上完成匹配、probe、remove的完整生命周期管理。2.2 传统写法为什么可行但不可扩展在真正使用Platform机制之前很多入门教程会教你直接用register_chrdev和platform_driver_register来写驱动。比如写一个LED驱动你可能会这样写static int __init led_init(void) { // 直接操作GPIO寄存器或者通过gpio_request/gpio_direction_output申请引脚 gpio_request(LED_PIN, led); gpio_direction_output(LED_PIN, 0); register_chrdev(MAJOR_NUM, led, led_fops); return 0; }这段代码在x86或者树莓派上能跑在i.MX6ULL上也可能能跑但问题在于它把“设备具体是什么”引脚号、寄存器地址、中断号和“驱动代码逻辑”完全耦合在了一起。今天你的板子LED接在GPIO1_IO03明天另一个项目LED接GPIO4_IO05你就得改代码重新编译。如果还有按键、LCD、触摸屏每个驱动里都硬编码一堆硬件资源——代码根本维护不下去。Platform机制的核心价值就是把“设备资源”和“驱动逻辑”剥离开。设备资源描述引脚、地址、中断放在设备侧传统方式是写死在platform_device结构体里设备树时代则是放在dts文件里驱动侧只写“如何处理这些资源”的逻辑。当设备与驱动匹配成功后内核把资源统一交给驱动去解析、使用。这样同一份驱动代码几乎不用改就能适配不同板卡。2.3 parent是谁Platform总线上挂着的“大杂烩”打开i.MX6ULL的内核源码arch/arm/mach-imx/目录下会看到大量mxc_board_init之类的函数老版本内核里都是通过platform_device_register一个一个注册设备。现在的设备树时代这些platform_device基本上都是由设备树节点自动生成的。但不管来源如何它们统统挂在platform_bus_type这条总线上。这条总线上挂着的设备五花八门从简单的GPIO按键、LED到复杂的LCD控制器、DMA控制器、以太网MAC都选择platform总线作为归属。它不关心设备的“物理属性”到底是什么只负责提供一个统一的匹配环境。从某种意义上说Platform总线就是Linux为“内建设备”准备的后院什么东西没有专属总线归属就往这个院子里放总线本身则提供一套标准的“相亲”规则。理解了这点你就能明白为什么在设备树时代platform_driver是绝对的主流几乎你在i.MX6ULL上写的每一个驱动最终都会落到它头上。3. 设备侧与驱动侧一根总线上的两端到底长什么样3.1 设备侧老式platform_device结构体在设备树还没普及的年代内核2.6、3.x时代platform设备是通过C代码静态定义的。它的核心结构体是platform_device定义在include/linux/platform_device.h中缩略来看大概是这样的struct platform_device { const char *name; /* 设备名字匹配的关键字段 */ int id; /* 设备实例id-1代表只有一个实例 */ struct device dev; /* 内嵌的核心device结构体 */ u32 num_resources; /* 资源数量 */ struct resource *resource; /* 资源数组IO地址、中断号等 */ };其中resource是最重要的字段之一用来描述设备的硬件资源包括寄存器物理地址范围、中断号、DMA通道等。举个例子如果你想在内核3.x时代定义一个UART设备大概是这样的static struct resource uart2_resources[] { { .start 0x021E8000, .end 0x021E8FFF, .flags IORESOURCE_MEM, }, { .start 32 12, /* 中断号是GIC_SPI 偏移 */ .flags IORESOURCE_IRQ, }, }; static struct platform_device uart2_device { .name imx6ull-uart, .id 0, .num_resources ARRAY_SIZE(uart2_resources), .resource uart2_resources, };然后在板级初始化代码里调用platform_device_register(uart2_device)内核对设备模型进行注册。驱动侧则调用platform_driver_register注册一个名字同样是“imx6ull-uart”的ian驱动两者名字对上probe就被调用了。这种纯C代码定义设备的方式最大的问题是每换一块板子就要重新修改内核源码里的设备信息重新编译内核。设备树Device Tree的引入彻底改变了这个局面——设备的硬件属性被抽象成dts/dtsi文件中的节点不再需要改动内核C代码。在i.MX6ULL这样的平台设备树方式已经是绝对主流所以那套platform_device静态定义的方式你现在基本只会出现在旧代码或面试题里。3.2 设备侧的新形态设备树节点如何变成platform_device在设备树时代你不再需要手动构造platform_device。内核在启动过程中会遍历设备树为每一个满足条件的节点动态生成platform_device。这里有一个非常重要的概念澄清不是所有设备树节点都会生成platform_device。只有那些没有挂载在“专属总线”上的节点才会走platform总线。比如i2c1节点下挂着的触摸屏节点它属于i2c总线设备会被i2c核心适配器扫描并创建i2c_client而不是platform_device。同样的道理spi节点下的设备会变成spi_device。对于i.MX6ULL来说我们经常碰到的这些节点——led-gpios、gpio-keys、backlight、pwm、lcdif、uart等它们通常直接挂在根节点或简单的总线节点下会被of_platform_bus_probe()递归扫描并转换为platform_device。我们来做一个对应关系梳理让这个转化过程更清晰设备树节点属性或结构转化后的platform_device内容reg属性填充platform_device的resourceflags为IORESOURCE_MEMinterrupts属性填充resourceflags为IORESOURCE_IRQcompatible属性用于后续与platform_driver的of_match_table匹配节点名name部分设备树节点名某些匹配方式下会用到其他自定义属性如gpios、pinctrl不直接体现在platform_device资源中驱动通过解析设备节点获取以i.MX6ULL官方评估板上的LED节点为例leds { compatible gpio-leds; pinctrl-names default; pinctrl-0 pinctrl_led; led0 { label heartbeat; gpios gpio1 3 GPIO_ACTIVE_LOW; linux,default-trigger heartbeat; }; };这个节点会被转换为一个name为“gpio-leds”的platform_device它的struct device结构体里会保存device_node的指针。驱动侧只需要在of_match_table中声明compatible为“gpio-leds”匹配成功之后在probe里用of_get_named_gpio之类的API去读取“gpios”属性即可。注意这里的gpios属性并没有被转换成platform_resource而是作为设备树自定义属性保留在了device_node中驱动通过设备树API去取。这个设计在逻辑上很清晰官方标准化的资源地址、中断走resource通道方便通用框架统一处理非标准化资源GPIO编号、时钟配置、自定义属性走device_node通道保持灵活性。3.3 驱动侧platform_driver结构体驱动侧的核心结构体是platform_driver定义在include/linux/platform_device.h中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); int (*resume)(struct platform_device *); struct device_driver driver; };实际项目中我们最关心的是probe和remove以及内嵌的driver成员。driver成员非常重要它是通用设备驱动结构体里面包含name、owner、of_match_table等字段。尤其需要记住的是of_match_table它是设备树时代的“门牌号”驱动靠它声明“我能支持哪些设备”。下面是一个典型的i.MX6ULL驱动入口写法static const struct of_device_id imx6ull_led_dt_ids[] { { .compatible gpio-leds }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx6ull_led_dt_ids); static struct platform_driver imx6ull_led_driver { .probe led_probe, .remove led_remove, .driver { .name imx6ull-led, .of_match_table imx6ull_led_dt_ids, }, }; module_platform_driver(imx6ull_led_driver);MODULE_DEVICE_TABLE宏的作用是当驱动编译为模块时把of_device_id表导出到模块的.modinfo段中这样insmod工具可以通过modinfo查看这个模块支持哪些设备同时在内核的模块自动加载机制中也起到索引作用。当你调用module_platform_driver时它展开为module_init和module_exit分别调用platform_driver_register和platform_driver_unregister。从这一刻起这个driver就进入了platform总线等待匹配。3.4 驱动侧的核心API用途再说一下platform_driver_register内部的运作它会将platform_driver包装成一个device_driver然后调用driver_register注册到总线核心。总线核心随后遍历该总线上的所有设备逐一调用匹配函数检查是否匹配。如果匹配成功总线核心会调用really_probe最终触发platform_drv_probe也就是我们熟悉的probe回调。有一个很常见的经验问题驱动注册比设备注册更早会怎样比设备注册更晚又会怎样答案是顺序无所谓。总线核心在驱动注册时会扫描已有设备在设备注册时也会反向扫描已有驱动无论哪一方先到位最终都能完成匹配。这一点跟I2C子系统行为一致因为这沿用的是一套通用的Linux设备模型机制。4. 匹配过程拆解四种匹配方式别只记住compatibleplatform总线上的设备与驱动匹配不是只有设备树compatible这一种方式。很多人在网上搜资料只知道看compatible这其实只覆盖了设备树这一条支线。完整地说platform_match函数drivers/base/platform.c会依次尝试四种匹配方式我按优先级顺序列出来4.1 方式一设备树compatible匹配设备树时代的绝对主力这是目前i.MX6ULL上最常见的匹配方式。当驱动和设备都是通过设备树节点生成时内核会比较设备节点compatible属性与驱动of_match_table中的compatible字符串。具体流程是内核会首先在设备树节点上寻找compatible属性然后遍历驱动of_match_table里的每一项逐个比较compatible字符串。比如设备树里uart3节点写的是uart3 { compatible fsl,imx6ull-uart, fsl,imx6ul-uart; pinctrl-names default; pinctrl-0 pinctrl_uart3; status okay; };注意这里有两个compatible字符串。设备树规范允许一个节点包含多个compatible值按优先级从高到低排列第一个是最精确的描述。匹配时内核会拿驱动的of_match_table去和这些字符串逐一对照。如果驱动声明的是static const struct of_device_id imx_uart_dt_ids[] { { .compatible fsl,imx6ul-uart }, { .compatible fsl,imx6q-uart }, { /* sentinel */ } };那么即使设备树节点写的是“fsl,imx6ull-uart”内核也会继续尝试下一个compatible直到碰到“fsl,imx6ul-uart”匹配上。这种“多级兼容”设计是为了让新芯片的驱动可以复用老芯片的兼容标识做向后兼容。4.2 方式二platform_device的name字段与driver.name匹配这种方式是设备树时代以前的遗留机制。在没有设备树或者设备树节点没有compatible属性的情况下内核会退而比较platform_device的name字段与platform_driver.driver.name字段。在内核3.x时代我们定义platform_device时给.name赋值驱动侧也给.driver.name赋相同的字符串两边相等即匹配成功。设备树时代机读属性不依赖name但在某些老式的.xlate实现或自定义总线节点中依然可能以节点名参与匹配兜底。总的来说它还在但已经不是这个时代的主角。4.3 方式三ACPI匹配ACPI高级配置与电源管理接口是x86平台和部分ARM服务器平台使用的硬件描述标准。在设备树平台上基本不会用到ACPIi.MX6ULL是典型的ARM Cortex-A7嵌入式平台这一条匹配方式在开发中可以直接忽略。之所以在platform_match里先于id_table检查只是为了保证通用总线模型行为一致。4.4 方式四platform_device的id_table匹配在驱动结构体中还有一个id_table成员类型为platform_device_id数组。它是纯C代码时代的另外一种匹配方式用于一个驱动支持多款设备的情况。static const struct platform_device_id imx6ull_led_ids[] { { .name gpio-leds, .driver_data (kernel_ulong_t)led_data }, { }, }; MODULE_DEVICE_TABLE(platform, imx6ull_led_ids);当id_table非空时内核会比较platform_device的name和id_table中的name。这个机制在设备树时代也有用武之地当设备树节点的compatible匹配失败后节点名可以作为最后一重保险参与匹配。不过从我在实际项目里看到的用法来说大多数i.MX6ULL驱动源码还是主打of_match_tableid_table更多用于非设备树场景。4.5 platform_match函数调用优先级小结用一个表格把这四种方式的优先级和触发条件整理清楚优先级匹配方式触发条件i.MX6ULL上是否常用1of_driver_match_device设备有device_node驱动有of_match_table最常用2acpi_driver_match_device支持ACPI固件的平台几乎不用3platform_match_id驱动有id_table匹配设备name少见旧代码较多4字符串name匹配驱动没有of_match_table也没有id_table与设备name比较少见实际工作中你几乎只需要关注第一和第四种其他的了解即可。提示很多人以为匹配成功就等于probe被调用这是对的。但要注意匹配成功之后内核对设备的probe顺序还会受到deferred probe和probe顺序策略的影响。如果设备的依赖资源还没准备好比如它的时钟控制器驱动还没probe那么这次匹配会上报延迟等依赖项就绪后再重新尝试。这也是你常看到内核log里出现“deferred probe pending”的原因。5. i.MX6ULL实战设备树节点与驱动的匹配链路分析5.1 写一个完整的驱动骨架验证匹配机制理论说完了下面我们实际走一遍i.MX6ULL上的开发流程。这里我以一个GPIO按键驱动为例展示设备树节点如何与platform_driver完成匹配以及probe函数如何获得硬件资源。整个过程可以在正点原子、野火等i.MX6ULL开发板上直接复现。先写设备树节点在一个i.MX6ULL板级dtsi基础上追加/ { mykey { compatible myvendor,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_key1; key-gpios gpio5 1 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_key1: key1grp { fsl,pins MX6UL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x17059 ; }; };在iomuxc节点里配置了引脚复用功能把SNVS_TAMPER1这个引脚复用为GPIO5_IO01电子特性值为0x17059表示使能上下拉、设置合适的驱动能力等等。接下来是驱动源码我给出了一个精简但完整的platform_driver匹配框架#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/interrupt.h #include linux/gpio.h static irqreturn_t key_isr(int irq, void *data) { printk(key pressed!\n); return IRQ_HANDLED; } static int mykey_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; int gpio, irq; int ret; /* 从设备树节点获取gpio号 */ gpio of_get_named_gpio(np, key-gpios, 0); if (gpio 0) { dev_err(dev, failed to get gpio\n); return gpio; } /* 申请并设置输入方向 */ ret devm_gpio_request_one(dev, gpio, GPIOF_IN, mykey); if (ret) { dev_err(dev, failed to request gpio\n); return ret; } /* 获取中断号并申请中断 */ irq gpio_to_irq(gpio); if (irq 0) { dev_err(dev, failed to get irq\n); return irq; } ret devm_request_irq(dev, irq, key_isr, IRQF_TRIGGER_FALLING, mykey, NULL); if (ret) { dev_err(dev, failed to request irq\n); return ret; } dev_info(dev, mykey probed, gpio%d, irq%d\n, gpio, irq); return 0; } static void mykey_remove(struct platform_device *pdev) { dev_info(pdev-dev, mykey removed\n); } static const struct of_device_id mykey_of_match[] { { .compatible myvendor,gpio-key }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mykey_of_match); static struct platform_driver mykey_driver { .probe mykey_probe, .remove mykey_remove, .driver { .name mykey, .of_match_table mykey_of_match, }, }; module_platform_driver(mykey_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Embedded Developer); MODULE_DESCRIPTION(A simple platform driver demo);编译并加载这个模块后可以在串口终端上看到类似这样的日志# insmod mykey.ko mykey mykey: mykey probed, gpio9, irq66gpio9是因为GPIO5_IO01映射到全局GPIO编号是1281129这里我故意不纠结编号因为每个板子的gpio基址可能不同实际打印以自己板子为准。关键要看的是“mykey probed”这一行输出。如果这行没出来就说明匹配链路有问题。5.2 解码匹配过程从insmod到probe的完整时序让我们把上面这个例子从insmod到probe的执行时序完整地梳理一遍这对理解整体机制特别有帮助调用insmod mykey.komodule_platform_driver宏触发platform_driver_register。platform_driver_register会把这个驱动挂到platform_bus_type总线的driver链表上。总线核心调用bus_add_driver为驱动创建sysfs属性目录/sys/bus/platform/drivers/mykey/。总线核心遍历该总线上的所有设备包括之前设备树扫描阶段就已经生成的platform_device对每个设备调用platform_match进行匹配。在platform_match中首先检查设备是否有device_node。设备树生成的platform_device都带device_node所以走of_driver_match_device分支。of_driver_match_device拿设备树compatible字符串“myvendor,gpio-key”和mykey_of_match数组中的compatible逐项比较找到一致项返回匹配成功。总线核心进入really_probe调用driver-probe实际上会经过platform_drv_probe包装也就是mykey_probe。mykey_probe内部通过dev-of_node取得设备树节点句柄用of_get_named_gpio解析key-gpios属性得到gpio编号申请中断完事。这8步中第5步和第6步是核心。如果你对匹配机制理解不透彻看到驱动加载后probe没执行第一反应往往是改代码加日志但实际上问题大概率出在第4步之前的设备扫描——设备树节点压根没有生成platform_device。5.3 设备树节点何时生成platform_device在i.MX6ULL平台上设备树节点生成platform_device的过程发生在内核启动早期。start_kernel之后setup_arch会调用unflatten_device_tree把dtb二进制解析成device_node的树形结构随后在init_machine流程中内核会调用of_platform_default_populate或类似函数遍历设备树为满足条件的节点动态创建platform_device。这里再强调一次条件只有“没有专属总线”的节点才可能变成platform_device。比如i2c2 { touchscreen38 { compatible goodix,gt911; reg 0x38; interrupt-parent gpio1; interrupts 13 IRQ_TYPE_EDGE_FALLING; }; };这个touchscreen节点挂在i2c2节点下内核会为i2c2本身创建platform_device因为它本身是imx6ull的I2C控制器属于片上外设走platform总线但不会为touchscreen创建platform_device。i2c2的driverdrivers/i2c/busses/i2c-imx.c在probe时注册了i2c适配器之后适配器会扫描总线上的设备发现0x38地址有设备结合设备树节点创建i2c_client这条链路由I2C子系统单独负责。类似的SPI总线下的从设备、MDIO总线下的PHY芯片都有各自的总线与类型不会挂在platform总线上。所以如果你发现自己设备树节点里的compatible和驱动完全一致但probe就是不被调用第一步就要确认这个节点到底是不是作为platform_device被生成了。最简单的验证方法是看/sys/bus/platform/devices/目录下有没有对应名字的软链接。如果没有说明设备压根不在platform总线上匹配根本无从谈起。5.4 i.MX6ULL上典型的Platform驱动来源在内核源码里drivers目录下大量驱动都是platform_driver。举几个i.MX6ULL开发中最常遇到的drivers/gpio/gpio-mxc.ci.MX系列GPIO控制器驱动compatible为“fsl,imx6ul-gpio”、“fsl,imx6ull-gpio”等drivers/tty/serial/imx.ci.MX系列UART驱动compatible为“fsl,imx6q-uart”等drivers/net/ethernet/freescale/fec_main.ci.MX6ULL内置的以太网MAC控制器驱动drivers/watchdog/imx2_wdt.c看门狗驱动drivers/pwm/pwm-imx.cPWM驱动drivers/mfd/mc13xxx-core.cPMIC相关驱动通过设备和驱动匹配挂载这些驱动源码里of_device_id数组的名字往往一眼就能看出来比如static const struct of_device_id imx_uart_dt_ids[] { { .compatible fsl,imx6q-uart }, { .compatible fsl,imx6ul-uart }, { .compatible fsl,imx7d-uart }, { /* sentinel */ } };注意比较一下这些官方驱动通常会列出多个compatible因为同一款IP核可能在多个i.MX系列芯片上复用只是封装不同。自己写驱动的时候也可以这样设计如果硬件IP与某个官方设备兼容最好在设备树中显式声明对应的compatible如果完全不兼容就自定义一个具有厂商前缀的compatible比如“myvendor,gpio-key”避免与官方驱动冲突。6. 匹配失败排查清单从问题现象到根因分析匹配失败是驱动开发中最常见的问题。这里我分享一份排查清单按我平时调试的顺序排列。这些经验都是无数次debug攒出来的每一条都能直接对应到一个具体的失败场景。6.1 排查项一设备树节点status字段设备树节点的status属性如果被设置为“disabled”那么在of_platform_default_populate扫描时会被直接跳过不会生成platform_device。这是最常见也最隐蔽的低级错误。我曾经在一个项目里遇到过这样的闹剧设备树里新增了一个按键节点compatible、pinctrl都写了内核编译烧写后驱动加载一切正常模块也注册成功但probe就是不被调用。排查到最后发现板级dtsi文件在这个节点下有一个“status ”disabled“;”的覆写是之前复用其他项目工程文件时遗留的。解决方法是搜索全文件确认status没有冲突或者用grep检查grep -rn status arch/arm/boot/dts/imx6ull-myboard.dts如果节点被disabled将其改为“okay”或直接删除该行默认就是okay。6.2 排查项二compatible字符串不一致这个错误最常见还好排查。把设备树里compatible的值和驱动of_match_table里的值逐字符比较注意大小写、下划线、连字符。用肉眼对比容易漏建议直接用十六进制字符串打印方式验证或者把设备树反编译出来看图dtc -I dtb -O dts -o /tmp/board.dts arch/arm/boot/dts/imx6ull-myboard.dtb grep -A 3 mykey /tmp/board.dts一个很容易踩的坑是驱动里写的是“myvendor,gpio-key”设备树里写的是“myvendor,gpio_keys”下划线和连字符的差异导致匹配失败。由于这些字符串都是普通ASCII文本编译器不会报任何错误只有到运行期才会暴露。6.3 排查项三节点是否被正确转换为platform_device确认设备树节点没有disabled、compatible也没问题之后看设备节点到底有没有出现在platform总线上。系统启动完成后执行ls /sys/bus/platform/devices/如果你的设备树节点name是mykey而且位次也正确应该能看到一个mykey的软链接指向形如mykey.1的目录。如果不存在说明节点压根没有被扫描到常见原因包括节点挂在不该挂的总线上比如i2c、spi子节点下、或者它的父节点链路里有个中间节点status是disabled。这里有一个特别容易忽略的点如果节点挂在根节点下它通常会被扫描到但如果挂在你自定义的一个中间bus节点下比如/ { mydevices { compatible simple-bus; mykey { compatible myvendor,gpio-key; }; }; };这里mydevices节点compatible为simple-bus内核会把它当作简单的内存映射总线处理继续扫描下面的子节点mykey会正常生成platform_device。但如果把compatible去掉或者写错内核就不会主动深入扫描子节点导致mykey节点被忽略。6.4 排查项四驱动和设备的注册顺序问题前面提到过Linux设备模型不要求设备先于驱动注册。但有一种情况是例外如果设备依赖的设备还没准备好probe会延迟执行。比如你的mykey使用了某个GPIO控制器而GPIO控制器驱动本身还没probe那么of_get_named_gpio检索GPIO描述符时对应的gpio_chip可能还没注册这是不是会导致匹配失败呢实际上不会。匹配机制本身与GPIO控制器是否就绪无关。匹配只看compatible字符串。但probe阶段申请GPIO时可能会失败因为gpio_desc未分配。这种情况下驱动可以检查返回值然后返回-EPROBE_DEFER内核会把驱动放入延迟probe队列等依赖解决后再重新尝试。这是一个专门设计的机制值得注意。i.MX6ULL上经常遇到I2C触摸屏驱动报错“failed to probe”然后过一会儿又自动probe成功背后就是这个机制在工作。6.5 排查项五驱动是否真的注册成功有一种很尴尬的情况模块insmod报成功了但驱动实际没注册上。可以查看/sys/bus/platform/drivers/目录在加载模块前后的变化ls /sys/bus/platform/drivers/ | grep mykey如果驱动注册成功会在这里出现mykey目录。如果目录存在但设备目录里没有对应的设备说明匹配逻辑有问题。如果目录都不存在说明platform_driver_register调用失败了多半是驱动的module_init和module_exit宏配错、参数错误、符号未导出等编译链接层面的问题。6.6 排查项六of_match_table的termial空指针of_device_id数组的末尾必须有一个全零的哨兵条目sentinel也就是{ /* sentinel */ }。如果漏掉这个内核遍历数组时会越界读取行为不可预期最常见的现象是匹配时随机失败或系统crash。为什么需要哨兵因为内核不知道数组长度只能逐个读取直到碰到一个全零的of_device_id。一个完全置零的of_device_id中compatible为NULL内核检查到就停止遍历。所以写of_match_table时务必在末尾加空条目并且该条目不能被MODELE_DEVICE_TABLE宏包含在内。这个坑在新手代码里出现频率很高。编译不会报错运行期随机表现非常让人抓狂。7. i.MX6ULL平台上的特殊注意事项与进阶扩展7.1 pinctrl子系统与probe的先后关系在i.MX6ULL上大多数设备的引脚复用和电气属性是通过pinctrl子系统管理的。设备树节点中的pinctrl-0属性指向某个配置节点内核在设备匹配之后的probe阶段之前会自动应用这些引脚配置。这个自动应用动作发生在really_probe的早期阶段具体来说是pinctrl_bind_pins函数。它根据设备树节点的pinctrl-names和pinctrl-0配置调用pinctrl子系统的API将引脚设置为指定状态。这就带来一个顺序问题如果你的platform_driver在probe里访问了某个GPIO寄存器而这些引脚还没有被pinctrl配置好读到的寄存器值可能是错误的复位默认状态。解决办法是不要在probe里过早访问GPIO或者确保设备树节点的pinctrl配置正确且被内核正确解析。pinctrl配置失败时通常会在内核日志中出现类似“could not get pin”或“pinconfig failed”的报错。配置引用不当也有可能直接导致probe不执行因为pinctrl_bind_pins失败会被当作probe的致命错误处理。7.2 与GPIO子系统的交互在i.MX6ULL上设备树节点的gpios属性引用如“gpio5 1 GPIO_ACTIVE_LOW”这种三元组。第一项是GPIO控制器节点引用gpio5第二项是该控制器内的引脚编号1表示GPIO5_IO01第三项是低电平有效的标志。当驱动调用of_get_named_gpio解析这个属性时内核会做一次映射将“gpio5 offset 1”转换为全局唯一的GPIO编号。这个编号是动态分配的不同板子可能不同不要把它硬编码到驱动里。驱动拿到全局GPIO号之后必须调用gpio_request或devm_gpio_request申请然后设置方向。申请失败常见原因是该GPIO已被其他驱动占用——比如设备树里两个不同节点引用了同一个gpio引脚。这种情况下第二个节点在probe时会收到-EBUSY或类似错误。7.3 中断与platform_device资源的关系在设备树时代中断资源并不像地址资源那样会被自动填充到platform_device resource数组中。设备树节点里的interrupts属性由中断核心维护驱动最常用的方式是先拿到设备节点句柄然后用irq_of_parse_and_map(np, index)或platform_get_irq(pdev, index)来获取irq号。platform_get_irq实际上会调用irq_of_parse_and_map来完成设备树中断属性的解析。在i.MX6ULL的驱动源码中platform_get_irq更常见因为它不依赖设备树的具体结构不关心OF机制是否存在使代码更通用。需要注意irq号与GPIO号是完全两回事。GPIO号由gpio_chip管理中断号由irq_desc管理。gpio_to_irq()是两者之间的桥接函数i.MX6ULL内部GPIO控制器同时提供了gpio_chip和irq_chip所以能完成转换。如果gpio_to_irq失败多半是GPIO控制器驱动没有初始化irq_chip。7.4 probe中的资源获取顺序建议下面是我在实际项目中一个推荐的probe编写顺序这个顺序能有效减少调试时间先用devm_kzalloc分配私有数据结构体保存必要信息。用platform_get_resource或of_get_named_gpio获取硬件资源。用devm_*系列的申请函数devm_gpio_request、devm_ioremap、devm_request_irq等让内核在驱动卸载时自动释放。注册子系统接口如misc_register、input_register_device等。通过dev_info打印关键资源号方便启动日志定位问题。这个顺序遵循“先获取资源再注册对外接口”的原则。如果先注册接口再获取资源接口可能在使用时访问未初始化的资源造成空指针。使用devm系列的API是一个强烈建议它把资源释放和driver生命周期绑定在一起即使probe中途失败已经申请的资源也会被自动清理避免内存泄漏和重复申请。7.5 浅谈Deferred Probe机制前面多次提到deferred probe这里展开解释一下。当probe函数返回-EPROBE_DEFER时内核不会把这次驱动与设备的匹配视为失败而是将设备放入一个延迟队列同时触发一次“重试驱动扫描”的调度。等待一段时间的延迟或相关子系统事件后通常是其他驱动完成了注册内核会再次尝试对队列中的设备执行匹配和probe。i.MX6ULL上有大量这种依赖关系的例子设备A的GPIO引脚来自gpio5控制器gpio5控制器的platform_driver必须先probe完成注册gpio_chip设备A才能拿到有效GPIO号。I2C触摸屏依赖i2c适配器i2c适配器依赖iomuxc引脚配置。MMC控制器依赖电源管理芯片PMIC提供的稳压器PMIC本身的I2C通信又依赖i2c适配器。所以你在启动日志里经常看到一串“probe deferred”的信息这是正常现象。真正的问题是如果某个设备一直处于deferred状态说明它的某个依赖项始终没有就绪这时候要看完整启动日志找到那个从未成功probe的依赖项链路排查。8. 实际调试技巧从systemtap到sysfs的辅助手段8.1 利用/sys文件系统观察匹配状态匹配成功并且probe成功的platform设备和驱动在sysfs中都有对应的表现可以快速确认状态/sys/bus/platform/devices/包含当前所有platform设备/sys/bus/platform/drivers/包含当前所有platform驱动/sys/bus/platform/drivers/mykey/mykey.1如果驱动和设备成功匹配并且probe成功设备名称会出现在驱动目录下形成一个符号链接用下面命令快速验证驱动是否绑定到了设备ls -l /sys/bus/platform/drivers/mykey/输出中如果出现类似mykey.1的条目就说明绑定成功。还可以查看设备和驱动的modalias信息cat /sys/bus/platform/devices/mykey.1/modalias它的输出在设备树设备上通常是“of:NmykeyTnullCmyvendor,gpio-key”这样的格式字符串中会包含compatible信息调试兼容性时可以一眼看出内核是如何解析设备树compatible属性的。8.2 在内核启动阶段加log如果问题出在内核启动早期insmod阶段根本来不及介入可以在内核启动参数append“loglevel8”来打开调试信息或者直接在platform_match和really_probe函数里暂时使用dev_dbg打印。很多情况下启动阶段的deferred probe原因比较难从普通日志里读到可以打开echo 8 /proc/sys/kernel/printk或者在内核命令行加上“dyndbgfile drivers/base/platform.c p”让动态调试输出platform.c内的调试信息。这种方式比改动内核源码更优雅不需要重新编译内核。8.3 设备树反编译设备和驱动匹配问题很大概率出在设备树内容上。把编译好的dtb反编译成dts再来校验是一种很有效的手段dtc -I dtb -O dts -o /tmp/decompiled.dts arch/arm/boot/dts/imx6ull-myboard.dtb然后重点关注目标节点的compatible、status、pinctrl引用是否正确引用的phandle比如gpio5、iomuxc是否被正确定义。有一次我排查了一整天才发现gpio5节点在dtsi文件中被覆盖成了“disabled”导致所有引用gpio5的节点全部无法正常请求引脚。8.4 模块参数的辅助调试在驱动中加一个模块参数控制是否打印详细资源信息这种办法在调试阶段很实用。比如static bool debug_param; module_param(debug_param, bool, 0644); MODULE_PARM_DESC(debug_param, enable debug output); #define DBG_INFO(fmt, ...) \ do { if (debug_param) printk(KERN_INFO fmt, ##__VA_ARGS__); } while (0)然后在probe的各关键步骤里调用DBG_INFO打印中间变量insmod mykey.ko debug_param1就能看到详细过程。这个办法不需要重新编译内核也不需要额外工具最适合板端快速调试。9. 重新理解设备模型从匹配机制延伸到Linux设备驱动设计思想9.1 一套机制遍地开花理解了Platform匹配机制之后你会发现Linux设备模型的其他总线I2C、SPI、USB、PCI核心逻辑都是相似的设备侧注册device结构体驱动侧注册driver结构体总线负责匹配、probe和remove。换掉的只是设备和驱动的具体类型以及匹配函数的实现细节。比如I2C子系统设备是i2c_client驱动是i2c_driver总线匹配时比较i2c_client的name与i2c_driver的id_table或of_match_table里面注册的兼容字符串。SPI子系统也是同样思路。掌握了platform这一套再去看i2c-imx.c、spi-imx.c这些驱动会感觉轻车熟路。这就是Linux设备模型的威力一旦你理解了“设备-驱动-总线”三位一体的结构整个内核的驱动框架对你来说就变得有章可循。9.2 为什么要有probe这个“中间人”在传统单片机开发中你想操作一个外设直接写这个外设的寄存器初始化函数然后在main函数里调用。这个方式是扁平的代码和硬件资源在同一个编译单元里。Linux驱动引入probe阶段本质上是把“外设初始化”这个动作标准化了。驱动不知道具体板子上有哪些设备设备也不知道驱动代码实现细节只有匹配成功之后双方才通过probe这个“介绍人”正式认识。操作系统可以在合适的时机完成资源分配、电源管理、热插拔处理驱动作者则只需关注probe回调里的业务逻辑。如果你是从单片机开发转过来的这个概念转弯很重要probe不是“初始化函数”它是驱动与设备完成绑定后的通知回调。驱动可能在没有probe的时候被注册到系统中但只有遇到匹配的设备时probe才被调用。9.3 设备树让platform_device“隐于无形”在设备树时代platform_device基本上不需要开发者显式定义。你写一个节点内核自动帮你生成对应设备。这个隐性的过程有一个好处设备的描述与代码解耦换板子不换驱动也有一个坏处调试时不知道设备是否已经生成只能通过sysfs或内核日志去验证。所以我现在的习惯是在每次驱动开发的初期先写设备树节点然后编译烧写启动后先确认/sys/bus/platform/devices/目标存在再开始写驱动代码。先确认“设备侧没问题”再动手写“驱动侧”能省掉大量两方互相猜的排查时间。这套流程简单、笨拙但有效。10. 我的一些沉淀和经验总结Platform设备与驱动匹配机制是Linux驱动开发从入门到进阶的一道分水岭。跨过这道坎你就能读懂内核里上千个platform_driver驱动跨不过去你写出来的代码就只能在特定平台上凑巧能跑。结合i.MX6ULL平台我再总结几条切身经验第一设备树时代的驱动部署优先使用of_match_table配合compatible匹配。不要依赖platform_device的name字段那是旧时代的玩法在设备树平台上很不可靠。第二写of_match_table务必记住末尾哨兵条目。这个习惯伴随整个驱动开发生涯漏掉它在编译期没有任何警告运行期却会让你怀疑人生。第三调试匹配问题遵循固定顺序看status是否disabled看compatible是否逐字符一致看设备是否真的在platform总线上看驱动名称在sysfs里是否出现。顺序不要乱按部就班排查效率最高。第四深入理解of_platform_default_populate这个函数。它不仅决定了哪些设备树节点会成为platform_device也是理解嵌入式Linux启动期间设备扫描机制的关键入口。i.MX6ULL的板级文件mach-imx6ul.c里大量逻辑都是在调整这个过程。第五多读官方驱动源码。drivers/gpio/gpio-mxc.c这种驱动本身就是很好的教材它把platform_driver、of_match_table、devm资源管理、pinctrl调用、中断申请整合在了一个完整的例子里代码量不大但覆盖的知识点很全。如果你正在i.MX6ULL上面写第一个platform驱动除了照抄本文的代码骨架之外我建议你额外做一件事把设备树里某一个成熟外设节点比如uart3的compatible在驱动里改成一个错误值观察启动日志的变化。亲手制造一次匹配失败再亲手修复它比读一百篇博客都更管用。