Linux Platform设备驱动匹配机制解析:从设备树到probe触发

Linux Platform设备驱动匹配机制解析:从设备树到probe触发 很多刚接触 i.MX6ULL Linux 驱动开发的朋友都会有同一个困惑以前在 2440、6410 上写字符设备驱动要自己申请设备号、注册miscdevice在init函数里手动把硬件地址ioremap出来。为什么到了 i.MX6ULL 上很多官方驱动看不到init和exit反而只留一个probe函数设备树里填几个属性硬件资源自己就送到驱动手上了答案就藏在这个标题里Platform 设备与驱动匹配机制。简单说内核在启动阶段会扫描设备树把一个个节点变成platform_device驱动模块加载后又变成platform_driver。它们都挂在一条虚拟的 platform 总线上总线负责给双方做“配对”配上了就调驱动的probe函数配不上就晾在一边。这篇文章我会结合 i.MX6ULL 的实际设备树结构把匹配流程从底层到应用完整讲一遍包括我自己开发过程中踩过的坑和总结的排查方法。无论你是刚入门驱动开发的新手还是已经在做项目但被“设备树改了驱动不生效”折磨过的老手这篇都值得看下去。1. 为什么要做 Platform一个设备模型的演进问题1.1 板级文件时代的“注册轰炸”Linux 早年并没有一套统一的设备描述手段。各个 ARM 板卡厂商在使用 Linux 时需要在arch/arm/mach-xxx下的板级文件里手动构造设备列表大概长这样static struct platform_device smc911x_device { .name smc911x, .id 0, .num_resources 2, .resource smc911x_resources, }; static struct platform_device *mini2440_devices[] __initdata { smc911x_device, leds_device, ... }; static void __init mini2440_init(void) { platform_add_devices(mini2440_devices, ARRAY_SIZE(mini2440_devices)); }这种做法在设备数量少的时候还能凑合但 i.MX6ULL 这种芯片内部外设几十个再加上外部总线设备板级文件的platform_device列表会膨胀得非常恐怖。而且只要换一颗芯片这些注册代码几乎全部要重写。当时平台设备的名字、资源、中断号全都硬编码在 C 文件里。硬件设计稍作调整比如把一个 GPIO 中断从 GPIO1_IO05 换到 GPIO1_IO06就得改源码重新编译内核非常痛苦。1.2 设备树接管后设备描述与驱动代码分离设备树Device Tree出现之后把设备的“描述”从内核代码里彻底剥离了。同一颗 i.MX6ULL 芯片不同板卡只需要替换一个.dts文件内核代码一行都不用动。设备树里的每个节点会被内核的of_platform_*系列函数转换成platform_device。而驱动这边不再需要关心设备到底在哪块板子上只要在of_match_table里声明自己支持的 compatible就能在设备树中找到对应的节点配对成功后进入probe。我自己的体会是Platform 机制最大的价值不是省了几行代码而是把“设备有哪些资源”和“驱动怎么用资源”彻底解耦了。驱动工程师关注的是后者前者交给设备树维护者。提示即便你没有实际硬件纯粹想学习驱动开发理解 Platform 匹配机制也是必要的。因为 Linux 下绝大多数总线设备I2C、SPI、USB 等的驱动框架都是参照 platform 总线的模型设计的。2. i.MX6ULL 上 platform_device 是从哪里冒出来的2.1 启动阶段的 of_platform_default_populate_init你可能会有个疑问我没写任何注册platform_device的代码设备节点是怎么变成内核对象的在 ARM 平台内核启动流程中有一个arch_initcall_sync级别的初始化函数of_platform_default_populate_init。它会在设备树被解析成device_node树之后调用of_platform_bus_probe对设备树进行深度遍历扫描。扫描的起点是根节点但不是所有节点都会被无脑变成platform_device。这里有一个筛选规则节点要么自身匹配of_default_bus_match_table也就是 compatible 为simple-bus、simple-mfd、isa要么它的父节点是匹配这个表的节点并且该节点自身带compatible属性才会被创建为平台的设备节点。我们用 i.MX6ULL 官方 BSP内核 4.1.15里的imx6ul.dtsi来看soc: soc { compatible simple-bus; #address-cells 1; #size-cells 1; interrupt-parent gpc; ranges; busfreq { compatible fsl,imx_busfreq; ... }; aips1: aips-bus02000000 { compatible fsl,aips-bus, simple-bus; ... }; };imx6ul.dtsi里soc节点 compatible 是simple-bus它下面的aips1、aips2、aips3这些总线节点都挂在它下级。当内核扫到soc时发现它匹配simple-bus就会继续往它的子节点遍历再扫到aips1时发现它也带simple-bus于是继续遍历aips1的子节点。最终像uart1、i2c1、gpio这些没有simple-bus的外设节点就逐个被创建成了platform_device。所以你在 i.MX6ULL 的/sys/bus/platform/devices目录下能看到一大堆设备但设备树里并没有一个叫platform的总线节点。Platform 总线是内核自己注册的一条虚拟总线所有设备最终都挂在它下面。2.2 一个容易踩的坑自定义节点必须挂在 simple-bus 下我刚学设备树时习惯把自定义节点直接写到根节点下面/ { mydevice { compatible mycompany,mydev; reg 0x10000000 0x1000; }; };结果驱动 insmod 之后无论如何都不触发probe。原因就是上面说的扫描规则根节点的直接子节点只有在 compatible 匹配simple-bus或simple-mfd时才会被递归展开。mydevice的 compatible 是mycompany,mydev不在of_default_bus_match_table里内核直接把它跳过了。提示设备树节点要生成platform_device优先把它挂在已有的soc或者aips下并确保自身带compatible属性。如果实在要放根下面必须给根或者中间父节点补compatible simple-bus。3. platform_match 背后的四条匹配路径3.1 of_match_table设备树驱动的主流入口驱动这边声明支持的设备核心是of_device_id数组和of_match_table。内核源码drivers/base/platform.c中的platform_match函数是匹配的枢纽。以 i.MX6ULL 上很多设备为例static const struct of_device_id imx6ul_ts_adc_of_match[] { { .compatible fsl,imx6ul-tsc, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx6ul_ts_adc_of_match); static struct platform_driver imx6ul_ts_adc_driver { .driver { .name imx6ul-tsc, .of_match_table imx6ul_ts_adc_of_match, .pm imx6ul_ts_adc_pm_ops, }, .probe imx6ul_ts_adc_probe, .remove imx6ul_ts_adc_remove, };匹配时会拿设备节点的compatible属性值与of_match_table中每一项的compatible字段做字符串比较。只要有一个完全相同就认为匹配成功调用probe。这里有一个新手经常忽略的细节of_device_id数组的最后一个空项是必须的。of_match_node会遍历整个数组直到遇到NULL终止项如果你定义时漏了{ /* sentinel */ }内核可能越界读取内存产生不可预料的后果。我在写 demo 时一直严格要求自己带上终止项。3.2 id_table从板级文件时代继承下来的兼容方案现在大部分场景都在用of_match_table但id_table也没被淘汰。它的作用是为那些没有设备树或者设备树节点根本没有compatible属性的老式设备提供匹配途径。static struct platform_device_id demo_ids[] { { .name demo-dev, .driver_data 0, }, { }, }; static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo-dev, }, .id_table demo_ids, };platform_match在of_match_table匹配失败之后会遍历id_table将每一项的name和pdev-name做比较。前面说过设备树节点生成platform_device时pdev-name会取节点名所以只在id_table里写demo-dev设备树节点只要命名为demo-dev也能匹配上但我不推荐依赖这个行为做新开发。3.3 一个容易误判的坑driver.name 与设备同名也会匹配如果你连id_table都没提供platform_match会走到最后一步直接比较drv-name和pdev-name。这听起来很方便但也是一个隐患来源。比如某次我调试时设备树里节点 compatible 明明写错了驱动probe还是被调用了。查了很久才发现节点的name恰好和driver.name相同走的是最后这条兜底逻辑而不是of_match_table。这种“侥幸匹配”的问题在于它掩盖了设备树配置错误。过段时间换了驱动名或节点名驱动就莫名其妙不生效了。所以排查probe没触发的问题时不要只看 compatible还要关注name匹配的干扰。三条路径在platform_match中的执行顺序总结如下匹配方式判断依据适用场景of_driver_match_device设备树节点的compatible与of_match_table比较现代设备树驱动最常用acpi_driver_match_deviceACPI 表匹配x86 / ARM 的 ACPI 启动场景platform_match_idid_table[i].name与pdev-name比较板级文件或旧设备strcmp(pdev-name, drv-name)驱动名与设备名直接比较兜底逻辑最容易产生误判4. probe 函数视角拿到设备树资源的标准姿势4.1 platform_get_resource匹配成功只是第一步probe函数被调用代表驱动和设备配对成功。真正的工作才刚刚开始你需要在probe里拿到硬件资源完成映射、注册中断、申请时钟等工作。最常用的资源获取 API 是platform_get_resourcestruct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (IS_ERR_OR_NULL(res)) { dev_err(pdev-dev, failed to get mem resource\n); return -ENXIO; }以 i.MX6ULL 某个外设节点为例设备树里写着mydevice10000000 { compatible mycompany,mydev; reg 0x10000000 0x1000; interrupts GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH; };内核在创建platform_device时解析reg和interrupts把它们存进struct resource数组。platform_get_resource(pdev, IORESOURCE_MEM, 0)取的是数组中第 0 个内存资源start就是 0x10000000end是 0x10000FFF也就是 reg 第一个 address-cells 和 size-cells 解析出来的结果。用platform_get_resource而不是直接读设备树好处是资源信息已经被统一抽象成了内核资源区后续request_mem_region、ioremap都基于它。4.2 devm_ 系列 API让资源管理自动化i.MX6ULL 官方驱动里你看不到传统意义上的资源释放代码这是因为大量使用了devm_前缀的 API。devm_全称是device managed资源生命周期和设备对象绑定。probe执行成功资源挂到dev上设备移除或probe失败内核自动释放。典型的写法如下static int demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct demo_dev *ddev; ddev devm_kzalloc(dev, sizeof(*ddev), GFP_KERNEL); if (!ddev) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; ddev-base devm_ioremap_resource(dev, res); if (IS_ERR(ddev-base)) return PTR_ERR(ddev-base); platform_set_drvdata(pdev, ddev); return 0; }devm_ioremap_resource取代了传统的request_mem_regionioremap。它内部先检查资源合法性然后完成映射任何一步失败都会返回ERR_PTR类型的错误用IS_ERR判断即可。真正省心的是异常路径。传统写法里如果后面的申请gpio失败你得手动iounmap、kfree用了devm_之后直接返回错误码就行。这个特性在设备驱动代码里大量存在i.MX6ULL 的 FEC 网卡驱动、UART 驱动几乎都是这个套路。4.3 从设备树读取自定义属性of_property 系列除了reg和interrupts这类标准资源很多设备需要自定义属性。比如我经常在节点里加一个idx参数表示设备编号mydevice10000000 { compatible mycompany,mydev; reg 0x10000000 0x1000; mydev,index 3; };驱动里用of_property_read_u32读取int ret; u32 index; ret of_property_read_u32(dev-of_node, mydev,index, index); if (ret) { dev_warn(dev, failed to get mydev,index, use default 0\n); index 0; }设备树属性名带厂商前缀如mydev,index是规范之一可以避免不同厂商驱动之间的属性名冲突。读取字符串用of_property_read_string读gpios用of_get_named_gpio读时钟用of_clk_get套路一致。dev-of_node是struct device里指向设备树节点的指针它是设备树匹配时代给驱动传参的入口。只要驱动是从设备树probe进来的这个指针一定非空。5. 实战在 i.MX6ULL 上写一个 platform 驱动并验证匹配5.1 驱动代码从匹配表到 probe/remove 完整实现下面给一个可以在 i.MX6ULL 官方 BSP 上直接编译加载的示例。它做的事情很简单在probe里打印设备树解析出来的资源信息在remove里打印退出信息方便你用dmesg观察匹配过程。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/io.h #include linux/slab.h #define DRV_NAME demo-platform-dev struct demo_platform_priv { void __iomem *base; struct resource *res; u32 index; }; static const struct of_device_id demo_platform_of_match[] { { .compatible mycompany,imx6ull-demo-platform, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_platform_of_match); static int demo_platform_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct resource *res; struct demo_platform_priv *priv; u32 index; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(dev, no memory resource found\n); return -ENXIO; } priv-base devm_ioremap_resource(dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); ret of_property_read_u32(dev-of_node, mydev,index, index); if (ret) { dev_warn(dev, cant read mydev,index, default to 0\n); index 0; } priv-res res; priv-index index; platform_set_drvdata(pdev, priv); dev_info(dev, probe ok, resource: start0x%llx end0x%llx size0x%llx index%u\n, (unsigned long long)res-start, (unsigned long long)res-end, (unsigned long long)resource_size(res), priv-index); return 0; } static int demo_platform_remove(struct platform_device *pdev) { struct demo_platform_priv *priv platform_get_drvdata(pdev); dev_info(pdev-dev, remove ok, index%u\n, priv-index); return 0; } static struct platform_driver demo_platform_driver { .probe demo_platform_probe, .remove demo_platform_remove, .driver { .name DRV_NAME, .of_match_table demo_platform_of_match, }, }; module_platform_driver(demo_platform_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(i.MX6ULL platform driver demo);如果用 NXP 官方 4.1.15 SDK 交叉编译命令类似export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make -C /path/to/kernel M$(pwd) modules会在当前目录生成demo-platform.ko。5.2 设备树节点必须挂在合适的总线节点下面设备树这边以imx6ull-14x14-evk.dts为例把自定义节点放在soc下soc { demo-platform10000000 { compatible mycompany,imx6ull-demo-platform; reg 0x10000000 0x1000; mydev,index 5; status okay; }; };这里选择 0x10000000 这个地址纯粹是演示用途它不映射到 i.MX6ULL 的真实外设。真实项目中请替换成实际外设的寄存器基址和长度。为什么要挂在soc下回到第二章节的结论soc节点是simple-bus内核递归扫描时不会跳过它的子节点。一个经常出现的错误是直接这样写/ { demo-platform10000000 { compatible mycompany,imx6ull-demo-platform; reg 0x10000000 0x1000; }; };因为根节点不是simple-busdemo-platform又不在of_default_bus_match_table中这个设备根本不会生成platform_device后面驱动再怎么写也是白搭。5.3 编译、加载、观察匹配过程重新编译设备树并替换到开发板后先确认设备节点已经生成ls /proc/device-tree/demo-platform10000000/能看到compatible、reg、name这些文件说明platform_device已经创建。再加载模块insmod demo-platform.ko然后用dmesg查看匹配结果dmesg | tail -20如果一切正常能看到我们设备树节点里写的资源被打印出来demo-platform 10000000.demo-platform: probe ok, resource: start0x10000000 end0x10000fff size0x1000 index5然后卸载rmmod demo-platform再查看 dmesg应该出现remove ok, index5。probe和remove的完整生命周期验证就通过。调试阶段还可以用 sysfs 观察绑定关系ls /sys/bus/platform/drivers/demo-platform-dev/驱动的目录下会出现一个指向10000000.demo-platform的符号链接这就是“设备已绑定到驱动”的直观证据。6. 匹配失败时的排查链路与方法论6.1 先确认设备节点有没有生成 platform_deviceprobe不被调用首先怀疑的不是驱动而是设备节点是否真的变成了platform_device。我自己的调试顺序固定如下查看设备树节点是否编译进 dtbls /proc/device-tree/ | grep demo如果看不到节点说明设备树没有生效重新检查dts文件路径、编译产物是否烧写正确。查看对应设备是否在 platform 总线上注册ls /sys/bus/platform/devices/ | grep demo如果设备树节点存在但这里没有说明节点所在位置不对或者状态为disabled。确认status属性。有些设备在基础dtsi里写status disabled板级 dts 忘了改为okay一样不会生成设备。status okay;这一步能过滤掉一半以上的低级问题。6.2 compatible 的“一个字符”陷阱接下来确认 compatible 是否完全一致。设备树侧和驱动侧哪怕差一个字符都无法匹配。最典型的错误是 i.MX6UL 和 i.MX6ULL 的差异。设备树里写fsl,imx6ul-tsc驱动匹配表里写fsl,imx6ull-tsc看似差不多实际就是匹配不上。因为 compatible 是纯字符串比较不做任何语义归一化。排查方法也很简单直接读取设备节点的 compatiblecat /proc/device-tree/demo-platform10000000/compatible驱动侧对应查看源码中的of_device_id数组。两个字符逐一对齐包括厂商前缀。我遇到过最诡异的一次是设备树里写属性时多了一个空格compatible mycompany,imx6ull-demo-platform;看起来没问题实际上字符串末尾混入了一个不可见字符。shell 里cat看不太出来但xxd能看到。这种问题浪费了我一个下午后来养成了一个习惯用十六进制查看设备树属性尤其是在复制粘贴设备树内容之后。xxd /proc/device-tree/demo-platform10000000/compatible6.3 从 dmesg 和 sysfs 反向验证匹配结果如果 compatible 已经确认一致probe还是没调用这时要看驱动本身是否注册成功。先确认模块加载阶段没有报错modprobe demo-platform-ko dmesg | tail -50如果出现Unknown symbol之类的错误往往是内核符号版本不匹配重新用同一个内核源码编译驱动即可。再看驱动的of_match_table是否生成了 module aliasmodinfo demo-platform.ko如果MODULE_DEVICE_TABLE(of, ...)写对了modinfo 里会有一行类似alias: of:Ndemo-platform...TNULLCmycompany,imx6ull-demo-platform。这行 alias 是modprobe自动加载依赖的依据。缺失这行即使设备树节点匹配modprobe也可能不会自动拉起驱动只能手动insmod。最后还有一个比较深入但值得了解的现象deferred probe。当probe执行时依赖某个还没准备好的子系统比如 pinctrl、clock内核不会直接报 fatal error而是把设备放到延迟队列里等依赖资源就绪后再重新尝试。此时 dmesg 里会反复出现platform 10000000.demo-platform: deferred probe pending遇到这种情况检查probe里依赖的clock、pinctrl、regulator对应的设备树属性是否配置完整。i.MX6ULL 上很多外设的 pinctrl 配置在iomuxc节点里如果引脚配置引用了不存在的 pin 函数也会卡在这里。我个人的一个经验是在probe一开始就打印一行dev_info不要等资源解析做完了才打印。这样能快速区分是probe没进还是probe中途失败。调试完再删掉或者降级为dev_dbg成本极低收益却很大。7. 最后分享一个调试习惯Platform 匹配机制本身不难难的是设备树和驱动两边各有一份“配置”肉眼比对时经常漏掉细节。我做 i.MX6ULL 驱动调试时拿到任何一个新的外设都会先在probe里无脑打印pdev-dev.of_node-full_name和pdev-name用这两行确认内核眼中的设备和设备树节点是否一致。很多玄学问题加一行打印之后立刻现出原形。另一个习惯是把of_match_table里的 compatible 全部复制到一个小文件里然后用脚本去设备树编译产物里做全量比对。因为人工校对字符串真的不可靠宁可多花两分钟写个脚本也不要抱着dmesg干等没有意义的输出。匹配机制弄明白了后面写 I2C、SPI、GPIO 子系统的设备驱动你会发现所有的struct xxx_driver都有相似的probe/remove和匹配表套路完全一致。这也是为什么我一直建议新手先从 Platform 驱动入门它把 Linux 设备模型最核心的心思都浓缩在了一条总线和两个结构体里。