1. 驱动“装上了”却不跑 probe:从 i.MX6ULL LED 驱动的第一个坑谈起
1.1 最初直接用字符驱动思路写代码,模块加载却一点动静没有
我第一次在 i.MX6ULL 上调 GPIO LED 驱动时,犯过一个很典型的错误。当时思路很简单:照着 Linux 字符设备驱动的例子写一个 file_operations,module_init里申请 GPIO、注册 miscdevice 或 cdev,然后用户程序 open/write 去点亮 LED。那一套其实没什么问题,只要把设备树里对应的 GPIO 引脚复用配置好,用户程序操作/dev/myled就能闪灯。
后来换了种写法,想用更“标准”的 platform 驱动风格来管理这个 LED 设备。于是写了 platform_driver 结构体、实现了 probe 函数、在驱动里用devm_gpiod_get()获取描述符,注册用的是module_platform_driver()。编译没问题,Makefile 也没写错,insmod 之后模块确实加载进了内核,modprobe 也显示加载成功。
但 probe 始终没有执行。
dmesg 里看不到任何 probe 成功的打印,/proc/misc里也不会出现对应的次设备号——因为我当时把 LED 的点亮逻辑放在 probe 里,probe 没跑,整个驱动等于什么都没干。那段时间我反复检查代码,甚至对 module_init 的入口打过断点,最后才意识到,问题根本不是函数有没有被调用,而是我压根没理解 platform 驱动的工作原理。
这里就得引出 topic 里最核心的概念:platform device 与 platform driver 需要先“配对成功”,内核才会调用这个 platform_driver 的 probe。你也可以把它理解成婚介所:设备是征婚者,驱动也是征婚者,两边先得互相看对眼,内核才会安排见面(probe)。如果匹配机制没走通,probe 永远是个摆设。
很多刚从裸机转过来的人,一开始都会卡在这个地方。因为裸机程序里没有“设备”和“驱动”要不要匹配的问题,寄存器就在那里,函数想调就调。但 Linux 驱动模型把设备和驱动拆开了,这是为了可移植性、可热插拔性、以及更灵活的硬件描述方式,代价就是你要先弄明白平台总线那一套匹配规则。
1.2 核心概念:什么是 platform device、platform driver 和 platform bus
在 i.MX6ULL 这类 SoC 上,有大量外设并不挂在 I2C、SPI、PCI 这样的物理总线上,而是通过内存映射方式挂在 CPU 的寄存器总线上。GPIO、UART、ECSPI、I2C 控制器、SDIO 控制器,这些外设的寄存器都在芯片内部,地址空间直接暴露给 CPU。内核不能为每一个外设都发明一种物理总线,于是设计了一条虚拟总线,把所有“直接挂在 CPU 总线旁的内存映射设备”统一管理起来,这条虚拟总线就叫 platform bus。
注意,platform 这个名字不是指“某个特定的硬件平台”,它指的是“这个设备所依赖的平台/设计”。你可以把它理解成 Linux 设备模型里专门负责承上启下的一层抽象。它的核心职责只有一个:在 platform_device 和 platform_driver 之间做匹配。
- platform_device:描述外设的存在,比如我在设备树里定义了一个 LED,那么这个 LED 节点将来就会被内核转成一个 platform_device。
- platform_driver:描述让外设工作的代码逻辑,比如这个 LED 驱动如何初始化 GPIO、如何控制电平。
- platform_bus_type:内核维护的一条虚拟总线数据结构,负责为 device 和 driver 牵线搭桥。
这三者缺一不可。很多 Linux 驱动书里会强调“设备与驱动分离”,真正的含义就是:设备的硬件属性用设备树描述,驱动的行为用 C 代码描述,两者通过 platform bus 匹配后组合起来。好处很明显,同一份驱动代码,只要设备树里 compatible 字符串对得上,几乎不用改动就能适配不同板卡。
所以,驱动不 probe,先别怀疑代码逻辑,最优先排除的一定是匹配问题。
2. i.MX6ULL 的片上外设如何变成 platform_device:设备树解析链路
2.1 从 imx6ull.dtsi 开始看 SoC 总线层级
既然匹配的前提是先有 platform_device,那就得先知道设备是从哪来的。现代 ARM Linux 早已不用板级文件注册 platform_device,而是通过设备树(DTB)描述硬件信息,内核启动阶段解析设备树,把每个可用的外设节点转换成 platform_device。
i.MX6ULL 的设备树源文件分层很明显。imx6ull.dtsi是 SoC 级描述,里面描述了芯片内部的总线结构和各类外设节点;具体的开发板 dts 文件再 include 这个 dtsi,允许板级覆盖一些属性。粗略结构类似这样:
/ { model = "Freescale i.MX6ULL"; compatible = "fsl,imx6ull"; soc { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; aips1: aips-bus@02000000 { compatible = "fsl,aips-bus", "simple-bus"; ... }; aips2: aips-bus@02100000 { compatible = "fsl,aips-bus", "simple-bus"; ... }; }; };注意这里有一个非常重要的细节:soc节点以及aips-bus这类节点的 compatible 里都包含了"simple-bus"。simple-bus是内核约定的一个标签,表示这个总线节点下的子节点,应该被当作普通的、可以直接映射到 CPU 地址空间的外设来枚举。
内核启动时,会有一个 initcall 调用of_platform_default_populate_init(),它从设备树根节点开始遍历,遇到compatible包含"simple-bus"的节点,就会继续递归创建它下面的 platform_device。i.MX6ULL 的 soC 节点、aips1、aips2、以及它们下层的 gpio 控制器、uart、i2c 控制器等,都会顺着这条链变成一个个 platform_device。
所以你在 i.MX6ULL 上看到dmesg里有大量类似这样的输出:
platform 1c40000.serial: probe of 1c40000.serial returned 0 after 123 usecs platform 20a0000.gpio: probe of 20a0000.gpio returned 0 after 45 usecs这些设备名里的前缀1c40000、20a0000就是节点在 SoC 内部的内存基地址,也是这些外设被访问的窗口。i.MX6ULL 的 SoC 外设资源,就是靠这种“总线—地址—外设”的层级关系组织起来的。
2.2 不是所有设备树节点都会出现在 platform 总线上
这里藏着很多新手最容易踩的坑:你会觉得,“我在设备树里加了一个节点,它就应该自动变成 platform_device”。但实际并非如此。
设备树里的节点能不能被转成 platform_device,和这个节点所在的位置、父节点的 compatible、以及节点自身的 status 都有直接关系。拿一个外接 I2C 触摸屏举例,触摸屏芯片挂在 i2c 控制器的子节点下:
&i2c1 { clock-frequency = <100000>; touchscreen@38 { compatible = "xx,touchscreen"; reg = <0x38>; }; };这个touchscreen@38节点最终并不会被创建成 platform_device,而是会由 i2c 控制器驱动在 probe 时注册 i2c_client,然后通过 i2c bus 和 i2c_driver 做匹配。原因很简单:它挂在 I2C 总线上,不是直接挂在 CPU 总线旁的内存映射设备,不再属于 platform bus 的管辖范围。
反过来,如果你打算驱动一个简单的、直接内存映射的控制器,那么节点就应该放在能通过simple-bus链被递归遍历的位置。一般情况下,开发板 dts 顶层加的根节点下的自定义节点,只要节点本身可用,也会在初始化阶段被处理成 platform_device。至于那些放在 i2c 子节点下却想当 platform_device 用的节点,多半是白加了。
在 i.MX6ULL 平台树中,如果你自己定义一个“小外设”,最稳妥的位置其实是把它放在自定义的、带compatible = "simple-bus"的父节点下面,或者干脆放在设备树根节点下。如果放在某些被其它专门 bus 驱动的控制器子节点里,最后可能什么设备都不会生成。
2.3 status = "disabled" 与可用节点的差别
设备树里很多节点默认是 disabled 的,特别是 SoC dtsi 里那些用不到的外设。以 i.MX6ULL 的 I2C 节点为例,imx6ull.dtsi 里写的是:
i2c1: i2c@21a0000 { compatible = "fsl,imx6ul-i2c"; reg = <0x021a0000 0x4000>; ... status = "disabled"; };当板级 dts 里使用&i2c1 { ... }添加外设时,往往不会主动把 status 改掉;而如果 status 一直保持 disabled,内核解析设备树时直接会跳过这个节点,不把它转成 platform_device。即使你写了一大堆子节点和 compatible,也不会产生设备。
因此平台驱动根本没设备可匹配时,第一件事就是确认这个对应的设备树节点有没有被使能。status = "okay"才是可用状态,通常默认缺省也被视为 enabled,但因为 SoC dtsi 里已经写死 disabled,使用某些复用节点前必须显式改成 okay。这也是为什么很多例程里总能看到类似这样的代码:
&iomuxc { pinctrl_myled: myledgrp { fsl,pins = <MX6UL_PAD_GPIO1_IO08__GPIO1_IO08 0x17059>; }; }; / { myled { compatible = "myvendor,led-demo"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_myled>; led-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>; status = "okay"; }; };节点里那个status = "okay"并不是随手的装饰,它是告诉内核:这个节点可用,请把我的 platform_device 创建出来。
3. platform_match 一锤定音:四种匹配规则的先后顺序
3.1 打开 drivers/base/platform.c 看真实判据
设备