直接说结论:i.MX6ULL 的 Linux 驱动开发,绕不开 Platform 机制。不管你是刚入门嵌入式 Linux 的新手,还是在应用层写了好几年逻辑、现在想往下沉搞内核驱动的人,只要你碰过设备树、碰过 GPIO、碰过 I2C 控制器驱动,你其实都已经在和 Platform 总线打交道了。但很多人会卡在同一个问题上:设备树里明明写了节点,驱动也编进去了,为什么 probe 就是不执行?为什么匹配不上?
这篇文章我不打算照搬内核文档,而是基于我自己在 i.MX6ULL 上跑通 Platform 设备与驱动匹配的完整过程,把它掰开揉碎讲清楚。内容包括:为什么需要 Platform 这个虚拟总线、设备侧怎么注册、驱动侧怎么匹配、匹配的优先级与源码实现、设备树在匹配过程中到底扮演什么角色,以及最后我踩过的那些坑。
1. 内容整体设计与思路拆解
1.1 为什么 Linux 需要一条“虚拟总线”
如果只看教科书,你会觉得设备模型挺抽象。但换一个角度,这事特别像公司里的人员分配机制。CPU 这边有大量的“硬件资源”:寄存器地址、中断号、时钟、GPIO 编号。而驱动就是一个一个“岗位”,需要有人认领这些资源来干活。问题是,这些硬件并不像 PCI 设备那样,天然自带厂商 ID、设备 ID,插到总线上就能被枚举出来。
SoC 内部集成的外设,比如 i.MX6ULL 上的 UART、I2C、SPI、GPIO 控制器,它们不是可插拔的,地址是固定的,中断是固定的,没有标准枚举过程。那内核怎么知道哪个驱动对应哪个设备?靠的不是硬件总线,而是一种软件抽象,也就是 Platform 总线。它把所有“挂不到真实总线上的设备”统一挂到一个虚拟总线上管理,让设备侧和驱动侧通过匹配规则互相找到对方。
1.2 i.MX6ULL 场景下 Platform 机制的核心作用
i.MX6ULL 是 NXP 推出的 Cortex-A7 内核应用处理器,在工业控制、物联网网关、带屏显的人机交互设备里用得非常多。它的外设资源不算特别复杂,但足够典型。你写一个 LED 驱动,本质上就是先定义一个 platform_driver,然后在设备树里加一个 compatible 匹配节点,内核启动的时候把设备和驱动配到一起,最终调用驱动里的 probe 函数,你在 probe 里做寄存器映射、GPIO 申请、初始化硬件。
所以理解 Platform 机制,实际上是在理解整个 Linux 设备驱动模型的基石。你在 i.MX6ULL 上跑通一个最简单的 Platform 驱动之后,回头再看 I2C 驱动、SPI 驱动、串口驱动,会发现套路几乎一样:设备侧描述有什么资源,驱动侧声明能处理什么设备,总线负责牵线搭桥。
1.3 为什么我选择从“匹配机制”切入
因为“失败”总是发生在匹配阶段。很多人设备树节点写对了,驱动代码看着也没问题,但 probe 就是不调用。原因往往就是对匹配机制理解不透。到底是看 compatible 字符串?还是看 platform_driver 里的 name 字段?还是看 id_table?优先级是怎么排的?设备树里的 status = "disabled" 会不会导致匹配失败?如果你不把这个机制吃透,就只能靠瞎试,运气好试出来了,换个平台又懵了。
这篇文章就是要把匹配机制这一层彻底掀开,从数据结构、注册流程、源码逻辑三个维度讲透,最后配上我在 i.MX6ULL 上的实操记录。
2. 核心细节解析与实操要点
2.1 设备侧与驱动侧的核心数据结构
先看设备侧。传统写法(非设备树)下,你要定义一个 platform_device 结构体,里面包含设备名、ID、资源数组等。但现代内核开发里,尤其是 i.MX6ULL 这种使用设备树的内核(4.x 之后),你几乎不需要自己构造 platform_device,因为内核在启动阶段会解析 DTB 文件,把每一个 compatible 匹配的节点转换成 struct platform_device 结构体,挂到 platform 总线上。
从设备树转换来的 platform_device 结构体中,关键字段包括:
name:优先取设备树节点的 name 属性,如果没有就取 node name。num_resources:设备树节点里 reg、interrupts 属性会被解析成资源。resource:资源数组,包含了 IO 内存起始地址、结束地址、中断号等。dev.of_node:指向设备树节点的指针,驱动里常用of_*系列 API 去读取额外属性。
再看驱动侧。你要定义一个 platform_driver 结构体,核心字段是:
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; ... };平时写驱动,最主要的工作是填充driver.name、driver.of_match_table、probe和remove。driver.name用于和传统 platform_device 的 name 字段匹配;of_match_table用于和设备树节点的 compatible 属性匹配。
2.2 driver_register 与 device_register 的注册路径
理解匹配之前,先搞清楚注册路径。驱动侧,你调用platform_driver_register(&led_driver),最终会走到__platform_driver_register,再调用driver_register。这里有一个关键动作:driver_register 会触发总线上的 device 遍历。也就是说,驱动注册的时候,内核会遍历 platform 总线上已经注册的所有 platform_device,逐个尝试匹配。如果匹配上,立刻调用对应的 probe。
设备侧,设备树方式下,内核在of_platform_bus_probe或of_platform_populate阶段,把设备树节点一个个变成 platform_device 并注册到 platform 总线上。设备注册的时候同样会触发总线上已有驱动的匹配。
这说明匹配动作是双向触发的:不管哪一侧先注册,只要另一侧注册进来,总线就会做一次全量匹配。这个设计很重要,也是理解“为什么设备树节点存在但 probe 没执行”的关键前提之一。
2.3 i.MX6ULL 上写一个 Platform 驱动的最小框架
在 i.MX6ULL 上,代码框架大致是这样:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> static int led_probe(struct platform_device *pdev) { struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "no memory resource found\n"); return -ENODEV; } dev_info(&pdev->dev, "led probe success, reg 0x%llx\n", (unsigned long long)res->start); return 0; } static int led_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "led remove\n"); return 0; } static const struct of_device_id led_of_match[] = { { .compatible = "mycompany,board-led" }, { } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "board-led", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE("GPL");注意module_platform_driver这个宏,它展开后相当于module_init调用platform_driver_register,module_exit调用platform_driver_unregister。别自己手写 init 和 exit,容易漏掉错误处理。
设备树部分,在根节点下加一个子节点,例如:
&iomuxc { pinctrl_led: ledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 >; }; }; / { led { compatible = "mycompany,board-led"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_led>; reg = <0x020c406c 0x4>; status = "okay"; }; };编译 DTB、烧录、加载驱动后,如果一切正常,控制台会打印:
led probe success, reg 0x20c406c2.4 匹配过程的核心优先级
内核里匹配 platform 设备和驱动的核心函数是platform_match,我直接说结论,匹配顺序是:
- 首先尝试
of_driver_match_device,即用设备树节点的 compatible 属性去匹配驱动里的of_match_table。只要驱动填充了of_match_table,而且设备是设备树节点转换来的,优先走这条路径。 - 如果上一步不成功,再尝试
acpi_driver_match_device,ARM 嵌入式场景基本用不到。 - 如果还不成功,尝试
platform_match_id,即用platform_device的 name 字段去匹配platform_driver的id_table数组中每一项的 name 字段。 - 如果 id_table 为空或者没匹配上,最后尝试
platform_match_name,即直接比较platform_device的 name 字段和platform_driver的driver.name字段。
这里有一个特别容易让人误解的点:很多人以为设备树节点里的 compatible 会和driver.name比较,其实不会。compatible 只匹配of_match_table。如果驱动只设置了driver.name而没有设置id_table,你要保证设备侧的 name 和它一致。但在设备树方式下,设备侧 name 最终来源于设备树节点的 name 属性(通常取 node name),不是你随便定的 compatible 字符串。
所以最稳妥的做法是:驱动里同时填好driver.name和of_match_table。设备树里 compatible 写清楚。这样不管走哪条匹配路径都能兜底。
3. 实操过程与匹配机制的源码级验证
3.1 调试环境与准备工作
我用的开发板是正点原子 i.MX6ULL 阿尔法,内核版本 4.1.15,交叉编译工具链为 arm-linux-gnueabihf-gcc。宿主机是 Ubuntu 18.04,通过 NFS 挂载根文件系统,驱动编译成模块(.ko)后拷贝到板子上加载,方便调试。
不建议一开始就把驱动编进内核里,每次改代码都得重新烧镜像,太浪费时间。编译成模块,配合insmod、rmmod、dmesg循环调试,效率高出不少。
交叉编译环境准备好以后,可以写一个 Makefile:
obj-m := led_platform.o KERNELDIR := /home/xxx/linux-imx-rel_imx_4.1.15_2.1.0_ga PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean注意 KERNELDIR 要指向你已经编译过的内核源码目录,而且这个源码目录必须已经完成了内核编译,生成了 Module.symvers 等文件,否则编译模块时会出现找不到头文件或者版本信息不一致的问题。
3.2 匹配过程源码追踪
我建议你亲手打开内核源码追踪一遍,路径是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); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv->id_table) return platform_match_id(pdev, pdrv->id_table) != NULL; /* fall-back to driver name match */ return (strcmp(pdev->name, drv->name) == 0); }你看到这段代码之后就会明白,of_driver_match_device是绝对的第一优先级。它内部调用of_match_device,最终把设备节点的 compatible 属性和驱动of_match_table里的 compatible 逐个比较。字符串完全相等才算匹配。
of_driver_match_device内部还会调用of_match_device,它会尝试匹配of_match_table中每一个of_device_id。of_device_id结构体里除了 compatible,还有 type 和 name 字段,分别对应设备树节点的device_type和name属性。但现在的设备树基本都不写device_type了,所以实际上就是 compatible 的字符串比较。
这解释了为什么我强调 compatible 值必须一字不差。多一个空格、大小写不一致,匹配都会失败,而且内核不会给出任何直观的报错,只会静默地不调用 probe。
3.3 从设备树节点到 platform_device 的转换历程
还有一个值得关注的点:设备树节点到底是怎么变成 platform_device 的。在 i.MX6ULL 的启动阶段,内核会调用of_platform_populate进行设备树节点的 platform 设备创建。
它的逻辑是:遍历设备树根节点下所有 compatible 为simple-bus或类似总线节点的子节点,将子节点一个个转换为 platform_device。非 simple-bus 节点则根据具体驱动框架决定是否创建 platform_device。
比如你根节点下直接挂一个led节点,根节点/本身就是一个 platform 总线宿主的展开点,所以 led 节点会被正常转换为 platform_device。如果你把 led 节点放在某个 I2C 控制器的子节点里,那它就不会被转换为 platform_device,而是由 I2C 核心框架处理,生成 i2c_client,走的是另一套匹配机制。
这也是很多新手感到困惑的地方:同样写一个 compatible 节点,放在根节点下面 probe 能执行,放在别的节点下面就不行。原因就在于设备树节点拓扑决定了你会用哪套总线机制。
3.4 实际跑通的时序观察
为了验证注册顺序,我在驱动里加了module_init打印,然后设备树里把节点状态设为status = "okay"。
开机后,内核先完成设备树解析,led 节点先被注册为 platform_device。此时我还没有 insmod 驱动模块,所以设备挂在总线上,没有驱动认领。等系统启动到用户态,我执行:
insmod led_platform.ko控制台立刻打印出:
led_probe called, matched compatible = mycompany,board-led这说明驱动注册的瞬间,内核遍历了总线上已经存在的 platform_device,找到了 compatible 匹配的设备,立刻调用了 probe。
反过来,如果我先把驱动模块加载进去,再用某种方式动态创建 platform_device,比如写一个测试模块调用platform_device_register,同样会在设备注册的瞬间触发 probe。
这一点在实际开发中非常有用:不要以为 probe 只会在开机阶段调用。比如你用mdev或udev做设备节点管理,驱动加载时机和设备注册时机不同,最终都不影响匹配结果。
3.5 匹配成功后的资源获取方式
匹配成功之后,probe 函数里最重要的事情之一就是获取硬件资源。设备树里写的 reg 属性、interrupts 属性,都会被转换成struct resource,放在platform_device的资源数组里。
读取方式有两种。
第一种,用platform_get_resource:
struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0);设备树里写了几个 reg,资源索引号就从 0 开始递增。比如:
reg = <0x020c406c 0x4>, <0x020e0100 0x4>;第一个 reg 对应索引 0,起始地址 0x020c406c,长度 0x4;第二个 reg 对应索引 1。
第二种方式,用设备树 API 直接读取属性。比如你要读clock-frequency、gpio这类自定义属性,使用of_property_read_u32、of_get_named_gpio等。这种方式是没有 reg 属性时的常见路径。
特别注意:`platform_get_resource` 只能拿到标准资源,比如 reg、interrupts。如果你需要读取 pinctrl-0 这种属性,或者某个定量参数,它是拿不到的,必须用 of API。4. 常见问题与排查技巧实录
4.1 设备和驱动都注册了,但 probe 始终不打印
这是我被问到最多的问题。排查路径按优先级走:
- 检查设备树节点状态:
status = "disabled"的节点不会生成 platform_device。你要么删掉 status 属性,要么显式写成status = "okay"。 - 检查 compatible 字符串是否完全一致。建议在驱动和设备树里把字符串复制出来,用
grep对比。 - 检查内核是否真的加载了你修改后的 DTB。uboot 里有没有设置正确的
fdt_file环境变量?很多板子默认加载的是旧 DTB,你以为改了,实际上没生效。 - 模块加载后,去
/sys/bus/platform/devices/下看有没有对应设备目录。如果有,说明设备已注册。再看/sys/bus/platform/drivers/下有没有你的驱动目录。如果两者都存在,但 probe 没打印,大概率是匹配字符串的问题。
一个快速验证方法:在驱动里临时把of_match_table删掉,只保留driver.name,然后把设备树节点的 name 设置为与driver.name相同。如果此时 probe 执行了,说明 compatible 匹配路径出了问题。
4.2 编译模块时报错:Unknown symbol 或版本 magic 不一致
这个属于环境问题。最常见原因是模块使用内核源码编译,但开发板运行的内核镜像不是由这份源码编译出来的。内核模块对内核版本和 vermagic 有严格检查。
解决方法是:确保交叉编译使用的内核源码目录,和你烧录到板子上的 zImage 来源于同一套代码,并且在你开始编译模块之前,内核源码目录已经完整编译过一次。最好连设备树也重新编译烧录,保证整体一致性。
还有一种情况:内核源码目录之前编译过,但你后来改了内核配置,比如改了 CONFIG_LOCALVERSION,导致 vermagic 变了。直接执行:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare重新生成模块相关的头文件和符号信息。
4.3 同一 compatible 被多个节点使用时,如何区分设备
有时候你只有一个驱动,但板子上有多个同类型设备,比如两个 LED、四路继电器。设备树里写两个节点,compatible 相同,这时候 probe 会被调用两次,每次传入的 pdev 指向不同设备。
要想区分它们,可以用struct device里的id,或者在设备树节点里添加自定义属性,在 probe 里读取。我常用方法是在设备树里加一个reg或者label属性来区分。
led0 { compatible = "mycompany,board-led"; reg = <0>; label = "led0"; }; led1 { compatible = "mycompany,board-led"; reg = <1>; label = "led1"; };在 probe 里:
u32 id; of_property_read_u32(pdev->dev.of_node, "reg", &id);这样就可以根据 id 初始化不同 GPIO 引脚或者控制不同外设。
4.4 热拔插场景下 remove 不执行
Platform 机制在 i.MX6ULL 这种嵌入式平台上,设备基本是固定不动的,一般不涉及热拔插。但如果你的平台支持设备树 overlay,动态加载设备树片段,那么卸载 overlay 时会触发 remove 调用。
实际调试发现,如果 remove 函数执行过程中出错,比如 iounmap 的地址不对,内核可能会报 stack trace,但系统不一定崩溃。所以 remove 函数里要释放干净:释放申请的中断、释放 GPIO、iounmap、释放内存等。
4.5 debugfs 与 sysfs 辅助调试技巧
除了看 dmesg,我强烈建议利用 sysfs 来辅助排查。
设备注册成功后,在/sys/bus/platform/devices/下会存在设备目录。你可以查看:
ls /sys/bus/platform/devices/ cat /sys/bus/platform/devices/led.0/modalias其中 modalias 会显示出设备对应的 compatible 信息,格式类似platform:led。这个信息就是驱动自动加载(modules.autoload)的依据之一。
驱动注册成功后在/sys/bus/platform/drivers/下会存在驱动目录,目录名就是driver.name。你还可以手动做绑定和解绑实验:
echo "led.0" > /sys/bus/platform/drivers/board-led/bindecho "led.0" > /sys/bus/platform/drivers/board-led/unbind如果你手动 bind 的时候提示No such device,说明设备名不对,去/sys/bus/platform/devices/下先确认设备目录名。如果手动 bind 后 probe 正常执行,但开机时不执行,问题大概率还是设备树状态或 DTB 未更新。
4.6 对匹配失败问题的速查表
| 现象 | 常见原因 | 排查手段 |
|---|---|---|
| probe 不打印 | compatible 不匹配 | grep 对比双方字符串 |
| probe 不打印 | status = "disabled" | 检查设备树节点状态 |
| probe 不打印 | DTB 未更新 | 检查 uboot fdt_file 参数 |
| probe 打印但硬件无动作 | 资源获取失败 | 检查 devm_ioremap 返回值 |
| probe 打印两次 | 设备树有两个同 compatible 节点 | 用 reg 属性区分 |
| insmod 报 version magic 错误 | 内核源码与运行内核不一致 | 统一编译环境并 modules_prepare |
| bind 失败 | 设备名错误 | ls /sys/bus/platform/devices/ |
5. 从匹配机制延伸:i.MX6ULL 平台驱动开发的关键经验
5.1 驱动分层与匹配机制的协同设计
不要把 Platform 匹配机制看成孤立的玩意儿。实际项目里,I2C 控制器驱动、SPI 控制器驱动、GPIO 控制器驱动本身都是 platform_driver,它们先通过 Platform 机制匹配到 SoC 内部硬件资源,完成初始化之后,再注册自己的框架(I2C 总线、SPI 总线),最后为挂载其下的子设备提供读写接口。
所以在 i.MX6ULL 上写驱动,脑子里要有三层概念:
- 最底层:platform_driver 负责匹配设备树节点,获取寄存器、中断资源,初始化硬件控制器。
- 中间层:注册具体的子系统框架,例如
i2c_add_adapter、spi_register_master。 - 最上层:具体设备的驱动,比如挂在 I2C 总线上的触摸屏驱动,挂在 SPI 总线上的 LCD 驱动。
每一层的注册和匹配机制都类似,但细节不同。吃透了 Platform 机制,再学别的总线模型,会轻松非常多。
5.2 设备树是“硬件描述语言”,不是“驱动配置语言”
接触过不少工程师,喜欢在设备树里塞各种自定义属性,然后在驱动的 probe 里读出来做业务逻辑。这种做法偶尔用用可以,但大规模使用就会导致设备树越来越臃肿,语义混乱,后期维护困难。
设备树的核心作用是把硬件怎么接的、资源在哪里描述清楚。业务逻辑、策略、参数计算应该放在驱动代码或其他配置系统里。比如一个 LED 的闪烁频率,如果用户需求频繁变化,应该做成 sysfs 属性或者通过应用层 ioctl 控制,而不是在设备树里反复改、反复烧写 DTB。
5.3 调试效率决定开发效率
我这个项目里,最大的体会是:驱动开发的主体时间不是写代码,而是调试。所以前期把调试手段建立起来,比什么都重要。
推荐几个高效手段:
- 串口控制台打印等级调高,
loglevel=8,让所有 dev_dbg 都打出来。 - 系统起来后用 NFS 挂根文件系统,方便直接拷贝 ko 文件,不用反复制作根文件系统镜像。
- 在驱动里多用
dev_info、dev_err这类带设备信息的打印,比单纯printk更容易定位是哪个设备实例出问题。 - 把设备树反编译出来检查:在板子上执行
dtc -I fs -O dts /proc/device-tree -o output.dts,看看实际的设备树和你想的是不是一样。
5.4 关于模块自动加载的补充
如果你的驱动需要开机自动加载,要么把驱动编进内核,要么在根文件系统里配置 modprobe 自动加载。自动加载依赖 modules.alias 文件,它是 depmod 根据MODULE_DEVICE_TABLE生成的。所以驱动代码里记得写:
MODULE_DEVICE_TABLE(of, led_of_match);没有这行,即使设备匹配上了,modprobe 也可能不知道该加载哪个模块。这行宏的作用是把of_match_table的内容导出到模块的 modinfo 信息里,depmod 读取之后生成 alias。我见过太多人漏掉这一行,导致手动 insmod 可以,但重启后系统不会自动加载驱动。
写在最后
说实话,Platform 设备与驱动匹配机制,属于那种“学会了觉得很简单,学之前觉得特神秘”的知识点。它的核心思想就是一句话:用软件抽象出一条虚拟总线,把不便于枚举的片上设备挂上去,让设备和驱动通过一套规定的规则互相匹配。而 i.MX6ULL 恰好是特别适合练手这个机制的平台,NXP 的参考手册清晰、资料多、硬件也简单,板上一个 LED 就能做全流程验证。
从我个人的经验来看,学习这块内容最好的方式就是动手改代码、写个最简驱动、故意制造几次匹配失败,然后自己去排查。每一次踩坑都会让你对机制的理解加深一层。今天分享的这些内容,如果你在项目里也遇到了类似的问题,尤其是 probe 不执行这一类,完全可以对照上面的排查表一步步查,希望对你有所帮助。