i.MX6ULL Linux驱动开发:Platform设备与驱动匹配机制解析
2026/9/6 12:12:42 网站建设 项目流程

做了几年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_table,Platform总线就只能比较设备名和驱动名。驱动结构体里:

.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。除了驱动源码,需要写一个Makefile:

obj-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) clean

KDIR指向你已经编译过的内核源码目录,我强烈建议用与板子上运行版本完全一致的内核源码,否则模块加载时会报版本或符号不匹配。

编译:

make ARCH=arm CROSS_COMPILE=arm-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总线给设备编号的原则是节点名加序号,多个同名设备时后面会递增。

如果想更直观地确认匹配状态,可以看sysfs:

ls -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里的compatible
probe没有执行设备树节点status属性为disabled查看设备树节点status,改回okay
probe没有执行驱动of_match_table为空或没配置检查.driver.of_match_table是否为NULL
probe被调用但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,每次硬件变更都只需要动设备树,驱动源码一行不改。这种维护体验,没有搞懂匹配机制之前是完全想象不到的。也希望你看完这篇文章之后,能少走一点我当时走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询