ARTICLE DETAIL

资讯详情

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

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

i.MX6ULL Linux驱动开发:Platform总线设备与驱动匹配机制全解析 做i.MX6ULL的Linux驱动开发很多人第一次接触Platform总线时都是一头雾水明明照着教程写完了一个platform_driverinsmod也成功了但probe函数就是不执行设备号也申请不到折腾一整天最后发现是设备树里compatible字段和驱动里的of_match_table没有对上。这个坑我踩过带过的不少新同事也踩过所以我想把i.MX6ULL平台下这套设备与驱动匹配机制彻底讲透从内核为什么要搞Platform这一套东西到匹配流程内部到底发生了什么事再到怎么自己写一个能被正常probe的驱动、怎么排查匹配失败的问题一次说清楚。这篇文章适合正在学嵌入式Linux驱动开发、尤其是用i.MX6ULL或者类似Cortex-A7核心板做项目的朋友阅读不管你是刚看完字符设备驱动、准备往设备树和Platform模型过渡还是已经写了不少驱动但一直没认真搞明白匹配原理都能从里面得到能直接拿去用的东西。1. 先搞清楚Platform机制到底解决了什么问题1.1 早期字符设备驱动写法为什么撑不住了咱们先把时间拉回到Linux 2.6之前。早期写驱动最常见的方式就是把板子上的硬件资源写死在驱动代码里。比如你要操作一个LED对应的GPIO寄存器直接在驱动里硬编码物理地址ioremap之后对着寄存器地址写值如果要注册一个中断也是直接写死中断号。这种方式在嵌入式开发早期确实简单粗暴能用但问题很快就暴露出来了。首先是资源冲突。同一个芯片可能被用在几十种板卡上A板子LED接在GPIO1_IO03B板子LED接在GPIO1_IO05同一份驱动代码在两块板子上就没法通用。其次是设备变更带来的维护地狱硬件工程师说改一下引脚连接驱动开发就得跟着改源码重新编译版本管理一混乱哪天改错了哪个地址排查起来极其痛苦。这就引出了Linux设备模型里最重要的思路驱动是驱动设备是设备两者分开维护通过一种统一的机制在运行时完成匹配和绑定。驱动只负责描述“我能操作什么类型的外设、怎么操作”设备则描述“这块板子上实际有哪些外设、外设的资源寄存器地址、中断号、GPIO等在哪里”。谁也不要写死谁。1.2 总线、设备、驱动三角色如何分工Linux为了解决上面的问题抽象出了总线bus、设备device、驱动driver三个角色。PCI设备有PCI总线USB设备有USB总线它们都是真实存在的物理总线设备和驱动都挂在这种总线上由总线子系统负责匹配。问题来了SoC内部的很多外设控制器比如I2C控制器、SPI控制器、UART、GPIO控制器、以太网MAC它们并不是挂在PCI或者USB这种物理枚举总线上面的。系统启动时这些设备是固定存在的不需要像USB设备那样去扫描发现。为了让这些“天生就存在”的片上外设也能套用总线-设备-驱动模型内核提供了一个虚拟总线叫platform_bus也就是咱们常说的Platform总线。Platform总线上挂的“设备”叫platform_device可以是芯片内部集成的外设控制器也可以是指定为platform_device的外部设备。在Device Tree设备树普及之后内核在启动阶段会解析设备树把其中匹配到的节点一个一个转换成platform_device挂到platform总线上。驱动这边只要你写的是platform_driver注册时就会挂到同一条虚拟总线上。总线负责撮合新来了一个platform_device总线就去看看现有的platform_driver有没有能匹配上它的新注册了一个platform_driver总线又会反过来扫描现有的platform_device。一旦匹配成功总线就会调用driver里的probe函数把设备相关的资源信息交给你你的驱动从这一刻才开始真正初始化硬件。1.3 i.MX6ULL上哪些资源依赖这套机制i.MX6ULL是NXP基于Cortex-A7内核设计的低功耗应用处理器内部集成了大量的外设控制器。可以这么说除了内存控制器等极少数基础组件芯片上几乎你能用到的所有外设控制器在Linux内核里都是以platform_device的形式存在或者由设备树节点转换而来的。举个例子你就会明白设备树里经常看到类似这样的节点i2c1 { clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; };这个i2c1节点在内核启动阶段会被转换成platform_device然后去匹配I2C控制器驱动里的platform_driver匹配成功进入probe之后驱动才会去初始化i2c适配器、注册i2c总线。之后再发现挂在这条i2c总线上的客户端设备就又是另一套i2c_client的匹配逻辑了。搞懂这个流程你就能明白为什么很多时候我们给某个外设控制器写驱动第一个函数入口不是file_operations里的open/release而是probe函数。因为你的驱动首先要被“系统认识”通过Platform匹配机制绑定到对应设备上才有资格去操作硬件资源。2. 匹配机制全解析一条probe是怎么被叫醒的2.1 驱动和设备在什么时机相互打量一个platform_driver从注册到它的probe被调用中间经历的过程非常关键。很多人只知道写驱动时要注册platform_driver但不知道probe调用的完整时机。当你在驱动里调用platform_driver_register的时候内核会把driver挂到platform_bus_type这根虚拟总线上然后立刻执行一次总线对已有设备的扫描。内核会遍历platform bus上所有的device对每一个设备调用platform_match函数看看你的driver跟哪个设备能配对。只要找到一个配对的设备立刻执行probe。反过来设备是怎么来的有两种常见来源。传统方式是通过platform_device_register主动注册平台代码里构造platform_device结构体然后塞给内核这在老内核或者不使用设备树的场景中很常见。现在的主流方式则是设备树内核在启动时用of_platform_default_populate_init遍历设备树根节点下的所有节点把 compatible 属性匹配的节点变成platform_device随后扫描驱动列表完成匹配。所以probe被调用的时机其实有两种典型场景如果你先加载驱动后注册设备比如设备树节点本来就在但驱动是后面insmod的那么在platform_driver_register内部扫描设备时就会触发probe。如果设备在你驱动的probe执行过程中又创建了子设备比如MFD多功能设备驱动创建子平台设备那是设备注册时反向扫描驱动触发probe。理解这个双向扫描逻辑后面排查“为什么probe没跑”时就能有个清晰的方向到底是设备始终没被创建还是设备与驱动的匹配条件不满足。2.2 内核源码里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. 尝试设备树匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 尝试ACPI匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试platform专用id_table匹配 */ if (pdrv-id_table) if (platform_match_id(pdrv-id_table, pdev) ! NULL) return 1; /* 4. 尝试驱动名字与设备名字直接匹配 */ return (strcmp(pdev-name, drv-name) 0); }需要注意的是在没有使能ACPI的嵌入式ARM平台上第二步通常不会生效关键的匹配路径就是设备树compatible匹配、id_table匹配和名字匹配三路。最初级的匹配方法是驱动程序名和设备名相同。早期platform驱动在driver结构体和platform_device结构体的name字段直接保持一致即可。这种匹配路线现在仍然存在但更多是兜底逻辑。设备树普及之后绝大多数场景走的是第一条of_driver_match_device。设备树匹配的核心是compatible属性。设备树里的每个节点只要是代表一个真实的设备基本都要写compatible属性。比如某个外设节点长得这样mydevice: mydevice020c406c { compatible myvendor,mydevice; reg 0x020c406c 0x4; ... };驱动侧就要在of_device_id数组里提供一个同样的compatible字符串static const struct of_device_id mydevice_of_match[] { { .compatible myvendor,mydevice, }, { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static struct platform_driver mydevice_driver { .probe mydevice_probe, .remove mydevice_remove, .driver { .name mydevice, .of_match_table mydevice_of_match, }, }; module_platform_driver(mydevice_driver);匹配时内核拿设备树节点的compatible属性字符串和of_match_table数组里所有compatible字段逐一比较只要有一个相等就算匹配成功。2.3 of_device_id里藏的小秘密data指针很多基础教程在讲of_device_id时只强调compatible字段必须对齐却忽略了一个很实用的字段data。我在这儿重点说一下。of_device_id结构体的data成员是一个void指针用于在驱动侧携带与某个compatible关联的私有数据。比如同一颗芯片上可能有多个功能相近但寄存器细节不同的IP核驱动里可以共用同一个probe函数但在of_device_id里通过data来区分IP版本probe时用of_match_device取出当前匹配到的of_device_id然后强转data拿到不同的硬件信息。我给你一个实际场景。假设板子上有两个LED控制器一个老版本寄存器地址偏移不同一个新版本两者的compatible分别是“myvendor,led-v1”和“myvendor,led-v2”。如果用两套of_match_table分别写两个驱动显然很蠢。更聪明的做法是一个驱动of_match_table里放两个条目data分别指向不同的led_hw_cfg结构体static struct led_hw_cfg led_v1_cfg { .reg_offset 0x00, .max_brightness 255, }; static struct led_hw_cfg led_v2_cfg { .reg_offset 0x10, .max_brightness 1023, }; static const struct of_device_id myled_of_match[] { { .compatible myvendor,led-v1, .data led_v1_cfg }, { .compatible myvendor,led-v2, .data led_v2_cfg }, { /* sentinel */ }, }; static int myled_probe(struct platform_device *pdev) { const struct of_device_id *match; struct led_hw_cfg *cfg; match of_match_device(myled_of_match, pdev-dev); if (match) cfg (struct led_hw_cfg *)match-data; ... }这个用法在NXP官方内核以及很多主流驱动里都能看到做好之后板级差异就被压缩到一个结构体数据里驱动代码的复用性明显提升。2.4 匹配成功后内核额外做的几件事匹配成功后内核会自动补齐一些基础信息接着才会调用你的probe。首先是dev_set_drvdata一类内部关联操作让设备与驱动之间建立关联之后你在probe内部用的dev_get_drvdata才有效。其次是sysfs文件系统视角下/sys/bus/platform/devices目录下的设备会出现一个driver符号链接指向/sys/bus/platform/drivers下面你的驱动目录。还有一点很多刚入门的新人会困惑为什么probe里已经用misc_register注册了字符设备但insmod之后/dev下并没有立刻出现对应节点这跟Platform匹配机制无关但发生场景经常重合。insmod执行后probe成功调用了设备注册动作在probe里也完成了但用户空间的udev或者mdev嵌入式环境常用busybox mdev需要在收到uevent事件后去创建/dev节点。如果你的系统里没有配置udev/mdev自动处理那/dev下确实不会自动出现设备节点。这一点在之后的调试过程中非常常见别把板子本身的问题和匹配机制混为一谈。另外如果设备节点被设置为status disabled那么内核扫描设备树时一般不会为该节点创建platform_device。注意是“一般不会”。有些场合下节点会被创建但驱动会被禁止绑定具体取决于内核的配置。在i.MX6ULL这种主流BSP中disabled节点基本不会生成platform_device因此你的驱动永远得不到probe的机会。这是一个非常隐蔽的坑后面排查部分我会专门提到。3. 在i.MX6ULL上实操写一个能匹配上的Platform驱动3.1 环境准备与基础工程建议咱们动手写一个完整的Platform驱动别空谈理论。我用的是i.MX6ULL开发板内核版本以4.1.15或者4.9.x为主这也是目前市面上不少i.MX6ULL开发板BSP的常见内核版本。交叉编译工具链一般是arm-linux-gnueabihf-根据你自己开发板配套的SDK来定。写驱动之前我建议先确认三件事内核源码目录已经准备好并且编译过一遍生成了Module.symvers。开发板内核里设备树已经支持你将要修改的节点或者你知道怎么把新的dtb烧进去。板子上的文件系统能够加载内核模块比如insmod/modprobe命令可用。写驱动文件之前先在板子上执行下面这条命令看看当前系统里platform设备的大致情况ls /sys/bus/platform/devices/你会看到很多设备比如soc、1001000.uart、2000000.spi之类名字一般是“寄存器地址.设备名”的格式这些就是内核从设备树里解析出来后挂到platform总线上的设备。等下咱们自己通过设备树创建的设备也会以类似的形式出现在这个目录。3.2 设备树中定义自己的平台设备节点我们的目标是创建一个叫“myled”的平台设备然后让驱动程序通过compatible匹配到它进入probe最后操作一个GPIO点灯。你需要在设备树源文件里找到根节点或者合适的父节点添加以下这个节点。很多i.MX6ULL开发板的设备树顶层是板级dts文件如imx6ull-14x14-evk.dts里面有根节点“/”往根节点里加就行了。有些为了方便管理会放在根节点的某个子节点中只要能被内核正常解析到就行放在根下最直观。/ { myled { compatible myvendor,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };注意这个节点里我引用了pinctrl_myled你必须在设备树里对应的iomuxc节点下添加这个引脚复用配置。以i.MX6ULL为例通常在设备树中可以找到类似这么一段iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };引脚的宏定义MX6UL_PAD_GPIO1_IO03__GPIO1_IO03在设备树头文件里已经定义好不同开发板这个宏可能不一样具体看你的BSP设备树。我这里只是给你一个示例实际操作时必须以自己板卡上的引脚为依据。改完设备树之后重新编译dtb然后烧录或者通过tftp/nfs方式加载到板子上启动。启动后在板子上执行ls /sys/bus/platform/devices/myled如果设备树解析正常你应该能看到这个目录存在。到这一步你的“平台设备”已经创建成功内核里已经存在一个platform_device就等着驱动来匹配了。如果看不到设备目录优先检查设备树有没有被正确编译加载compatible、status属性有没有问题。3.3 编写Platform驱动并正确注册接下来写驱动。为了把注意力集中在Platform机制上我直接用一个最简单的点灯驱动示例通过miscdevice注册字符设备用户程序用ioctl控制亮灭极简版。完整代码示例#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/miscdevice.h #include linux/uaccess.h #include linux/fs.h #define MYLED_ON 0x01 #define MYLED_OFF 0x00 struct myled_dev { struct gpio_desc *led_gpio; }; static struct myled_dev *myled_data; static long myled_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case MYLED_ON: gpiod_set_value(myled_data-led_gpio, 1); break; case MYLED_OFF: gpiod_set_value(myled_data-led_gpio, 0); break; default: return -EINVAL; } return 0; } static const struct file_operations myled_fops { .owner THIS_MODULE, .unlocked_ioctl myled_ioctl, }; static struct miscdevice myled_miscdev { .minor MISC_DYNAMIC_MINOR, .name myled, .fops myled_fops, }; static int myled_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int ret; dev_info(dev, myled probe success\n); myled_data devm_kzalloc(dev, sizeof(*myled_data), GFP_KERNEL); if (!myled_data) return -ENOMEM; myled_data-led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(myled_data-led_gpio)) { ret PTR_ERR(myled_data-led_gpio); dev_err(dev, failed to get led gpio: %d\n, ret); return ret; } gpiod_set_consumer_name(myled_data-led_gpio, myled); ret misc_register(myled_miscdev); if (ret) { dev_err(dev, failed to register misc device\n); return ret; } return 0; } static int myled_remove(struct platform_device *pdev) { misc_deregister(myled_miscdev); return 0; } static const struct of_device_id myled_of_match[] { { .compatible myvendor,myled, }, { /* 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 device match demo);这个驱动用module_platform_driver宏完成platform_driver_register和platform_driver_unregister的封装。加载时注册驱动总线扫描设备后如果匹配成功probe就会被调用。3.4 编译、加载与查看匹配效果对应的Makefile非常简单obj-m : myled.o KERNELDIR : /path/to/your/kernel CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) clean编译之后得到myled.ko拷贝到板子上insmod加载insmod myled.ko正常的话dmesg里能看到myled myled: myled probe success此时查看platform总线的设备与驱动绑定关系ls -l /sys/bus/platform/devices/myled/driver如果驱动已经成功绑定这个符号链接会指向drivers目录下的myled驱动。再用设备名匹配方式来验证一下纯名字匹配路径的优先级问题如果设备树里没有compatible而你的platform_device是内核代码直接注册的driver结构体里的name字段和设备name相同也可能匹配成功。这一点虽然老派在兼容旧代码时很有用。3.5 GPIO子系统在Platform机制中扮演的辅助角色在刚才的驱动里我用到了devm_gpiod_get和gpiod_set_value这是内核GPIO子系统提供的基于描述符的接口。你可能会疑惑这跟Platform匹配机制有什么关系关系其实不直接但属于Platform设备操作最典型的资源获取方式。传统开发中要操作GPIO需要ioremap并配置引脚复用寄存器很繁琐。现在的推荐姿势是在设备树节点里通过led-gpio这样的属性描述“我需要哪个引脚”驱动中通过devm_gpiod_get获取GPIO描述符。这个API能正常工作前提是你传入了指向struct device的指针也就是platform_device里的pdev-dev。设备树、GPIO子系统、Platform驱动模型这三者是紧密配合的。Platform总线匹配只是给你一个“入场券”入场之后怎么拿资源、怎么使用硬件又注册了另一套内核API。很多教程把这几个维度混在一起新手就容易懵。我建议你在实验时把这条链路拆开理解设备树节点 设备说明书描述有什么资源。Platform驱动匹配 验证“这个驱动能管这个设备”。probe函数 驱动读取说明书实际操作资源。misc或者字符设备框架 把操作能力暴露给用户空间。3.6 id_table匹配方式与非设备树场景聊完设备树方式咱们也花点时间说说id_table匹配。毕竟并非所有内核和所有平台都使用设备树而老平台代码里大量存在platform_device_id的使用方式。如果你有一个平台设备它不是从设备树生成的而是通过platform_device_register注册的例如内核板级文件里这么写static struct resource myled_resources[] { { .start 0x020c406c, .end 0x020c406f, .flags IORESOURCE_MEM, }, }; static struct platform_device myled_device { .name myled, .id -1, .num_resources ARRAY_SIZE(myled_resources), .resource myled_resources, }; static int __init myled_device_init(void) { return platform_device_register(myled_device); }你的驱动需要通过platform_driver里的id_table来匹配而不只是名字。使用示例static const struct platform_device_id myled_id_table[] { { myled, 0 }, { }, }; MODULE_DEVICE_TABLE(platform, myled_id_table); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, }, .id_table myled_id_table, };platform_match内部执行顺序是先查of_match_table再查ACPI再查id_table最后才是driver name与device name直接比较。但这里有个细节如果驱动同时配置了of_match_table和id_table设备树匹配又是首要路径许多时候驱动作者会把compatible匹配的设备也放进id_table里主要是为了MODULE_DEVICE_TABLE生成模块别名方便modprobe自动加载模块。“为什么我明明把驱动编译成模块放到文件系统了modprobe能识别到模块却还是会找不到设备”这类问题经常就是模块别名机制的问题。modprobe在加载模块前要根据设备uevent里的MODALIAS环境变量去寻找对应的模块MODALIAS内容与设备树compatible相关而模块中通过MODULE_DEVICE_TABLE导出的表决定了内核知道这个模块能支持哪些设备。如果你只写了of_match_table没有同步MODULE_DEVICE_TABLE自动加载时可能就会出问题。这个知识点一般很少人讲透遇到了先检查这里不会错。4. 匹配失败排查与调试经验4.1 最常见的匹配失败原因匹配失败是驱动开发里最磨人的问题之一。根据我带项目的经验下面这些原因几乎覆盖了八成以上的失败场景你排查时不妨按顺序过一遍。compatible字符串不一致这是头号问题。设备树里写着myvendor,myled驱动of_match_table里写的是myvendor,myled 多了个空格或者大小写不一致都匹配不上。排查方法很简单在板子上执行cat /sys/bus/platform/devices/myled/uevent看输出的MODALIAS字段OF_NAMEmyled OF_FULLNAME/myled OF_COMPATIBLE_0myvendor,myled MODALIASof:NmyledTNULLCmyvendor,myled然后对比驱动模块中由of_match_table生成的模块别名。也可以直接看/proc/device-tree下设备树节点里的属性是否和你期望的一致。设备树没过。这个问题尤其隐蔽因为很多开发板启动时用的不是最新编译的dtb你改了dts但忘了重新编译或者编译了没有传到板子对应的启动分区。排查方法很直接在板子上重新查看设备树里你添加的节点是否存在例如ls /proc/device-tree/myled/ cat /proc/device-tree/myled/compatiblestatus属性被设成disabled。如前所述disabled状态可能导致platform_device根本不创建。检查节点status是否为okay或者干脆去掉status属性。有些BSP中特定父节点下的status控制逻辑还会有差异需要具体问题具体分析但第一怀疑对象一定是这里。pinctrl配置出错。节点里的pinctrl-0引用了不存在的pinctrl_myled节点或者引用的引脚宏跟iomuxc里实际配置不一致虽然不直接影响Platform匹配但probe里GPIO请求时会失败导致probe返回错误码驱动被卸载现象看起来跟匹配失败一样。排查时可以先用以下命令确认这个设备是否绑定过驱动ls -l /sys/bus/platform/devices/myled/driver如果driver链接存在说明匹配成功过probe里报错的可能性更大如果不存在才说明匹配阶段都没走通。GPIO号冲突或者重复申请。i.MX6ULL的GPIO资源管理通过GPIO子系统完成如果你在设备树里把同一个引脚用在两个不同节点或者驱动里重复请求了同一个GPIOdevm_gpiod_get会返回错误。典型报错是EBUSY或者EINVAL同样会让probe失败。这类问题和Platform匹配本身无关但极易干扰判断建议出现probe失败时先把dmesg完整拉出来看最终错误码而不是只盯着最前面的日志。拿不到寄存器资源。如果节点里声明了reg属性驱动用platform_get_resource和devm_ioremap_resource去获取但寄存器地址范围跟别的设备重叠devm_ioremap_resource也会返回错误。很多人在probe失败时只看到return -EBUSY却不会想到是物理地址资源冲突。4.2 如何快速定位匹配路径到底走到哪一步匹配失败时核心问题是搞清楚内核到底在哪个环节放弃了。我常用的办法有以下几步第一步打开内核的驱动调试信息。在cmdline里加上ignore_loglevel和dyndbg或者简单点使用dmesg -n 8。由于Platform总线匹配在出错时不一定都有日志输出这个方法有时不够直观但先把日志级别放到最高没坏处。第二步在驱动里加打印。如果你不确定自己的probe是否被调用过直接在probe入口加一条dev_info或者pr_info。很多人会嫌打印土但实际调试中这永远是最直接有效的办法。注意probe里返回错误码时内核会打印类似“myled: probe of myled failed with error -16”的消息看到这类log就知道驱动和设备已经匹配成功是probe内部处理出了问题。第三步利用sysfs节点检查匹配状态。这是最高效的方式# 查看设备是否绑定驱动 ls -l /sys/bus/platform/devices/myled/driver # 查看驱动的名字和它支持的设备id cat /sys/bus/platform/drivers/myled/uevent ls /sys/bus/platform/drivers/myled/如果设备下driver链接指向了你的驱动说明已经绑定成功probe被调用过。如果驱动目录完全是空的说明设备树里根本没有符合条件的设备或者设备的compatible和你驱动里的of_match_table不一致。走到这一步匹配路径基本就能定位出来。第四步抓总线级调试日志。内核的驱动模型在drivers/base/dd.c里提供了许多调试信息输出需要把CONFIG_DEBUG_DRIVER打开编译内核时启用这个选项然后dmesg里会出现“matched device”之类的日志。这类信息量很大但如果你怀疑是匹配逻辑本身出了问题它是最权威的排查工具。4.3 一个真实的踩坑案例设备树改名引发的诡异问题我之前在一个项目里调试一款外设驱动遇到过一个特别迷惑的现象驱动模块加载没报错但probe就是不执行。设备树节点添加到dts里了也确认过dtb烧录成功/sys/bus/platform/devices目录下能看到设备的目录名字也跟我预期的一样。按照常规排查compatible我也对比过两者看起来一模一样。最后我实在没办法把两个字符串逐个字符用十六进制打了出来这才发现问题设备树里写了“myvendor,my-led”但驱动里of_device_id写的是“myvendor,myled”中间少了一个连字符“-”肉眼看久了确实很难发现。这就是为什么我强烈建议用uevent里的MODALIAS字段做严格比对而不是凭肉眼去检查dts和代码。还有一次是同事反馈说insmod之后probe没执行我过去查看发现/sys/bus/platform/devices下设备目录能看到了但内核日志里没有任何关于这个设备probe失败的记录。通过对比dtb和dts源码才发现板子实际加载的dtb是一份旧的备份文件压根没有他改的这个节点目录虽然能看到但那是另一份设备树里本来就有的同名设备compatible和寄存器地址都不同。这种环境问题最容易浪费时间排查时一定先确认板子真正加载的设备树是不是你改的那个。4.4 probe成功后却被移除的常见原因定位到probe被调用了但执行到一半驱动就被移除了这类问题也很典型。最常见的原因是probe返回了错误码。例如我让你看内核在probe失败时打印的那行信息myled: probe of myled failed with error -16这个-16对应- EBUSY。它可能来自platform_get_resource返回NULL后你直接return -EINVAL也可能来自gpiod_get返回错误。需要注意的是probe一旦返回非零内核的driver core会认为驱动绑定失败之后会自动执行清理操作驱动和设备会被解除关联看起来就像模块被卸载了一样。所以当dmesg里能看到probe被调用但随后设备driver链接消失不要盯着Platform匹配机制看问题基本都在probe内部逻辑里。要做的就是把probe里每一步可能的错误码都打印出来确定是哪一步返回的再去排查对应子系统的问题。很多人不知道一个小细节如果probe在某个子系统返回EPROBE_DEFER延迟探测内核不会立刻判定失败而是把设备挂到一个等待队列等对应依赖的驱动加载完成后再重新触发probe。这在多驱动依赖场景里非常重要。在i.MX6ULL的驱动开发中如果设备树里引用的某个时钟或中断控制器对应的驱动还没就绪你的驱动probe可能会被反复推迟执行。dmesg里看到“deferred probe”相关的字眼别慌这是内核的合理等待机制你需要回头检查自己依赖的外设驱动有没有加载成功。4.5 使用内核deferred probe机制多说一句EPROBE_DEFER因为我发现很多新手第一次遇到它都会误判成匹配失败。在你的probe函数中当你请求某个资源但该资源对应的设备驱动还没准备好时比如调用clk_get、regulator_get、gpiod_get时返回了-EPROBE_DEFER你应该直接把这个错误码原样返回给内核。内核的driver core看到返回-EPROBE_DEFER会把你的设备放到一个延迟列表中后续每当有新的driver注册都会重新尝试对这些设备进行绑定和probe。这个机制保证了设备之间的依赖关系可以通过“你等我一会”的方式动态解决而不是简单在probe里死循环等待资源就绪。实际调试时你可以在内核cmdline中添加deferred_probe_timeout2这样系统启动后如果还存在EPROBE_DEFER的设备会在超时后打印详细信息提示你到底在等待哪个资源。这个功能在排查启动早期驱动顺序问题时几乎是救命稻草。4.6 常见问题速查表我把上面所有问题整理成一张速查表方便你日后直接对照排查。现象常见原因排查与解决方向probe完全没有执行compatible不匹配对比uevent的MODALIAS与of_match_tableprobe完全没有执行设备节点不存在或statusdisabled检查/proc/device-tree下节点与statusprobe没有执行但设备目录存在设备驱动匹配没触发确认驱动有没有注册成功查看/sys/bus/platform/driversprobe执行后打印错误GPIO或寄存器资源冲突查看dmesg错误码检查pinctrl和reg地址范围insmod后/dev节点不出现udev/mdev规则未配置手动mknod或配置mdev自动创建规则probe反复不执行出现deferred probe依赖资源尚未就绪查询相关驱动是否加载成功必要时deferred_probe_timeout模块无法modprobe自动加载MODULE_DEVICE_TABLE缺失或alias生成异常检查模块的modinfo输出与MODALIAS匹配情况这张表你保存下来遇到问题时先对号入座。别小看这些检查项很多资深工程师排查这类问题也基本走的是这套流程只不过他们已经把每一步都内化成了肌肉记忆。5. 升级认知从一套机制到整个设备模型思维5.1 Platform只是Linux设备模型里的一个缩影如果你只是想在i.MX6ULL上调通一个点灯驱动其实上面4节内容已经足够。但如果你想把Linux驱动的知识体系搭得更完整我建议你从Platform这套机制中跳出来思考一下它背后统一的Linux内核设备模型。Platform总线设备驱动模型本质上只是Linux通用设备模型的一个应用。Linux内核维护了一个设备模型的骨架包括kobject、kset、uevent、sysfs等底层设施在这个骨架上衍生出不同类型的总线。不管是platform_bus_type、i2c_bus_type、spi_bus_type还是pci_bus_type它们都有共同的语义设备注册、驱动注册、总线匹配、probe调用、电源管理回调等。你在这篇文章里学到的compatible匹配、driver绑定、sysfs节点检查这些技能在后续接触I2C客户端驱动、SPI设备驱动时同样适用区别只是总线类型和匹配函数不一样。I2C设备的client与driver通过id_table或者设备树compatible匹配SPI设备也是类似逻辑。很多讲Platform总线的教程喜欢铺陈各种概念但我觉得内核设备模型背后的核心设计思想才是真正值得玩味的东西把易变的、板级相关的设备描述从稳定的驱动逻辑中剥离出来。设备树解决了设备描述“写在哪”的问题Platform总线解决了设备与驱动“怎么见面”的问题而GPIO、时钟、中断等内核子系统则解决了资源“怎么共享不冲突”的问题。5.2 理解匹配机制对日常开发的实际影响哪怕你已经能熟练地照着别人的驱动模板抄代码我也建议你花时间把匹配机制吃透。因为不理解匹配机制你在调试时会缺少一个方向感只知道代码写出来没反应抓不住“系统如何认识我的设备”这条主线。举个例子。设备树里如果出现这个节点uart1 { status okay; };按我们前面说的流程内核会把uart1节点转换成platform_device去匹配IMX6ULL的uart驱动。如果你哪天发现一个串口设备死活打不开第一步就应该去确认uart1节点是否成功创建了platform_device然后再看uart驱动有没有绑定到这个设备、probe有没有执行。理解了这套机制之后你排查硬件驱动的顺序就会变得很清晰从设备树到平台设备再到驱动绑定最后才是驱动内部硬件初始化的逻辑。另一个常见场景是外设的时钟和电源管理。Platform驱动在probe里拿到设备之后往往要操作clk和regulator子系统比如打开外设时钟、调节电压。这些资源同样可以在设备树节点中描述。如果资源描述不正确probe里clk_prepare_enable就会失败。你只有理解了整个链条才能在设备树、驱动代码、内核日志三个视角之间来回切换定位问题而不是逮着compatible死磕。5.3 如何继续深入学习Platform相关技术如果你想在这个方向上有更扎实的积累我给几条具体建议。第一条翻内核源码时别只看platform.c还要看device.h、platform_device.h、of_device.h这几个头文件把platform_device、platform_driver、of_device_id这几个结构体的字段都弄明白尤其是resource结构体理解IORESOURCE_MEM、IORESOURCE_IRQ的用法。probe里那些platform_get_resource、platform_get_irq之类的API其实都是在操作resource结构体明白了数据结构你再去看接口函数不会觉得它们散乱。第二条自己独立完成一次从设备树建节点到用户空间应用程序调用完整链路的实验。别只抄教程看完文章后关掉网页自己凭理解写一版踩过的坑记得越牢成长越快。实验时可以刻意制造几个错误比如把compatible故意写错、把status设为disabled观察probe失败的具体表现然后再修正回来。这种做法比重复抄代码有用得多。第三条条件允许的话用QEMU或者虚拟平台去读内核源码里更多总线模型的实现。i.MX6ULL开发板适合做真实的硬件对接但如果你想在更深层面理解Linux设备模型比如分析bus_type结构体的match、uevent、probe回调在driver core里如何被调度一台能编译内核的Linux主机配合QEMU的virt平台调试效率可能比反复烧录板子更高。6. 最后再分享几个调试小技巧调试Platform驱动时有几条经验我觉得很有用分享给你们。第一dmesg一定要养成随手清空的习惯。开发板长期运行内核日志缓冲区会被各种无关消息占满容易把关键日志冲掉。每次insmod之前先dmesg -c把旧日志清掉加载之后再看dmesg输出这样probe里打印的信息一目了然不会遗漏。第二多利用sysfs少依赖暴力加打印。sysfs是内核设备模型的实时投影/sys/bus/platform/devices目录下的每个符号链接和uevent文件都是判断设备状态的最可靠依据。很多问题看一眼驱动链接、cat一下uevent就能定位完全不需要重新编译模块加打印。虽然加打印在调试probe内部逻辑时仍然必要但先通过sysfs确认匹配状态能帮你少走很多弯路。第三改设备树之后一定要确认板子加载的是你编译的新dtb。嵌入式开发中启动流程各有差异有的从SD卡读dtb有的从EMMC指定分区读有的通过u-boot环境变量指定tftp加载。改完设备树却忘记更新对应位置的dtb是我见过最频繁的低级错误。调试设备树相关问题时先在板子上执行ls /proc/device-tree/看看节点是否真实存在再往下排查能省掉大量无意义的工作。第四合理使用devm系列的资源管理API。我在示例驱动里大量使用了devm_kzalloc、devm_gpiod_get这些devm前缀的函数本质上是把资源释放的责任挂在struct device的生命周期上。probe成功之后设备与驱动解除绑定时内核会自动帮你释放这些资源。这能有效避免probe失败时忘记释放之前申请的资源导致的问题。不夸张地说内核里devm系列API的出现让驱动资源管理的出错率降低了一个量级。第五也是我觉得最重要的一点不要急着一次写完整个驱动。你可以先把probe函数只留一条打印确认匹配链路通顺再一步步加上GPIO申请、寄存器映射、设备注册等逻辑。每一次小步验证都能让问题边界变得非常清晰。真按这种方式做大多数Platform匹配类问题在半个小时内就能定位远比你一股脑写完两百行驱动再调半天高效得多。我在实际项目里用这套方法解决过的问题不计其数甚至有时候不是i.MX6ULL平台换成其它ARM SoC或者RISC-V芯片只要内核还是Linux这套Platform设备与驱动匹配机制基本不会变调试思路也是相通的。这就是花时间把机制理解透的价值。
返回列表