ARTICLE DETAIL

资讯详情

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

Platform设备与驱动匹配机制全解析:从原理到i.MX6ULL实战

Platform设备与驱动匹配机制全解析:从原理到i.MX6ULL实战 搞Linux驱动的人应该都见过这种场面照着教程把platform_driver注册、probe函数写好insmod加载结果dmesg里什么都没有probe死活不跑。然后开始怀疑人生查设备树、查compatible、查驱动注册顺序折腾半天发现只是设备树节点compatible拼错了一个字母。这种问题我在i.MX6ULL上踩过太多次后来把Platform设备与驱动的匹配机制完整过了一遍才算真正明白驱动probe为什么会被调用、什么时候被调用、匹配不上到底差在哪里。这篇博文就把Platform机制从原理到实战完整拆一遍。适合刚入门嵌入式Linux驱动的新手也适合做了几个项目但对总线模型还一知半解的开发。看完你不仅能写出标准的Platform驱动还能自己排查匹配失败的问题再也不用靠瞎试。1. 为什么要有Platform机制设备驱动解耦的核心思想1.1 传统驱动开发里最让人头疼的两件事在讲Platform之前先回忆一下最原始的驱动写法。很多初学者写第一个字符设备驱动都是这样的在驱动代码里直接写死寄存器地址、中断号、GPIO引脚编号然后ioremap、request_irq、gpio_request一切正常。这种写法在单片机裸机开发里没问题但放到Linux里就麻烦了。第一个麻烦是设备信息变了就要改驱动代码。比如同一个板子原来LED接在GPIO1_IO03改版后接到了GPIO1_IO04按传统写法就要改驱动源码重新编译重新烧写内核。如果驱动已经发布给客户这简直是灾难。第二个麻烦是驱动和设备纠缠在一起没法独立维护。驱动本该只关心怎么操作硬件被写成了操作哪个硬件、怎么操作硬件混在一起。换一款硬件整个驱动推倒重来。Linux内核为了解决这个问题引入了总线-设备-驱动模型。这个思想其实特别简单用生活里的事打个比方你家墙上有个插座总线插座上插着台灯设备电灯厂商不需要知道你家墙里电线怎么走的他只需要按国家标准做一个插头驱动插上去就能亮。设备变了换灯泡驱动不用改驱动升级了设备也不用跟着换。1.2 Platform总线的定位承接没有总线的设备说清楚了总线模型接下来说Platform的具体位置。Linux里有PCI、USB、I2C、SPI这些真实存在的物理总线但还有一类设备很尴尬它们既不在PCI上也不在USB上而是直接挂在CPU的内存地址空间里比如GPIO控制器、UART控制器、LCD控制器这些SoC内部外设。这些设备没有物理总线可挂但内核也希望能用统一的设备-驱动匹配机制来管理它们。于是内核搞了一条虚拟总线叫platform_bus专门收纳这一类设备。凡是直接映射到内存地址的、不依附于其他物理总线的设备都算Platform设备。在i.MX6ULL上绝大部分内部外设都是Platform设备。这个SoC用的是Cortex-A7内核由NXP出品在嵌入式Linux学习板里非常流行。它的GPIO、UART、I2C、SPI、SDIO这些控制器设备树里描述好之后内核启动时会为它们创建platform_device再和对应的platform_driver去做匹配。1.3 设备树在Platform机制里扮演的角色提到现代Linux驱动设备树绕不开。设备树的作用就是描述硬件信息让内核知道板子上有哪些设备、用什么寄存器地址、中断号是多少、接在哪个GPIO上。i.MX6ULL的内核是从设备树启动的编译完的dtb文件会在U-Boot启动时被加载到内存内核解析这个dtb把每一个兼容的设备树节点转换成platform_device。这里有个关键点不是所有设备树节点都会变成platform_device只有满足一定条件的才会后面第三部分会细说。打个比方设备树就像一张硬件配置清单内核照着这张清单去创建设备对象。驱动则是技能包设备对象和驱动对象通过匹配机制对上号之后驱动就能上岗操作硬件了。2. 匹配机制到底怎么运转platform_match的完整流程2.1 三种匹配路径优先级有讲究驱动和设备是怎么对上眼的答案就在drivers/base/platform.c里的platform_match函数。这个函数是匹配机制的总入口每次系统尝试给某个platform_driver寻找设备或者给某个platform_device寻找驱动的时候都会调用它。看内核源码platform_match的判断逻辑是有先后顺序的匹配方式判断依据适用场景设备树匹配设备节点compatible是否匹配of_match_table基于设备树启动的ARM平台最常用ACPI匹配设备ACPI ID是否匹配acpi_match_tablex86平台为主ID表匹配设备name是否匹配id_table无设备树时代的传统方式驱动名匹配设备name是否等于driver.name最朴素的兜底方式先走设备树匹配再走ACPI再走ID表最后拿驱动名字兜底。这个顺序不是随便排的设备树和ACPI能提供更精确的硬件描述信息优先级自然高。ID表和驱动名匹配是历史遗留的简易方式用来兼容老代码。i.MX6ULL基本全靠设备树匹配所以我重点讲第一种。2.2 设备树匹配compatible就是暗号设备树匹配要满足的条件很直接设备树节点的compatible属性值要和驱动里of_match_table数组中某个元素的compatible字段一致。举个例子设备树里有这么一段myled { compatible myvendor,my-led; led-gpios gpio1 3 GPIO_ACTIVE_HIGH; };驱动里的匹配表static const struct of_device_id myled_of_match[] { { .compatible myvendor,my-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match);只要这两个字符串一模一样就能匹配成功。这个compatible字符串的命名是有规矩的规范是厂商名,设备型号全部小写比如fsl,imx6ull-uart、myvendor,my-led。这么做的目的是避免重名。不同厂商可能都有叫led的设备加上厂商前缀就能区分。匹配成功之后内核会调用这个驱动的probe函数。probe就像是驱动上岗时的入职仪式系统把匹配到的设备信息通过参数传进来驱动在这里完成硬件初始化。2.3 of_match_device往下走匹配成功的内部逻辑platform_match里当判断设备有of_node时会走of_driver_match_device这个函数它层层往下调用of_match_device最终比对of_device_id里的compatible字段。这里有个细节值得注意匹配时不只是比较compatible。of_device_id结构体里还有type和name字段分别对应设备树节点的device_type和name属性。如果of_device_id设置了这些字段也会参与匹配。但实际开发中绝大多数驱动只设置compatible就足够了device_type在设备树规范里已经被标记为不建议使用。再看一个容易踩坑的点驱动里写的MODULE_DEVICE_TABLE(of, myled_of_match)。这个宏的作用是生成一个alias信息让内核的模块管理子系统知道这个模块支持哪些设备。如果你是直接把驱动编进内核而不是用insmod加载这个宏加不加都能跑。但如果你打算用insmod加载模块或者让系统开机自动加载模块这个宏必须加。不加的话模块即使装进去了probe也可能不执行。2.4 驱动和设备谁先注册probe什么时候被触发很多初学者搞不清楚的一个问题是驱动的probe什么时候跑这就要分两种情况。第一种是设备先注册。内核启动时设备树解析完先创建了platform_device这时候platform_driver还没注册。等驱动模块加载或者驱动已经编进内核初始化时platform_driver_register调用之后内核会遍历bus上已有的设备一个个去做匹配匹配上了就立刻调probe。第二种是驱动先注册。驱动模块加载时bus上还没有设备probe不会执行。等设备树解析创建设备后新设备加入bus时内核会反向遍历已注册的驱动去做匹配匹配上就调probe。所以设备先注册还是驱动先注册无所谓只要两边最终都注册完成匹配总会发生。这也是bus模型最优秀的点设备和驱动的生命周期解耦谁先来谁后来都能正常工作。3. i.MX6ULL实战写一个能被Platform匹配的LED驱动3.1 准备工作和整体规划下面用一个完整的例子来演示从零开始在i.MX6ULL平台上写一个Platform驱动控制开发板上的LED。环境说明开发板i.MX6ULL我这边用的是正点原子alpha开发板其他板子原理类似内核源码NXP官方linux-imx版本4.1.15或者5.x都可以原理一致交叉编译器arm-linux-gnueabihf-gcc规划一下要做的事在设备树中新增LED节点描述清楚compatible和GPIO信息编写Platform驱动只做一件事probe函数获取LED对应的GPIO并初始化输出低电平编内核、编驱动、加载验证3.2 设备侧设备树节点怎么改在i.MX6ULL的设备树里我直接新建一个根节点不用挂在某个外设控制器下面因为LED这种设备本身就是SoC外部的简单设备没有物理总线可挂正好作为Platform设备。在设备树根节点下加/ { myled { compatible myvendor,my-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpios gpio1 3 GPIO_ACTIVE_HIGH; }; };同时需要在iomuxc节点里增加引脚复用配置iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里需要注意一个问题像这种根节点下的子节点内核启动时会不会自动变成platform_device答案是会。对于根节点下直接挂着的、带compatible属性的普通节点内核的of_platform_default_populate_init会去扫描并创建platform_device。但如果节点挂在某个特殊总线下或者父节点的compatible被标记为simple-bus情况会稍有不同。simple-bus的节点里子设备也会被展开成platform_device这不在本文展开但是匹配不上的时候需要排查这个点。还有个更符合工程习惯的做法直接把LED做成gpio-leds节点用内核自带的leds-gpio驱动。但对于学习Platform匹配机制自定义compatible更能说明原理所以这里用自研的方式。3.3 驱动侧完整的Platform驱动代码下面是完整的驱动代码我刻意简化了功能只在probe里获取GPIO并设置初始状态让大家专注看匹配流程。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h static const struct of_device_id myled_of_match[] { { .compatible myvendor,my-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *desc; desc devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(desc)) { dev_err(dev, failed to get led gpio: %ld\n, PTR_ERR(desc)); return PTR_ERR(desc); } dev_info(dev, probe ok, led gpio desc: %p\n, desc); return 0; } static int my_led_remove(struct platform_device *pdev) { dev_info(pdev-dev, my led removed\n); return 0; } static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my-led, .of_match_table myled_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform led driver);注意第22行devm_gpiod_get传入的第二个参数是led这里对应设备树里的led-gpios属性。devm开头的接口是设备资源管理机制自动帮你释放资源probe失败或者设备移除时不需要手动释放对新手很友好。remove函数不是必须的但对于可卸载的驱动模块建议实现。当设备从总线移除或者驱动卸载时内核会调用它在这里做清理工作。3.4 编译验证怎么确认匹配真的成功了驱动编译用标准的模块Makefileobj-m : myled.o KDIR : /home/user/linux-imx CROSS_COMPILE : arm-linux-gnueabihf- all: $(MAKE) -C $(KDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) modules clean: $(MAKE) -C $(KDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) cleanKDIR指向内核源码目录M$(PWD)表示把编译出的模块放到当前目录。编译完成后把myled.ko拷贝到开发板insmod加载insmod myled.ko紧接着看dmesg输出my_led myled: probe ok, led gpio desc: 00000000abcdef12出现这行就说明匹配成功、probe执行了。为了确认绑定关系还可以查看sysfsls /sys/bus/platform/devices/能看到myled这个设备目录。再查看驱动目录ls /sys/bus/platform/drivers/myled/里面应该有myled这个设备的符号链接。这说明设备和驱动已经绑定上。如果没有实现MODULE_DEVICE_TABLE宏insmod时可能报Unknown symbol或者加载不报错但不触发probe。如果加载后sysfs里没有对应关系优先检查这个宏。4. 匹配过程中的常见坑故障排查现场实录4.1 最常见的原因compatible长得不一样我见过最多的匹配失败原因就是设备树里的compatible和of_match_table里的字符串不完全一致。多一个空格、大小写不同、少个前缀都会导致匹配失败。这种问题最好排查因为内核提供了现成的工具。在开发板上有设备树源文件时可以这样查节点compatiblels /proc/device-tree/myled/ cat /proc/device-tree/myled/compatible对比一下输出和驱动源码里的字符串肉眼就能发现问题。还有个技巧在probe里加dev_info打印设备名和匹配id确认匹配来源dev_info(dev, device name: %s, id name: %s\n, pdev-name, id-name);4.2 设备树节点没变成platform_device有一种情况更隐蔽设备树的节点本身没问题compatible也对得上但节点根本没有被解析成platform_device导致匹配无从谈起。前面提到过根节点下的带compatible的子节点一般会被展开。但如果节点挂在某个父节点下而父节点的compatible不是simple-bus那这个子节点就不会被自动创建为platform_device。比如你把LED节点放在某个自定义compatible的控制器节点下面而这个控制器的驱动没有主动去创建设备LED节点就不会生成platform_device。排查方法启动后看/sys/bus/platform/devices/里有没有这个节点名。没有的话检查设备树节点的父级是否符合展开条件。4.3 insmod成功但probe不跑驱动加载没报错但probe就是没执行。优先级最高的排查点是of_match_table写没写对、驱动结构体的of_match_table成员有没有赋值。我之前犯过这个错误在driver结构体里只设置了name忘记设置of_match_table结果不管设备树怎么写都匹配不上。后来查阅platform_match源码才发现如果驱动没有of_match_table设备树匹配直接被跳过只能走id_table或driver.name匹配。而id_table没写那唯一能匹配的就是driver.name和platform_device的name一致。这种情况下如果驱动名字和设备节点名碰巧一致可能也能匹配上但这种匹配方式会被视为兜底不推荐依赖它。4.4 设备树改动没生效改完设备树重新编译dtb、烧写、启动后发现改动没有反映到系统中。这种问题先别怀疑平台机制先检查两件事dtb到底烧进去没有、内核启动时用的哪份dtb。U-Boot环境变量里fdt_file指定的文件名对不对。开发板如果从网络加载dtb要注意tftp根目录下是不是放了新编译的dtb。验证方法还是看/proc/device-tree/如果里面有新节点说明设备树生效了没有说明加载的还是旧版本。4.5 驱动编进内核但模块也能加载导致混乱调试阶段驱动既编进了内核又用insmod加载一份可能造成资源冲突。这时候probe可能被执行两次第一次是内核初始化时第二次是insmod触发时。排查确认当前用的是哪个驱动lsmod | grep myled如果输出显示模块已加载而内核里也编了一份建议把内核里的支持关掉统一用模块方式调试省得混乱。4.6 匹配排查速查表把上面这些经验整理成一张速查表方便大家实际开发时对照排查现象可能原因排查方向dmesg没有probe信息compatible不匹配对比设备树和of_match_table字符串设备节点在sysfs中不存在节点没有被展开为platform_device检查父节点是否simple-bus、节点是否挂在正确位置insmod成功但无probeof_match_table未设置或MODULE_DEVICE_TABLE缺失检查driver结构体和宏probe报GPIO获取失败设备树gpios属性名/编号错误核对led-gpios是否与devm_gpiod_get参数对应probe执行两次内核内建驱动和模块驱动同时存在关掉内核内建配置只用模块加载compatible一致但始终匹配不上设备树加载的是旧dtb检查U-Boot环境变量和实际加载的dtb5. 调试心得与后续扩展思路最后分享几个个人习惯。这些年写驱动我越来越依赖sysfs这个目录它几乎把内核的设备模型完整暴露了出来。/sys/bus/platform/devices和/sys/bus/platform/drivers是排查匹配问题的第一现场比任何调试工具都直观。另一个习惯是驱动加载之前先cat /proc/device-tree/确认设备树结构再ls /sys/bus/platform/devices确认设备有没有生成。两分钟就能把问题范围缩小一半。从Platform机制再往后走i.MX6ULL上还有pinctrl子系统和gpio子系统两个大块值得深入。pinctrl负责引脚复用和电气属性配置gpio子系统统一管理GPIO操作。驱动开发中设备树里看到pinctrl-0就是引用了pinctrl配置devm_gpiod_get则是在和gpio子系统打交道。理解了Platform匹配机制就等于搭好了骨架再去理解pinctrl、gpio、中断、时钟这些子系统思路会顺很多。说回匹配这件事本身它没有多高深核心就是设备树说我是谁驱动说我要谁两者说对暗号就合作。把这个原理刻在脑子里以后写任何Platform驱动都不会再对着dmesg发呆。
返回列表