ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

i.MX6ULL平台总线设备驱动匹配机制与probe触发问题解析

i.MX6ULL平台总线设备驱动匹配机制与probe触发问题解析 搞过 i.MX6ULL 驱动开发的朋友应该都经历过这么一幕设备树里节点写得清清楚楚驱动代码也编进内核了可系统启动后probe就是不执行dmesg里干干净净仿佛代码从未存在过。这种问题排查到最后绝大多数都指向同一个根因——Platform 设备与驱动的匹配机制没对齐。今天我就把这套机制完完整整捋一遍结合 i.MX6ULL 的实际场景讲清楚平台总线到底是怎么把设备信息“交接”给驱动代码的以及匹配失败时怎么快速定位。这篇文章不端着尽量把你当成在同一个工作台前调板子的同事。适合两种人看一种是刚开始学嵌入式 Linux 驱动被 device、driver、bus 这些概念绕晕的新手另一种是已经会写简单驱动但 probe 偶尔不触发想系统搞明白底层逻辑的进阶选手。看完之后你至少能回答三个问题Platform 总线解决了什么、设备树怎么变成平台设备、内核在匹配时到底比对了哪些字段。1. 先想明白一件事为什么内核要搞出 Platform 这套东西1.1 没有总线设备驱动怎么“对上眼”计算机系统里的外设大多挂在某种物理总线上比如 PCI、USB、I2C、SPI。这类总线有个共同特点硬件上支持枚举软件可以通过扫描、查询等手段把挂在上面的设备一件件“摸”出来。USB 设备插入后主机会读取设备描述符知道它的厂商 ID、产品 ID、设备类型然后据此找到匹配的驱动。PCI 设备也有 Vendor ID、Device ID内核通过pci_match_id这类逻辑完成配对。但 i.MX6ULL 这种嵌入式 SoC 里的绝大多数外设情况完全不一样。GPIO 控制器、UART、ECSPI、I2C 适配器、FEC 网卡、LCD 控制器它们并没挂在一个可枚举的物理总线上而是直接内嵌在芯片里地址、中断、时钟等资源在芯片出厂时就固定了。内核没法通过“问一下硬件”来发现它们如果没有一套统一机制每个驱动都得自己想办法找设备信息驱动代码必然和板级硬件强耦合混乱程度难以想象。Platform 总线也叫平台总线就是为解决这个问题而生的虚拟总线。它不依赖具体硬件协议把所有片内外设抽象成一个个平台设备把寄存器地址、中断号、时钟等资源打包成标准的数据结构再通过统一的匹配逻辑去找到对应的驱动。对驱动开发者来说你只需要注册一个platform_driver内核会在合适的时机遍历已注册的平台设备找到匹配项后调用你的probe函数。设备和驱动彻底解耦换板子改硬件大多数时候只改设备树驱动源码几乎不用动。1.2 设备模型三件套device、driver、busLinux 设备模型的核心就三个对象device代表一个硬件设备driver代表操作这个设备的软件程序bus负责把两者关联起来。这里说的 bus 不一定是物理总线它更像一种连接规则、一个匹配仲裁者。平时我们接触最多的 USB、PCI、I2C都属于真实总线而 Platform 则是内核提供的一种虚拟总线实现目的是把“无法枚举”的设备纳入这套统一模型。我用中介公司来打比方device是求职者driver是招聘岗位bus是中介系统。求职者的简历里写清楚自己叫什么、擅长什么、有哪些资源招聘岗位的 JD 里写明白要招什么人、要求具备什么技能中介系统拿着 JD 和简历逐条比对。平台总线里的“简历”在设备树时代就是设备树节点compatible是其中最关键的一行招聘要求则写在驱动结构体的匹配表里最常见的是of_match_table。匹配不成功会怎样岗位招不到人求职者没活干。具体表现就是设备树里有节点内核里也注册了驱动但驱动probe函数永远不会被调用。这个看似简单的问题在实际项目里能折腾掉半天甚至一天时间就是因为很多人没把这套匹配逻辑吃透。1.3 i.MX6ULL 的哪些外设走了 Platform 这条路打开 i.MX6ULL 的设备树源文件imx6ull.dtsi就能发现几乎每个外设节点最终都会对应一个 Platform 驱动。FEC 网卡对应fec驱动SD 卡控制器对应sdhci-esdhc-imx驱动LCD 控制器对应mxsfb驱动底层注册的基本都是platform_driver。哪怕是 GPIO 这类最基础的外设其驱动的核心也是一个平台驱动只是它同时还会注册到 GPIO 子系统向上层提供通用接口。换句话说如果你在 i.MX6ULL 上做一个字符设备驱动想要控制任意一个片内外设你写的大概率就是一个platform_driver。这也是我为什么建议刚入门嵌入式 Linux 的人先把 Platform 匹配机制研究透因为它是理解后续设备驱动子系统的地基。地基不牢后面学 input 子系统、I2C 驱动框架、SPI 驱动框架时总会觉得到处是空洞。2. 认识两个主角platform_driver 与 platform_device2.1 驱动侧的简历platform_driver 结构体先看驱动侧。platform_driver定义在include/linux/platform_device.h里核心字段其实没几个。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); int (*resume)(struct platform_device *); struct device_driver driver; const struct platform_device_id *id_table; bool prevent_deferred_probe; };日常写驱动主要关心probe、remove、driver和id_table。probe是匹配成功后被回调的入口驱动在这里完成硬件初始化、中断申请、注册字符设备等操作remove是驱动卸载或设备移除时的清理入口。driver字段是一个内嵌的device_driver结构体里面包含name和of_match_tablename用于传统方式匹配of_match_table用于设备树匹配。注册一个平台驱动通常直接用内核提供的宏module_platform_driver(my_platform_driver);这个宏展开后会帮我们生成module_init和module_exit函数分别调用platform_driver_register和platform_driver_unregister。你可以理解为它把“把简历投给中介系统”这个动作封装好了。如果驱动的生命周期和模块一致用这个宏就足够了。2.2 设备侧的信息设备树节点如何变成 platform_device设备树里那些带compatible属性的节点并不是天生就是platform_device。系统启动过程中内核会调用of_platform_default_populate之类的函数遍历设备树中所有满足条件的节点为它们创建对应的platform_device。这一步很关键设备树节点是静态描述platform_device是内核动态生成的运行时对象。生成过程中设备树节点里的reg、interrupts等属性会被解析成struct resource挂到platform_device的resource数组里。这就是为什么驱动里可以用platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到寄存器地址用platform_get_irq(pdev, 0)拿到中断号。设备树节点名会成为平台设备的 name节点的compatible会被保留在设备的of_node里供后续匹配使用。所以设备树里写什么直接决定了驱动能拿到什么资源。reg写错了驱动里ioremap就会拿到错误的地址interrupts写错了申请中断就会失败compatible写错了匹配直接失败probe 根本不会执行。2.3 匹配顺序内核源码里的几条路设备与驱动到底怎么匹配答案在drivers/base/platform.c的platform_match函数里。内核源码是学习匹配机制最好的老师它告诉我们的匹配顺序大致如下第一如果设备设置了driver_override直接用它和驱动名字比较这个机制主要用于强制绑定一般驱动开发用不上。第二如果驱动提供了of_match_table且设备有of_node则通过of_driver_match_device按照设备树compatible值进行匹配。第三条路是 ACPI 匹配x86 平台或者支持 ACPI 的嵌入式设备才会用到i.MX6ULL 基本不用关心。第四如果驱动设置了id_table则用platform_match_id把设备的 name 与id_table里的每一项比较。最后如果上面都没匹配成功而且驱动没有设置id_table内核还会退回到比较device_driver的name和设备 name。从实际使用频率来看第一条和第三条是冷门路线第二条款的compatible匹配是设备树时代的主流第四条第五条算是老式板级文件时代的产物。理解这一点后再写驱动时就能判断该用of_match_table还是id_table或者两个都配。大多数现代 i.MX6ULL 驱动只需要把of_match_table配好即可。3. 实战在 i.MX6ULL 上写一个 platform 驱动 demo3.1 实验环境准备纸上谈兵没有意义直接示范。我以常见的 i.MX6ULL 开发板为例假设你已经具备以下基础环境一台装有 Linux 的宿主机交叉编译工具链arm-linux-gnueabihf-以及一份和板子内核版本匹配的内核源码。我的实验用的是 4.19 内核但后面讲的机制在 5.x 上也一样适用。交叉编译工具链可以用arm-linux-gnueabihf-gcc如果没装好大多涉及工具链的问题会直接报找不到编译器这类问题可以后续单独处理。内核源码必须在实验前完成至少一次配置和编译这样scripts目录下的工具才可用模块编译的中间依赖才会被正确生成。3.2 设备树节点与编译第一步在设备树源文件里新增一个测试节点。以最简单的寄存器区域模拟一个外设假设这块“外设”有一组寄存器地址从0x0209c000开始长度0x100。这个地址对应 i.MX6ULL 的某个 GPIO 控制器基地址区域实际项目中你完全替换成自己要操作的外设地址即可。example_dev: example-dev { compatible mycompany,example-dev; reg 0x0209c000 0x100; status okay; };注意compatible的写法格式一般是“厂商,型号”全程小写字母中间是英文逗号后面不要跟空格。很多新手在这里踩坑把mycompany,example-dev写成了mycompany, example-dev内核严格按字符串比较多一个空格都可能导致匹配不上。所以这个字段宁可多检查三遍也别凭感觉写。修改完设备树源文件后重新编译设备树生成新的 dtb 文件make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs然后把新 dtb 烧写到板子的设备树分区或者通过 tftp/nfs 等方式加载。如果板子原来已经有一个可以启动的内核你只需要替换 dtb不必重新编译整个内核这也是设备树方式最方便的地方之一。启动后用ls /proc/device-tree/ | grep example检查节点是否真实存在于运行时设备树中如果这步就看不到说明 dtb 没换成或者设备树源文件没生效。3.3 驱动源码与编译第二步写平台驱动源码。目标很简单注册一个平台驱动匹配设备树里的节点匹配成功后把reg里的资源解析出来并打印在dmesg里。这个 demo 足够验证匹配机制又不会掺杂太多硬件细节让人分心。#include linux/module.h #include linux/platform_device.h #include linux/io.h #include linux/of.h static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; dev_info(pdev-dev, probe called\n); res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get memory resource\n); return -ENXIO; } base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) { dev_err(pdev-dev, failed to ioremap resource\n); return PTR_ERR(base); } dev_info(pdev-dev, resource start0x%llx size%lld\n, (unsigned long long)res-start, (unsigned long long)resource_size(res)); /* 这里可以继续做硬件初始化和字符设备注册比如 * device_create、register_chrdev 等。 */ return 0; } static int my_remove(struct platform_device *pdev) { dev_info(pdev-dev, remove called\n); return 0; } static const struct of_device_id my_of_match[] { { .compatible mycompany,example-dev }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name my_example_dev, .of_match_table my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform match demo);MODULE_DEVICE_TABLE这行很多人会忽略它的作用是告诉模块工具这个驱动支持的设备列表是什么编译后会在模块信息里生成 alias。没有这行自动加载工具如 udev在设备出现时可能不知道加载哪个模块。虽然手动insmod时感受不到区别但专业项目里这行必须写。新建一个 Makefile用于编译模块。注意内核编译模块时并不会用你宿主机上的 gcc而是通过 Kbuild 系统调用交叉编译工具链。obj-m my_platform_drv.o然后在终端执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- M$(pwd) modules如果内核源码没有事先编译过这里会报缺少Module.symvers之类的问题所以前面强调必须先完成一次内核编译。3.4 加载与验证怎么确认 probe 执行了编译完成后得到my_platform_drv.ko把它拷贝到板子上执行insmod my_platform_drv.ko dmesg | tail正常情况下能看到类似输出my_example_dev my_example_dev: probe called my_example_dev my_example_dev: resource start0x0209c000 size256如果驱动已经编译进内核而不是模块就看不到 insmod 过程但同样可以在dmesg里搜索probe called。内核日志中设备名往往是设备树节点名也就是example-dev而驱动名是my_example_dev。通过日志里的名称也能大致判断匹配走的是哪条路径。验证匹配是否成功还有一个更直观的入口就是/sys/bus/platform目录。分别查看设备和驱动两边的情况ls /sys/bus/platform/devices/ ls /sys/bus/platform/drivers/my_example_dev/如果设备节点和驱动节点都存在但驱动目录下没有绑定对应设备就说明匹配失败。匹配成功时驱动目录下通常会生成一个指向设备的符号链接。这个 sysfs 视图是调试匹配机制的第一现场比单纯看日志更明确。4. probe 不执行、资源读不到排查套路在这里4.1 三类经典症状与定位思路匹配机制出问题现象千奇百怪但总结起来无非三大类。第一类是设备侧异常设备树节点没有生成platform_device或者节点status是disabled内核直接忽略。第二类是驱动侧异常驱动模块没加载成功或者of_match_table里的compatible和设备树对不上。第三类是匹配成功但资源解析失败reg写错、地址冲突、ioremap失败导致 probe 中途返回错误。这三类问题的排查入口不一样我列个速查表方便对号入座。症状可能原因优先排查地点设备树节点不存在dtb 没更新或 dts 语法错误/proc/device-tree 对应节点设备存在但驱动不存在模块未加载或驱动注册失败lsmod、/sys/bus/platform/drivers/设备驱动都在但无绑定关系compatible 不匹配或 name 匹配失败sysfs 下两边的名称和 aliasprobe 执行但报资源错误reg 属性错误或地址被占用dmesg 中的具体报错probe 没执行且无报错匹配表没配好或驱动里没设 of_match_table核对 of_match_table、MODULE_DEVICE_TABLE4.2 sysfs 和 dmesg 的组合用法我调试这类问题时习惯先把 sysfs 里的设备和驱动两边都看一遍。用设备树节点名在/sys/bus/platform/devices下找到对应目录再用驱动名在/sys/bus/platform/drivers下找到驱动目录。如果两边都在但没建立绑定关系下一步就该检查匹配字段。cat /sys/bus/platform/devices/example-dev/uevent会显示这个设备相关的匹配信息在设备树场景下能看到OF_COMPATIBLE_0mycompany,example-dev之类的内容。把这个值和驱动里of_match_table里定义的字符串仔细比对重点查全角半角、大小写、空格。肉眼比对不放心可以直接复制出来用xxd看十六进制确认没有隐藏字符。dmesg方面除了启动日志还可以临时打开驱动调试信息。比如在驱动代码里加上dev_dbg然后通过动态调试dynamic debug开启它echo file my_platform_drv.c p /sys/kernel/debug/dynamic_debug/control这样不用重新编译模块就能看到更详细的内部日志。不过需要内核开启CONFIG_DYNAMIC_DEBUG这个选项在多数嵌入式发行内核里都是默认打开的。4.3 compatible 踩坑笔记与设备树埋点检查i.MX6ULL 项目里我遇到的匹配问题大半出在compatible上这里单独把常见坑列出来。第一个坑是大小写。设备树的compatible规范上约定使用小写字母加数字和连字符但实际项目里总会有人写成MyCompany,ExampleDev。内核字符串比较不分“看起来差不多”它对就是对错就是错差一个字母都匹配不上。第二个坑是厂商前缀。compatible里的厂商前缀应该使用内核公认的 vendor prefix可以在内核源码Documentation/devicetree/bindings/vendor-prefixes.yaml里查到。比如 NXP 是fsl如果你的设备实际上是 NXP 的 IP 核却写成了nxp老版本内核里可能也匹配不上。当然自己开发的私有设备用自定义前缀没问题但建议选一个不会和现有厂商混淆的名字。第三个坑是设备树节点没有正确被解析。比如你给节点设置了status disabled内核就不会为它创建platform_device驱动自然无从匹配。有些 SoC 的设备树里外设节点默认就是disabled由板级 dts 文件通过node { status okay; }开启新加节点时很容易漏掉这一步。第四个坑是 reg 地址写错导致资源解析失败。reg 0x0209c000 0x100表示起始地址 0x0209c000长度 0x100。如果把这个长度写成一个寄存器地址值比如0x0209c100devm_ioremap_resource会认为资源长度异常直接返回错误码probe 也会因此失败。Co 这种错误不会导致系统崩溃但设备寄存器访问会读到错误区域甚至触发总线异常调试时容易迷惑。5. 更进一层匹配机制的源码级阅读与扩展方向5.1 把 drivers/base/platform.c 读一遍如果上面的内容你都实践过一遍我强烈建议你直接去读内核源码里的drivers/base/platform.c。老版本内核对应platform_match新版本可能改叫platform_match_device函数本身不长一两百行比任何博客教程都准确。读的时候重点看of_driver_match_device和platform_match_id这两个函数分别做了什么前者遍历设备树节点的compatible后者遍历驱动的id_table。读源码时我会顺手在调试器里加断点或者直接打印匹配函数的入参值看看设备侧的compatible到底是什么。更多时候排查匹配问题时打开内核的CONFIG_OF相关调试选项让内核在遍历设备树时打印更多信息也能省掉很多无效尝试。源码阅读的意义在于建立心理模型以后遇到非典型问题你能猜出内核在哪个步骤卡住而不是像无头苍蝇一样乱试。5.2 从 Platform 走向真实子系统一个platform_driver只是入口真正复杂的是 probe 里注册的各种子系统接口。i.MX6ULL 的 GPIO 驱动probe 里会把设备注册为 gpio_chip提供给上层通用 GPIO APIFEC 网卡驱动probe 里会调用mdio_bus相关接口注册 PHY再注册net_deviceI2C 适配器驱动probe 里会注册一个 i2c_adapter让上层可以挂载 i2c_client。所以学习路径应该是先把 Platform 匹配机制彻底搞明白再选一个具体的外设驱动比如 I2C 控制器顺藤摸瓜把从设备树解析到 probe 执行再到子系统注册的完整链路读下来。一个外设能讲透其他外设基本可以举一反三。我个人最深的体会是真正的驱动力来自“看懂一条完整链路”而不是背一堆 API 名字。5.3 我的几点经验最后分享几条实际开发中沉淀下来的经验。第一不要过度依赖设备树编译器的语法检查它只能发现格式错误发现不了语义错误。每次改完设备树启动后第一件事就是在/proc/device-tree下确认节点存在且属性值正确。第二不要在一个驱动里同时依赖of_match_table和id_table完成同一件事匹配顺序固定容易造成困惑。除非有兼容老板级文件的需求否则设备树场景只留of_match_table就够了。第三probe 函数里能失败的操作一定要有清晰的日志。一个驱动多的要初始化十几个资源如果每个失败点都只是返回错误码后期排查时只能靠猜。我在 probe 里固定用dev_err输出失败上下文包括是哪个资源、哪个步骤失败这个习惯帮我省了大量时间。第四善用/sys/bus/platform/drivers/driver/bind和unbind这两个文件它们允许你在系统运行时手动绑定或解绑设备。修改驱动后不想重启板子就可以先unbind重新 insmod再手动bind循环验证匹配逻辑效率会高很多。Platform 匹配机制说白了就是一场“对暗号”的游戏。设备树里写出你的暗号驱动里声明你认识的暗号内核拿着两边的那张纸逐字比对对上了就走进 probe对不上就谁也不理谁。把这场游戏的每个环节都摸透下次再遇到驱动不执行、probe 不触发你就不会再手足无措了。
返回列表