Linux Platform设备与驱动匹配机制深度解析:从设备树到probe调用
发布时间:2026/9/4 10:05:20 作者:尧图编辑部 阅读量:1,286

我写过不少i.MX6ULL的驱动也带过几个刚入门的同事。发现大家最容易卡住的地方不是GPIO操作不是中断申请而是一个看似基础的概念——Platform设备与驱动到底是怎么匹配起来的。很多人照着教程写了个platform_driver注册完发现probe函数根本不执行也不知道去哪查原因。这篇文章就把Platform总线的匹配机制从头到尾拆开讲清楚结合i.MX6ULL这颗芯片把设备树、驱动模型、匹配顺序这些关键点一次说明白。1. 为什么需要Platform总线机制1.1 Linux设备模型里的“三级结构”要说清楚Platform机制得先从Linux设备模型说起。内核里所有设备、驱动、总线都不是散落的个体而是挂在一条条总线上形成一套完整的树形结构。这套结构的核心是kobject、kset这些底层设施往上依次是device设备、device_driver驱动、bus_type总线三个大件。每条总线维护两条链表一条挂设备一条挂驱动。每当有新设备加入或者新驱动注册总线就会触发一次匹配动作——拿着设备去遍历驱动链表看看有没有“对上眼”的。这种机制最典型的代表是PCI、USB这些热插拔总线设备是物理存在的插上去就能被枚举出来。但问题来了SoC内部的许多外设控制器比如I2C控制器、SPI控制器、UART它们不像PCI设备那样有标准的枚举机制也不支持热插拔。它们焊死在芯片内部地址固定、中断固定没法靠硬件自己去“发现”谁是谁。如果让它们挂到PCI总线上显然不合适如果让它们各自为政、不挂任何总线又没法复用设备模型带来的电源管理、sysfs展示、热插拔事件等通用能力。Platform总线就是为这个场景设计的。它不是物理存在的总线而是内核抽象出来的“虚拟总线”专门用来挂载那些直接集成在SoC内部、地址映射固定的设备。i.MX6ULL上的GPIO控制器、UART、I2C、SPI、看门狗全部走Platform这套机制。1.2 设备树引入后的匹配方式变化早期内核里平台设备通过板级文件arch/arm/mach-xxx/board-xxx.c静态定义代码里一个个填充platform_device结构体然后注册进内核。这种方式维护成本极高换一块板子改个引脚就要重新编译内核。设备树出现后平台设备的定义方式彻底变了。设备树里每个节点都对应一个device内核启动时解析设备树自动为每个节点创建platform_device。驱动那边基本不变还是注册platform_driver。那么问题就集中在设备树里那个节点和驱动里的id_table或者compatible字符串到底怎么对上这正是很多人写驱动时糊里糊涂的地方。有些人只记了套路——设备树里写个compatible驱动里写个of_match_table然后就完事了。至于匹配的优先级、四种匹配方式分别查什么、失败后走哪条路一概不清楚。一旦probe没被调用就不知道从哪下手排查只能反复确认引脚、反复编译设备树试来试去浪费大量时间。2. 参与匹配的核心数据结构2.1 platform_device与platform_driver先看驱动端的数据结构这是每个平台驱动都要用到的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 state); int (*resume)(struct platform_device *); struct device_driver driver; const struct platform_device_id *id_table; bool prevent_deferred_probe; };日常写驱动最少要实现probe和remove两个回调然后把driver成员里的name、of_match_table、pm指针填好最后通过platform_driver_register()注册进内核。设备端那边如果走设备树方式你不用手动构造platform_device内核在启动阶段解析DTS时遇到合适的节点就会自动创建。如果要手动注册可以用这个结构struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; /* ... */ };手动注册的场景在i.MX6ULL上也有但较少主要是一些不在设备树里的虚拟设备或测试设备。绝大多数情况下设备端都是设备树自动生成的。2.2 设备树节点如何转化为platform_device设备树里并非所有节点都会变成platform_device这是关键。转换的前提是节点根路径下有compatible属性同时它的父节点是根节点或者父节点有自己的compatible属性。简单说内核会把那些“看起来像独立设备”的节点逐个转换为platform_device。转换过程中设备树的reg属性会变成platform_device里的resourceinterrupts属性会变成IO resource里的IRQ。这就是为什么probe里可以这样获取硬件信息struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; int irq platform_get_irq(pdev, 0); if (irq 0) return irq;这些resource完全来自设备树节点的属性解析如果节点里没写regplatform_get_resource就会拿到NULL如果不写中断platform_get_irq拿到负值。很多人在probe里一上来就要资源资源拿不到就返回错误但这些错误往往被内核日志轻描淡写地带过了不仔细看根本发现不了。2.3 device_driver里的匹配相关成员device_driver是platform_driver里的内嵌成员匹配相关的字段都藏在这里struct device_driver { const char *name; struct bus_type *bus; struct module *owner; const char *mod_name; bool suppress_bind_attrs; const struct of_device_id *of_match_table; int (*probe)(struct device *dev); int (*remove)(struct device *dev); void (*shutdown)(struct device *dev); int (*suspend)(struct device *dev, pm_message_t state); int (*resume)(struct device *dev); const struct attribute_group **groups; const struct dev_pm_ops *pm; struct driver_private *p; };匹配时重点关注三个字段name、of_match_table、id_table在platform_driver里。其中of_match_table是设备树方式的核心id_table是传统板级文件方式的核心name则是最早的纯字符串匹配方式遗留物。3. Platform设备与驱动的匹配机制深度拆解3.1 匹配的入口与触发时机设备和驱动的匹配动作发生在哪答案是bus_type的match回调。每个总线类型都注册了自己的match函数Platform总线的match函数就是platform_match。触发时机主要有三个设备注册时内核向总线添加一个新设备总线会遍历驱动链表逐一调用platform_match进行匹配。设备树解析创建platform_device时就会走这一步。驱动注册时驱动调用platform_driver_register()注册进内核总线会遍历设备链表逐一用该驱动去匹配每个设备。设备或驱动状态变化导致重新匹配时比如驱动解绑后重新绑定或者设备节点被重新探测。也就是说哪怕设备先存在、驱动后加载也能通过驱动注册这个入口找到设备顺序无所谓最终都会聚合在一起。3.2 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. 尝试OF类型匹配基于设备树compatible属性 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 尝试ACPI匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试platform_driver的id_table匹配 */ if (pdrv-id_table) if (platform_match_id(pdrv-id_table, pdev) ! NULL) return 1; /* 4. 尝试platform_device.name与driver.name直接匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }所以匹配有四种途径按优先级分别是设备树compatible匹配、ACPI匹配、id_table匹配、名字直接匹配。3.3 OF匹配的细节与compatible优先级i.MX6ULL上最常用的就是第一种OF匹配。of_driver_match_device最终会调用of_match_node遍历驱动of_match_table里的每个of_device_id和设备节点的compatible属性逐一比对。of_device_id长这样struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };实际匹配时只有compatible字段真正参与比较。name和type基本被废弃了不要指望靠它们匹配。所以驱动里的of_match_table要这样写static const struct of_device_id my_imx6ull_dt_ids[] { { .compatible myvendor,mydevice, }, { /* sentinel */ } }; static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name mydevice, .of_match_table my_imx6ull_dt_ids, }, };注意of_match_table最后必须有一个空项作为结束标记否则内核遍历时不知道数组在哪结束会越界访问轻则匹配异常重则内核崩溃。设备树那边的节点这样写mydevice: mydevice02000000 { compatible myvendor,mydevice; reg 0x02000000 0x1000; interrupts GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH; status okay; };两个compatible字符串一模一样才能匹配上。很多新手在这里踩坑驱动里写的是“myvendor,mydevice”设备树里写的是“myvendor,mydevice1”或者漏了一个逗号或者大小写不一致直接匹配失败。一个比较实用的小知识compatible支持多字符串匹配。设备树里可以写compatible myvendor,mydevice2, myvendor,mydevice1;驱动of_match_table里只要有一个of_device_id的compatible和其中任意一个相同就算匹配成功。这在实际项目里非常有用一颗芯片可能有多个版本或者同一驱动要兼容多个厂商的芯片就可以把旧版本的compatible保留下来实现驱动向后兼容。3.4 id_table与name匹配的适用场景id_table匹配是传统板级文件时代的主力。platform_device_id结构如下struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };platform_match_id会拿id_table里每个entry的name和pdev-name作比较。判断逻辑是如果pdev-id_entry已经赋值说明设备之前在匹配时已经通过id_table找到了驱动直接用这个缓存的entry否则遍历id_table数组逐个比较。需要特别注意的是id_table匹配的比较对象是pdev-name这取决于设备是怎么创建的。走设备树创建时platform_device的name来自设备树节点的node name而不是compatible。举个例子节点写成mydevice02000000 { compatible myvendor,mydevice; };那么pdev-name是“mydevice”不是“myvendor,mydevice”。而name直接匹配更古老直接比较pdev-name和drv-name。它的优先级最低只有在OF匹配、ACPI匹配、id_table匹配都失败之后才会走到。在设备树时代的实践中它基本只能作为“兜底”存在。还有一种必须提醒的情况如果of_match_table和id_table都为空仅靠driver.name匹配时有个隐藏行为。在platform_match里如果id_table为空且name能直接匹配内核会调用platform_match_id用pdev-id_entry做检查这会导致一个警告或者直接匹配失败。确切地说platform_match函数在走第四步之前会先判断id_table是否为空再判断name是否相等。所以尽量不要只依赖driver.name在设备树时代老老实实写of_match_table最稳妥。我见过一个案例一个老驱动从板级文件迁移到设备树驱动里只保留了driver.name没有of_match_table。结果设备树解析出来的platform_device根本匹配不上这个驱动probe不执行。查了很久才发现是这个问题。后来把of_match_table补上一次就通了。3.5 匹配成功后发生了什么匹配成功后总线会调用device_reprobe或类似的流程最终调用driver的probe函数。对于platform_driver来说这个probe是platform_drv_probe它主要做了几件事将传给它的struct device *转换为struct platform_device *。设置pdev-id_entry如果匹配是通过id_table完成的就把匹配上的entry缓存到设备上。调用platform_driver的probe回调。如果probe执行成功设备和驱动就正式绑定dev-driver被设置随后会在sysfs中创建相关符号链接设备进入正常工作状态。如果probe失败并返回错误码绑定关系会被解除设备保持未绑定状态等待下次匹配机会。4. i.MX6ULL实操从设备树到驱动全流程4.1 硬件场景描述我用一个具体的场景来讲。假设要在i.MX6ULL上外挂一个简单的SPI设备芯片上接了SPI3控制器设备名叫“demo_spi_device”我们要为它写一个平台驱动。整个过程包括修改设备树、编写platform驱动、编译、测试。4.2 设备树编写与匹配验证设备树里除了要被转换的节点本身更重要的是这个节点必须挂在正确的总线上。如果SPI设备挂在SPI控制器的节点下那它会被注册为spi_device而不是platform_device匹配机制走的就不是platform_match而是SPI总线的匹配。这是初学者最容易搞混的点。要模拟Platform设备最简单的做法是直接把设备节点挂在根节点下/ { demo_spi_device: demo-spi-device { compatible myvendor,demo-spi-device; reg 0x02018000 0x1000; interrupts GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH; status okay; }; };这里的reg地址我故意写成SPI3外设控制器的寄存器地址i.MX6ULL上SPI3基址是0x02018000这样probe里platform_get_resource拿到的就是真实的控制器地址空间便于演示。等设备树编译并烧录到板子上启动后在/sys/bus/platform/devices/下就能看到demo-spi-device这个目录说明内核已经成功为它创建了platform_device。注意设备树节点的name是“demo-spi-device”compatible是“myvendor,demo-spi-device”在匹配时pdev-name取的是节点名。4.3 驱动代码拆分驱动代码有三个关键部分probe、remove、driver定义。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/resource.h #include linux/interrupt.h #include linux/io.h static void __iomem *base_addr; static irqreturn_t demo_spi_isr(int irq, void *dev_id) { printk(KERN_INFO demo_spi_device: interrupt triggered\n); return IRQ_HANDLED; } static int demo_spi_probe(struct platform_device *pdev) { struct resource *res; int irq; int ret; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get MEM resource\n); return -ENODEV; } base_addr devm_ioremap(pdev-dev, res-start, resource_size(res)); if (!base_addr) { dev_err(pdev-dev, failed to ioremap\n); return -ENOMEM; } irq platform_get_irq(pdev, 0); if (irq 0) { ret devm_request_irq(pdev-dev, irq, demo_spi_isr, 0, pdev-name, NULL); if (ret) dev_warn(pdev-dev, failed to request irq %d\n, irq); } dev_info(pdev-dev, demo spi device probed, reg%pa\n, res-start); return 0; } static int demo_spi_remove(struct platform_device *pdev) { dev_info(pdev-dev, demo spi device removed\n); return 0; } static const struct of_device_id demo_spi_dt_ids[] { { .compatible myvendor,demo-spi-device, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_spi_dt_ids); static struct platform_driver demo_spi_driver { .probe demo_spi_probe, .remove demo_spi_remove, .driver { .name demo_spi_device, .of_match_table demo_spi_dt_ids, }, }; module_platform_driver(demo_spi_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform device demo driver);这段代码有几个需要留意的点platform_get_resource取的是IORESOURCE_MEM类型的第0个资源对应设备树里的reg属性。设备树里reg写了两个值起始地址和长度内核会自动把这段区域封装成一个resource。devm_ioremap是资源管理的映射函数驱动卸载时会自动释放不需要在remove里手动调用iounmap。用devm系列函数可以大大减少内存和资源泄漏问题。platform_get_irq读的是interrupts属性解析结果可以是硬件中断号。i.MX6ULL的中断控制器是GIC设备树里的中断号经过内核处理在驱动里拿到的已经是Linux中断号。module_platform_driver是个便捷宏展开后就是module_init和module_exit分别调用platform_driver_register和platform_driver_unregister省得手动写。还要注意MODULE_DEVICE_TABLE这个宏。它把of_device_id表导出到模块的符号信息里这样当驱动编译成模块时insmod的工具可以通过modinfo看到这个驱动支持哪些设备。更重要的是有些自动化工具加载模块前会先读这个表来判断模块是否适用于当前硬件。不加这个宏模块也能工作但不够规范。4.4 编译与加载验证把文件放到内核源码目录的drivers/misc/下或者自己的驱动目录修改Makefile然后编译成模块make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules编译成功后得到.ko文件拷贝到开发板加载insmod demo_spi_device.ko刚加载完立刻去dmesg看内核日志demo_spi_device: demo spi device probed, reg0x2018000这行日志说明probe被正确调用匹配成功资源获取也正常。再去/sys/bus/platform/drivers/demo_spi_device/下查看ls /sys/bus/platform/drivers/demo_spi_device/能看到一个指向demo-spi-device的符号链接说明设备与驱动已经完成绑定。如果insmod后没有任何probe日志优先怀疑of_match_table的compatible和设备树节点里的compatible不一致。可以用这条命令确认设备树里实际的compatible值cat /proc/device-tree/demo-spi-device/compatible输出应该是一串带“\0”分隔的字符串肉眼可能看不全可以用hexdump查看hexdump -C /proc/device-tree/demo-spi-device/compatible然后和驱动里的字符串逐字节对比。我遇到过一种情况设备树里写成“myvendordemo-spi-device”用的是中文逗号肉眼几乎看不出来区别但内核匹配时严格按字节比较结果就是匹配失败。这种问题排查起来非常隐蔽只能靠hexdump才能发现。5. 常见问题与排查技巧实录5.1 probe没有被调用这是平台驱动开发里最常遇到的问题没有之一。我的排查顺序是这样确认内核是否真的为设备生成了platform_device。检查/sys/bus/platform/devices/下有没有对应设备的目录。如果没有说明设备树节点没有被解析要么节点写错位置要么父节点没compatible属性导致内核不认为它是个独立设备。确认驱动是否真的注册成功。检查/sys/bus/platform/drivers/下有没有对应的driver目录。确认设备树compatible与of_match_table里的compatible完全一致。在开发板上用hexdump读/proc/device-tree/xxx/compatible对照驱动源码里的字符串。确认驱动不是被内核同名的其他驱动抢占匹配了。有时候设备树节点写得太“泛”比如compatible写成了通用的“simple-bus”被其他驱动匹配走。device与driver是一对一绑定的一旦被别的驱动绑定你的驱动就没机会了。查看dmesg日志搜索platform、probe、fail这些关键词。内核很多时候会打印匹配失败的线索只是容易被大量启动日志淹没。5.2 匹配到了但probe失败返回错误码probe返回非零值设备和驱动仍然不会绑定。常见错误码和对应原因返回码含义常见原因-ENODEV设备不存在platform_get_resource或platform_get_irq拿不到资源-ENOMEM内存不足devm_ioremap失败、devm_kzalloc失败-EINVAL参数无效设备树节点属性格式错误-EBUSY资源忙中断号被其他设备占用、ioremap的物理地址已被映射排查时在probe的每个失败分支加dev_err打印把错误码透出来。但要注意驱动卸载或设备解绑时probe失败可能导致死循环所以devm资源管理函数的好处在这里体现得很明显——probe失败时已经申请的资源会被自动释放不需要手动清理。5.3 __init和__exit宏的注意事项写模块时很多人在probe函数上加__init属性。这有个隐患如果驱动编译成模块__init修饰的函数会在模块加载时放入.init.text段模块卸载时这段内存被释放。但如果驱动不是模块而是编译进内核init函数在系统启动后就会被丢弃如果此时probe被延迟调用比如deferred probe函数已经不存在了就会跳到非法地址内核直接崩溃。所以正确的做法是编译进内核的驱动remove用__exit_p宏编译成模块的驱动probe和remove都不建议加__init和__exit。最稳妥的方式是干脆什么都不加让编译器决定。i.MX6ULL的内核默认开启了deferred probe机制很多外设的时钟、电源依赖其他驱动先完成初始化probe会等待条件满足再触发这时如果probe有__init属性就极其危险。5.4 设备与驱动绑定后如何解绑测试内核提供了sysfs接口可以手动解绑和绑定设备与驱动。这在调试时特别好用# 解绑设备 echo demo-spi-device /sys/bus/platform/drivers/demo_spi_device/unbind # 重新绑定 echo demo-spi-device /sys/bus/platform/drivers/demo_spi_device/bind解绑后设备变为孤儿状态重新绑定时内核会再次调用platform_match。如果设备树里改了compatible而驱动没改或者反过来立刻就能从bind操作的结果看出来。这个技巧在验证设备树修改和驱动匹配时非常高效可以不用反复重启板子。5.5 一个容易被忽略的历史坑name冲突当driver.name和某个platform_device的name相同但of_match_table匹配失败时内核会走到底层的name字符串比较可能“意外”匹配上一个不该匹配的设备。这种问题在旧内核版本上出现过后来内核修复了判断逻辑要求id_table非空才允许pdev-name匹配。但最好还是保持driver.name和节点name、compatible之间的逻辑一致性不要为了省事随便起名。6. 几种匹配方式的选型建议做个对比总结匹配方式依赖数据适用场景优先级OF匹配设备树compatible 驱动of_match_table所有设备树平台最推荐1ACPI匹配ACPI表x86平台居多i.MX6ULL上不用2id_table匹配pdev-name和platform_device_id传统板级文件设备树时代少见3name直接匹配pdev-name和driver.name兜底方案不建议主动使用4在i.MX6ULL开发中我们实际项目里全部使用OF匹配。理由很直白一是芯片定位就是设备树驱动开发官方BSP也都是这么写的二是of_match_table里可以携带data指针通过of_device_id的data字段把不同版本的硬件差异参数直接传给probe代码更干净。比如要支持多个版本的芯片可以这样static const struct of_device_id my_device_ids[] { { .compatible myvendor,device-v1, .data v1_config }, { .compatible myvendor,device-v2, .data v2_config }, { /* sentinel */ }, }; static int my_probe(struct platform_device *pdev) { const struct of_device_id *match; struct my_config *config; match of_match_device(my_device_ids, pdev-dev); if (match match-data) { config (struct my_config *)match-data; /* 根据config里的参数做差异化初始化 */ } return 0; }这套写法在驱动需要兼容多款硬件时非常有用。配合设备树里compatible的多种字符串一块内核镜像就能适配多块板子而不用为了每个板子单独编译驱动。7. 写在最后的心得平台设备与驱动的匹配机制看起来只是内核里一个小小的函数但它背后是Linux设备模型设计思想的集中体现。弄懂了匹配原理再去理解I2C、SPI、USB等其他总线的match逻辑会轻松很多因为它们万变不离其宗——都是device和driver通过bus_type搭桥匹配成功就走probe匹配失败就继续等。对刚入门的同学我的建议是先别急着写驱动把/sys/bus/platform/devices/和/sys/bus/platform/drivers/这两个目录玩熟手动绑定、解绑几个设备看看匹配前后的sysfs变化。这个过程非常直观比读十篇内核源码分析文章都管用。真正在实际项目里踩过坑之后你就会明白设备树里的一个逗号、驱动里一个of_match_table忘记写sentinel、probe函数多加了一个__init这些看似不起眼的细节才是驱动开发中最消耗时间的地方。而这些细节恰恰是代码规范、调试工具、sysfs交互这些“软件工程”层面的基本功。最后再分享一个调试习惯每次修改设备树或者驱动之后我先用dtc -I dtb -O dts反编译dtb确认修改生效再通过ls /sys/bus/platform/devices/确认设备存在最后才加载驱动看probe日志。三步走下来90%的匹配问题都能快速定位。