ARTICLE DETAIL

资讯详情

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

i.MX6ULL Platform驱动匹配机制:从compatible到probe的完整指南

i.MX6ULL Platform驱动匹配机制:从compatible到probe的完整指南 做过一阵子嵌入式Linux驱动开发的人大概率会碰上一种很“玄学”的状况驱动代码看着没问题insmod加载也不报错可probe函数就是不执行。设备节点明明在设备树里写着驱动模块也注册了两边就是碰不到一起。我第一次在正点原子的i.MX6ULL开发板上写外设驱动时就被这个问题卡了整整一下午最后发现是设备树里的compatible字符串少写了一个字母。从那以后我就意识到Platform设备与驱动匹配机制这只“看不见的手”其实是嵌入式Linux驱动开发里最值得先弄明白也最常被新手忽略的一块内容。先给还不熟悉的朋友交代一下背景。i.MX6ULL是NXP基于Cortex-A7内核推出的低功耗应用处理器在工业控制、物联网网关、智能家居这类场景里出场率很高。它内部的绝大多数外设——GPIO、UART、I2C、SPI、定时器、看门狗——都不是挂载在PCIe、USB这类可枚举总线上而是直接集成在SoC内部由芯片手册固定好地址。Linux内核没有办法像USB设备那样通过枚举去发现它们于是引入了一套虚拟的Platform总线机制来管理这类“没有真实物理总线”的设备。这套机制的核心就是设备和驱动之间的匹配规则。搞懂它你在i.MX6ULL上写任何外设驱动都会顺手很多。1. 为什么i.MX6ULL驱动开发绕不开Platform机制1.1 从“找不到设备”说起先讲一个很直白的类比。你去快递驿站取件驿站里货架上摆满了包裹设备取件窗口贴着一张工作人员名单驱动。工作人员要干活第一步是确认哪个包裹归他管。USB设备像包裹上贴了完整条码一扫就知道是哪个快递公司的这就是可枚举总线的自动发现机制。而i.MX6ULL这种片上外设相当于驿站的货架上只写了一个编号硬件地址没有条码。Linux内核没办法自动识别这个编号对应谁只能靠一套约定好的规则来判断。Platform总线就是在这种情况下诞生的。它是一条虚拟总线在/sys/bus/platform/这个路径下可以看到它的身影。它的核心工作只有一件事把内核里注册的platform_driver和platform_device配对。配上了就调用驱动的probe函数配不上两边都只能干等着谁也不知道对方存在。1.2 设备和驱动是两张独立的表很多刚入门的同学会有一个错觉写驱动就是写一个.ko文件然后加载到内核里就行了。实际上在Linux设备模型里设备device和驱动driver是两套完全独立的东西。设备代表“硬件是否存在”驱动代表“内核有没有能力处理这个硬件”。它们之间没有从属关系只有通过总线上的匹配机制才会产生关联。在i.MX6ULL的BSP里设备信息通常写在设备树Device Tree文件.dts中由内核在启动阶段解析生成一个个platform_device。驱动信息则是我们自己写的.ko模块注册为platform_driver。两者各记各的账Platform总线负责对账。哪个驱动模块在加载时找到了和自己匹配的设备probe函数就会被执行驱动才算“真正活了”。这就是为什么很多驱动代码里probe函数才是核心init和exit往往就几行注册代码。1.3 这套机制解决了什么问题如果不用Platform机制行不行理论上你可以直接在驱动模块的init函数里用ioremap映射寄存器、request_irq申请中断把硬件初始化全干了不需要任何匹配过程。但这样做的后果非常严重资源没有一个统一的管理入口设备树里配置的信息无法自动传递到驱动里多个驱动模块之间容易产生地址冲突更别提电源管理、休眠唤醒这些依赖设备模型的功能了。Platform机制的核心价值就是把“设备描述”和“驱动逻辑”解耦让硬件配置灵活性大幅提高。换一个型号的晶体振荡器、改一个GPIO引脚只需要改设备树驱动代码完全不用动。2. 匹配机制的底层原理内核到底是靠什么对上号的2.1 官方文档里的四份“花名册”Linux内核的platform_match函数是所有匹配工作的起点。它在一个设备与一个驱动之间做判断按顺序检查四种情况命中任意一条就算匹配成功。源码在drivers/base/platform.c里核心逻辑我可以直接给大家列出来看的要点第一of_driver_match_device。这是设备树模式下最常用、优先级最高的一条。它拿驱动的of_match_table里的compatible字段去和设备树节点的compatible属性做比较。只要有一个字符串完全一致就返回匹配成功。第二acpi_driver_match_device。这是给x86平台用的ACPI匹配方式在i.MX6ULL这种ARM平台基本不用但了解有这回事就行。第三platform_match_id。这是用platform_driver里的id_table来匹配。每个id_table条目包含一个名字字符串和设备私有数据指针。内核拿这个名字和platform_device的name字段比较相等就匹配成功。第四也是兜底的一条直接比较platform_device的name和platform_driver的driver.name。这个最直接也最简单粗暴适合那些不设备树的传统注册方式。匹配顺序上compatible优先于id_tableid_table优先于name。这个顺序极其关键我在后面会解释为什么会踩坑。2.2 谁的优先级最高只有compatible能穿透设备树在我们i.MX6ULL的实际开发中设备信息已经全部搬进设备树了所以真正需要关心的就是第一条——of_driver_match_device。这条匹配规则具体做的事情是拿设备树节点里的compatible属性和驱动of_match_table中的compatible值做字符串比较。字符串怎么做比较就是一个字符一个字符地比对要求完全一致少一个字母、多一个空格都不行。这里要特别强调一点compatible本质上是一个“设备身份标识”。在设备树规范里它通常采用“厂商前缀,型号”的格式比如fsl,imx6ull-uart、gpio-leds、ns16550a这样的写法。反斜杠没有下划线没有全部小写。i.MX6ULL的芯片手册和NXP官方BSP里每个外设的compatible值都有明确的约定。写错了匹配不上并不是内核机制的问题而是你没有遵守规范。2.3 匹配完成后发生了什么一旦Platform总线判定设备和驱动匹配成功就会调用platform_driver结构体里的probe函数指针。这个函数是驱动的“主战场”申请资源、映射寄存器、注册字符设备、创建sysfs和procfs条目都在这里完成。需要注意的一点是probe函数什么时候执行取决于设备还是驱动先注册。如果设备树里的platform_device已经在内核启动时生成好了你在应用层用insmod加载驱动模块那么加载过程会立刻触发匹配随后马上调用probe。反过来如果驱动已经编译进内核built-in而设备树里的节点是在系统启动后才被某种机制注册进来的那也会在设备注册时触发匹配调用probe。理解这个先后顺序对排查“为什么probe不执行”非常有帮助。3. i.MX6ULL硬件环境下的设备树配置实战3.1 一个最简设备树节点的解剖在i.MX6ULL的BSP里打开arch/arm/boot/dts/imx6ull.dtsi你会看到一堆外设节点。这里以最简单的GPIO点灯为例我们添加一个自定义LED节点/ { myled { compatible mycompany,board-led; pinctrl-names default; pinctrl-0 pinctrl_myled; gpios gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };这个节点常见于用户自己的板级文件比如imx6ull-myboard.dts。节点名myled其实无关紧要内核匹配时看的不是节点名而是compatible属性。真正起作用的是mycompany,board-led这个字符串。gpios属性里指定了GPIO1的IO03引脚GPIO_ACTIVE_LOW表示低电平有效。pinctrl-0引用了我们在iomuxc节点里配置的引脚复用状态。3.2 设备树节点是如何变成platform_device的内核在启动阶段会进行设备树解析。核心处理函数在drivers/of/platform.c里主流程是of_platform_bus_probe和of_platform_populate这两个函数。它们会遍历设备树中的每个节点对符合条件的节点创建platform_device。哪些节点会被创建成platform_device呢判断标准是如果一个节点有compatible属性而且它的父节点的compatible不是某些特定的“总线容器”类型那么它就会在Platform总线上注册成为一个设备。像I2C子节点不会生成platform_device因为I2C控制器节点被认为是绑定在I2C总线框架下的内部子节点由I2C核心用另外一套机制处理。理解这条规则很重要否则你会疑惑为什么自己设备树里写的SPI从设备节点probe函数在platform驱动里却不执行。3.3 compatible字符串的编写规范写设备树的时候很多人图省事随便写一个compatible。比如自己测试写个test就完事了。这在早期实验阶段确实能跑通因为只要驱动里也写这个字符串两边一致就能匹配。但在真正的项目里这种写法隐患很大。Linux内核社区对compatible有一个约定俗成的格式厂商名,器件型号。厂商名通常是公司名或者项目名的缩写全部小写器件型号是具体的硬件名。这样做的原因是为了避免不同芯片厂商之间的compatible冲突。比如NXP的fsl,imx6ull-uart和TI的ti,omap-uart一眼就能看出是谁家的设备。还有一点设备树节点里可以写多个compatible用逗号隔开优先级从前往后。这是为了兼容新旧驱动新内核用新的匹配项老内核退而求其次匹配旧的。但在i.MX6ULL上我们基本用不到这个特性项目里保持只有一个compatible就够了。4. 驱动的完整实操从insmod到probe执行4.1 最简Platform驱动模板先把一个完整的、可以直接在i.MX6ULL上跑起来的platform驱动代码给大家看这个代码会匹配上面设备树里的LED节点。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/of_gpio.h static int myled_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *led_gpio; int ret; dev_info(dev, myled probe success\n); /* 使用devm_gpiod_get获取GPIO描述符这个API会自动解析设备树里的gpios属性 */ led_gpio devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { ret PTR_ERR(led_gpio); dev_err(dev, failed to get gpio: %d\n, ret); return ret; } /* 到这里说明GPIO已经申请成功可以先点个灯验证 */ gpiod_set_value(led_gpio, 1); dev_info(dev, LED turned on\n); /* 在实际项目中这里通常是注册字符设备、创建类、创建设备节点的逻辑 */ return 0; } static int myled_remove(struct platform_device *pdev) { dev_info(pdev-dev, myled remove\n); return 0; } static const struct of_device_id myled_of_match[] { { .compatible mycompany,board-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform driver example);4.2 代码里最有讲究的三处细节第一处是of_match_table。它定义了一个of_device_id数组里面的compatible必须和设备树里的值完全一致。数组末尾要放一个全零的哨兵项内核靠它判断数组结束。MODULE_DEVICE_TABLE(of, ...)宏非常重要它在编译时生成模块的别名信息。如果没有这行宏即使驱动已经编进内核或加载设备树匹配也会因为缺少模块别名而失败尤其是在内核把驱动编译成模块时差别很大。第二处是driver.name。这里写myled。注意这个字段和设备树节点名无关它会在最后一条策略中才会用到。但是内核要求driver.name不能和同总线上其他驱动重名否则注册会失败。我见过有人把所有驱动都叫platform结果第二个驱动注册时直接报错/sys/bus/platform/drivers/下面出现了带数字后缀的目录。第三处是module_platform_driver宏。它相当于是module_init和module_exit的快捷方式展开后定义好init和exit函数在init里调用platform_driver_register在exit里调用platform_driver_unregister。使用这个宏可以少写很多模板代码几乎所有的内核模块都推荐这样写。4.3 编译和加载验证在i.MX6ULL的SDK环境下把上述代码放到内核源码树的drivers/misc/目录下或者作为独立模块编译。这里以独立编译举例需要指定内核源码目录和交叉编译工具链前缀make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -C /path/to/kernel M$(pwd) modules编译出的myled.ko拷贝到开发板上加载前先确认设备树节点已经使能ls /proc/device-tree/myled/ cat /proc/device-tree/myled/compatible如果看到输出mycompany,board-led说明设备树节点存在。执行insmod myled.ko正常的话dmesg里就会出现myled probe success和LED turned on。如果只看到模块加载成功没有probe打印那就要进入下一节的排查环节了。4.4 用id_table方式注册的补充方案设备树模式之外还有一种传统的Platform驱动注册方式用platform_device_id表匹配。在i.MX6ULL这种设备树普及的平台上这种方式主要用于兼容旧的、没有设备树的内核。static const struct platform_device_id myled_ids[] { { .name myled, .driver_data (kernel_ulong_t)NULL }, { } }; MODULE_DEVICE_TABLE(platform, myled_ids); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, }, .id_table myled_ids, };这种方式的匹配条件是platform_device的name字段和id_table中的name相等。而非设备树模式下platform_device的name通常来自板级代码里的platform_device结构体定义。在i.MX6ULL的现代BSP里这种方式已经没有用武之地了但阅读老代码时还是会碰到理解即可。5. 调试方法与排查技巧实录5.1 用sysfs查看匹配状态Linux内核把platform总线上的设备和驱动都挂在/sys/bus/platform/下面。设备挂在devices/子目录驱动挂在drivers/子目录。目录结构清晰得像一张表格非常适合排查问题。查看当前有哪些platform设备ls /sys/bus/platform/devices/如果你在设备树里添加的led节点生成成功了这里会出现一个类似myled的目录。如果没出现说明设备树解析环节有问题可能节点被status disabled禁用了或者父节点根本就没有被解析到。查看当前有哪些platform驱动ls /sys/bus/platform/drivers/如果在设备树里写myled的话这里应该出现myled目录。驱动加载后如果和设备匹配成功/sys/bus/platform/drivers/myled/下面会出现一个指向设备目录的符号链接。这是一个非常直观的匹配成功标志。5.2 常见匹配失败原因一览第一个高频原因是compatible拼写不一致。设备树里写的是mycompany,board-LED驱动里写的是mycompany,board-led大写LED和小写led完全不同匹配必然失败。这种错误最隐蔽因为编译不会报错系统也不会给任何警告信息。第二个高频原因是设备树节点状态被禁止。NXP原厂BSP里很多外设节点默认是disabled需要板级设备树里显式改写成okay。如果你直接拿原厂dtsi在自定义板卡上不加修改地使用很多外设节点根本不会生成对应的platform_device。第三个高频原因是of_match_table没有正确设置。有人写驱动时用了platform_driver_register却忘了给driver.of_match_table赋值。这种情况下无论如何都不会匹配到设备树里的节点probe永远无法执行。第四个高频原因是驱动模块没有编译进内核而是放在了drivers/misc/下却忘了在Kconfig和Makefile里配置。这个不算是Platform机制本身的问题但排查起来会绕很大一圈。我见过有人折腾了几个小时最后发现模块压根没编进内核镜像里。5.3 一个完整的排查路径当probe函数不执行时我一般按下面这个顺序排查第一步确认设备树节点是否生成成功。在开发板上执行ls /proc/device-tree/看看你的顶层节点在不在。如果这个目录下都找不到那就是设备树编译、传递环节出问题了需要回查uboot的fdt_addr和bootargs设置或者dts的include关系。第二步确认驱动是否注册成功。加载模块后执行ls /sys/bus/platform/drivers/找到你驱动名字对应的目录。如果这里都没有说明module_platform_driver这个宏没有生效检查模块加载时有没有报错比如符号找不到之类的。第三步确认驱动和设备是否“见面”了。执行ls -l /sys/bus/platform/drivers/myled/看有没有符号链接指向../devices/myled。有符号链接说明匹配成功只是probe出错退出了没有符号链接说明根本没匹配上需要核对compatible字符串。第四步核对compatible。在开发板上执行cat /proc/device-tree/myled/compatible看看设备树实际解析出的字符串。注意这里输出的内容末尾可能带一个换行符或空格肉眼不易察觉最好用od -c或xxd查看十六进制避免因为不可见字符导致匹配失败。第五步如果以上都正常就需要核对内核日志了。执行dmesg | grep myled看看有没有更详细的错误信息比如gpio请求失败、ioremap失败、中断申请失败等。很多时候probe函数确实执行了只是中途返回了错误码从用户视角看就像“没执行”一样。5.4 我踩过的最隐蔽的一个坑在i.MX6ULL上做GPIO按键驱动时我在设备树里用了gpio-keys这个标准节点。这个节点是内核自带的gpio_keys驱动在处理不需要自己写platform驱动。结果我自己写的platform驱动和一个旧的、没有从内核里移除的gpio_keys驱动产生了竞争。两边的compatible都是gpio-keys我的模块先加载把设备抢走了但功能没实现完整导致整个按键功能不正常。这个问题的根源就是compatible冲突。所以给自定义设备起compatible的时候一定要用自己独有的前缀比如公司缩写避免和内核已有的标准驱动冲突。这也是为什么业界普遍推荐厂商,型号格式的原因。6. 常见问题速查表现象可能原因排查方法probe不执行compatible不匹配cat /proc/device-tree/节点名/compatible核对大小写和空格probe不执行设备树status为disabled检查dts里的status字段改为okayprobe不执行of_match_table未设置检查platform_driver结构体里的of_match_tableprobe不执行设备树节点没有生成设备检查父节点是否设置为disabledprobe返回-EINVALgpios属性解析失败检查GPIO编号、GPIO控制器别名设置probe返回-ENOMEM内存分配失败检查是否大量使用devm_kmalloc释放是否及时模块加载失败driver.name重名换一个不冲突的name字段设备树解析阶段崩溃dtsi语法错误用dtc工具验证dts/dtb文件格式设备树里写了statusdisabled但仍然生成设备驱动是built-in不是模块built-in驱动注册后晚于设备注册时匹配注意匹配顺序这张表是我做记录时自己整理的里面的每一条都对应真实踩过的坑。尤其是最后一条statusdisabled的节点如果驱动是编译进内核的仍然会注册生成platform_device只是设备树框架会标记它为disabled但of_platform_populate函数对disabled节点的处理在不同内核版本上略有差异。总之不要抱有“disabled了肯定没设备”的侥幸心理真出了问题还是要从sysfs一层层查起。7. 关于匹配机制的一个延伸思考Platform匹配机制看似简单其实就是字符串比较但它的设计思路贯穿了 Linux设备模型的整个体系。理解它之后你会发现I2C设备驱动、SPI设备驱动、USB设备驱动里的匹配逻辑本质上都是一样的套路——设备端有id_table或compatible驱动端也有对应的匹配表总线负责对账对上号就调probe对不上就挂起等待。我在i.MX6ULL上做过的几个项目从GPIO点灯、按键中断到I2C触摸屏、SPI LCD都离不开这套机制。现在每接触一块新板子我都会花几分钟先打开设备树里对应外设节点看看compatible再在驱动源码里找到匹配表确认两边写法一致再做下一步开发。这一步省下来的时间绝对值回票价。
返回列表