资讯动态

i.MX6ULL设备树与Platform驱动匹配机制详解

发布时间:2026/9/6 10:11:15 来源:尧图企业网站定制
上周帮朋友调一块 i.MX6ULL 的板子遇到一个很典型的问题驱动代码 insmod 进去之后dmesg 干干净净probe 根本没跑。查了半天最终问题出在设备树节点 compatible 跟 of_match_table 里写的不一致。这类问题在嵌入式 Linux 驱动开发里太常见了尤其对刚接触 i.MX6ULL、刚开始写 platform 驱动的人来说往往盯着驱动代码看半天很难想到是设备树描述和驱动之间的“配对规则”出了问题。今天这篇就把 i.MX6ULL 上的 Platform 设备与驱动匹配机制彻底讲清楚。什么是 platform bus、设备树节点怎么变成 platform device、compatible 是怎么参与匹配的、probe 不执行时应该怎么排查都会用实际代码和调试命令串起来。适合正在学 Linux 驱动开发、或者做 i.MX6ULL 项目但被“设备树对上了却 probe 不了”折磨过的同学。看完之后你至少能快速定位 80% 的设备与驱动匹配问题。1. i.MX6ULL 上为什么到处都在说 Platform 总线1.1 设备模型的“总线-设备-驱动”三角关系Linux 内核的设备模型核心其实就三个角色bus、device、driver。bus 是纽带device 是硬件实体driver 是操作逻辑。当一个 device 和一个 driver 挂到同一条 bus 上bus 负责判断它们“配不配”配上了就调用 driver 的 probe 函数让驱动接管设备。这个模型有点像招聘平台的匹配机制候选人driver发布简历公司device发布岗位平台bus根据关键词撮合。关键词对上了双方便开始面试probe。如果你理解了这层比喻后面所有匹配规则都不会跑偏。在 Linux 里总线不止物理意义上的 USB、PCI、I2C、SPI还有一种非常重要的虚拟总线叫 platform bus。它不是真实存在的硬件总线而是内核为了方便管理那些“没有专门总线承载的设备”而抽象出来的容器。i.MX6ULL 是 NXP 的 Cortex-A7 系列 SoC内部大量外设如 GPIO、UART、I2C 控制器、SDIO 控制器、PWM 等在设备模型里往往都是通过 platform bus 来组织。你在 i.MX6ULL 上写驱动绕不开 platform_driver 和 platform_device。1.2 设备树成为“设备清单”之后platform_device 从哪来早期内核没有设备树每个开发板都要在 C 代码里手动注册 platform_device比如调用 platform_device_register 把设备资源写死。这种方式可移植性差换一块板子就要重新编译内核。现在的 i.MX6ULL 内核默认使用设备树开发板启动时由 bootloader 把 dtb 传给内核内核解析设备树后动态生成大量 platform_device。所以你在 i.MX6ULL 的 dts 文件里看到的每一个外设节点例如uart1、i2c2、gpio1内核启动阶段都有可能被转换成 platform_device 对象挂到 platform bus 上等待驱动。驱动则通过 compatible 字符串与其配对。值得注意的是并非所有设备树节点都会变成 platform_device。I2C 总线下的从设备节点会被 I2C 核心解析成 i2c_client而不会生成 platform_deviceSPI 总线下的设备同理。platform 总线主要负责“片上设备”和“虚拟设备”以及那些没有专用总线的设备。i.MX6ULL 内部的 GPIO-LED、蜂鸣器、自定义逻辑外设用 platform_driver 驱动是完全正确的选择。1.3 没有设备树的老派写法手动注册 platform_device很多刚入门的朋友看旧书或者老源码会看到这种写法static struct resource beep_resources[] { { .start 0x020C4000, .end 0x020C4003, .flags IORESOURCE_MEM, }, }; static struct platform_device beep_device { .name imx6ull-beep, .id -1, .num_resources ARRAY_SIZE(beep_resources), .resource beep_resources, }; static int __init board_beep_init(void) { platform_device_register(beep_device); return 0; }配合驱动注册时指定.driver.name imx6ull-beep匹配逻辑很简单设备名字和驱动名字一样就配对。但在 i.MX6ULL 的现代内核中这种方式已经很少用设备树才是主流。不过理解它有个好处你会明白 platform_match 除了设备树匹配之外还保留了 name 匹配作为兜底。2. 设备与驱动到底怎么“对上眼”platform_match 的完整规则2.1 内核源码级别的判断顺序Platform 总线的匹配逻辑集中在 drivers/base/platform.c 的 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. driver_override 强制指定时只和指定的驱动匹配 */ if (pdev-driver_override) return platform_match_override(pdev, pdev-driver_override, drv); /* 2. 首先尝试设备树风格匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 3. 然后尝试 ACPI 风格匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 4. 接着尝试驱动 id_table 匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 5. 最后退回到最简单的 name 匹配 */ return strcmp(pdev-name, drv-name) 0; }这段代码的核心思想是优先用设备树描述信息匹配因为设备树信息最精确ACPI 主要用在 x86/ARM server 平台i.MX6ULL 上用不到id_table 是纯 platform 驱动时代留下来的匹配方式name 匹配是最朴素的兜底方案。2.2 设备树下 compatible 是第一关键字在 i.MX6ULL 设备树中外设节点都有 compatible 属性例如uart1 { compatible fsl,imx6ul-uart; ... };驱动的 of_match_table 里声明它支持的 compatiblestatic const struct of_device_id uart_of_match[] { { .compatible fsl,imx6ul-uart }, { /* Sentinel */ } }; static struct platform_driver uart_driver { .probe uart_probe, .driver { .name imx6ul-uart, .of_match_table uart_of_match, }, };设备树节点的 compatible 字符串和 of_device_id 中的 compatible 字符串必须完全一致差一个字符都不行。这一点看起来简单却是实际开发中踩坑最多的地方。有的板子 dts 里写的是myir,imx6ull-uart驱动里写的是fsl,imx6ull-uart结果自然匹配不上。我见过的很多 probe 不执行案例最后都会归到这个原因。2.3 id_table 和 driver.name 的兜底逻辑如果设备树匹配不成功platform_match 会看 platform_driver 有没有提供 id_table。id_table 是 platform_device_id 数组static const struct platform_device_id beep_id_table[] { { imx6ull-beep, 0 }, { } }; static struct platform_driver beep_driver { .probe beep_probe, .driver { .name imx6ull-beep, }, .id_table beep_id_table, };id_table 中的 name 会与 platform_device 的 name 比较。设备树生成的 platform_devicename 通常是设备树节点名。所以如果你的设备树节点是beep那么 id_table 里的字符串也要写beep而不是驱动名imx6ull-beep。如果没有 id_table最后一步就是直接比较pdev-name与drv-name。这种匹配方式对设备树生成的设备通常不太可靠因为节点名往往比较短驱动名往往带前缀。所以现代内核中设备树环境下的 platform 驱动应当优先使用 of_match_table。2.4 driver_override强制指定的“内推通道”linux 内核还留了一个强制匹配接口 driver_override写在 sysfs 里echo imx6ull-beep /sys/bus/platform/devices/beep/driver_override echo beep /sys/bus/platform/drivers/imx6ull-beep/bind第一条命令把设备 beep 的匹配范围强制限定为 imx6ull-beep 这个驱动第二条命令手动触发绑定。调试阶段可以用它验证驱动或设备是否异常但它属于“走后门”的做法正式产品里不要依赖。如果 driver_override 存在platform_match 会直接忽略 compatible、id_table、name 这些规则只检查名字是否对得上。3. 从 dts 到 probei.MX6ULL 设备树节点如何变成平台设备3.1 simple-bus 与设备树解析机制设备树不是一上来就把所有节点都变成 platform_device 的。内核在启动阶段调用 of_platform_default_populate 遍历设备树它会为符合条件的节点创建 platform_device。这里的判断规则和节点的 compatible 有关。如果节点 compatible 中包含simple-bus内核认为这个节点是一条“简单总线”会递归去创建它下面的子节点。i.MX6ULL 的 imx6ull.dtsi 里soc 节点以及它下面的 aips1、aips2、aips3 节点都是 simple-bussoc: soc { compatible simple-bus; #address-cells 1; #size-cells 1; ranges; ... };所以 soc 节点下面的外设节点基本都会生成 platform_device。如果你把自定义设备节点放在根节点/下面它作为根的直接子节点在初始化时也会被尝试创建。这就是为什么很多 i.MX6ULL 开发板的 led、beep、按键设备树节点都直接写在根节点下。反过来如果你把自定义节点放在一个既不是 simple-bus、又不会被对应总线子系统识别的父节点下内核可能根本不会为它创建设备probe 自然不可能触发。这是“设备树节点存在但设备不存在”的常见原因。3.2 一个自定义蜂鸣器驱动的完整实现以一个 i.MX6ULL 蜂鸣器为例设备树节点放在根下/ { beep { compatible embedfire,imx6ull-beep; pinctrl-names default; pinctrl-0 pinctrl_beep; beep-gpio gpio5 1 GPIO_ACTIVE_HIGH; }; }; iomuxc { pinctrl_beep: beepgrp { fsl,pins MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x17059 ; }; };注意这里的 pinctrl 引用了 iomuxc 节点的 pin 配置实际引脚宏和电气属性值需要根据具体板子修改。设备树里声明了 beep-gpio 属性驱动在 probe 阶段通过 gpiod 接口获取。驱动侧代码#include linux/module.h #include linux/platform_device.h #include linux/gpio/consumer.h #include linux/of.h #include linux/of_device.h static int beep_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *beep_gpio; beep_gpio devm_gpiod_get(dev, beep, GPIOD_OUT_LOW); if (IS_ERR(beep_gpio)) { dev_err(dev, failed to get beep gpio: %ld\n, PTR_ERR(beep_gpio)); return PTR_ERR(beep_gpio); } dev_info(dev, beep probe ok\n); return 0; } static int beep_remove(struct platform_device *pdev) { dev_info(pdev-dev, beep removed\n); return 0; } static const struct of_device_id beep_of_match[] { { .compatible embedfire,imx6ull-beep }, { /* Sentinel */ } }; MODULE_DEVICE_TABLE(of, beep_of_match); static struct platform_driver beep_driver { .probe beep_probe, .remove beep_remove, .driver { .name imx6ull-beep, .of_match_table beep_of_match, }, }; module_platform_driver(beep_driver); MODULE_LICENSE(GPL);编译并把 .ko 文件拷到板子上insmod 之后就能看到beep probe ok的打印。这套代码是 i.MX6ULL 上最标准的 platform_driver 框架。3.3 status、reg、pinctrl 对 probe 的影响设备树节点有三个属性会直接影响 probe 是否执行。status 属性设备树节点如果设置status disabled内核不会为它创建设备。有些板子默认把某些外设节点设为 disabled如果你想启用需要在板级 dts 里显式改为status okay。自定义节点如果不写 status默认就是 enabled。reg 属性对于需要映射寄存器地址的设备reg 里定义地址和大小。probe 里一般通过 platform_get_resource 或 of_iomap 获取。如果 reg 格式有问题设备可能注册失败或者 ioremap 失败导致 probe 返回错误。i.MX6ULL 的 dtsi 里 aips 总线下的外设节点都有 reg 属性地址不能与其他节点冲突。pinctrl 属性i.MX6ULL 的引脚复用由 pinctrl 子系统管理。设备树中通过 pinctrl-0 引用 pin 配置节点。内核在调用 probe 之前会自动将设备切换到 default 状态也就是会自动配置好引脚复用和电气属性。如果 pinctrl 配置无效设备可能进入 EPROBE_DEFERdefer 到 pinctrl 驱动准备好后再重新尝试 probe。3.4 EPROBE_DEFER 与依赖设备的先后问题beep 设备依赖 gpio5 控制器、iomuxc 控制器。启动时设备树的解析顺序不保证这些依赖设备一定先完成 probe。如果 beep 设备的 probe 里调用 devm_gpiod_get 时 GPIO 控制器还没就绪内核会返回 EPROBE_DEFER。这不是错误而是内核的一种延迟重试机制。遇到 EPROBE_DEFER设备会被加入 deferred list等 GPIO 控制器、pinctrl 控制器等依赖设备 probe 完成后内核会再次尝试 probe beep 设备。所以你在开发自定义驱动时看到 dmesg 里有 probe defer 的 log 不必慌它往往只是说明当前依赖还没有就绪。4. probe 不执行时的完整排查链路4.1 先确认“设备有没有”sysfs 里的证据遇到 probe 不执行不要急着改驱动代码先确认 platform_device 是否真的生成了。在板子上执行ls /sys/bus/platform/devices/ | grep beep如果没有任何输出说明设备树节点没有变成 platform_device问题在设备树解析阶段。这时继续检查内核是否真的加载了新设备树ls /sys/firmware/devicetree/base/ | grep beep这个目录是设备树的运行时视图。如果这里都看不到 beep 节点说明 dtb 没更新bootloader 加载的还是旧设备树文件。这是很常见的低级坑我遇到过好几回。如果 /sys/firmware/devicetree/base 能看到 beep 节点但 platform devices 下看不到说明节点所在的位置不被 platform 总线解析。检查父节点是否是 simple-bus或者节点是否被其他总线子系统接管了。4.2 再确认“驱动有没有”日志与注册状态设备节点存在接下来看驱动是否成功注册。module_platform_driver 会帮我们调用 platform_driver_register注册成功后驱动目录会出现在ls /sys/bus/platform/drivers/imx6ull-beep/如果这个目录都不存在说明驱动没有注册成功常见原因是 insmod 时依赖的 symbol 没找到、内核配置禁用了相关模块、或者编译插入时版本不匹配。驱动注册成功但设备没有被绑定可以看 dmesgdmesg | tail -50如果 dmesg 有类似platform beep: Driver imx6ull-beep requests probe deferral的信息就按 EPROBE_DEFER 的方向查。也可以直接查看 debugfs 里的 deferred 列表cat /sys/kernel/debug/devices_deferred这里会列出所有处于 defer 状态的设备和原因对调试非常有用。4.3 匹配失败的真实案例compatible 差一个字符一次调试中设备树里写的是compatible embedfire,imx6ull-beep;驱动里写的是{ .compatible embedfire,imx6ul-beep },imx6ull 和 imx6ul 只差一个 lprobe 就是跑不了。这种错误最难查因为代码看起来完全正常设备节点也在驱动也注册了但你手动去对比字符串才能发现差异。匹配失败最直接的验证方法是在内核里开启设备模型调试信息或者用 tracepoint。更简单的方式是写一个测试驱动打印传入的 compatiblestatic int beep_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; const char *compat NULL; of_property_read_string(np, compatible, compat); dev_info(pdev-dev, compatible: %s\n, compat); return -ENODEV; }故意让 probe 返回失败但先打印设备树实际的 compatible。这样就能快速确认设备树侧的真实字符串。当然更常见的做法是直接用 dtc 反编译设备树确认。还有一个容易忽略的地方of_match_table 数组最后一个元素必须是空哨兵。如果数组结尾没有{ /* Sentinel */ }遍历可能会越界导致匹配行为不可预测。编译时不报错运行时就出诡异问题。匹配失败问题定位清单检查项命令/方法说明设备树节点是否存在ls /sys/firmware/devicetree/base确认 dtb 已更新设备是否生成ls /sys/bus/platform/devices确认节点解析成功驱动是否注册ls /sys/bus/platform/drivers确认驱动注册成功是否 defercat /sys/kernel/debug/devices_deferred确认依赖是否就绪compatible 是否一致对比 dts 与 of_match_table差一个字符都不行5. 匹配成功后 probe 里的事资源获取与生命周期管理5.1 devm_ 系列 API 为什么是首选设备匹配成功只是开始真正的工作在 probe 里。现代内核推荐使用 devm_ 开头的资源管理接口例如 devm_gpiod_get、devm_ioremap_resource、devm_platform_get_and_ioremap_resource、devm_kmalloc。devm 的意思是“设备生命周期管理”。这些资源在设备与驱动解绑或设备销毁时自动释放不需要手动调用对应的 free 函数。对于嵌入式驱动来说这能减少很多内存泄漏和资源泄漏问题。我在 i.MX6ULL 上用 devm_gpiod_get 申请 GPIO卸载模块时不需要手动调 gpiod_put简化了很多。5.2 从设备树读取 GPIO、reg、中断资源读取 GPIO 属性已经在蜂鸣器例子里演示过。读取寄存器地址用 devm_platform_get_and_ioremap_resource 最方便struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base);对于 i.MX6ULL 上有些外设没有专门子系统驱动、需要直接操作寄存器的情况这种写法最常用。读取中断则用 platform_get_irqint irq platform_get_irq(pdev, 0); if (irq 0) return irq;设备树里对应的节点需要配置 interrupt 属性这样中断号会自动从设备树解析出来不需要硬编码。probe 里也可以直接通过 of_node 读取自定义属性u32 val; ret of_property_read_u32(pdev-dev.of_node, max-speed, val); if (ret) dev_warn(pdev-dev, max-speed not found\n);这种自定义属性可以存放设备的差异化配置比如同一块板子的不同版本使用不同参数通过设备树灵活调整不用重新编译驱动。5.3 remove 与热插拔场景下的释放逻辑设备模型中remove 函数负责在设备移除时做清理。对于 devm 管理的资源remove 里甚至什么都不用做内核会统一释放。但要注意如果驱动里有自己创建的 class、字符设备节点、内核线程、中断等非 devm 资源必须在 remove 里手动清理。在 i.MX6ULL 这类嵌入式平台上大多数外设焊在主板上不会真的热插拔。但驱动卸载时 remove 一样会被调用。如果你用了 devm_ 系列资源释放顺序由内核保证建议能 devm 就 devm不要自己维护一堆 release 逻辑。内核在 device_release 阶段会按注册顺序的反序释放资源这个机制用好了能省很多事。还有一个细节probe 返回非零值并不代表设备对象销毁。对于内置编译的驱动probe 失败后设备仍然存在于 platform bus 上只是没有驱动绑定。只有驱动被卸载、或者设备被注销时设备才会真正销毁。搞懂这个生命周期你就明白为什么有些模块反复 insmod/rmmod 会出现奇怪状态。最后再分享两个小经验第一个是调试阶段多利用 sysfs 而不是反复改代码看 dmesg。每次修改设备树后先用ls /sys/firmware/devicetree/base和ls /sys/bus/platform/devices检查设备树是否生效再决定要不要动驱动代码。这套习惯能帮你节省大量时间。第二个是 compatible 命名尽量遵守厂商,设备名的规范驱动和 dts 两端同时维护一个公共头文件或者文档避免手滑写错。我在实际项目里会专门写一个设备树 compatible 和 of_match_table 对照表每次新增外设时先更新这张表再写代码。排查问题的时候这张表往往能让你一眼发现是哪边写错了。i.MX6ULL 的 Platform 匹配机制说到底就是一套“设备树是数据、驱动是方法、总线是中间人”的模型。compatible 匹配通了probe 就水到渠成匹配不通按上面四个步骤排查基本都能定位到源头。希望这些经验对你有帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价