尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

i.MX6ULL Linux驱动开发:Platform设备与驱动匹配机制解析

i.MX6ULL Linux驱动开发:Platform设备与驱动匹配机制解析 做了几年i.MX6ULL平台的Linux驱动开发后我最大的感受是驱动开发的重心早就不是“写寄存器操作”而是把设备模型、设备树、总线匹配这一整套机制吃透。很多新手拿着正点原子或野火的教程第一步就是照着粘一个字符设备驱动模板点亮LED就算完事但一旦涉及多个设备、多个驱动源文件、内核版本升级或板卡改版马上就懵了。问题的根源几乎都指向同一个地方——Platform设备与驱动的匹配机制。这篇文章不打算给你抄一段代码就走人而是想带着你从i.MX6ULL这个具体平台出发把Platform总线是什么、设备树怎么生成设备、驱动怎么被匹配、匹配不上怎么排查完整捋一遍。你可以把它当作一篇带实操的项目笔记也可以当成排查手册用。只要你手上有一块能跑Linux的i.MX6ULL板子跟着做一遍基本上就能把这条线彻底打通。1. 先理解为什么需要Platform总线机制1.1 那些“天生没法枚举”的硬件设备先琢磨一个最基础的问题为什么Linux要专门搞一条Platform总线我们平时用USB设备、PCIe设备它们插到系统里硬件总线会主动去枚举设备自带厂商ID、设备ID驱动只需要声明“我支持这个ID”总线就能自动把设备跟驱动配对。这套流程对PC来说非常自然你插个U盘进去系统能立刻识别出它是哪个厂商、什么型号、该加载哪个驱动。但i.MX6ULL这类嵌入式SoC完全不同。芯片内部的UART、GPIO、I2C、SPI控制器甚至外部扩展的某些外设它们都直接焊在板子上没有热插拔也没有硬件枚举机制。内核要想管理它们就只能靠软件在系统启动时“登记”一个又一个设备节点再让对应的驱动去认领。问题来了这些设备和驱动该以什么形式存在谁去匹配它们匹配规则是什么总不能靠一堆全局变量和initcall硬凑吧。于是内核虚拟出了一条总线叫Platform总线也常被译作“平台总线”。它本身不是一条真实的硬件总线更像是一个软件层的数据结构。专门用来管理那些“不挂在任何标准硬件总线上”的设备让它们也能用统一的方式做设备注册、驱动注册和匹配。1.2 Platform总线在i.MX6ULL上的具体体现你在i.MX6ULL开发板上跑起Linux之后可以进到串口终端执行一条命令ls /sys/bus/platform/devices/你会看到一大堆设备名比如soc 5b000000.i2c 5b020000.spi 20e0000.ethernet 21a0000.serial 1c4000.gpio这些名字看起来很奇怪前面是地址后面是功能。这其实就是设备树里各个节点被解析之后注册到Platform总线上的Platform设备。注意看它们并不是凭空生成的每一个节点都对应着设备树源文件.dts里的一个外设节点。比如20e0000.ethernet这个节点来自imx6ull.dtsi说明以太网控制器的寄存器基地址在0x20E0000。内核在启动阶段解析设备树时发现这个节点匹配了“simple-bus”之类的总线类型就会把它转换为一个platform_device然后挂到Platform总线上等待驱动匹配。这个过程让我第一次理解到在设备树时代写设备驱动的第一步不是急着写代码而是先搞清楚这个设备在设备树里的节点结构。因为设备树的节点几乎决定了一个Platform设备能不能生成、以及生成后长什么样。1.3 设备、驱动、总线一场“红娘式”的匹配如果你学过面向对象可以把Platform机制理解成三层关系总线platform_bus_type维护设备和驱动两个链表负责撮合。设备struct platform_device描述硬件的资源比如地址、中断、GPIO、时钟等它只负责“我有什么”。驱动struct platform_driver描述驱动的能力比如支持哪些硬件、如何初始化、如何读写它只负责“我能驱动什么”。每当有设备或驱动注册到总线上时总线都会调用一次匹配函数在对方链表中找有没有跟自己“配得上”的。如果匹配成功就回调驱动里的probe函数驱动拿到设备资源正式完成硬件初始化。这个机制最大的好处是解耦。设备端不用管驱动是怎么写的驱动端也不用管设备挂在哪儿、地址是多少只要匹配上资源自然会被传进来。设备树改地址驱动代码一行都不用动。2. 设备端落地从设备树节点到Platform设备2.1 老式作法直接在板级文件里注册Platform设备在早期的内核版本里还没有设备树或者设备树刚起步那时候要注册一个Platform设备得在板级文件比如arch/arm/mach-imx/里手动定义一个platform_device结构体然后调用platform_device_register()把它挂到总线上。我早期看过一些老代码大概是这样的写法static struct resource led_resource[] { { .start 0x020C406C, .end 0x020C4070, .flags IORESOURCE_MEM, }, }; static struct platform_device led_device { .name imx6ull-led, .num_resources ARRAY_SIZE(led_resource), .resource led_resource, }; static int __init board_init(void) { platform_device_register(led_device); return 0; } device_initcall(board_init);这样写的问题很明显代码跟硬件地址绑死换一块板子或者改引脚就要重新编译内核板子越复杂板级文件就越臃肿。所以后来ARM平台全面转向设备树这种方式基本被淘汰了。2.2 现代作法设备树节点自动生成platform_device现在的主线内核只要你修改的是设备树并在内核配置里使能了对应的驱动启动流程基本是这样的内核启动时在start_kernel()流程中会调用unflatten_device_tree()把dtb里的二进制数据解析成一个树状结构然后由of_platform_default_populate_init()遍历树节点对符合条件的节点调用of_platform_bus_probe()生成platform_device对象并注册到Platform总线上。哪些节点会生成platform_device主要有几类根节点下带有compatible属性的节点simple-bus类的总线节点里面带compatible且声明为simple-bus子节点中已经有一个Platform设备节点的后代节点显式调用of_platform_device_create()创建的节点。对你来说最常用的就是在设备树里添加一个自定义节点例如led_test { compatible my-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; };只要这个节点在设备树中解析正常Linux启动后你就能在/sys/bus/platform/devices/下看到它。注意设备名可能与你自定义的节点名略有差异但通常会保留节点名或后续拼接。2.3 在i.MX6ULL上怎么把设备树节点变成实际设备以我的实际操作为例我在i.MX6ULL的imx6ull-myboard.dts中新增一个完整节点大概是这样的/ { model My i.MX6ULL Board; compatible my,imx6ull-board; /* 自定义LED平台设备 */ led_test { compatible my-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里有几个关键点我把自定义节点放在了根节点/下面这样内核会直接为它创建platform_device。pinctrl-0引用了iomuxc里配置的引脚组这保证了GPIO1_IO03被复用为GPIO功能。led-gpio属性用于告诉驱动这个LED控制引脚是gpio1组的3号引脚且低电平点亮。设备树改完之后要重新编译设备树。在i.MX6ULL开发环境里通常有两种方式一是进入内核源码目录直接make dtbs二是用独立工具链配合dtc编译。我一般直接在内核目录操作make imx6ull_myboard.dtb然后把生成的dtb文件拷贝到开发板的/boot分区或SD卡设备树分区重启生效。重启后不要急着写驱动先在系统里确认设备是否创建成功ls -l /sys/bus/platform/devices/ | grep led如果看到类似led_test这样的设备目录说明设备树解析正常接下去就可以写驱动了。3. 驱动端实现platform_driver的核心套路3.1 probe函数驱动的“主战场”平台驱动的大部分代码都围绕platform_driver结构体展开。它里面最核心的是probe函数设备与驱动匹配成功后内核会调用到这个函数相当于“驱动正式接管设备”的入口。一个标准的驱动骨架是这样#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/of_gpio.h #include linux/err.h static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *led_gpio; int ret; led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, failed to get led gpio: %ld\n, PTR_ERR(led_gpio)); return PTR_ERR(led_gpio); } /* 先把灯点起来代表probe跑通 */ gpiod_set_value(led_gpio, 1); platform_set_drvdata(pdev, led_gpio); dev_info(dev, my led probe ok\n); return 0; } static void my_led_remove(struct platform_device *pdev) { struct gpio_desc *led_gpio platform_get_drvdata(pdev); gpiod_set_value(led_gpio, 0); } static const struct of_device_id my_led_of_match[] { { .compatible my-led, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my-led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A simple led platform driver for i.MX6ULL);这里我用的是gpiod接口而不是老式的gpio_request gpio_direction_output。因为gpiod是与设备树自然衔接的它可以从设备节点的led-gpio属性里拿到GPIO描述符并且自动处理GPIO极性不需要关心GPIO编号。我在i.MX6ULL上实测下来这套接口配合设备树代码量能少一半。3.2 module_platform_driver背后干了什么很多初学Linux驱动的人都见过module_platform_driver()这个宏但不知道它展开后是什么。我简单展开一下它实际上等价于static int __init my_led_init(void) { return platform_driver_register(my_led_driver); } static void __exit my_led_exit(void) { platform_driver_unregister(my_led_driver); } module_init(my_led_init); module_exit(my_led_exit);也就是说这个宏一次性定义了模块加载和卸载函数并且帮你调用了platform_driver_register()。当驱动注册进内核时Platform总线就会拿着驱动的of_match_table去设备链表里寻找compatible属性匹配的设备找到后立即把probe调起来。3.3 驱动里资源获取的几种姿势probe函数里最常见的操作是从platform_device中拿资源。这里我总结一下我在i.MX6ULL上最常用到的几种方式获取寄存器物理地址区域用platform_get_resource(pdev, IORESOURCE_MEM, 0)或者新版内核里更方便的devm_platform_ioremap_resource(pdev, 0)一次性完成资源查找和ioremap。获取中断号用platform_get_irq(pdev, 0)拿到中断号后传递给request_irq或devm_request_irq。获取GPIO用devm_gpiod_get(dev, led, GPIOD_OUT_LOW)设备树里对应led-gpio属性。获取时钟用devm_clk_get(dev, NULL)如果设备树里有clocks属性直接按名字取。我之前在一个I2C控制器驱动里就是用devm_platform_ioremap_resource直接映射了整个控制器的寄存器空间省掉了手动ioremap和release_mem_region那套流程。4. 匹配机制的底层逻辑五种匹配方式与优先级4.1 内核里的platform_match函数到底做了什么很多教程讲到这里就停了只告诉你“compatible对上就行”。但我觉得要想真正调好驱动最好还是去看一眼匹配函数的实现。内核里drivers/base/platform.c的platform_match()是主入口大致的匹配顺序如下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类型匹配优先看dts的compatible */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 如果驱动有id_table用platform_match_id去匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 3. 最后回退到设备名和驱动名是否相等 */ return (strcmp(pdev-name, drv-name) 0); }我刻意把顺序标记出来了OF匹配最优先然后是id_table最后才是驱动名字匹配。这条线理解清楚之后你就不会出现“明明device注册了driver也注册了probe就是没反应”这种问题多半是几种匹配条件交叉搞混了。4.2 compatible匹配设备树场景下的绝对主力在i.MX6ULL上90%的设备都走的是compatible匹配。设备树节点里会有compatible my-led;驱动的of_match_table里会有static const struct of_device_id my_led_of_match[] { { .compatible my-led, }, { /* sentinel */ } };只要两者字符串相等匹配就成立。注意设备树compatible可以写多个字符串比如compatible my,led-v2, my,led;内核会按顺序逐个与of_match_table比较只要有一个相同就匹配成功。这意味着你可以利用这个特性做兼容性设计老的设备树节点用旧compatible新驱动同时兼容新老。4.3 id_table匹配给非设备树时代留的口子有些Platform设备并不是来自设备树而是通过在板级代码里手动注册platform_device并且没有compatible属性。这时内核会走id_table匹配也就是platform_driver里的id_table成员。static const struct platform_device_id my_led_id_table[] { { my-led, (kernel_ulong_t)my_led_data }, { }, };当platform_match_id()遍历id_table时它拿pdev-name和id_table里的name做比较同时还能从id-driver_data里给驱动传入一些平台数据。这种方式在老代码里很常见如果哪天你看到某个驱动的probe里用了id-driver_data那它很可能就是通过id_table匹配来的。4.4 名字匹配最后的兜底方案如果驱动既没有of_match_table也没有id_tablePlatform总线就只能比较设备名和驱动名。驱动结构体里.driver { .name my-led, },设备端name为“my-led”两者相等也能匹配。我实际调试中很少用这种因为太容易撞名字而且没法携带更多信息。除非在极简的测试驱动里否则我建议优先用设备树的compatible匹配。4.5 匹配成功后probe里能拿到什么匹配成功后驱动框架会把platform_device指针传给probe。这时候你有几条路可以拿到设备和硬件信息static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); /* 或者直接devm_platform_ioremap_resource一步到位 */ base devm_platform_ioremap_resource(pdev, 0); }设备树里的reg属性就是通过IORESOURCE_MEM传给驱动的interrupt属性就是IORESOURCE_IRQ其他自定义属性比如gpio、clocks、dmas则要配合对应的子系统函数去获取。理解了这一点你会突然明白为什么设备树里reg地址明明写的是物理地址驱动里却直接映射来用因为内核早就帮你把这些信息转换成了标准resource结构。5. 实战从零写一个可用的i.MX6ULL LED驱动并验证匹配这一节我完整走一遍从设备树到驱动加载的流程全都是我在实际板子上验证过的操作。5.1 硬件引脚确认与设备树修改我的板子上LED灯接的是GPIO1_IO03低电平点亮。首先确认这个引脚没有被其他功能占用。在设备树中我修改imx6ull-myboard.dts添加LED节点并配置iomuxc引脚/ { model My i.MX6ULL Board; compatible my,imx6ull-board; led_test { compatible my-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里0x10b0是引脚配置寄存器值包括上下拉、驱动强度等。我一般会根据原理图和实际情况调整但LED这种简单输出0x10b0够用。5.2 编译设备树并烧录进入内核源码目录make imx6ull_myboard.dtb如果内核配置正确编译成功后会在arch/arm/boot/dts/下生成新的dtb文件。把它复制到开发板的/boot分区或者用tftp、nfs等方式更新。我调试时习惯直接替换SD卡上的dtb文件重启后从串口看到内核启动日志里没有解析错误说明dtb加载正常。重启后确认设备节点ls -l /sys/bus/platform/devices/led_test如果看到这个目录说明Platform设备已经注册成功。5.3 编写驱动程序与Makefile驱动代码我直接复用上一节的my_led.c。除了驱动源码需要写一个Makefileobj-m : my_led.o KDIR ? /home/user/linux-imx PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) cleanKDIR指向你已经编译过的内核源码目录我强烈建议用与板子上运行版本完全一致的内核源码否则模块加载时会报版本或符号不匹配。编译make ARCHarm CROSS_COMPILEarm-linux-gnueabihf-得到my_led.ko。5.4 加载驱动并验证probe执行把my_led.ko传到开发板执行insmod my_led.ko如果一切正常串口会输出my_led: loading out-of-tree module taints kernel. my-led my-led.0: my led probe ok同时LED灯亮起。注意设备名变成了my-led.0这是因为Platform总线给设备编号的原则是节点名加序号多个同名设备时后面会递增。如果想更直观地确认匹配状态可以看sysfsls -l /sys/bus/platform/drivers/my-led/这个目录下会出现my-led.0 - ../../devices/platform/my-led.0这个软链接就是“设备与驱动成功匹配”的铁证。5.5 拔掉驱动时的资源释放卸载驱动rmmod my_led此时内核会调用my_led_remove()我在里面把GPIO拉低也就是关灯。因为使用了devm资源管理GPIO描述符、内存映射这些资源都不需要手动释放设备模型会在remove返回后统一回收。这也是我坚持在probe里用devm_系列函数的原因少写很多release代码也少踩很多资源泄漏的坑。6. 匹配不上怎么办问题排查速查表驱动开发最常见的问题永远是“设备树有了驱动也写了就是probe不进来”。我把我在i.MX6ULL上踩过和帮别人查过的典型案例汇总成一张表。现象可能原因排查方法insmod时提示设备不存在设备树没有生成对应platform_device检查/sys/bus/platform/devices/下是否出现节点没有就检查设备树解析probe没有执行compatible字符串不一致对比设备树节点和of_match_table里的compatibleprobe没有执行设备树节点status属性为disabled查看设备树节点status改回okayprobe没有执行驱动of_match_table为空或没配置检查.driver.of_match_table是否为NULLprobe被调用但gpiod获取失败led-gpio属性名与gpiod_get参数不一致确认devm_gpiod_get的第二个参数与属性名对应引脚电平状态不对pinctrl配置错误或GPIO极性反了检查iomuxc引脚配置和GPIO_ACTIVE_LOW/GPIO_ACTIVE_HIGH挂载模块报version magic错误内核源码版本与运行内核不一致用与运行内核匹配的源码重新编译设备节点存在但驱动找不到compatible设备树dtb没有真正更新确认启动加载的dtb路径驱动已经加载但probe只跑了一次这是正常的匹配成功后设备已绑定用rmmod/insmod重新触发有几条值得单独强调。第一设备树改了之后一定要确认dtb真的被启动加载了。我在开发中经常遇到“明明改了dts板子上看还是老值”的情况原因往往是uboot的bootcmd里指定了固定分区加载dtb或者tftp拉取的是老文件。最快的方式是启动后执行ls /proc/device-tree/如果能看到你新增的节点目录说明dtb已经加载看不到就跑偏了。第二GPIO获取失败很可能不是GPIO本身的锅而是pinctrl没生效。因为GPIO控制器默认并不会帮你配置引脚复用引脚必须通过pinctrl子系统先设置好。驱动里devm_gpiod_get其实是在gpiolib体系下工作它本身不负责pinctrl设备树里的pinctrl-0引脚配置是由驱动模型在probe前自动应用的。如果pinctrl节点写错或者引脚的dts属性没对上pin脚就没被正确复用。第三如果你的驱动是编译进内核而不是模块probe的执行时机比模块加载更早很可能在系统启动时就完成了绑定你在用户态dmesg里看到的时间点相对靠前。这时候想验证匹配是否成功别用insmod直接看启动日志dmesg | grep my-led说到底Platform设备和驱动的匹配机制就是一个“注册-匹配-回调”的模型。你只要把设备树节点当成设备的“简历”把platform_driver注册当成“求职登记”内核里的platform_match就是“筛选简历”的规则。简历里compatible写得对不对、status是不是okay、驱动有没有声明支持这个岗位任何一个环节掉了链子probe都不会被调用。我在实际项目中经常用同一个驱动模块去兼容不同板卡上的同类外设做法就是让设备树各自写自己的compatible驱动里of_match_table列出一组compatible每次硬件变更都只需要动设备树驱动源码一行不改。这种维护体验没有搞懂匹配机制之前是完全想象不到的。也希望你看完这篇文章之后能少走一点我当时走过的弯路。
返回列表