我做了这么多年i.MX6ULL的Linux驱动开发,Platform总线匹配机制是绕不开的一座山头。刚接触驱动开发那阵子,我也被设备树compatible、of_match_table、platform_driver这些概念搞得晕头转向,明明照着教程抄了代码,probe函数就是不执行,dmesg里干干净净,查半天不知道问题出在哪。后来把Linux内核里platform_match这条匹配链彻底啃了一遍,才算是真正开了窍。
这篇内容想跟你聊透一件事:在i.MX6ULL这种ARM嵌入式平台上,Platform设备与驱动是怎么“对上眼”的,匹配机制底层怎么运作,以及我在实际项目里踩过的那些坑和排查经验。适合刚入门嵌入式Linux驱动开发、背过设备树但没搞懂匹配原理的同学,也适合写了好几个驱动但从未深究过匹配顺序的同行。
1. Platform总线机制出现的原因与整体设计思路
先别急着看代码,得先把“Platform总线到底为了解决什么问题”这件事搞明白。很多人第一次见到platform_driver和platform_device的时候,会本能地问一句:i.MX6ULL的GPIO、UART、I2C控制器,这些外设不都老老实实焊在SoC里面吗,哪来的总线?它们也不像USB设备那样可以热插拔,为什么非要挂一条虚拟的总线上?
1.1 为什么需要Platform总线
Linux驱动模型的核心思想是“设备与驱动分离”,设备由板级描述或设备树描述,驱动只需要关心自己支持哪些硬件,两边独立加载、通过某种规则匹配到一起,匹配成功就调用驱动的probe函数完成初始化。这个模型在PCI、USB这类可枚举总线上天然成立:总线扫描能得到设备的厂商ID、设备ID,驱动声明自己支持的ID列表,内核在两边的ID表里做匹配就行。
问题是,i.MX6ULL上的大多数片上外设并不挂在PCI或USB这种可枚举总线上,它们就是SoC内部一组寄存器区域,没有标准化的枚举能力。如果每次都靠驱动直接去读某个物理地址,代码里写死一堆硬件资源,那驱动的通用性就全没了。
Platform总线的思路很直接:既然硬件没有枚举能力,那我们就用软件把设备信息“枚举”出来。在设备树时代,每个外设节点就是一份设备描述,然后内核以平台设备的形式呈现;驱动侧则以platform_driver的形式注册自己。这条虚拟总线负责把两边拉在一起,匹配成功后驱动就能拿到设备树里配置的寄存器地址、中断号、DMA通道这些资源,不再需要在代码里硬编码。
说白了,Platform总线就是Linux对“不可枚举设备”的一种抽象,把原来靠board文件或驱动里硬编码的“板级信息”收编成了统一模型。
1.2 Platform机制与传统设备驱动模型的区别
早期内核(2.6之前以及3.x的部分阶段)在没有设备树的时候,ARM平台普遍用arch/arm/mach-xxx/board-xxx.c 这类板级文件来描述硬件。当时在板文件里要调用platform_device_register(),把一个一个platform_device手动注册进去。这种方式最大的问题是内核与板级硬件信息强耦合,换个板子就要改内核源码重新编译,非常痛苦。
设备树引入之后,硬件描述从C代码里剥离了。bootloader把dtb传给内核,内核解析设备树,为每个匹配的“compatible”节点创建platform_device。驱动侧的变化其实不大,还是注册platform_driver,但是匹配方式更多了。这就把“硬件长什么样”从驱动里彻底解耦出去,同一份内核镜像放到不同的i.MX6ULL板卡上,只要设备树不同,就能适配不同外设。
理解这个演进过程很重要。你会看到很多老代码里用“.name = "xxx"”来匹配platform_device,新代码则大量用“.of_match_table = xxx_of_match”,这本身就是不同历史阶段留下的痕迹。在i.MX6ULL这类现代BSP里,设备树是主流,of_match_table是主力匹配方式,但老式的name匹配也没有彻底消失,了解它们各自的适用场景,排查问题时候思路会宽很多。
2. Platform核心数据结构与匹配规则拆解
要说清楚匹配机制,绕不开两个结构体。一个是描述“设备侧”的platform_device,另一个是描述“驱动侧”的platform_driver。在i.MX6ULL开发里,设备侧的信息绝大多数来自设备树,驱动侧则是我们自己写的代码。
2.1 三个核心数据结构
先看platform_driver,这个结构体在include/linux/platform_device.h里定义,我们写驱动时基本都会用:
struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; const struct platform_device_id *id_table; bool prevent_deferred_probe; };平时用的最多的是probe、remove、driver、id_table这几个字段。probe是匹配成功后的入口,remove是设备移除(或驱动卸载)时的清理函数。注意driver字段本身又是个struct device_driver,它是通用驱动模型的“基类”,里面有几个对匹配机制至关重要的成员。
再说platform_device,它在设备树机制下通常由内核自动创建,但我们也要理解它的结构:
struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; char *driver_override; };其中resource数组保存寄存器、中断等硬件资源,驱动里用platform_get_resource()取。设备树节点下的reg属性、interrupt属性会被内核转换成resource。平时我们写驱动时拿到的struct platform_device *pdev,就是从这里来的。
最后是struct device_driver里的of_match_table,它是设备树“compatible”匹配的关键:
struct device_driver { const char *name; struct bus_type *bus; const struct of_device_id *of_match_table; int (*probe)(struct device *dev); int (*remove)(struct device *dev); ... };struct of_device_id里最重要的就是compatible和data两个字段:
struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };驱动通过of_match_table声明自己支持哪些设备树节点,设备树节点通过compatible属性声明自己是什么设备,两边字符串一比对,就完成了握手。
2.2 匹配规则的优先级和流程
内核里真正执行匹配动作的函数是platform_match,被调用时机包括总线注册新设备、注册新驱动等。我直接说结论,匹配顺序是这样的:
第一优先是of_match_table的方式。内核会拿设备树节点的compatible属性,去和驱动of_match_table数组里每个of_device_id的compatible字段逐个比较。比如设备树节点写compatible = "mycompany,mydev",那驱动of_match_table里也要有compatible = "mycompany,mydev"这一项。这一轮用了设备树最标准的匹配规则,也是现代i.MX6ULL驱动开发里最常用的方式。
第二是id_table机制。内核会拿platform_device的name和id_table里每个platform_device_id的name字段去比较。platform_device_id长这样:
struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };这种方式在老式板文件注册platform_device时非常常见,比如板文件里platform_device_register_simple("mydev", -1, res, ARRAY_SIZE(res)),那设备对象name就是“mydev”,驱动的id_table里也放一个name为"mydev"的项就能匹配。在新设备树模式下,这种匹配方式用得少了,但你在读一些老驱动时还会遇到。
第三是driver_override机制。这个比较专项,一般用于用户态强制指定某个设备使用哪个驱动,我实际项目里很少用,简单知道即可。
第四也是最朴素的方式:直接比较platform_driver.driver.name和platform_device.name。也就是说,如果驱动里driver.name填了"mydev",设备名也叫"mydev",哪怕没有of_match_table和id_table,也能匹配上。
这里有个关键点容易忽略:匹配成功不等于resource自动可用。of_match_table匹配靠的是设备树节点与驱动声明兼容,硬件资源获取还需要在probe里用platform_get_resource或devm_platform_ioremap_resource去拿。很多新手匹配成功了,probe也进了,但读寄存器地址读出来全是0,就是没搞懂这层关系。
3. 实战:i.MX6ULL平台下的Platform驱动完整实现
原理说得再多,不如手把手跑一个例子。我在i.MX6ULL开发板上用设备树加Platform驱动的方式做过一个简单的LED控制例程,把整个过程拆给你看。
3.1 在设备树里声明一个设备节点
假设我们的板子上有一颗LED接在GPIO1_IO03上(i.MX6ULL的常见引脚)。在设备树里新建一个节点来描述它,我一般放在根节点或者专门的gpio-led节点下面:
example-led { compatible = "mycompany,example-led"; reg = <0x0209c000 0x1000>; interrupts = <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; status = "okay"; };注意reg和interrupt只是用来演示资源获取的示例,实际操作LED并不需要这些,但我们要看Platform驱动怎么把设备树里的资源映射出来。reg里的0x0209c000是i.MX6ULL的GPIO1基地址,0x1000是地址范围大小。内核在创建platform_device时会把reg填充到resource数组的第一个元素里,中断则会放到second resource。
编译设备树后把dtb烧到开发板上,内核起来后会在/sys/bus/platform/devices/下出现一个example-led.0这样的目录(具体后缀数字跟内核分配有关,也可能是example-led.0或没有后缀)。这一步就标志着设备侧已经就绪。
3.2 编写Platform驱动端完整代码
驱动端代码核心就是一个platform_driver结构体、一个of_match_table、一个probe函数、一个remove函数。
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/io.h> #include <linux/interrupt.h> static const struct of_device_id example_led_of_match[] = { { .compatible = "mycompany,example-led", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, example_led_of_match); static int example_led_probe(struct platform_device *pdev) { struct resource *res; void __iomem *reg_base; int irq; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "failed to get MEM resource\n"); return -ENODEV; } reg_base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(reg_base)) { dev_err(&pdev->dev, "failed to ioremap resource\n"); return PTR_ERR(reg_base); } irq = platform_get_irq(pdev, 0); if (irq < 0) { dev_err(&pdev->dev, "failed to get IRQ resource\n"); return irq; } dev_info(&pdev->dev, "probe success, reg_base=%px, irq=%d\n", reg_base, irq); return 0; } static int example_led_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "remove called\n"); return 0; } static struct platform_driver example_led_driver = { .probe = example_led_probe, .remove = example_led_remove, .driver = { .name = "example-led", .of_match_table = example_led_of_match, }, }; module_platform_driver(example_led_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("i.MX6ULL Platform driver example");module_platform_driver宏会帮我们完成platform_driver_register和platform_driver_unregister的封装,insmod加载时自动注册驱动,rmmod时自动注销。
3.3 设备与驱动匹配过程的实测验证
把驱动编译成.ko,烧到开发板上insmod,正常情况下dmesg里会打印:
example-led example-led.0: probe success, reg_base=0000000000000000, irq=67等等,reg_base打印出来不该是0,devm_platform_ioremap_resource已经做了内存映射,返回的是虚拟地址,%px打印出来的应该是一个0x....开头的虚拟地址。这个细节我们后面在常见问题里细说。
另一个更直接的验证方式是看sysfs。加载驱动成功后,/sys/bus/platform/drivers/example-led/目录下会出现一个example-led.0的符号链接,指向设备端目录。这说明总线成功地把设备和驱动绑定到了一起。如果匹配失败,probe不执行,设备端会一直停留在“无驱动”状态,/sys/bus/platform/drivers/下也不会有对应的绑定链接。
4. 常见问题与排查技巧实录
我把实际项目里遇到过的、以及带新人时他们最常见的问题汇总一下,做成一个速查表,对号入座排查效率很高。
| 现象 | 常见原因 | 排查手段 |
|---|---|---|
| probe函数没被调用 | compatible字符串不匹配、of_match_table没设置、设备树节点status为disabled | dmesg看日志;比对dts与驱动的compatible;检查/sys/bus/platform/devices下节点是否存在 |
| probe被调用但打印ENODEV | resource获取失败,device tree的reg属性缺失或顺序不对 | 检查dts节点reg属性;用platform_get_resource打印res字段;确认IO资源索引 |
| 映射后读写寄存器异常 | reg地址写错、ioremap失败、地址被其他驱动占用 | 查看芯片手册确认基地址;对比cat /proc/iomem的占用情况 |
| 设备树刚修改但没生效 | dtb没重新编译或没写入正确分区 | 编译dtb后确认u-boot环境变量fdt_file路径;启动后cat /proc/device-tree查看节点内容 |
| /sys/bus/platform/devices下没有节点 | compatible不在内核支持的列表中,或节点真的没被解析 | 挂载debugfs后检查寄存器值,或者直接在/sys/firmware/devicetree/base/下找节点 |
4.1 probe函数没有被调用怎么办
这是我被问得最多的问题。先别怀疑内核,90%的情况是compatible字符串两边不一致。设备树里写的是"mycompany,example-led",驱动of_match_table里写成了"mycompany,mydev",一个字母对不上都匹配不了。
排查方式第一个是看dmesg里有没有平台驱动注册成功的日志,然后看/sys/bus/platform/devices下有没有设备。两个都在,再看设备是否已经绑定了驱动:
ls -l /sys/bus/platform/devices/example-led.0/driver如果没有这个driver链接,说明还没匹配上。此时把设备树里的compatible和驱动of_match_table里的compatible逐一字符比对,最笨但最有效。另一个容易踩的坑是系统里同时有多个内核dtb分区,你以为烧了新dtb,其实u-boot加载的还是老dtb。
4.2 resource获取为空或地址不对
probe跑进来了,但platform_get_resource返回NULL,这类问题在新手写带reg的设备树节点时非常常见。核心原因往往是reg属性写的格式内核解析不了,比如reg = <0x0209c000>只写了地址没写长度。内核在构resource时会对reg进行解析,如果length没有有效值,地址会被当作0或直接跳过。
更隐蔽的问题是索引搞错。platform_get_resource(pdev, IORESOURCE_MEM, 0)拿的是第一个内存资源,如果你dts里reg有两个范围,驱动里却以为只有一个resource,就会拿错。我习惯在probe最开始把pdev->num_resources打印出来,先把资源数量看清楚再动手。
4.3 设备树更新后没生效
i.MX6ULL开发板上改dts后不生效,大多数时候不是内核问题,而是dtb路径和缓存问题。比如用tftp加载内核时dtb还是旧的;或者u-boot设置的fdt_addr和实际烧录分区不一致。我一般习惯在系统启动后立刻检查:
cat /proc/device-tree/example-led/compatible如果输出不是我们预期的字符串,说明设备树根本没加载对。这一点卡住的研发不在少数,因为驱动代码改对了,设备树源文件也改对了,但中间烧录或加载的环节错了,导致一直找不到原因。
另一个常见情况是status = "disabled"。设备树继承里,如果子节点status是disabled,内核不会创建platform_device。很多BSP默认把不用的节点都disabled,我们自己的测试节点如果忘了写status = "okay",那设备侧压根不存在。
5. 从Platform机制到整个Linux设备驱动模型的串联理解
写了不少实践内容,最后从稍微高一点的角度聊聊Platform机制在整个驱动模型里的位置。理解了这个,后续学input子系统、gpio子系统、regmap框架都会顺很多。
5.1 sysfs里的设备驱动模型
Linux设备驱动模型里有三个基本概念:bus、device、driver。Platform总线是其中的一种bus,它管理着所有以platform方式注册的设备和驱动。你可以把/sys/bus/platform/下面想象成一个大型婚介所,设备目录放着“征婚者”信息(device),驱动目录放着“求偶者”资料(driver),而总线就是那个红娘,每次有新设备或新驱动进来,它都会跑一遍匹配逻辑,配对成功就把两边用符号链接连起来。
设备树解析后创建的诸多platform_device会挂在/sys/devices/platform/下,驱动注册后会挂在/sys/bus/platform/drivers/下。两者通过driver链接形成绑定。我们前面排查时用ls -l看driver链接,本质就是看红娘有没有牵线成功。
理解了这一层,再看device_driver和device这两个结构体里的bus、kobject、kset等成员就不觉得奇怪了。整个模型是围绕sysfs和生命周期管理设计的,probe和remove只是这个生命周期里的两个业务回调而已。
5.2 进阶:从Platform驱动到子系统框架
我在i.MX6ULL上写完第一个Platform驱动时,一度以为理解到这里就够了,后来看GPIO子系统和中断子系统的代码才发现,纯粹写Platform驱动只是入门。现代内核里,一个外设驱动往往不是直接操作寄存器,而是通过子系统API工作。
比如我们案例里那个LED,真正的产品驱动通常不会直接在probe里ioremap寄存器,而是调用gpiod_get()、gpio_led_register_device()这类接口,让gpio-led框架去处理。Platform驱动在这时候扮演的角色更像是“粘合剂”:它负责匹配设备、获取资源,然后把资源交给更上层的子系统。
所以我的建议是:Platform匹配机制一定要吃透,但不能停留在“匹配成功、probe跑通”就完事。还要继续学习platform_get_resource拿到的resource如何与regmap衔接、如何与中断子系统协同、如何与设备树里的pinctrl设置联动。i.MX6ULL的中断控制器、GPIO控制器、引脚复用控制器,全都是Platform驱动上面盖着子系统,机制完全一致,理解一套就能通一套。
这套机制我前前后后在不同项目里反复用过,踩过的坑不少,但对“设备与驱动分离”这个设计思路也越来越认可。你如果正在被probe不进、匹配不上这些问题折磨,别急,先把compatible字符串逐字符核对,再把/sys/bus/platform下的目录结构看懂,大部分问题都能迎刃而解。之后你再去看i.MX6ULL内核里那些具体的平台驱动代码,会发现它们长得都差不多,匹配机制那一层你已经彻底掌握了。