ARTICLE DETAIL

资讯详情

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

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

i.MX6ULL Linux Platform设备驱动匹配机制详解 做嵌入式Linux驱动开发的人迟早都会撞上Platform这个机制。尤其是用i.MX6ULL这种入门级SoC做项目第一次把自己的设备树节点和驱动对上、看到串口里终于打出一行“probe ok”的时候很多人其实并不清楚背后那条虚拟总线到底怎么把设备和驱动撮合到一起。今天这篇就围绕i.MX6ULL上的Linux驱动开发把Platform设备与驱动的匹配机制从头到尾掰开聊一遍从内核代码到实战排查把那些文档里没讲透的细节一次说清楚。1. 为什么非要搞懂Platform匹配机制1.1 从裸机思维到Linux驱动思维的转变很多从STM32裸机或RTOS转来做i.MX6ULL驱动的人最初都会有一种感觉寄存器手册上写得很明白UART、GPIO的物理地址和控制位就在那里为什么在Linux里不能像裸机那样直接往寄存器地址写值原因不复杂Linux不是一个“单任务裸机程序”所有外设资源都由内核统一管理。你不能随便拿一个物理地址就访问也不能想申请中断就申请而是必须先向内核证明自己是这个硬件设备的合法驱动。内核要管理成百上千种设备、兼顾不同厂商的SoC就必须建立一套统一的抽象这就是Linux设备模型。Platform机制就是这套设备模型里最核心的一环。它在驱动开发里的地位相当于“万能插座”所有直接挂在CPU总线上、没有即插即用枚举能力的外设控制器比如i.MX6ULL上的UART、I2C、SPI、GPIO模块都会被统一规范成platform device。我们写的驱动则注册成platform driver由内核根据设备树信息决定两者是否匹配匹配成功后再调用驱动里的probe函数。这里有一个很多新手容易忽略的点裸机开发是“代码主动找硬件”而Linux驱动开发是“内核帮你把硬件和代码配对”。整个驱动生命周期的起点不在你的初始化函数里而在platform总线那一次次match调用中。1.2 i.MX6ULL的“总线-设备-驱动”模型i.MX6ULL是Cortex-A7内核NXP面向物联网和工业控制推出的低成本SoC国内学习板和实际产品里用得非常多。它内部几乎所有外设控制器在Linux内核里都以platform设备的形式存在。从UART1到GPIO控制器、从看门狗到SDIO控制器设备树里这些带compatible属性的节点在系统启动过程中都会被创建为platform device挂到同一条虚拟总线上。这里说的“总线”不是物理意义上的总线而是一条虚拟总线。它的作用就像婚介所一边接收设备一边接收驱动然后不断调用匹配函数想方设法把两边撮合起来。一旦匹配成功驱动里的probe函数就会执行你在probe里完成的ioremap、中断申请、GPIO初始化才算真正生效。整个流程从uboot加载dtb文件开始内核启动早期会把设备树展开成device_node结构随后of_platform_default_populate_init之类的初始化代码扫描这些节点把带compatible属性的节点一个个注册成platform device。也就是说设备树不仅是给驱动传递参数的“配置文件”它本身就是设备的来源。理解了这个模型再回头看i.MX6ULL的BSP和内核启动日志很多行为就能对上了。2. Platform设备和驱动是怎么“对上眼”的2.1 内核里platform_match到底干了什么匹配的核心代码在drivers/base/platform.c里函数名叫platform_match。这个函数极其经典每次platform总线上注册新设备或者新驱动时内核都会遍历总线上已有的另一侧逐个调用它来判断是否匹配。platform_match的匹配顺序大致如下先用设备树方式匹配也就是检查of_match_table和节点的compatible属性。再看ACPI方式这个在x86和部分ARM服务器场景才用i.MX6ULL基本不涉及。接着用驱动的id_table进行匹配。最后退而求其次比较驱动名字和设备名字是否相等。用代码说话就是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. 设备树of匹配i.MX6ULL主要走这里 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. ACPI匹配嵌入式基本用不到 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. id_table匹配老式board文件时代入口 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 4. 名字兜底比较 */ return (strcmp(pdev-name, drv-name) 0); }注意看这个顺序只要设备树匹配成功就直接返回1后面的id_table和名字比较根本不会执行。这意味着在i.MX6ULL这种纯设备树环境下你的设备树节点compatible属性和驱动里的of_match_table能否对上直接决定驱动会不会被调用。id_table是早期ARM板文件board-xxx.c时代的产物现在基本只出现在老驱动兼容代码里新人可以了解但没必要深究。2.2 compatible属性与of_match_table的匹配细节驱动侧我们通常定义一个of_device_id数组声明这个驱动能兼容哪些硬件然后在platform_driver结构体里引用static const struct of_device_id led_of_match[] { { .compatible mycompany,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name myled, .of_match_table led_of_match, }, };设备树侧对应的节点必须有compatible属性myled { compatible mycompany,myled; status okay; };匹配时内核的__of_device_is_compatible函数会把设备树节点compatible属性里的字符串列表和of_device_id数组里的每一项逐一对碰只要其中一个commpatible完全相等就算匹配成功。这里有个细节值得强调设备树节点compatible可以写多个字符串比如compatible mycompany,myled-v2, mycompany,myled;匹配时只要驱动里的compatible能命中其中任何一个就算成功这种写法常用于“新硬件兼容旧驱动”。但驱动侧的of_device_id数组里任何一项都不要写空字符串或写成NULL数组最后一定要加一个空结构体作为哨兵否则内核遍历时可能越界直接崩溃。还有一个容易忽略的点compatible字符串的命名习惯最好是“厂商,型号”。NXP官方的imx6ull.dtsi里清一色是fsl,imx6ull-uart、fsl,imx6ull-gpio这种格式。不要小看这个习惯产品一多、设备树一复杂规范的命名能避免大量命名冲突和排查时间。2.3 设备和驱动谁先注册都能匹配上吗很多刚入门的人会问设备树先解析还是驱动先注册如果我用insmod加载驱动模块而设备树节点对应的platform设备还没创建是不是就匹配不上了答案是都能匹配上。因为platform总线的匹配是动态的、双向的。设备先注册、驱动后注册驱动注册时会回头遍历总线上已有的设备逐个匹配反过来驱动先注册、设备后创建设备注册时也会遍历已有驱动。只要双方最终都挂在platform总线上先后顺序不影响结果。实际开发中驱动模块一般用module_platform_driver这个宏来注册module_platform_driver(led_driver);展开以后就是module_driver(led_driver, platform_driver_register, platform_driver_unregister);模块加载时调用platform_driver_register卸载时调用platform_driver_unregister。如果把驱动编译成内核模块再配合MODULE_DEVICE_TABLE导出的模块别名系统甚至能做到“设备树里出现对应compatible节点时自动加载这个模块”这就是udev/modprobe与设备模型配合的效果。在i.MX6ULL上我个人的建议是做产品把驱动直接编进内核probe顺序固定、减少模块依赖问题做学习调试就编成模块改代码重新交叉编译快配合insmod/rmmod测试很方便。两种方式下匹配机制本身完全一样区别只是注册时机和驱动加载方式。3. i.MX6ULL平台下Platform驱动怎么落地3.1 设备树里怎么写自己的设备先看一个实际例子。假设要在i.MX6ULL上控制一颗LED接在GPIO1_IO03引脚上设备树可以这样描述它myled { compatible mycompany,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; };同时需要在iomuxc节点下补一个pinctrl子节点把GPIO1_IO03复用成GPIO功能iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里强调两个关键点。第一设备树里自定义节点的名字比如myled只影响/sys目录里的展示名称和部分辅助功能真正决定匹配的是compatible属性两者千万别搞混。第二pinctrl子节点的写法名字带不带grp后缀其实不影响功能但NXP官方例程都这么命名跟着写最容易和同事的代码保持一致也方便在pinctrl调试节点里快速查找。那个0x10b0是引脚配置值里面包含了上下拉、驱动能力、施密特触发器使能等bit位具体含义要查NXP参考手册的IOMUXC章节。新手阶段直接用官方evk设备树里相似功能的数值就行跑起来后再逐个bit研究效率更高。如果你的设备不仅要GPIO还要寄存器地址就在节点里加reg属性myuart { compatible mycompany,myuart; reg 0x02020000 0x4000; interrupts GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH; status okay; };reg里的第一个address、size对会变成probe里platform_get_resource拿到的第一个IORESOURCE_MEM资源interrupts属性对应platform_get_irq的返回值这一点后面会细说。3.2 从零写一个Platform驱动的骨架下面是一个完整的、可以在i.MX6ULL上编译加载的最简platform驱动。功能是probe时从设备树里取到GPIO初始化并点亮LED同时注册一个misc设备给用户态操作。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h static struct gpio_desc *led_gpio; static const struct of_device_id led_of_match[] { { .compatible mycompany,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; led_gpio gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, failed to get led gpio, err%ld\n, PTR_ERR(led_gpio)); return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); dev_info(dev, myled probed, led on\n); return 0; } static void led_remove(struct platform_device *pdev) { gpiod_set_value(led_gpio, 0); gpiod_put(led_gpio); dev_info(pdev-dev, myled removed\n); } static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name myled, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(i.MX6ULL led platform driver);加载后如果设备树里有compatible为mycompany,myled的节点led_probe就会被调用dmesg里能看到“myled probed, led on”的日志。这个骨架虽然简单但已经把platform匹配机制的主干流程完整走了一遍。这里特别强调一点probe里获取资源尽量用devm_开头的API比如devm_gpiod_get、devm_ioremap_resource、devm_platform_get_and_ioremap_resource。这些API会在设备解绑或驱动卸载时自动释放资源能省掉一大半错误处理代码。我见过太多人手动管理资源某个分支忘记释放GPIO第二次insmod就报already requested折腾半天找不到原因。3.3 probe里到底该干什么probe函数是platform驱动的核心入口它的职责是把“设备树里的抽象描述”变成“可用的操作系统资源”。对i.MX6ULL上的外设驱动来说probe里通常按下面几步组织第一步是从设备树取资源。GPIO用gpiod_get(dev, led, ...)按属性名取中断用platform_get_irq(pdev, 0)取寄存器地址段用platform_get_resource加devm_ioremap_resource取典型写法是这样的struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base);这里的“0”表示取第0个reg段。指令里的物理地址经过ioremap后得到的是内核虚拟地址后面读写寄存器就通过base加上偏移完成绝不能直接拿物理地址操作。第二步是初始化硬件。包括设置GPIO方向、配置中断、复位外设、开启时钟。注意probe是运行在进程上下文里的可以睡眠但不要太慢不要在probe里做几百毫秒以上的阻塞操作否则系统启动或者模块加载时会卡顿明显。第三步是注册子系统或者字符设备把能力暴露给用户态。最简单的用misc_register复杂一点可以用register_chrdev或者cdev接口。我上面那个例子里没有注册设备只是为了突出匹配流程实际产品驱动一定要把操作接口补上。第四步是保存私有数据。通常用devm_kzalloc申请一个自定义结构体把GPIO、寄存器基地址、锁等装进去用platform_set_drvdata存起来remove里用platform_get_drvdata取回。这样驱动就不会到处是全局变量多实例支持也更好。还有一点非常关键probe返回错误码时要区分普通负错误码和-EPROBE_DEFER。如果你的外设依赖的时钟、pinctrl或某个父设备还没就绪直接返回-ENODEV会让内核认为驱动不想要这个设备以后不会再尝试。正确做法是返回-EPROBE_DEFER内核会把设备挂到延迟探测队列等依赖资源就绪后自动重新调用probe。在i.MX6ULL上pinctrl子系统的初始化顺序晚于部分platform设备是常态所以EPROBE_DEFER非常常见千万不要忽略这个返回值。4. 调试手段与常见问题排查4.1 从/sys和/proc入手定位问题驱动写好了设备树也改了上电后发现probe根本没执行。这时候第一件事不是反复看源码而是去运行时环境里找证据。先看设备有没有被创建成platform device。设备树编译成dtb加载后内核启动时会把带compatible属性的节点变成platform device通过下面命令能看见ls /sys/bus/platform/devices/目录里会有一堆像20c0000.uart、209c000.gpio这样的条目前缀是寄存器地址后缀是设备树节点名。如果找不到你的设备节点说明设备树本身没生效优先检查dtb有没有烧写正确、节点status是不是okay。接着看驱动注册了没有ls /sys/bus/platform/drivers/myled/驱动目录里如果有一个符号链接指向设备目录说明匹配成功probe肯定被调用过。如果没有可以试着手动绑定echo myled /sys/bus/platform/drivers/myled/bind内核4.x以后提供的这个bind/unbind接口在调试匹配问题时非常好用不用反复卸载加载模块。想看内核到底在哪一步匹配失败可以打开platform核心代码的动态调试echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control打开后dmesg里会打印出platform_match的调用路径能看到驱动名、设备名、compatible的比较过程。别小看这个手段复杂匹配问题很多时候就靠它一锤定音。4.2 常见报错与排查速查表把i.MX6ULL开发中围绕Platform匹配的典型问题整理成一张表基本覆盖了新人能遇到的大部分坑现象常见原因排查方法probe不执行/sys里找不到设备设备树没生效或status不是okay检查dtb加载查看/proc/device-tree下节点probe不执行设备在但没匹配compatible拼写不一致逐个字符对比of_match_table与设备树compatible模块加载报Invalid module format内核版本或配置不匹配用开发板同版本内核编译核对.configprobe返回-ENODEV后不再执行资源获取失败返回了普通错误码改成-EPROBE_DEFER检查GPIO/中断/时钟依赖启动卡在gpiolib相关日志GPIO被占用或pinctrl配置冲突查dmesg的pin already requested看gpio调试节点设备树改了没反应dtb没重烧或uboot加载路径不对确认环境变量里的fdtfile指向正确dtb文件特别提一下“pin already requested”这类问题。i.MX6ULL的引脚是高度复用的同一个引脚可能同时被GPIO、UART、I2C等多个功能需要使用。如果另一个驱动提前把引脚占用了你的gpiod_get就会失败报错信息往往只有一行。这时从驱动代码看不出任何毛病必须去查pinctrl和gpio的运行时状态cat /sys/kernel/debug/gpio cat /sys/kernel/debug/pinctrl/20e0000.iomuxc/pins第一个命令能看到所有GPIO的占用者和方向第二个能看到引脚当前被哪个function复用。这两条命令在排查资源冲突时是救命稻草。4.3 几个容易掉的坑第一个坑也是最高频的of_match_table里的compatible与设备树节点compatible不是严格相等。设备树里写mycompany,myled驱动里写mycompany,myled-v2或者多了个空格都匹配不上。OF匹配是严格的字符串比较没有模糊匹配一个字符都不能差。第二个坑设备树节点status被设成disabled。NXP官方的imx6ull.dtsi里大量外设节点默认disabled板级dts里要显式打开。很多新手只加了自定义节点忘了检查既有节点的status。自定义节点如果不写status默认就是okay这点反而容易迷惑人因为行为不一致导致排查时总有一种“为什么它不需要我写”的错觉。第三个坑是模块卸载不干净。有些驱动remove逻辑写得不完整GPIO没释放、字符设备没删除第二次insmod虽然probe会执行但请求资源时就会失败。养成习惯每次改完驱动重编后先看dmesg有没有already requested之类的历史报错再继续往下调。第四个坑是Makefile和交叉编译环境问题。内核源码树外编译模块时KERNELDIR要指向开发板上正在运行的那个内核版本源码并且最好用与板子一致的.config。否则编译出来的模块会因为内核头文件不匹配加载时报undefined symbol或者Invalid module format。我习惯把编译模块用的Makefile固定成这样KERNELDIR : /home/developer/imx6ull/linux-imx CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc obj-m : myled.o all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) clean这几行看起来简单却能避免一大部分“驱动写对了却加载不了”的问题。5. 一次典型的匹配失败排查实录最后分享一个我在i.MX6ULL上踩过的真坑整个排查过程刚好能把前面说的机制串起来。当时一个同事在他的板上加了一个自定义传感器节点驱动已经编进内核。上电后dmesg里完全看不到probe日志设备没有任何反应。我先看了/sys/bus/platform/devices发现他的设备节点压根不在列表里说明在这一步就已经断了驱动连被匹配的机会都没有。继续查发现设备树节点确实存在但status被写成了disabled。为什么会这样因为同事在dtsi文件里改了节点板级dts里也有同名节点两个文件合并时同名节点的属性是逐属性合并的dtsi里设置成okay板级dts里如果没写status最终还是会被默认值或覆盖行为改成disabled。这个问题如果只盯着一处文件看根本看不出来。后来把板级dts里的同名节点补上statusokay重新编译dtb烧录设备目录出来了。但probe还是没执行进一步对比发现他的compatible写的是“mycompany,myled”驱动里写的是“mycompany,myled”——看起来一样实际却一个用了中文逗号。这种复制粘贴导致的低级错误在代码编辑器里很难分辨最后是拿设备树原始字节流hexdump才发现的。从那以后我每次改完设备树都习惯先上板子看一眼节点原始内容cat /proc/device-tree/mydevice/compatible | xxd确认没有隐藏字符、没有编码问题再去做驱动侧排查。省下来的时间足够多做两个功能模块了。这个习惯也推荐给所有做i.MX6ULL或类似平台开发的朋友。
返回列表