我最早在i.MX6ULL上调驱动时遇到过一件非常玄学的事驱动代码编成模块insmod之后没有任何报错模块也加载了但probe就是不打印一点日志。当时我以为驱动写错了翻了半天结构体才发现问题出在设备树节点根本没被内核当成platform_device注册上来。从那天起我再理解Platform驱动时都会把“设备”和“驱动”两条线分开看。这篇文章想把 i.MX6ULL Linux驱动开发里这套 Platform设备与驱动匹配机制 讲透不光是给一段能跑的模板而是说清楚设备树里那个节点到底经历了什么驱动里那几张表又是怎么被内核拿去比对的最后还要能解决“为什么我的probe没执行”这种经典问题。内容适合刚接触Linux驱动的初学者也适合已经在写外设驱动但没系统梳理过设备模型的人。只要你有C语言基础、能看懂DTS文件学完这套机制之后再去看NXP官方BSP里那些driver就不会再一头雾水。1. 为什么嵌入式Linux绕不开Platform机制1.1 从一次“probe死活不进”开始先说场景。i.MX6ULL这颗Cortex-A7核心的SoC跑NXP官方BSP或者自己移植的内核都很常见。我把一个外部LED对应的platform_driver编译成.koinsmod之后lsmod能看到模块存在但dev_info里的打印始终没出现。这时候我怀疑的方向是先看“设备”有没有被创建再看驱动对不对。看/sys/bus/platform/devices/下面设备树里我写的节点在不在。如果不在说明dtb没更新或者节点写了但没被内核解析成platform_device。如果在再看/sys/bus/platform/drivers/下面驱动目录里有没有绑定到这个设备。如果驱动目录存在、设备也存在于总线上却始终没有链接关系那问题就100%出在“匹配”这一步。后来我查出来是compatible字符串大小写不一致。设备树里写的是mydev,led驱动of_match_table里写的却是mydev-led当然匹配不上。这里的关键是平台驱动的probe不是靠菩萨显灵调用的它是设备模型里一条“设备-总线-驱动”三方协作的产物。1.2 没有物理总线的外设靠什么被内核组织起来CPU内部的外设比如GPIO控制器、UART、I2C控制器、看门狗并没有一条像PCI或者USB那样真实的物理总线去枚举它们。内核为了把这些设备都纳入统一的设备模型就发明了一条“虚拟总线”叫platform_bus_type。挂在它下面的设备叫platform_device处理它们的驱动叫platform_driver。在老的板级文件时代比如早期的ARM Linux开发者直接在mach-xxx.c里用platform_device_register逐个注册设备资源地址、中断号都硬编码在C代码里。一块板子一套内核换个板子就得重新编译。这种写法到了设备树时代已经被大量取代哪怕i.MX6ULL这种资源相对固定的SoC现在也基本全是设备树方案。理解了这段历史你就明白了Platform机制不是技术炫技而是为了把“硬件长什么样”和“驱动怎么工作”彻底拆开。设备树负责描述硬件资源Platform驱动只负责声明自己能处理什么样的设备然后由总线帮你牵线搭桥。1.3 设备树不是Platform但它们是绝配经常有人把设备树和Platform驱动混为一谈其实它们是不同层面的东西。设备树是一种数据结构描述硬件拓扑和资源比如地址、中断、GPIO、时钟。Platform是Linux设备模型里的一条总线负责设备与驱动的配对。设备树里的节点要变成内核能管理的设备必须靠内核的of_platform层去解析。这个过程大致是内核启动早期通过of_platform_default_populate之类的路径遍历设备树根节点下面的节点遇到合适的compatible节点就创建出platform_device再挂到platform总线上。之后驱动加载时总线机制会尝试让设备和驱动完成匹配。所以在i.MX6ULL的开发流程里这三者其实是一条链先有DTS描述“有个LED在GPIO1_IO03”再有内核解析出platform_device然后你写的platform_driver声明“我能匹配mydev,led”匹配成功才进probe。2. 设备侧设备树节点如何成为Platform设备2.1 compatible属性是设备树里的关键一行设备树每个节点里最核心的属性就是compatible。它是一串字符串格式通常遵循“厂商,型号”的约定比如fsl,imx6ull-uart。这个属性有两个作用一是让内核找到对应的platform驱动二是在系统启动时作为匹配依据。如果你在根节点/下面写了一个自定义子节点里面有compatible属性那么在设备树被展开后这个节点可能会被创建为platform_device。需要注意的是不是所有带compatible的节点都会被丢进platform总线。比如I2C控制器下面的从设备虽然也写在设备树里但它们匹配的是i2c_client驱动机制走的不是platform设备模型。简单记忆SoC内部外设控制器、简单内存映射设备才直接作为platform_device挂在I2C、SPI这类真实总线上的从设备由各自的总线型驱动处理。另外节点是不是生效还要看status属性。如果写成status disabled或者根本没写status而且默认就是disabled那这个设备不会参与匹配。这是新手很容易忽略的点。2.2 在i.MX6ULL上写一个最小设备树节点假设我要在i.MX6ULL上接一个GPIO控制的LED最直接的做法是在imx6ull-14x14-evk.dts或者自己的板级dts里追加内容/ { my_led { compatible mydev,led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpios gpio1 3 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里有个细节属性名写成led-gpios而不是led-gpio是因为内核GPIO子系统在解析时会自动在属性名后面加一个-gpios后缀去查找。如果我在驱动里用devm_gpiod_get(dev, led, ...)它实际找的是led-gpios。反过来如果dts里写led-gpio驱动会拿不到描述符。pinctrl部分的作用是先把引脚复用成GPIO功能。MX6UL_PAD_GPIO1_IO03__GPIO1_IO03这个宏来自NXP头文件不同内核版本可能路径不同但它描述的本质上就是“把GPIO1_IO03这个pad复用为GPIO功能”。宏后面的0x10b0是pad配置值包括上下拉、驱动能力、压摆率等。这些细节确定好后LED设备的node就算写完了。2.3 如何确认你的设备已经上线写完DTS不要急着写驱动先编译dtb替换掉boot分区里的dtb重启后在板子上执行ls /sys/bus/platform/devices/如果看到my_led或者类似节点名的目录说明设备已经成功注册到了platform总线。还有一个更底层的检查方式ls /proc/device-tree/my_led/这是内核解析后的设备树视图能看到里面的compatible、status等属性。如果你在/proc/device-tree里找不到这个节点先别折腾驱动去查dtb是不是真的加载对了。很多情况下uboot环境变量里设置的fdt_file还是旧文件名内核读到的根本不是最新的dtb这种问题不看底层目录根本发现不了。3. 驱动侧三段匹配到底是怎样完成的3.1 一个platform_driver的骨架先把一个最小驱动写出来后面围绕它拆解#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *led; led devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led)) { dev_err(dev, failed to get led gpio: %ld\n, PTR_ERR(led)); return PTR_ERR(led); } gpiod_set_value(led, 1); dev_info(dev, my_led probe ok\n); return 0; } static void my_led_remove(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, my_led remove\n); } static const struct of_device_id my_led_of_match[] { { .compatible mydev,led, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name mydev-led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL);模块加载时module_platform_driver会展开成platform_driver_register的调用。驱动注册完成后内核会拿这个驱动去和总线上所有还没有绑定驱动的设备挨个比对。比对方法就是platform总线注册时设置好的match函数也就是platform_match。3.2 从内核源码看三种匹配方式platform_match的核心逻辑并不复杂不同内核版本略有出入但整体顺序可以理解成下面三步。第一步设备树匹配。如果驱动里设置了driver.of_match_table那么会通过of_driver_match_device把设备的compatible、node名等和设备树匹配表里的条目做比对。i.MX6ULL等现代ARM平台绝大多数driver都走这条路。这也是上面代码里of_device_id数组存在的意义。第二步id_table匹配。如果驱动里有platform_driver.id_table内核会拿设备的name去和id_table里的每个name_entry比对。这个机制主要用在老式板级文件、ACPI或者设备没有被设备树描述的场合。第三步name直接匹配。如果前面都没匹配上内核最后会看platform_device.name和platform_driver.driver.name是不是一样。这是一种很原始的兜底方式在新设备树环境下能不用就不用。以下表格是实际开发时推荐记住的对照匹配方式核心判断字段典型使用场景常见注意事项of_match_table设备树节点compatible与of_device_id.compatible设备树时代、i.MX6ULL等嵌入式平台编译成模块时要有MODULE_DEVICE_TABLE导出别名id_tableplatform_device.name与platform_device_id.name传统板级文件或设备动态创建时指定namename不一致时静默失败日志很难查driver.name驱动名直接等于设备名旧式platform驱动兜底设备树节点生成的设备名往往带基地址容易坑很多驱动新手只填了.driver.name以为设备树compatible一样就行结果死活不probe。原因就在于现代内核默认把设备树匹配放在前面但你既然没有提供of_match_table它就只能退化到name匹配而name匹配很可能因为设备名带地址后缀而失败。3.3 注册时机与probe调用链路还要理解匹配的触发时机。设备和驱动这两个东西出现的时间不一定分先后但platform总线必须保证无论谁晚到都能重新触发匹配。如果驱动先注册设备后加入platform总线那么在设备加入时会去总线上找没有绑定驱动的driver。如果设备先存在驱动后注册那么驱动注册时会遍历所有挂在总线上的设备。PCI、USB这类总线天然支持热插拔Platform设备虽然大多不热插拔但在驱动模型层面依然保留了这套动态配对能力。module加载就属于典型的“设备早就存在、驱动晚到”场景因此insmod后驱动会立刻扫描已有的platform设备找到匹配就调用probe。调用链条大体是这样的platform_driver_register内部会完成driver注册最终触发bus_probe_device然后device_attach会调用bus的match方法确认是否能配对能配对就执行really_probe然后在里面调用platform_drv_probe最终调到你的probe函数。这条链路上任何一环掉链子probe都不会执行。如果驱动编译进内核而不是模块由于DTS解析发生在非常早的initcall阶段驱动注册时设备可能还没创建所以内核会在后续设备创建时再触发“晚到匹配”。这也是为什么设备树驱动的probe时机可能早于initcall也可能晚于initcall但整体对开发者透明。4. 匹配成功之后probe函数与资源获取实操4.1 probe返回值里藏着的门道probe函数返回0表示设备与驱动绑定成功。返回负错误码则表示绑定失败内核会认为设备无人认领。这里有一个特殊返回值-EPROBE_DEFER表示“我需要的某个资源还没准备好请内核以后再试一次”内核会把这个设备放入延迟探测队列稍后重新尝试。这在涉及时钟、pinctrl、regulator这类依赖时非常常见。例如你的驱动probe里要获取一个由其他驱动提供的电源或时钟如果那个驱动还没probe自然拿不到直接返回失败会导致后续永远没机会。正确做法是返回-EPROBE_DEFER让内核在依赖驱动注册后重新触发匹配。在i.MX6ULL上如果SPI、I2C、串口等控制器驱动依赖pinctrl驱动和GPIO驱动那么它们很可能在启动早期被推迟探测。你在dmesg里会看到大量defer消息不要慌那是正常现象。4.2 用platform_get_resource获取寄存器地址与中断匹配只是第一步probe里真正要做的事情是根据硬件需求去拿资源。假设设备树里写了一个简单的内存映射外设my_led { compatible mydev,led; reg 0x020c4000 0x10; interrupts GIC_SPI 87 IRQ_TYPE_LEVEL_HIGH; };驱动里可以这样取资源static int my_led_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq platform_get_irq(pdev, 0); if (irq 0) return irq; dev_info(pdev-dev, base%px irq%d\n, base, irq); return 0; }platform_get_resource做的工作本质上就是从platform_device内部的资源数组里取数据而这些资源数组又来源于设备树解析。你用devm_ioremap_resource而不是手动ioremap好处是资源生命周期跟着设备走内存映射失败时还能自动打印提示probe失败或设备销毁后自动释放。这套机制在设备模型里已经封装得很完善没必要自己裸写。4.3 基于GPIO和时钟的资源获取更建议用devm系列对于GPIO、时钟、中断这类资源内核有比手写ioremap更高级的封装方式。比如前面代码里用的devm_gpiod_get以及devm_clk_get、devm_request_irq。devm前缀表示资源由设备驱动模型自动管理不需要在remove里手动释放。使用devm_gpiod_get时dts里绑定的是xxx-gpios属性驱动的gpiod consumer id不带后缀。拿前面LED来说devm_gpiod_get(dev, led, GPIOD_OUT_LOW)对应的设备树属性就是led-gpios。这种命名规则一开始很容易搞混误写成单数结果返回-ENOENT。另外platform_get_irq在旧内核中返回0表示错误新内核返回负错误码。如果你在不同内核版本间移植驱动这个地方要看一眼返回值判断方式否则可能把有效的中断0误判成错误。5. 匹配失败时的排查与调试经验5.1 先确认“设备是否存在”再看“驱动是否注册”设备与驱动匹配失败时最常见的现象就是没有probe日志。此时我的调试顺序一般是固定的先查设备树节点是否真的生成了platform_devicels /sys/bus/platform/devices/ | grep my_led没有输出说明设备没生成回到dts检查节点位置、status、dtb是否更新。如果设备存在就查驱动是否注册ls /sys/bus/platform/drivers/ | grep mydev驱动目录存在的话再看设备目录下是否出现driver符号链接ls -l /sys/bus/platform/devices/my_led/如果设备存在、驱动存在但设备目录下没有driver链接那就是匹配没成功。这时再去核对compatible字符串是最省时的方法。sysfs里还能看到另外一个线索在/sys/bus/platform/devices/my_led/uevent里通常有OF_COMPATIBLE_0mydev,led这样的内容可以和驱动of_match_table里的字符串做精确比对。5.2 一张速查表从现象到可能原因实际调试中踩坑最多的场景我整理成了一张表现象可能原因先查哪里insmod成功但没有probe日志设备树节点未生成或compatible字符串不一致/proc/device-tree与/sys/bus/platform/devices/dmesg报“no such device”设备节点不存在或status不是okaydtb文件是否更新、uboot环境变量模块自动加载失败缺少MODULE_DEVICE_TABLE导出别名modinfo查看alias信息probe返回-EPROBE_DEFER后成功依赖的clk/pinctrl/gpio驱动还没就绪cat /sys/kernel/debug/devices_deferredioremap失败reg地址或长度与硬件实际不符设备树reg属性和芯片手册对照gpiod_get返回-ENOENTdts属性名没有以-gpios结尾otaku的命名规则对照设备树节点名和驱动名都对但不probeof_match_table没填或填充错误驱动结构体里的driver.name别写错这张表是我调驱动时反复用到的。系统给你报的错往往很晚最靠谱的方法还是从设备和驱动两条线分别确认状态。5.3 name匹配绕不开的“地址后缀”问题在老的platform驱动写法里常见到有人只设置.driver.name希望能和设备节点名匹配上。但在现代ARM设备树环境下很多platform_device的设备名并不等于你在dts里写的节点名而是会带上基地址。比如i.MX6ULL的UART节点如果在dts里叫serial02020000那生成的platform_device在sysfs里很可能是2020000.serial这种形式。如果你驱动的name写的是serial而设备名是2020000.serial那么最朴素的name匹配方式就会失败。所以现代驱动里做设备树匹配一定优先写of_match_table把compatible fsl,imx6ull-uart这种字符串填进去。这个教训我反复提过很多次因为它真的太容易踩。5.4 deferred probe与你必须了解的MODULE_DEVICE_TABLE再补充两个容易被忽略的细节。第一个是deferred probe。有时候驱动probe不是没执行而是执行后返回了-EPROBE_DEFER。你看dmesg可能看到类似“probe deferral”的信息但默认日志级别下并不显眼。这时候可以打开内核的debugfscat /sys/kernel/debug/devices_deferred这个文件会列出当前处于defer状态等待重试的设备。如果你要调试的驱动一直卡在这个文件里说明它依赖的某个资源始终没有就绪需要先解决上游依赖的问题而不是在probe里加打印埋头苦找。第二个是模块自动加载。如果你的platform驱动以模块方式编译并且希望通过设备树里的compatible让系统自动加载模块那么必须在of_match_table之后加上MODULE_DEVICE_TABLE(of, my_led_of_match);这行宏会把of_device_id表导出到模块的alias信息里modinfo就能看到类似alias: of:N*T*Cmydev,led的条目udev和modprobe才能通过设备树提供的MODALIAS环境变量自动装载模块。很多人省了这行结果发现每次都要手动insmod这就是原因。6. 一些实际操作经验项目做多了之后我对“驱动不probe”这类问题的判断速度比以前快了很多。核心习惯就一条先看设备再看驱动最后才看代码逻辑。因为设备没生成、驱动没注册、匹配没成功、probe运行时报错这四类问题的表现可能都是“看不到预期输出”但如果一开始就钻进probe代码里加打印很容易白忙活。先从sysfs和设备树目录确认前面三层都正常再往下查会显著减少排查时间。再分享一个我在i.MX6ULL上调驱动时养成的小习惯调试阶段会把of_match_table里的compatible写成一个非常独特的字符串比如mydev,led-test-2024然后在设备树里也写成同样的值这样能避免跟BSP里已有的设备驱动产生冲突。确认probe可以正常进入后再改成正式的compatible。这个习惯帮我避免了好几次“匹配到了别人家的驱动”这种迷惑问题。Platform设备与驱动匹配机制本身不复杂它就是设备模型里一次很规律的比对过程。真正复杂的往往是设备树解析、资源依赖、模块加载时机这些外围因素。把“设备-总线-驱动”这根主线的思路理清了i.MX6ULL平台上的大多数驱动移植和调试问题都能顺着这条线找到答案。
