ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

i.MX6ULL平台驱动开发:从设备树到probe匹配机制详解

i.MX6ULL平台驱动开发:从设备树到probe匹配机制详解 做嵌入式Linux驱动开发这几年i.MX6ULL算是我用得最多的一块主控Cortex-A7内核、资源适中、资料也多从入门学习到小批量产品都很合适。不管点灯、读按键还是驱动外设Linux下都绕不开Platform设备和驱动匹配机制这个问题。很多朋友从裸机转到Linux写驱动还习惯把寄存器地址、中断号直接硬编码在驱动里一换板子就崩。要真正理解现代Linux驱动必须先把平台总线、设备树文件、probe函数这一整套流程串起来。这篇基于i.MX6ULL把匹配机制从源码到实操完整拆一遍适合刚接触字符设备驱动框架、想搞懂设备树到底怎么和驱动对上号的开发者。1. 先搞清楚Platform总线解决了什么问题1.1 传统驱动写法硬件信息硬编码的痛点从裸机或者RTOS转过来的人写的驱动基本就是“寄存器操作集合”。比如点亮一个LED直接在代码里写#define GPIO1_DR (0x0209C000) #define GPIO1_GDIR (0x0209C004) static void led_init(void) { *(volatile unsigned int *)GPIO1_GDIR | (1 18); *(volatile unsigned int *)GPIO1_DR ~(1 18); }这种写法在单片机裸机里问题不大因为整个系统的硬件是确定的地址就那几个。但到了Linux、到了i.MX6ULL这种一个SoC可以搭配几十种底板的场景问题就出来了地址、中断号、DMA通道这些硬件资源写死在代码里换了板子就要改代码重新编译内核的电源管理、运行时PM、设备热插拔模型全都没法接入驱动和系统是割裂的。我在一个量产项目里就吃过亏底板改了版按键从一个GPIO挪到另一个GPIO驱动里改了一个宏定义、重新编译结果忘了另一处硬编码的中断标志排查花了一晚上。那时候我就强烈意识到Linux驱动必须把“硬件长什么样”和“驱动怎么操作”这两件事彻底分开。1.2 Platform机制的引入设备与驱动分离分开的思路就是引入总线模型。Linux内核的设备模型里device代表硬件设备driver代表驱动逻辑bus负责把它们匹配到一起。对于PCI、USB这种可以枚举的总线设备是硬件上报来的但GPIO控制器、UART、看门狗这类SoC内部外设没有枚举能力系统怎么知道它们存在所以内核搞了platform总线一条虚拟的总线专门用来挂“非可枚举设备”。你可以把platform总线理解成一个中介平台。设备侧把硬件资源登记成一份简历我有哪几个寄存器地址、哪几个中断、兼容什么型号驱动侧也提交一份能力说明我能处理哪些兼容型号、需要哪些资源。总线负责比对简历和能力匹配上了就安排一次“入职见面会”——这个见面会就是probe函数。对应到i.MX6ULL的实际开发里设备侧现代内核都是用设备树Device Tree描述硬件编译出dtb后启动时解析。设备树文件里每个带compatible属性的节点都会在内核初始化阶段被转成platform_device。驱动侧你写的platform_driver结构体注册到platform总线上。匹配成功内核调用你的probe匹配失败则一直等待不报错也不执行。这也是为什么很多新手把驱动写好了、insmod也成功了却看不到probe打印信息——不是驱动没加载而是匹配根本没成功。这个体会后面细说。2. 匹配机制核心原理probe为什么会被调用2.1 platform_match的完整匹配顺序要理解匹配直接看内核源码最靠谱。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步driver_override强制指定 */ if (pdev-driver_override) return !strcmp(pdev-driver_override, drv-name); /* 第2步设备树OF匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 第3步id_table匹配 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 第4步name名称匹配 */ return (strcmp(pdev-name, drv-name) 0); }第1步driver_override是调试和强制绑定时用的正常开发很少碰到先忽略。第2步的of_driver_match_device核心是拿设备树节点的compatible属性和drv-driver.of_match_table里的每个条目逐个比较。以i.MX6ULL为例imx6ull.dtsi里的usdhc1节点usdhc1: usdhc02190000 { compatible fsl,imx6ull-usdhc, fsl,imx6sx-usdhc; reg 0x02190000 0x4000; interrupt-parent gpc; interrupts GIC_SPI 22 IRQ_TYPE_LEVEL_HIGH; ... };如果这个节点的compatible跟sdhci-esdhc-imx驱动of_match_table里的任何一个条目相等就匹配成功。这也是设备树驱动最常用的匹配方式我后面讲的按键驱动就走这条路。第3步id_table是传统板级文件时代的产物。驱动用platform_device_id结构体数组声明自己支持的设备名比如static const struct platform_device_id imx_uart_ids[] { { .name imx-uart, }, { }, }; MODULE_DEVICE_TABLE(platform, imx_uart_ids);第4步name匹配更古老直接把device名称和driver名称做字符串比较。现在还有不少驱动在用但没有设备树灵活所以我的看法是花时间把第2步搞透性价比最高。2.2 设备树是怎么变成platform_device的问题来了你写了设备树节点它什么时候变成platform_device的答案是内核启动阶段。在start_kernel之后内核的initcalls会执行到of_platform_default_populate_init这个初始化函数它从根节点开始遍历设备树。遍历有规矩只有带compatible属性的节点才会创建platform_device中间层的节点必须compatible带有simple-bus、simple-mfd这类标记才会继续向下扫描。这就是为什么设备树里soc节点一般写成这样soc { compatible simple-bus; #address-cells 1; #size-cells 1; ... };i.MX6ULL所有外设节点都挂在soc下面所以都会被逐个创建为platform_device。节点里的reg属性会被转成IORESOURCE_MEM资源interrupts属性会被转成IORESOURCE_IRQ资源of_node指针则会指向设备树里的原始节点。这几个资源就是你probe里最常用的platform_get_resource、platform_get_irq依赖的数据。注意创建platform_device不等于驱动会运行。它只是把设备挂到了platform总线上进到总线的设备列表里等“有心人”来领。之后设备与驱动的匹配动作可以发生在设备注册时系统启动也可以发生在驱动注册时你insmod的时候。2.3 of_match_table、id_table与name匹配的取舍作为驱动作者到底要用哪种匹配我的经验是新板子、新驱动默认就用设备树compatible匹配。硬件的差异引脚、地址、频率、参数全部交给设备树描述驱动只关心这类设备怎么操作不同板子通过修改dtb适配驱动代码一行都不动。除非你在维护老代码或者设备树改造代价太大才去考虑id_table和name匹配。of_match_table里可以挂多个compatible。比如i.MX6ULL的网卡驱动可能同时兼容imx6ull和imx6sx的多个型号就在表里多放几条。每条of_device_id还有个.data指针可以携带自定义数据结构probe里通过of_match_device取出适合区分同一驱动兼容多个硬件型号、初始化参数不同的情况。一句话总结现在做i.MX6ULL驱动请把所有注意力放在“设备树compatible对上of_match_table”这条匹配链路上。3. 实操在i.MX6ULL上从零写一个Platform按键驱动3.1 设备树节点与引脚复用配置拿GPIO按键举例因为按键驱动麻雀虽小五脏俱全设备树节点、pinctrl配置、GPIO申请、中断注册全都能演示到。假设按键接在GPIO1_IO18上低电平表示按下。先在项目dts的根节点下添加子节点/ { mykey { compatible mydev,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_mykey; key-gpio gpio1 18 GPIO_ACTIVE_LOW; status okay; }; };然后需要在iomuxc节点里配置引脚复用。i.MX6ULL的UART1_CTS_B引脚可以复用为GPIO1_IO18所以iomuxc { pinctrl_mykey: mykeygrp { fsl,pins MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 ; }; };这里的0xF080是电气配置属性含义大致包括上下拉、驱动能力、施密特触发器。实际项目中建议对照参考手册的IOMUXC章节选值别盲目照抄因为同样的功能值在不同引脚上的默认状态可能不一样。我习惯先选一个带内部上拉的值避免按键悬空时电平抖动。pinctrl-0这个属性很重要它让内核在probe之前先帮你把引脚复用和电气属性配好驱动里就不用再手动操作IOMUX寄存器了。这也是“硬件描述交给设备树”思想的直接体现。3.2 驱动的核心源码结构接下来写驱动完整代码如下我加了一些注释#include linux/module.h #include linux/platform_device.h #include linux/of_gpio.h #include linux/gpio.h #include linux/interrupt.h #include linux/slab.h struct mykey_data { int gpio; int irq; }; static irqreturn_t mykey_isr(int irq, void *dev_id) { struct mykey_data *data dev_id; int val gpio_get_value(data-gpio); pr_info(mykey irq, gpio%d val%d\n,>export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make mykey.ko把生成的ko文件拷贝到板子上依次执行insmod mykey.ko dmesg | tail如果一切正常dmesg里会看到mykey mykey: mykey probe ok, gpio18 irq...注意这里的“mykey mykey”第一个是platform_device层名字第二个是驱动层名字都来自设备树节点名和driver.name。看到这一行说明匹配成功、probe已经执行。还可以去sysfs里确认绑定关系ls /sys/bus/platform/drivers/mydev_gpio_key/ cat /sys/bus/platform/drivers/mydev_gpio_key/mykey如果能读出设备信息说明设备和驱动已经绑定。这时候按一下按键dmesg里就会打印出中断日志。我的习惯是先验证匹配再验证功能。匹配都没有就开始折腾中断出了问题根本分不清是哪一环。4. 高频问题排查与调试心得4.1 probe不执行的常见原因我见过的probe不执行九成出在这几件事上现象可能原因排查方法dmesg完全没输出compatible拼写不一致对比dts与of_match_table注意大小写、逗号后的空格dmesg完全没输出节点statusdisabled检查节点status改为okay后重新编译dtbdmesg完全没输出dtb没更新确认启动加载的是新编译的dtb重新烧写dmesg完全没输出节点挂在非simple-bus下确认父节点compatible包含simple-bus或直接挂在根节点没输出但模块加载成功设备已被同类型驱动绑定cat /sys/bus/platform/devices/ 查看设备占用情况这里特别提醒dtb更新这一步。很多朋友改了dts编出dtb却忘了确认uboot实际加载的是哪个分区、哪份dtb。排查的时候我从来不猜直接先跑cat /proc/device-tree/mykey/compatible如果在板子上看不到这个属性就说明dtb根本没生效驱动写得再对也没用。另外如果你的驱动是编译进内核而不是模块probe会在系统启动阶段执行。此时如果dts节点不匹配启动日志里会有与platform相关的提示可以用dmesg搜索“platform”关键字。4.2 资源申请与引脚冲突排查还有一种情况是probe进去了但中途返回错误。最常见的是devm_gpio_request_one失败返回-EBUSY意思是这个GPIO已经被别的驱动申请走了。排查方法打开内核的GPIO调试接口mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/gpio这里会列出所有GPIO的使用状态。如果看到gpio-18显示被某个驱动占用而且不是你的那就得去设备树里查谁和它冲突了。i.MX6ULL的引脚复用在pinctrl里也是一样道理多个节点引用同一个引脚会导致pinctrl配置冲突。此时可以查cat /sys/kernel/debug/pinctrl/20e0000.iomuxc/pinmux-pins过往项目里“明明dts写了、驱动也probe了但引脚就是不工作”的问题用这个方法定位过很多次。基本套路是先看GPIO占用再看pinctrl复用最后看电气配置值。中断请求失败也会导致probe失败。gpio_to_irq返回0或者负数通常意味着GPIO还没有正确配成输入或者父中断控制器没有就绪。遇到时先别急着看驱动回设备树里检查pinctrl有没有配到中断对应的输入引脚上。4.3 我常用的几个调试手段最后说几个自己常用的调试思路算不上高端但真的省时间。第一用dev_err、dev_info而不是printk裸奔。dev_xxx会自动带出device和driver名字日志里能看到“mykey mykey”这种上下文不用自己拼设备名排查多设备时特别有用。第二善用sysfs的手动绑定。如果想要在运行时来回测试可以往bind/unbind写值强制解绑再绑定echo mykey /sys/bus/platform/drivers/mydev_gpio_key/unbind echo mykey /sys/bus/platform/drivers/mydev_gpio_key/bind每次绑定都会重新走probe等效于反复插拔设备。调probe代码时我经常这么干比反复重启板子快得多。第三遇到设备树相关怀疑优先看/proc/device-tree。它是当前生效设备树的完整展开包含属性和实际启动用的dtb严格一致。所有“我觉得我写了”都不算数以这个目录下的内容为准。第四匹配顺序也别忘。有的驱动同时写了of_match_table和id_table甚至有name字段。一旦id_table存在只要id_table里的某个name和设备名相同也会匹配上。所以看到probe被调用了先想想是不是走了你没想到的匹配路径。我自己有一个习惯所有新写驱动里只挂of_match_table让匹配路径保持唯一排查起来一眼就能定到问题所在。
返回列表