资讯动态

Linux Platform驱动开发:设备树匹配与probe机制详解

发布时间:2026/9/7 13:56:49 来源:尧图企业网站定制
我接手过一个项目板子的i.MX6ULL上挂了一个自定义SPI接口的传感器同事写好驱动模块insmod后系统日志干干净净什么都没发生。明明编译没报错加载也提示成功可他的probe函数从头到尾就没被调用过。查了整整一下午最后发现是compatible字符串少了一个逗号后的空格。这类的坑在Platform驱动开发里几乎人人都会踩一遍。如果你正准备入门嵌入式Linux驱动开发或者已经写过几个字符设备驱动、但一遇到“Platform设备与驱动匹配机制”就发怵那这篇文章就是写给你的。我会用i.MX6ULL这颗NXP的Cortex-A7处理器作为载体从Platform总线到底层匹配的四种方式从设备树节点到probe回调的完整链路再到我实际排错的经验一次性讲清楚。1. Platform总线的存在逻辑为什么SoC内部设备不能直接套用PCI那套机制1.1 传统总线模型与SoC集成外设之间的矛盾Linux设备模型里bus总线负责把device和driver牵到一起。PCI、USB这类总线有一个显著特点设备是“可枚举”的设备自己会报告“我是谁”总线扫描就能找到它驱动可以通过厂商ID、设备ID来自动匹配。PC上插一张网卡内核能自动识别靠的就是PCI配置空间里的Vendor ID和Device ID。但i.MX6ULL这种嵌入式SoC内部的UART、I2C控制器、GPIO控制器不是这样。这些外设焊死在SoC内部地址固定、中断固定不会自己说话没有枚举机制。内核怎么知道这颗芯片上有几个串口、它们的寄存器地址在哪只能靠板级代码或设备树硬编码描述。早期内核的做法是在arch/arm/mach-xxx下的C文件里用platform_device_register()一个设备一个设备注册。这种做法把平台硬件信息写死在代码里。后来设备树Device Tree成为主流硬件描述从C代码中彻底剥离只需要一个dts文件描述硬件同一份内核镜像就能适配不同板卡。但无论硬件描述如何变化Linux内核都需要一种统一的抽象将这些“挂在CPU总线上的集成外设”纳入标准设备模型中管理。1.2 Platform设备的本质看不见的虚拟总线Platform不是硬件意义上的物理总线它是一条虚拟总线。内核为这类设备创建了虚拟的platform_bus将每个SoC集成外设抽象成一个platform_device再将对应的驱动程序抽象为platform_driver。platform_bus的作用就是实现device和driver的匹配与绑定。在i.MX6ULL的驱动开发中你见到的绝大多数设备驱动底层都是platform_driver。比如drivers/pinctrl/freescale/pinctrl-imx6ul.c、drivers/clk/imx/clk-imx6ul.c、drivers/tty/serial/imx.c它们都是platform驱动。理解了这个机制整个i.MX6ULL的驱动框架就解开了一大半。1.3 device、driver、bus三者的配合关系在内核源码里它们的关系可以这样看platform_driver_register()向内核注册一个platform_driver设备树被解析后内核为每个节点生成对应的platform_deviceplatform_bus的匹配逻辑判断两者是否匹配匹配成功调用驱动中实现的probe函数。用大白话讲platform_device是“硬件有什么”platform_driver是“我能驱动什么”platform_bus的匹配机制就是“红娘”。红娘牵线成功才轮到probe里的代码去初始化硬件、注册字符设备。我见过不少初学者试图绕过这一套直接在驱动文件里用ioremap硬映射寄存器来做初始化。不是不行但完全脱离了内核的设备模型无法利用设备树、无法管理电源域、无法与内核统一框架配合做一个Demo可能够用做产品没有人会这么写。2. 匹配机制的底牌四种匹配方式的原理与优先级2.1 设备树compatible匹配现代内核的绝对主角在支持设备树的平台最重要的匹配依据是设备树节点里的compatible属性。内核会检查 platform_driver 的of_match_table实际类型是struct of_device_id数组把其中的compatible字符串与设备树节点的compatible字符串逐个比对。以i.MX6ULL的UART驱动为例驱动文件中会定义static const struct of_device_id imx_uart_dt_ids[] { { .compatible fsl,imx6ul-uart, .data imx_uart_data, }, { .compatible fsl,imx7d-uart, .data imx_uart_data, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx_uart_dt_ids); static struct platform_driver imx_uart_platform_driver { .probe imx_uart_probe, .remove imx_uart_remove, .driver { .name imx-uart, .of_match_table imx_uart_dt_ids, }, };你可以用of_match_ptr()宏包一层。在支持设备树的配置下它展开为of_match_table在不支持的配置下变成NULL用来做条件编译兼容。现代内核基本强制依赖设备树这个宏使用场景少了很多但老驱动里经常见。设备树节点的写法是这样的uart1 { pinctrl-names default; pinctrl-0 pinctrl_uart1; status okay; };UART驱动中只关心compatible和status两个属性。compatible fsl,imx6ul-uart与驱动里的fsl,imx6ul-uart对上且status不是disabled匹配就成立。这里有一个经常被忽视的细节MODULE_DEVICE_TABLE(of, imx_uart_dt_ids)看起来不起眼但它生成的模块别名在驱动模块化加载时非常重要。如果你把驱动编成模块.ko没有这一行modprobe就无法根据设备树中的compatible自动加载对应的模块只能手动insmod。产品发布时这个差别直接决定系统能否自动化挂载驱动。2.2 id_table匹配老代码习惯与新的应用空间在设备树普及前驱动通过platform_driver_id_table里的name与platform_device的name进行匹配。即使现在这种机制依然存在因为很多平台设备并不是从设备树生成的而是其他驱动在运行过程中动态创建的。platform_device有一个name字段platform_driver的id_table中也有name字段。只要两者相等就可以匹配。如果驱动没有提供id_table内核会退而使用platform_driver.driver.name与设备名比较。这就是struct platform_driver里.driver.name的另一个用处。举个例子你要在驱动里动态创建一个platform设备struct platform_device *pdev; pdev platform_device_alloc(my-test-device, -1); platform_device_add(pdev);驱动要匹配它有两种写法写法一提供id_tablestatic const struct platform_device_id my_test_ids[] { { .name my-test-device, }, { } }; static struct platform_driver my_test_driver { .probe my_test_probe, .driver { .name my-test-device, }, .id_table my_test_ids, };写法二不提供id_table直接用driver.name。这种情况下.id_table为NULL内核就直接比较pdev-name和drv-driver.name。我在写MISC驱动和虚拟设备驱动时经常会依赖这种name匹配方式。比如一个提供用户态交互接口的测试驱动设备节点由另一个驱动创建两边约定好名字就行不需要干扰设备树。2.3 ACPI匹配特定平台才会用到的分支这一项在x86平台比较重要。当系统固件通过ACPI描述硬件时ACPI表中的HID/CID会与驱动中的acpi_match_table进行匹配。但i.MX6ULL这片市场基本不会出现ACPI它属于设备树阵营。知道有这回事面试时被人提起不至于懵住。2.4 driver_override强制指定的特殊通道driver_override是platform设备模型里一个比较有意思的字段它允许你强制指定某个platform设备必须由哪个platform_driver来绑定跳过所有默认匹配规则。操作入口是sysfs中的driver_override文件echo my-char-driver /sys/bus/platform/devices/my-device/driver_override echo my-char-driver /sys/bus/platform/drivers/my-char-driver/bind这种需求在驱动调试与故障排查中偶尔遇到比如设备树compatible写错了但你又不想重新编译设备树或者设备树烧进evk镜像里一时无法修改可以用driver_override硬性指定驱动名来验证。它在产品发布中不常见但在实验环境是个很有用的逃生通道。2.5 优先级内核源码里的顺序platform_bus匹配时匹配函数的调用关系大致是如果驱动设置了driver_override强制匹配该值检查ACPI匹配检查设备树的compatible匹配检查id_table匹配检查driver.name与设备名的匹配。也就是说设备树compatible并不是第一优先但在i.MX6ULL上它仍然是绝大多数驱动匹配的实际路径。了解优先级顺序排错时就能快速判断为什么我改了id_table不生效可能是因为设备树compatible先命中了。3. 从设备树到probe回调手写一个完整的Platform驱动3.1 设备树节点设计假设要为i.MX6ULL板卡新增一个虚拟传感器设备我给它分配了一段片选空间和一条中断线。设备树节点的写法如下mychar-sensor { compatible example,mychar-sensor; reg 0x0209c000 0x4000; interrupts GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH; status okay; };注意这个节点直接放在了根节点下reg地址是i.MX6ULL上的一段空闲内存映射区。实际项目中更规范的做法是把它挂在某个总线节点下并且使用pinctrl来配置引脚复用。我这里为了演示platform匹配机制取最简单的形态。编译设备树把它加载到板子上。启动后进入/sys/bus/platform/devices/你应该能看到一个名为mychar-sensor的目录。如果你看不到说明设备树没生效或者内核没解析到这个节点。3.2 platform_driver的核心结构驱动程序的核心很简单就做三件事填充of_device_id匹配表实现probe实现remove。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/io.h #define MYCHAR_SENSOR_BASE 0x0209c000 #define MYCHAR_SENSOR_SIZE 0x4000 static void __iomem *sensor_base; static int mychar_sensor_probe(struct platform_device *pdev) { struct resource *res; int ret; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, no memory resource found\n); return -ENODEV; } sensor_base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(sensor_base)) { dev_err(pdev-dev, ioremap failed\n); return PTR_ERR(sensor_base); } dev_info(pdev-dev, probe ok: reg0x%llx size%lld\n, (unsigned long long)res-start, (unsigned long long)resource_size(res)); return 0; } static int mychar_sensor_remove(struct platform_device *pdev) { dev_info(pdev-dev, remove called\n); return 0; } static const struct of_device_id mychar_sensor_of_match[] { { .compatible example,mychar-sensor, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mychar_sensor_of_match); static struct platform_driver mychar_sensor_driver { .probe mychar_sensor_probe, .remove mychar_sensor_remove, .driver { .name mychar-sensor, .of_match_table mychar_sensor_of_match, }, }; module_platform_driver(mychar_sensor_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A simple demo platform driver for i.MX6ULL);这里我用了module_platform_driver()这个宏它展开后就是标准的module_init/module_exit加注册注销函数的样板操作比自己手动写platform_driver_register再包一层干净得多。3.3 probe里get资源不要直接硬编码地址有人会问设备树里已经写明了reg 0x0209c000 0x4000为什么probe里不直接用一个宏定义地址非要用platform_get_resource绕一圈直接用宏定义确实能让代码短一点但它有致命问题驱动的通用性被破坏了。同样一个驱动在一颗寄存器地址布局不同的芯片上可能需要重新编译而用设备树描述资源驱动只负责“读取并使用”具体地址是板子说了算这样同一份驱动内核二进制就能在不同硬件上运行。这也是设备树设计的初衷把硬件配置从代码中剥离开来。resource_size(res)用于计算资源长度就是end - start 1注意这地方有经典的差一错误。我看到过一些新手自己手写res-end - res-start没加那个1导致映射范围少一个字节访问到边界寄存器时产生异常。内核提供了现成的宏就别自己造轮子了。devm_ioremap_resource()这一系列 devm 开头的API还有一层好处它们是由设备资源管理框架托管的。probe中一旦在后面步骤出错返回之前用devm分配的IO映射会被自动释放remove时平台框架也会统一清理。不用自己在remove里一个一个手动iounmap少了很多内存泄漏隐患。3.4 模块编译与加载验证Makefile按标准写法指定内核源码目录和交叉编译工具链obj-m : mychar_sensor.o KDIR : /home/user/linux-imx CROSS_COMPILE : arm-linux-gnueabihf- all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) clean编译后在开发板上加载测试insmod mychar_sensor.ko dmesg | tail正常情况下会看到类似这行日志mychar-sensor mychar-sensor: probe ok: reg0x0209c000 size16384这时你还可以通过sysfs看到绑定关系ls -l /sys/bus/platform/drivers/mychar-sensor/ cat /sys/bus/platform/devices/mychar-sensor/ueventuevent文件里的OF_COMPATIBLE_N会列出这个设备对应的compatible这些是排查匹配问题时的关键线索。4. probe不回调我的一次完整排查链路记录4.1 现象insmod静默成功probe毫无反应回到文章开头那个项目。同事驱动模块可以正常加载dmesg里也能看到mychar_sensor: loading out-of-tree module taints kernel.这一类提示但probe函数就是没有进入。调用cat /proc/devices看不到注册的字符设备。当时我第一反应就是匹配失败。4.2 排查第一步确认设备树节点是否生成platform_device先查看ls /sys/bus/platform/devices/找驱动对应的设备节点。结果发现mychar-sensor这个目录根本不存在。这说明设备树解析阶段就没有成功创建platform_device。不是驱动匹配逻辑的问题而是源头就没有设备。再看/sys/firmware/devicetree/base/下的设备树结构确认节点到底有没有被打进dtb。发现设备树里确实写了mychar-sensor节点但是检查寄存器地址时发现节点被放在了某个父节点下面而这个父节点自身设置了status disabled。按设备树的规则父节点disabled子节点不论status是什么都是不生效的。4.3 排查第二步确认compatible字符串一致性把父节点状态改成okay重新引导系统。这次/sys/bus/platform/devices/mychar-sensor出现了但probe依然没有被调用。于是开始查compatible。驱动里写的是{ .compatible example,mychar-sensor, },设备树里写的是compatible example,mychar-sensor ;注意了字符串末尾多了一个空格。这个空格在视觉上几乎看不出来但字符串比较是逐字节的sensor后面多出的0x20就让匹配彻底失败。这个坑不是段子我遇到过不止一次。事后反思为什么不直接用of_match_node对比测试因为正常流程中platform bus已经做过了只是我们没有在代码里打印中间结果。更好的做法是probe没被调到的时候先看设备树节点的uevent再对照驱动的of_match_table逐一确认字符串、大小写、空格。4.4 修复与验证清除多余空格后重新编译设备树、烧录、启动mychar-sensor mychar-sensor: probe ok: reg0x0209c000 size16384probe正常执行。这个案例是典型的设备树与驱动compatible不匹配导致的“静默失败”。记下几个容易踩的盲点reg属性格式错误。reg要求有“地址长度”成对出现如果只写了一段地址没有长度解析时可能导致设备被丢弃。interrupts属性格式错误。中断号超出范围、中断触发类型不对设备树内核解析可能直接失败。大小写不一致。compatible比较是严格区分大小写的IMX6ULL和imx6ull在机器眼里没有任何关系。设备树语法错误不会直接告诉你“某个节点没生成”而可能在make dtbs阶段报错或者在引导阶段吞掉整棵子树。4.5 利用uEvent与sysfs快速判断绑定关系如果probe已经执行但初始化的后半段失败你可以在dmesg里看到部分日志。如果probe完全没进入建议按这个顺序检查# 1. 看设备是否注册 ls -l /sys/bus/platform/devices/ | grep mychar # 2. 看设备端匹配信息 cat /sys/bus/platform/devices/mychar-sensor/uevent # 3. 看驱动端是否已注册 ls -l /sys/bus/platform/drivers/mychar-sensor/ # 4. 强制绑定绕开自动匹配 echo mychar-sensor /sys/bus/platform/drivers/mychar-sensor/bind第四步的强制bind如果成功基本可以判断是自动匹配逻辑出了问题如果bind也失败问题多半出在probe函数内部或设备模型之外。5. 老式platform_device注册与设备树时代的资源获取差异5.1 传统资源注册方式的内核代码套路在没有设备树的年代注册一个platform_device需要在板级C文件里手写resourcestatic struct resource mychar_sensor_resources[] { [0] { .start 0x0209c000, .end 0x0209c000 0x4000 - 1, .flags IORESOURCE_MEM, }, [1] { .start 151, .end 151, .flags IORESOURCE_IRQ, }, }; static struct platform_device mychar_sensor_device { .name mychar-sensor, .id 0, .num_resources ARRAY_SIZE(mychar_sensor_resources), .resource mychar_sensor_resources, }; static int __init board_init(void) { platform_device_register(mychar_sensor_device); return 0; }这种做法在当时的i.MX28、i.MX35时期很常见。每个板子一份代码大量硬件信息平铺在C文件里改板型就要改代码重编内核。维护成本非常高不同厂商的板级文件风格各异内核邮件列表为此吵了好多年。设备树普及后这些resource数组大多被dts中的reg和interrupts属性取代。内核启动时设备树子系统自动为每个compatible匹配的节点分配platform_device和对应的resource。5.2 设备树生成platform_device的启动路径从设备树生成platform_device核心流程可以简化成start_kernel()初始化设备树unflatten_device_tree()将dtb解析成树状结构of_platform_populate()遍历根节点以下的设备节点创建platform_device每个platform_device注册到platform_bus上platform_bus的match逻辑开始工作。这个过程你不用自己写代码内核全部代劳。但有个概念必须想清楚并不是所有设备树节点都会变成platform_device。如果一个节点的compatible对应的驱动在init阶段已经被内建它可能直接走of_platform_bus_probe()流程由驱动自己填充设备如果节点挂在I2C或SPI总线上则它生成的是i2c_client或spi_device不是platform_device。很多新人把SPI设备也写成platform_driver去匹配然后发现probe永远不执行其实是因为总线类型完全不同。5.3 设备树驱动的资源获取方式对比老式platform_device直接用platform_get_resource()就能拿到resourceres platform_get_resource(pdev, IORESOURCE_MEM, 0);设备树驱动同样用这个API不需要修改因为从设备树解析出的资源已经被内核转成了统一的struct resource。这层抽象就是Linux设备模型的经典设计。如果你需要获取中断号可以用int irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, no irq found\n); return irq; }这里的0表示获取第一个中断。如果设备树里写了多个interrupts条目可以用索引依次取。有一点值得注意platform_get_irq()可能返回负数这是错误码而不仅仅是-ENOENT所以判断条件千万不要写成if (!irq)否则遇到中断号0合法值也会被误判为错误。5.4 老驱动迁移设备树时的兼容技巧老驱动的兼容性处理其实不难。以内核源码里各种支持多平台的驱动为参考最常见的写法是同时保留of_match_table与id_tablestatic const struct platform_device_id my_driver_ids[] { { .name mychar-sensor, .driver_data (kernel_ulong_t)sensor_v1_data }, { } }; static const struct of_device_id my_driver_of_match[] { { .compatible example,mychar-sensor, .data sensor_v2_data }, { } }; static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name mychar-sensor, .of_match_table my_driver_of_match, }, .id_table my_driver_ids, };因为compatible匹配优先级高于id_table在同一次启动中如果设备树节点存在就会走设备树匹配的分支如果没有设备树节点则走id_table的name匹配。这样一套驱动就能同时支持设备树平台和老式板级注册平台。6. 调试Platform匹配问题时要养成的几个习惯6.1 打开动态调试与设备模型日志排查匹配问题时最朴素但最有效的办法是先打开相关日志echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control这会输出platform_bus匹配过程中的关键决策信息。内核的dynamic debug机制能让你在不重新编译内核的情况下精确开启某个源文件的调试输出。另一个常被忽略的地方是设备树本身dtc -I fs -O dts /sys/firmware/devicetree/base/ dtb_export.dts如果系统已经跑起来可以用这种方式把实际的设备树源码导出来。它反映的是内核真正解析到的内容而不是你写的dts源码排查“写的和编译的是否一致”非常有用。6.2 匹配失败时先怀疑compatible再怀疑status我个人的排查顺序是ls -l /sys/bus/platform/devices/确认设备是否存在如果设备存在cat /sys/bus/platform/devices/name/uevent看OF_COMPATIBLE_N是否与驱动匹配表一致如果设备不存在检查设备树节点的父节点状态以及compatible是否写入了让人困惑的字符如果devices下能看到设备但drivers下找不到驱动检查模块是否真的被加载、是否使用了MODULE_DEVICE_TABLE(of, ...)probe里面打印多一点dev_info但要注意printk级别dmesg -n 8确保低等级日志不被刷掉。6.3 正确认识probe失败返回值probe函数返回负数时platform bus会把驱动与设备的绑定关系解除。这是正常行为不是内核bug。有些驱动在probe早期的资源获取阶段出错返回-ENODEV你可能看到设备还在但已经处于未绑定状态。要避免probe“半成功半失败”的尴尬状态尽量使用devm API管理资源生命周期。这样即使probe中途返回错误已经分配的资源也会被设备资源管理框架自动回收驱动卸载时也能保持干净。6.4 从误改设备树地址中吸取的教训有一次为了给某个外设腾出一段映射空间我改了设备树里的reg地址结果系统启动后该外设的probe报出内存冲突。内核里devm_ioremap_resource()会检查映射区域是否与其他已注册的资源区重叠一旦重叠直接返回-EBUSY。这样的机制其实是在保护硬件防止两个驱动同时对同一段寄存器做映射操作。改动设备树地址时要先确认目标区域没有被其他设备占用查看dts里的地址分配。i.MX6ULL的芯片手册里有完整的内存映射图对照reg属性时务必留意地址段用途。6.5 善用 /sys/bus/platform/ 下的接口做实验调试Probe逻辑可以绕过正常引导流程手动挂载绑定# 解绑当前驱动 echo mychar-sensor /sys/bus/platform/drivers/mychar-sensor/unbind # 重新绑定 echo mychar-sensor /sys/bus/platform/drivers/mychar-sensor/bind如果bind返回No such device再看一下设备目录是否存在如果返回Device or resource busy说明设备已被占用。这些sysfs操作在热插拔模拟和驱动热加载验证中很实用我调试备用驱动时多次依靠它不用反复重启板子。

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

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

免费获取报价