我刚开始接触嵌入式Linux驱动的时候,一直有个困惑:网上那些驱动例程,入口不是module_init吗?怎么到了公司代码或者SDK里,到处都是probe函数?而且这个probe还不用自己调用,加载完模块它自己就跑了。后来才明白,90%的片上外设驱动都是Platform驱动,而probe能不能被调用,全看“设备与驱动匹配机制”有没有生效。
这篇文章我就以i.MX6ULL平台为背景,把Platform设备与驱动的匹配机制彻底讲透。既说清楚内核源码级别的匹配逻辑,也把设备树节点怎么变成platform_device、驱动注册后probe为什么会被自动调用这两条链路梳理通。最后给出一套可以在开发板上直接验证的实操步骤,以及排查“probe不执行”的完整思路。适合正在学嵌入式Linux驱动、或者已经被“Platform总线”搞得一头雾水的朋友。
1. 为什么Linux要造出一条虚拟总线——Platform总线的设计初衷
要弄懂Platform设备与驱动匹配机制,首先得明白一个更基础的问题:为什么内核非要设计一条“虚拟总线”出来?这事得从Linux设备模型的底层逻辑说起。
1.1 设备模型的三要素:总线、设备、驱动
Linux内核中,任何设备都跑不出设备模型(Device Model)的框架,这个框架由三个核心对象组成:总线(bus)、设备(device)、驱动(driver)。它们三者的关系可以这样理解:
- 总线:是设备与驱动之间的“红娘”,负责给设备和驱动牵线搭桥。
- 设备:是物理硬件在内核中的抽象,代表“有一个硬件存在”。
- 驱动:是软件逻辑的载体,代表“我能操作某类硬件”。
在一个典型的PC机上,USB设备插入主机,USB总线控制器会检测到硬件变化,在内核里创建一个usb_device;然后USB总线会去扫描已注册的所有usb_driver,看看谁的id_table能对上这个新设备。对上了,就调用驱动的probe,设备正式开始工作。
这套模型的核心思想是设备与驱动分离:设备只管描述自己是谁,驱动只管声明自己能处理谁,两者不需要在代码里互相引用。总线匹配机制是连接它们的唯一纽带。
1.2 SoC片上外设的尴尬处境
问题来了。PCI有PCI总线,USB有USB总线,SDIO有SDIO总线,它们都有物理存在、有标准枚举协议、支持热插拔。但i.MX6ULL这种SoC芯片里的UART、I2C、SPI、GPIO控制器怎么办?
这些外设并不是插在一个可枚举的物理总线上,而是直接集成在芯片内部,挂在SoC内部总线上,通过寄存器地址映射来访问。从硬件上看,它们不支持“热插拔”,也没有标准的“厂商ID+设备ID”供软件枚举。但它们依然是实实在在的设备,也需要挂到某条总线上,才能被设备模型管理。
你总不能给每种片上外设都发明一条具体总线吧?比如i2c控制器挂i2c总线、uart控制器挂uart总线?那样维护成本太高,而且它们本质上都是Memory-Mapped设备,行为模式一模一样。
于是内核的开发者在很久以前做了一个极具实用主义风格的决定:统一挂到一条虚拟总线上,命名为platform_bus_type。
1.3 Platform到底是干什么的
Platform总线在/Sysfs中对应的目录是/sys/bus/platform,但它没有任何物理硬件对应。它的作用只有一个:给“直接集成在SoC上、通过内存映射访问的设备”提供统一的挂载点。
这类设备在内核里就叫platform_device,专门驱动它们的就叫platform_driver。凡是满足下面特征之一的设备,通常都会用Platform框架来写驱动:
- 设备直接集成在SoC内部,通过寄存器地址访问(GPIO控制器、UART、I2C控制器等)。
- 设备挂在无标准总线的外部总线上(比如板级扩展IO芯片)。
- 系统自带的虚拟设备(比如定时器、GPIO LED、寄存器映射的杂项设备)。
我总结过一句话,特别适合入门时建立直觉:Platform总线就是主板上的“焊死区”。USB设备像插线板上的插头,可以随时插拔;而片上外设就像焊接在电路板上的芯片,物理上不可移动,但软件层面它们同样需要“通电工作”,Platform总线就是给它们统一供电供软件的通道。
有了这个背景,Platform设备与驱动匹配机制的必要性就很清楚了:既然一个SoC上有几十上百个片上外设,每个外设可能都有对应的驱动,内核必须有一套高效且可靠的匹配规则,让每个设备准确找到自己的驱动,绝不能出现GPIO驱动跑到了UART设备上的情况。
2. 匹配的入口:platform_match函数到底在比什么
知道了Platform总线存在的意义,接下来要直面最核心的问题:设备和驱动怎么匹配?这个问题的答案全部集中在内核源码的一个函数里:platform_match。它在drivers/base/platform.c中,我以Linux 5.x内核为例,把核心流程拆开看。
2.1 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. 设备树匹配:优先检查 compatible 属性 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. ACPI 匹配:x86/ARM64服务器场景使用,嵌入式基本不涉及 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. id_table 匹配:驱动设备ID表,逐个比对设备名 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* 4. 设备名与驱动名直接比对 */ return (strcmp(pdev->name, drv->name) == 0); }这四步,就是整个Platform设备与驱动匹配机制的“法律条文”。它按优先级从上到下执行,任何一条命中了就直接返回匹配成功,后面的逻辑不再执行。理解这四步,基本就理解了匹配机制的全部精华。
2.2 设备树匹配是绝对主流
在i.MX6ULL这类用设备树(Device Tree,DT)的平台中,第1条of_driver_match_device几乎就是唯一命中的路径。它做的事情非常纯粹:把设备树节点上的compatible属性,和驱动里of_device_id表中的compatible逐个比对。
驱动侧的示例代码如下:
static const struct of_device_id led_of_match[] = { { .compatible = "myvendor,led-demo" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "my_gpio_led", .of_match_table = led_of_match, }, };设备树节点里面是这么写的:
led_demo: led-demo { compatible = "myvendor,led-demo"; led-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>; status = "okay"; };匹配时,内核拿着设备树节点里的"myvendor,led-demo"去遍历led_of_match数组,字符串一致就返回匹配成功。这里有几个开发中容易踩的细节:
- compatible的命名规范:标准格式是
"厂商名,器件型号",厂商名必须不带资本化、尽量用公司域名风格,例如"fsl,imx6ull-uart"。这个前缀在设备树规范里有明确要求,如果不按规范写,虽然内核不会报错,但很难看,而且会被上游维护者拒绝。 - 数组必须以空结构体结尾:上面代码里的
/* sentinel */就是结束标记。如果没有这个哨兵项,内核遍历会越界,直接崩溃,这是新手最容易犯的错误之一。 - MODULE_DEVICE_TABLE的作用:它的宏展开后,会把
of_device_id表编译进模块的一个特殊section。这个表在编译时会生成modinfo信息,用户空间的udev(或udevadm)根据设备uevent里的MODALIAS,就能自动加载对应驱动模块。嵌入式里虽然经常手写insmod,但这个宏习惯上一定会写上,别省。
2.3 id_table和名字匹配:老平台的后备方案
在没有设备树的年代,或者某些老驱动逻辑中,会使用platform_device_id表来匹配。这种场景下,设备侧是通过板级文件(board.c)注册的platform_device,设备名直接写在结构体里,例如platform_device_register时指定name = "mydrv"。
驱动侧写法如下:
static const struct platform_device_id mydev_ids[] = { { .name = "mydev-v1", .driver_data = (kernel_ulong_t)&v1_data }, { .name = "mydev-v2", .driver_data = (kernel_ulong_t)&v2_data }, { } }; MODULE_DEVICE_TABLE(platform, mydev_ids); static struct platform_driver mydev_driver = { .probe = mydev_probe, .remove = mydev_remove, .driver = { .name = "mydev", }, .id_table = mydev_ids, };这种方式的匹配逻辑是:遍历id_table里的每一个.name,和pdev->name做字符串比较。命中后,对应项的.driver_data会被内核保存下来,驱动可以在probe里通过platform_get_device_id(pdev)拿回来,根据不同的硬件版本做差异化初始化。
至于第4条直接比较驱动名和设备名,在设备树时代已经很少单独依赖了。它更像一个兜底:如果驱动没配置of_match_table也没配置id_table,那就比较drv->driver.name和pdev->name。这是最原始的匹配方式。
这里要特别注意一点:同时配置了of_match_table和id_table时,设备树匹配永远先执行。也就是说,如果设备树节点的compatible命中了,内核根本不会去看id_table。所以你在SDK驱动里经常看到两个表同时存在,compatible给设备树平台用,id_table给老式板级文件平台用,两者互不干扰。
2.4 compatible匹配背后隐藏的“厂商前缀陷阱”
很多人第一次写设备树和驱动,会觉得compatible就是一对一的字符串对应,没什么坑。但实际上compatible可以包含多个字符串,比如某节点可能这样写:
compatible = "myvendor,led-demo", "gpio-leds";内核匹配时按顺序依次尝试,任何一个字符串与驱动的of_match_table命中即算成功。这种“多个compatible”的写法在i.MX6ULL非常常见,因为很多外设驱动可能被多个兼容芯片复用。比如"fsl,imx6ul-uart"和"fsl,imx6ull-uart"通常会在同一个节点里出现。
但这个机制也带来一个隐蔽问题:如果你的驱动led_of_match[]里写的是"myvendor,led-demo",而设备树节点里compatible的第一个字符串是"othervendor,led-demo",第二个才是"myvendor,led-demo",此时依然能匹配成功,因为内核会遍历所有compatible字符串。反过来,如果设备树里只写了"othervendor,led-demo",而你的驱动表里只有"myvendor,led-demo",那就匹配不上,probe不会执行。
这种字符串匹配的“一族多兼容”逻辑,在排查问题时迷惑性很强。我建议在开发自定义驱动时,设备树和驱动里的compatible只保留一个,尽量少写多兼容,除非你真的需要兼容多个硬件版本。
3. 设备侧管线:设备树节点是如何变成platform_device的
理解了匹配逻辑,你可能会困惑另一个问题:设备树里那些节点,到底是什么时候、被谁变成内核里的platform_device的?很多人以为设备树被内核解析后,所有节点天然就是platform_device,这个认知是错的。
3.1 设备树节点与platform_device的区别
设备树节点本质上是硬件描述信息,它存储在dtb二进制文件中,本身不是一个内核对象。内核启动时会把dtb解析成device_node结构,形成一棵树。但device_node只是“设备描述”,不是“设备实例”。要让设备模型管理它,必须创建对应的struct device,具体到Platform机制,就是创建struct platform_device。
谁来创建?答案是内核的设备树填充机制:of_platform。
3.2 of_platform_populate的扫描过程
在内核启动流程中,start_kernel早期完成设备树展开,之后在init_machine阶段会调用of_platform_default_populate_init(这是一个arch_initcall_sync级别的调用),它会扫描设备树根节点下的所有子节点,凡是被判定为“平台设备”的节点,都会被动态创建为platform_device。
判定规则简单粗暴但很有效:
- 节点必须有
compatible属性(否则无法匹配驱动)。 - 节点不能是
status = "disabled"(被禁用则不创建设备)。 - 节点的父节点如果已经被某类特殊总线驱动接管(比如PCI、I2C、SPI、MDIO),子节点不会作为platform_device创建,而是作为对应总线的client设备。
最后一条特别重要。举个例子,i.MX6ULL的设备树里,I2C控制器节点本身会被创建为platform_device,但它下面的某个触摸屏子节点touchscreen@1a就不会是platform_device,而是由I2C核心创建为i2c_client。这也是驱动面试里经常问的一个细节。
在i.MX6ULL的imx6ull.dtsi中,你能看到大量外设节点:
uart1: serial@02020000 { compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart"; reg = <0x02020000 0x4000>; interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6UL_CLK_UART1>; ... };这个uart1节点,就是典型的会在init阶段被自动转换为platform_device的节点。转换时,reg属性会变成platform_device的resource,interrupts会变成中断资源,这样驱动在probe里就可以用platform_get_resource和platform_get_irq来获取硬件信息,而不需要硬编码地址。
3.3 验证设备有没有成功注册
在开发板上,可以用以下命令快速确认设备是否已经变成platform_device:
ls /sys/bus/platform/devices/ dmesg | grep platform如果设备被成功创建,dmesg里通常会有类似这样的字样:
platform 20a0000.uart1: Fixed dependency cycle(s) with /soc/aips-bus@02000000/spba-bus@02000000或者直接去看sysfs:
cat /sys/bus/platform/devices/20a0000.uart1/uevent会看到MODALIAS=of:NserialTserialCfsl,imx6ul-uart之类的字符串。这个字符串就是设备树匹配机制和用户空间模块自动加载之间的桥梁。有点绕,但理解它对你排查驱动不生效问题很有帮助。
我自己遇到过一种情况:在设备树里加了一个外设节点,编译烧写后ls /sys/bus/platform/devices/里死活找不到对应设备,最后发现是status没有写"okay",默认被禁用了。排查这类问题,第一反应就应该是去看设备有没有被创建出来,而不是盯着驱动代码改来改去。
4. 驱动侧管线:platform_driver_register到probe的全链路
设备侧搞清楚了,再看驱动侧。为什么驱动模块一加载,probe就自动被调用了?这背后是一条清晰的中断调用链。理解这条链路,比死记硬背“注册驱动要写probe”有价值得多。
4.1 module_platform_driver宏的展开
驱动侧通常用module_platform_driver来注册:
module_platform_driver(led_driver);这个宏展开后相当于:
static int __init led_driver_init(void) { return platform_driver_register(&led_driver); } static void __exit led_driver_exit(void) { platform_driver_unregister(&led_driver); } module_init(led_driver_init); module_exit(led_driver_exit);所以一切的核心就是platform_driver_register。
4.2 从driver_register到driver_attach的接力
platform_driver_register内部,会把pdrv->driver.bus设为platform_bus_type,然后调用driver_register。driver_register经历一系列初始化后,最终会调用bus_add_driver,它会做两件事:
- 把驱动加入到总线的驱动链表:
klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers)。 - 调用
driver_attach(drv),去遍历总线上当前已经注册的所有设备,为这个新驱动寻找匹配的设备。
driver_attach内部调用的是bus_for_each_dev(drv->bus, NULL, drv, __driver_attach),也就是遍历platform总线设备链表上的每一个device,对每个设备都执行__driver_attach。
__driver_attach的逻辑可以用伪代码概括:
static int __driver_attach(struct device *dev, void *data) { struct device_driver *drv = data; /* 如果设备已经有driver了,跳过 */ if (dev->driver) return 0; /* 调用platform_match,判断驱动与设备是否匹配 */ if (!driver_match_device(drv, dev)) return 0; /* 匹配成功,建立设备与驱动的绑定关系 */ device_driver_attach(drv, dev); return 0; }可以这样理解:driver_attach像是拿着新来的简历,逐个问已经坐满的招聘摊位上的应聘者(设备)“你愿意跟这个HR走吗”。
4.3 really_probe:probe真正被调用的地方
匹配成功后,流程来到driver_probe_device,它内部会调用really_probe,这是整个设备驱动模型中最核心的“临门一脚”。
really_probe执行的关键步骤包括:
- 设置
dev->driver = drv,标记设备与驱动的绑定关系。 - 调用
dev_pm_domain_attach和pinctrl_bind_pins,处理电源管理域和引脚控制(pinctrl)。 - 尝试调用
dev->bus->probe,对于platform总线来说,这个函数是platform_drv_probe。 platform_drv_probe内部最终调用驱动的pdrv->probe(pdev),也就是你在驱动里编写的probe函数本体。- 如果probe返回0,设备进入“已绑定”状态,同时会在sysfs中创建驱动与设备之间的符号链接。
整条链路用文字描述是:
platform_driver_register -> driver_register -> bus_add_driver -> driver_attach -> bus_for_each_dev -> __driver_attach -> driver_match_device -> platform_match -> driver_probe_device -> really_probe -> platform_drv_probe -> pdrv->probe(pdev)从头到尾,没有任何一步需要你手动调用probe。它之所以能被自动触发,是因为内核在总线层面实现了一个“驱动注册时扫描设备、设备注册时扫描驱动”的双向匹配机制。
4.4 设备后注册的情况:另一个方向的匹配
看到这里你可能会问:如果加载驱动时设备还没创建呢?在设备树环境下,platform_device通常在init_machine阶段就创建好了,驱动是后来insmod的,所以上面那条链路足够用。但也有反过来的情况,比如设备通过platform_device_register动态注册,而驱动早已存在。
这种情况走的是另一条路。platform_device_register内部会调用device_add,它里面有一个关键调用bus_probe_device,会去该总线上已注册的驱动链表中寻找匹配项。也就是说,设备注册时也会触发一次匹配扫描。
这正是Linux设备模型设计巧妙的地方:不管谁先谁后,只要设备和驱动都注册到同一条总线上,系统总会在“后到者”注册时主动跑一次匹配,确保不错过。实际效果就是:probe一定会在设备和驱动都就绪后的某个时间点被调用。
4.5 probe里的常见失败模式
很多新手以为匹配成功probe就必然成功,其实不是。really_probe在调用probe返回非0值时,会认为“匹配成功但绑定失败”,然后做一些清理,设备会被标记为探测失败。
我在i.MX6ULL上写驱动时,probe里最容易出的问题是:
platform_get_resource拿到错误的地址或拿不到中断号。ioremap失败(地址被占用或无效)。devm_gpiod_get或gpio_request失败,GPIO被pinmux占用了。- 请求中断时使用了错误的中断号。
这些错误不会导致匹配机制本身失败,但会导致probe返回错误码。排查时dmesg里能看到驱动自己的dev_err打印,以及内核的probe of LED_DEMO failed with error -22这类信息。
所以,当你说“我的probe不执行”时,先分清楚是“压根没匹配上”还是“匹配上了没绑定成功”。这两类问题的排查方向完全不同,下一章我会给一套标准排查手册。
5. i.MX6ULL实战:从零验证一次完整的设备与驱动匹配
理论讲再多,不如动手跑一遍。这一章,我以一个自定义LED设备为例,带你在i.MX6ULL开发板上完整验证Platform设备与驱动的匹配过程。环境假设你用NXP官方的Linux SDK或者正点原子/野火等常见板卡BSP,交叉编译工具链已经就绪。
5.1 第一步:写一个带of_match_table的platform驱动
先在开发目录里创建led_platform_demo.c,内容如下(精简了核心逻辑,重点看匹配相关的部分):
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/gpio/consumer.h> static int led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct gpio_desc *desc; dev_info(dev, "led_probe enter\n"); desc = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); if (IS_ERR(desc)) { dev_err(dev, "failed to get led gpio: %ld\n", PTR_ERR(desc)); return PTR_ERR(desc); } dev_info(dev, "led probe success, gpio assigned\n"); return 0; } static void led_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "led_remove\n"); } static const struct of_device_id led_of_match[] = { { .compatible = "myvendor,led-demo" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "led_demo", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("i.MX6ULL platform match demo");这段代码重点看两个地方:of_device_id数组里compatible是"myvendor,led-demo",它有.of_match_table = led_of_match。这两项缺一不可,否则就是回到名字匹配的老路,不推荐。
5.2 第二步:修改设备树并编译DTC
在设备树源文件里(i.MX6ULL通常在arch/arm/boot/dts/imx6ull-14x14-evk.dts或你的板级dts中追加,也可以放到根节点/下做测试),添加一个节点:
/ { led_demo { compatible = "myvendor,led-demo"; led-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>; status = "okay"; }; };编译设备树:
make ARCH=arm imx6ull-14x14-evk.dtb把生成的dtb文件和你编译好的内核zImage一起烧录到开发板。如果你使用NAND或SD卡启动,要把dtb放到对应的启动分区。
这里要特别强调:修改设备树后,必须确认新dtb确实被bootloader加载了。我见过太多人改了设备树没烧录,或者烧错了分区,导致“设备树改了跟没改一样”。排查这个问题最简单的方法,是在板子上执行:
ls /proc/device-tree/ | grep led_demo如果能看到led_demo目录,说明新的设备树已经生效。如果看不到,多半是dtb没烧对,后边所有probe排查都没意义。
5.3 第三步:编译模块并加载
编译驱动模块,需要先确保内核源码已配置,并启用了模块支持:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules单独编译这个模块可以这样:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=$(pwd) modules把生成的led_platform_demo.ko拷贝到开发板,然后在板子上:
insmod led_platform_demo.ko此时立刻观察dmesg输出,正常情况应该能看到:
led_demo led_demo: led_probe enter led_demo led_demo: led probe success, gpio assigned这说明驱动加载时,经过platform_match,通过设备树compatible匹配到了设备,并成功调用了probe。
5.4 第四步:从sysfs验证匹配结果
除了dmesg,sysfs是验证“设备与驱动是否绑定”的最好途径。执行:
ls -l /sys/bus/platform/devices/led_demo ls -l /sys/bus/platform/drivers/led_demo/如果绑定成功,你会在第一个命令看到多了一个指向driver的符号链接,内容类似:
/sys/bus/platform/devices/led_demo/driver -> ../../../../bus/platform/drivers/led_demo同时驱动目录下也会出现设备链接:
/sys/bus/platform/drivers/led_demo/led_demo -> ../../../../devices/platform/led_demo这两个符号链接就是设备模型“绑定成功”的最直观证据。看/sys/bus/platform/devices/led_demo/uevent还能看到MODALIAS=of:Nled_demoT...Cmyvendor,led-demo,证明设备树匹配生成的modalias字符串是符合预期格式的。
5.5 手动解绑与重新绑定
调试时经常需要让设备与驱动解绑,以便重新测试probe流程。Platform总线提供了运行时控制接口:
echo led_demo > /sys/bus/platform/drivers/led_demo/unbind echo led_demo > /sys/bus/platform/drivers/led_demo/bind执行unbind后,dmesg会输出led_remove;再执行bind,会再次看到led_probe enter。这组命令在验证remove逻辑、或者测试驱动热插拔逻辑时极其好用,不用反复rmmod/insmod。
5.6 匹配不上的常见根因排查手册
最后,我把自己在i.MX6ULL上排查“probe不执行”的完整套路整理成了一张表,直接对照操作就行:
| 症状 | 直接原因 | 排查方法 | 修复思路 |
|---|---|---|---|
| 设备树里找不到节点 | dtb没烧对 | ls /proc/device-tree/ | 重新编译并烧录dtb |
| 节点存在但没有platform_device | status为disabled | cat /proc/device-tree/led_demo/status | 改为okay并重新编译dtb |
| 有设备但驱动不匹配 | compatible不一致 | 比对节点compatible和of_match_table | 统一字符串,注意厂商前缀 |
| 有设备有驱动但probe没跑 | 驱动没加载 | lsmod、`dmesg | grep myvendor` |
| probe报了error | 绑定失败而非匹配失败 | `dmesg | tail`看具体错误 |
在最终排查时,我个人的习惯是先用dmesg搜platform和led_demo两个关键字,基本能覆盖80%的线索。然后再去看sysfs的设备目录是否存在、driver链接是否建立。这个顺序比一上来就打开驱动源码逐行读要高效得多,因为大多数问题其实发生在设备树和dtb这一层,而不是驱动代码本身。
6. probe不是终点:匹配成功后的那些“隐形机制”
匹配机制把设备和驱动拉到一起,probe执行也不代表万事大吉。在probe真正跑起来之前和之后,内核还做了很多你可能察觉不到的工作。这些机制在i.MX6ULL上尤其重要,因为片上外设依赖的pinctrl、时钟、中断资源,都跟它们相关。
6.1 pinctrl在probe之前的自动绑定
先看一个在i.MX6ULL上绕不开的东西:引脚控制(pinctrl)。设备树节点里经常有类似下面的属性:
pinctrl-names = "default"; pinctrl-0 = <&pinctrl_led>;pinctrl核心在really_probe阶段、probe函数运行之前,会根据pinctrl-names里的名字去设备树里找对应的pinctrl-0、pinctrl-1等属性,然后调用pinctrl_select_state,把引脚切换到指定状态。
这意味着,你在probe里用devm_gpiod_get获取GPIO时,引脚其实已经被设置好了。这也就是为什么很多i.MX6ULL的驱动probe里看不到任何pinmux配置代码,它已经被框架自动处理了。
如果设备树的pinctrl配置有误,probe之前就可能失败。dmesg里会出现类似failed to get default pinctrl state的警告,但很多时候它不会阻断probe,只是引脚复用不对,硬件功能异常。排查外设不工作时,一定要记得检查pinctrl配置。
6.2 设备树资源与platform_get_resource
匹配成功并进入probe后,驱动第一件事通常是获取硬件资源。这些资源就是设备树节点里的reg、interrupts属性,内核在创建platform_device时已经把它们转换成了struct resource数组。
在probe里这样获取:
struct resource *res; void __iomem *base; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base = devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base);这里有一个常见误区:很多新手直接ioremap(0x02020000, 0x4000)硬编码地址。这在裸机开发没问题,但在Linux驱动里是反模式,因为设备树已经描述了资源,硬编码会破坏可移植性。用platform_get_resource才能确保你的驱动在不同设备树配置下都能正确工作。
顺便补充一个i.MX6ULL的细节:它的外设地址空间都在0x02000000到0x02100000附近,但ioremap后的虚拟地址是不固定的,不要试图打印虚拟地址去和芯片手册里的物理地址对应,它们是两码事。
6.3 probe之后的生命周期:remove与 shutdown
匹配成功后,设备与驱动建立了绑定关系,但这个关系不是永久的。系统关机、设备卸载、驱动rmmod时,内核会依次调用驱动的shutdown、remove等回调。
在really_probe里,remove对应关系会被记录。driver_unregister时,所有绑定在该驱动上的设备都会触发解绑流程,逐个调用remove。这也是为什么驱动框架要求在remove里释放所有资源、注销设备节点。
我在i.MX6ULL上调试LED驱动时,就遇到过一个典型问题:remove里没有释放request_irq申请的中断,导致rmmod后再次insmod时中断号被占用,probe直接失败。后来改用devm_系列接口(如devm_request_irq),问题彻底消失。
devm_系列API是设备管理资源机制,它会在probe失败或设备移除时自动释放资源,是现代Linux驱动的标配。写新驱动时,能用devm_就用devm_,能大大减少资源泄漏的坑。
6.4 同一外设多驱动叠加的情况
还有最后一个值得专门提的场景:i.MX6ULL上经常出现一个硬件设备被多个驱动“层层接管”的情况。最典型的是GPIO子系统和GPIO-LED子系统。
比如设备树里一个compatible = "gpio-leds"的节点,它会被LED子系统注册的platform驱动匹配。但这个驱动操作GPIO时,真正干活的其实是底层GPIO控制器驱动(compatible = "fsl,imx6ul-gpio")。也就是说,一个物理LED背后可能涉及两层platform驱动:LED层和GPIO层。
这就是设备模型中“设备叠加”的核心思想:一个platform_device的probe里,可能去请求另一个platform_device提供的能力,比如通过gpiod_get请求GPIO,而这个GPIO正是另一个platform驱动在管理。理解这套嵌套关系,你在排查“为什么GPIO点不亮”时,就不会只盯着应用层驱动看了,还会去检查底层GPIO控制器驱动是否正常工作。
在i.MX6ULL开发过程中,我最后悔没早点做的事,就是认真阅读/sys/bus/platform/devices下的完整设备列表。那里不是一堆乱码路径,它真实反映了内核里所有platform设备的注册情况、资源信息、绑定状态。配合设备树源码,基本能把整个SoC的外设框架摸透。
Platform设备与驱动匹配机制,其实并不神秘。它无非就是一条虚拟总线、四步匹配逻辑、两个方向触发的扫描机制。但正是这套机制,把硬件描述(设备树)、软件逻辑(驱动)、内核对象(platform设备)三者彻底解耦,让嵌入式Linux驱动开发变成了一件可拼接、可复用的工程活。你在i.MX6ULL上学到的这套Platform匹配流程,换到任何一款现代SoC上,思路依然成立,这才是这篇文章最想让你带走的。