去年我在调试一块 i.MX6ULL 板子时,遇到过一种特别迷惑的现象:驱动模块加载之后没有任何报错,insmod 输出干干净净,/proc/devices 里却没有我想看到的设备,register_chrdev 明明执行了,但 probe 就是不出来。后来把内核日志翻到底,才发现 platform_driver_register 确实被调到了,可设备与驱动匹配的环节根本没把我写的驱动和我设备树里的节点对上。也是从那时候起,我才把 Linux 驱动开发里的 Platform 设备与驱动匹配机制从头到尾认真梳理了一遍,终于懂了为什么论坛里隔三差五就有人问“probe 为什么不调用”。这篇就结合我实际调试 i.MX6ULL 的经验,把设备与驱动是怎么匹配的、匹配不上时要查哪些地方讲透。适合已经能写简单字符设备驱动、正准备跨入设备树驱动和 probe 模式的人阅读。
1. 从地址写死的驱动到 device/driver 分离:Platform 到底在解决什么问题
新手入门 i.MX6ULL 的时候,一般都会先接触一种很原始的 LED 驱动写法:在 file_operations 的 open 或 ioctl 里直接 ioremap 寄存器,写某个 GPIO 的 MUX、方向、数据寄存器,然后 register_chrdev 把设备注册进内核。这套代码能跑,也能控制灯,可一旦项目要换引脚、换板子、换一颗相近的芯片,就得打开源码一个个改地址,重新编译。硬件信息像胶水一样糊在驱动逻辑里,时间一长,代码根本没法维护。
Linux 设备模型其实就是冲着这个问题来的。内核把“设备本身”和“驱动本身”拆成两类对象。设备对象负责描述硬件资源:基地址是多少、中断号是多少、时钟名字是什么;驱动对象负责描述操作逻辑:怎么初始化、怎么读、怎么写、怎么关机。两者之间通过总线结构做媒人,而总线最核心的职责之一,就是提供 match 回调,把一个驱动和一块设备“对上眼”。在 i.MX6ULL 这种 ARM SoC 上,SoC 内部的大多数外设并不像 USB、PCI 那样有真实的物理总线可以枚举,于是内核捏造出了一条虚拟总线,叫 platform bus,也翻译成平台总线。UART、GPIO、I2C 控制器、网卡 MAC、LCD 控制器,这些没有物理总线枚举机制、但 CPU 可以直接访问的设备,统统挂在这条虚拟总线上。
打个比方,驱动就是你开的一家店,里面有一套完整的服务流程;设备就是上门来的客人,身上带着身份证和需求单;Platform 总线则像一个拥有全市户口的政务窗口,先把客人的资料录入系统,再拿着你的营业范围去核对。platform_match 就是这个窗口里负责核对的那个人。过去那种裸寄存器驱动,相当于你在路边摆摊,看到谁像你的客户就直接拉过来,完全不走系统登记,Single 板子能行,项目一多必然失控。
还有一个值得注意的点:Platform 这个名字很容易让人以为只有在 ARM 上才有。实际上 x86 上也有很多设备没有强制的总线枚举机制,同样会落到 platform bus 上。你在 Linux 里看到 /sys/bus/platform/devices/ 下面那一大串,基本都是这一类“住在 SoC 里、不挂在 PCI/USB 下”的设备。
1.1 软件层面的双层结构
代码上,Platform 机制涉及两个关键结构体:platform_device 和 platform_driver。它们并不是凭空设计的,而是在通用 device_driver 和 device 结构体基础上包了一层,额外携带了驱动开发最关心的资源信息和接口信息。
platform_device 结构体里除了内嵌 struct device 之外,还有 resource 数组,用来描述内存地址、中断号等传统资源。即使现在大家都用设备树,probe 里读取寄存器地址时,背后仍然会把这些信息从设备树解析成 resource,再通过 platform_get_resource 或 devm_platform_ioremap_resource 拿回来。platform_driver 则是给驱动注册的入口,它包含 probe、remove、shutdown 回调,同时有一个指向 struct device_driver 的 driver 成员。启动时你写 platform_driver_register,实际上主要是在注册内嵌的那个通用驱动对象;而设备和驱动的匹配,最后也就是拿这个通用驱动对象和设备对象进行比较。
用 i.MX6ULL 最常见的外设举例:系统里的 ecspi、fec、uart1 这些节点,假如不是挂在 i2c、spi 这种专门总线上(指控制器本身),内核就会把它们注册成 platform_device。对应驱动里声明 struct platform_driver,提供 probe,注册到 platform bus。只要 match 成功,probe 就会被调用,然后你在这个函数里完成寄存器映射、申请中断、注册字符设备或者框架接口。
2. 设备端是怎么来的:设备树节点与 platform_device 的转换链路
既然驱动要匹配设备,那就得先搞清楚设备对象是从哪条流水线里生产出来的。对 i.MX6ULL 这种现代 ARM 平台来说,答案几乎都是设备树。少数老内核 or 板级文件还在用的 platform_device_register 方式,现在已经不是主流,但理解它对你排查老代码仍有帮助。
最直白的一句话:设备树不是直接交给驱动使用的,它要经过内核解析,转变成一颗 struct device_node 树,然后其中符合条件的一部分节点会被转换成 struct platform_device,注册到 platform bus 上。完成这个转换的核心代码在 drivers/of/platform.c,常见入口叫 of_platform_default_populate_init。Linux 启动时会从根节点往下遍历,如果节点的 compatible 属性表明它是一个“平台总线下的普通设备”,或者所在的父总线 compatible 带 simple-bus、fsl,aips-bus 这类标识,子节点就逐层被递归创建为 platform_device。整个工作发生在内核很早期的 initcall 阶段,所以你完全不用自己在驱动里手动把设备树节点注册为 platform_device,更不要在 probe 里再 platform_device_register。
比如 i.MX6ULL 官方设备树里经常能看到这样的节点片段:
&uart1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart1>; status = "okay"; };板上实际做的,是在 dts/dtsi 里把串口控制器的属性配好。内核启动后,这个节点如果挂在了合适的父总线下,就会成为一个带 of_node 的 platform_device。你再打开 /sys/bus/platform/devices/ 就能看到它。
2.1 设备身份的核心是 compatible 而不是节点名
这里要强调一个非常容易搞混的点:对设备树来的 platform_device,它的“身份”不是节点名,而是 compatible 属性。compatible 相当于设备的身份证号,是一个字符串,通常写成“厂商,型号”的形式。厂商在设备树里声明自己是谁,驱动在 of_match_table 里声明自己能认谁,两边能对上,match 才成立。
i.MX6ULL 的串口节点 compatible 经常是这样的:
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";驱动侧,imx 串口驱动里通常有这样一个表:
static const struct of_device_id imx_uart_dt_ids[] = { { .compatible = "fsl,imx6q-uart" }, { .compatible = "fsl,imx53-uart" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);比较的时候,设备树里那一串 compatible 会从左到右逐个去 of_match_table 里扫描,找到任意一个完全相等的字符串就算匹配成功。注意是“完全相等”,不是包含、不是正则、不是模糊拼写。compatible 里多一个字母、少一个短横线,都会直接匹配失败。这也是新手排查 probe 不执行时最容易被忽视的原因之一。
2.2 老内核里没有设备树,那时靠什么匹配
如果去看一些比较老的 i.MX6ULL 教程,或者拿到一份陈旧的内核代码,你可能会看到板级 board 文件里这样写:
static struct resource led_resources[] = { DEFINE_RES_MEM(0x020C406C, 0x4), }; static struct platform_device led_device = { .name = "imx6ull-led", .id = -1, .num_resources = ARRAY_SIZE(led_resources), .resource = led_resources, }; static int __init led_device_init(void) { return platform_device_register(&led_device); }这种不是从设备树创建的 platform_device,它的 dev->of_node 是空的,所以设备树那套 of_match_table 匹配规则根本不参与。platform_match 会落到 id_table 或者最后的 name 比较那条规则上去。你会看到很多老驱动常年维持一个 platform_device_id 表,也就是为了兼容这种没有设备树 or 没有 of_node 的老式注册路径。
我自己刚开始从板级文件思维跳到设备树思维时,最吃亏的就是以为自己把 .name 改成和设备树节点名一样就能匹配上,结果怎么改都不 probe。原因后面讲平台匹配源码时会说清楚:设备树时代的核心身份字段已经变了,老一套的 name 匹配不是主要规则。
3. 配对时的真实现场:platform_match 里的优先级排序与常见误区
现在我们终于可以进入本文最核心的地方:Platform 总线上设备和驱动到底是怎么比较的。直接打开内核源码,具体路径是 drivers/base/platform.c,里面有一个 platform_match 函数。不同内核版本细节略有差异,但 Linux 4.x/5.x 里,逻辑