开头(≥200字)
做嵌入式Linux驱动开发,绕不开一个词:platform。尤其当你拿到一块i.MX6ULL核心板,翻开内核源码,会发现大量驱动文件的probe函数里,第一行参数几乎都是struct platform_device *pdev。很多初学者在学到这里时都会感到困惑——我在裸机里操作寄存器,明明是直接写地址,怎么到了Linux下,一个LED驱动要拆成设备、驱动、总线三部分?设备树里明明写了compatible = "my-led",驱动里的of_match_table也写了同样的字符串,可probe就是不执行,到底为什么?这不是你一个人遇到的问题,而是几乎所有Linux驱动开发者都会经历的一道坎。
这篇文章我想以i.MX6ULL为底板,完整拆解Platform设备与驱动匹配机制。不只是告诉你“怎么配对”,更想讲清楚“为什么内核要设计这一套机制”“设备树节点怎么一步步变成platform_device”“匹配的四种方式分别解决什么问题”,以及我在实际调试中踩过的坑。如果你是刚入门嵌入式Linux驱动的开发者,或者已经写了几个字符设备驱动但始终没吃透设备模型,这篇文章应该能帮你把这块拼图补齐。
1. 内容整体设计与思路拆解
1.1 从裸机到Linux:驱动的思维转变
老实说,我最早学i.MX6ULL是用的正点原子那套裸机例程,直接操作寄存器地址,LED一秒闪烁,代码直来直去,感觉“驱动”也没多难。但是一进入Linux驱动,整个人就被绕晕了。我在初始化函数里明明调用了register_chrdev,设备节点也手动创建了,可一加载就报No such device。后来才明白,Linux驱动开发的核心思维不是“操作硬件”,而是“描述硬件、匹配硬件、管理硬件”。
这里我打一个生活化的比方:Linux内核就像一个大型公司的HR系统。裸机开发是“你直接去工位上干活,不需要打卡”;Linux驱动则要求你先“入职注册”——你的身份、部门、岗位职责都要录入系统。设备端提交简历(设备树节点或platform_device),驱动端发布岗位JD(platform_driver中的兼容表),总线就是HR系统里的匹配算法,只有简历和JD对得上,内核才会打电话通知你“入职”(调用probe)。
Platform机制解决的正是“外部设备如何挂接到内核统一管理框架”的问题。在Linux设备模型中,总线(bus)、设备(device)、驱动(driver)是三个核心对象。总线负责匹配设备和驱动,设备代表物理硬件或虚拟硬件,驱动则是对硬件的软件抽象。对于I2C设备有i2c总线,SPI设备有spi总线,USB设备有usb总线,那像LED、GPIO、串口这种直接挂在CPU内存总线上的设备,该归谁管?答案就是platform总线——它不属于任何真实的总线协议,而是内核虚构出来的一条“虚拟总线”,专门用于管理那些直接挂载在SoC内部总线上的设备。
1.2 为什么i.MX6ULL的资料这么多,但很多人还是学不会
i.MX6ULL是NXP推出的一款Cortex-A7内核、单核主频800MHz的处理器,因为价格低、功耗小、资料齐全,成为了嵌入式Linux学习者的“街机”。但我在各种技术群里观察到一个现象:很多人照着教程把驱动编译进内核,insmod也成功,但问他“设备树节点是怎么解析的”“compatible匹配的优先级是什么”“如果同时匹配上多个驱动会发生什么”,就答不上来了。
这个现象背后暴露出的问题,是大家把“驱动开发”等同于“写代码”,而忽略了“理解框架”。Platform匹配机制恰恰是整个Linux驱动框架的地基。如果你不理解设备树compatible属性如何与驱动of_match_table关联,不理解id_table和of_match_table的匹配顺序,不理解platform_device的注册时机,那你换一块板子、换一个外设,依然只会照葫芦画瓢。真正碰到“probe进不去”这种问题,就会无从下手。
所以我在这篇文章里的思路是:先讲清楚platform机制的总体设计——总线、设备、驱动三方如何协作;再分别从设备侧(设备树节点怎么变成platform_device)和驱动侧(platform_driver是怎么被注册的)两条线拆开看;然后重点分析匹配机制的几种路径;最后用一个完整的i.MX6ULL LED驱动例子串起整个流程,并附上我实际调试中的问题清单。如果你能把这一套逻辑吃透,后面学input子系统、RTC驱动、PWM驱动,会发现万变不离其宗。
2. Platform设备模型拆解:总线、设备、驱动三件套
2.1 platform_bus_type:那条看不见的“总线”
在Linux内核源码中,platform总线的定义在drivers/base/platform.c文件里,核心是一个platform_bus_type结构体。我刚开始看这个文件的时候很困惑:所谓“总线”不应该有物理传输信号的电路吗?为什么platform总线连个match函数都这么简单?
下面这个结构体就是platform_bus_type的核心定义(不同内核版本略有差异):
struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups, .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops, };注意看,它没有read、write、transfer这类方法。因为platform总线本身不负责数据传输,它唯一的“职责”就是把设备和驱动拉到一起开个会。每次有新的platform_device或platform_driver注册到内核时,总线都会触发一次match扫描,看有没有合适的对象可以配对。
match回调函数指向platform_match,这是整个匹配机制的核心,后面会专门展开。你只需要先记住这条逻辑:设备或驱动注册时,内核会调用bus_type的match函数,如果匹配成功,就调用驱动的probe函数完成初始化。这个“注册——扫描——匹配——probe”的过程,是贯穿整个Linux设备模型的主线。
2.2 platform_device:硬件资源的信息登记表
在Linux设备模型中,struct device是一个极其庞大的结构体,而platform_device是对它的一个扩展封装。看看include/linux/platform_device.h中的定义:
struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; };面对这个结构体,初学者最容易问的问题是:device和platform_device到底啥关系?其实很简单:platform_device是device的“子类”,它比device多出来的核心成员是resource(资源)和id_entry。“资源”指什么?就是设备使用的中断号、寄存器物理地址范围、DMA通道等硬件信息。
以i.MX6ULL的GPIO控制器为例,它的寄存器基地址、中断号等都是通过resource数组来描述。当设备树解析完成后,这些信息会被填入platform_device的resources字段,驱动在probe里用platform_get_resource或devm_platform_get_and_ioremap_resource来获取。
实际操作中,i.MX6ULL里很多外设驱动(比如I2C控制器、SPI控制器、SDIO控制器)本身就是platform_device。举个例子,在内核设备树arch/arm/boot/dts/imx6ull.dtsi中,usdhc1节点的定义里通常会包含reg = <0x02190000 0x4000>、interrupts = <GIC_SPI 22 IRQ_TYPE_LEVEL_HIGH>这样的属性,解析后就是两个resource条目,分别描述寄存器地址和中断号。
2.3 platform_driver:驱动侧的“岗位JD”
与platform_device对应,驱动侧的结构体是:
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; bool prevent_deferred_probe; };写驱动时,probe和remove是必填的,它们是驱动生命周期中最重要的两个函数:
- probe:匹配成功后,驱动开始“上岗”的入口。在这里完成资源获取、寄存器映射、初始化硬件、注册字符设备或杂项设备等一系列工作。
- remove:驱动卸载或者设备拔出时调用的“下岗”函数,负责释放资源、注销设备节点,确保不留垃圾。
driver这个内嵌结构体是设备模型的核心包装,里面记录驱动名、所属总线、of_match_table等信息。这里我强调一下:platform_driver.driver.name不是设备匹配的最终依据,device_driver里的of_match_table和id_table才是。很多人在加载模块后看到sys/bus/platform/drivers/xxx目录下没有绑定设备,就以为是name不匹配,这个锅name不背。
注册一个platform_driver使用platform_driver_register()函数,现在的推荐写法是:
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_platform_driver这个宏,它相当于在module_init中调用platform_driver_register,在module_exit中调用platform_driver_unregister,省得你手写两个函数。
2.4 Linux设备模型的整体联动
理解了上面三个对象后,整个联动关系就很清晰了。总线的match函数会被以下时机触发:
- 向总线注册新的设备时,遍历总线上所有驱动,找匹配项。
- 向总线注册新的驱动时,遍历总线上所有设备,找匹配项。
- 设备或驱动的属性发生变化,也会触发重新匹配。
这就解释了一个现象:为什么insmod一个驱动模块后,内核日志里几乎同时出现platform xxx.1: Driver my_led requests probe deferral这种提示?因为驱动注册时,内核立刻扫描了当前总线上所有已注册的设备。如果设备树节点已经解析并生成了platform_device,那么当场就能匹配上并调用probe;如果匹配还没准备好,驱动中返回-EPROBE_DEFER,就会等待后续时机重新探测。
我在i.MX6ULL上调一个I2C触摸屏驱动时就遇到过这种deferral情况:触摸屏的probe需要依赖I2C控制器驱动先准备好,但两者注册顺序不确定,此时触摸屏驱动返回-EPROBE_DEFER,内核把它放到延迟列表里,等I2C控制器就绪后再触发一次匹配。这个机制非常优雅,但也常常让新手看到probe deferral字样时一头雾水——其实这只是“请稍后再试”,不是错误。
3. 设备树如何变成Platform Device:设备侧深度解析
3.1 设备树节点的生命周期
在i.MX6ULL这种现代ARM Linux平台上,设备树的解析是platform_device生成的核心路径。很多教程直接告诉你“在设备树里加一个节点,驱动就能匹配”,但没人说清楚中间过程。我把流程拆解如下:
- 内核启动早期,在
setup_arch阶段会调用unflatten_device_tree(),将DTB(设备树二进制)解析成树状结构体device_node。 - 当内核初始化到一定阶段,会调用
of_platform_default_populate_init()(内核版本不同,名称有差异),遍历根节点下所有带compatible属性的节点。 - 对每个节点,内核会调用
of_platform_bus_create()递归创建platform_device。 - 每个platform_device通过
platform_device_add()注册到platform总线,触发总线匹配。
这里有个关键知识点:不是所有设备树节点都会变成platform_device。如果你为某个节点编写了驱动,并要求它绑定平台总线,必须先满足“节点位于platform总线的管辖范围”。一般来说,根节点下的子节点、或者simple-bus兼容节点下的子节点,会被递归展开为platform_device。
拿i.MX6ULL的imx6ull.dtsi来说,soc节点带有compatible = "simple-bus",所以它下面的ipu1、usdhc1、gpio等节点会依次被创建为platform_device。如果你自己往根节点下加了一个myled节点,只要根节点本身是platform总线扫描的起点(实际上根的compatible通常为空,但子节点会被扫描),那么你的节点同样会被转换成platform_device。
3.2 compatible属性:最关键的“简历关键词”
设备树节点的compatible属性是识别设备身份的关键。一个规范设备树节点的样子大概是:
led_test { compatible = "myvendor,myled"; reg = <0x020c406c 0x4>; status = "okay"; };在驱动侧,of_match_table的写法是:
static const struct of_device_id my_led_of_match[] = { { .compatible = "myvendor,myled", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match);匹配时,内核会比较设备树节点的compatible字符串和of_device_id中的compatible是否一致。如果你在设备树写myvendor,myled,在of_match_table里写"myled",那么永远匹配不上。这个坑,我自己踩了不止一次。
这里再延伸一个知识点:MODULE_DEVICE_TABLE(of, my_led_of_match)的作用。很多初学者以为它只是“为了生成模块别名”的技术细节,其实它有两层价值:
- 对内:让
modprobe能根据设备树节点的compatible自动加载对应驱动模块。内核会扫描设备树,发现不认识的设备时,通过MODULE_ALIAS生成“of:NxxxTxxx”这样的别名,然后通知用户空间modprobe加载具有相同别名的驱动。 - 对外:生成模块的符号信息,配合
modinfo命令可以看到alias: of:N*T*Cmyvendor,myled。
所以在编写设备树和驱动时,compatible字符串一定要保持完全一致,包括大小写和逗号。我通常的习惯是"厂商名,设备名",并且全部小写、不用下划线,这是设备树社区推荐的做法。
3.3 reg与interrupts:resource的“翻译官”
设备树节点描述了设备的硬件资源,驱动probe时需要把它们翻译成resource。拿GPIO控制器节点来说:
gpio1: gpio@0209c000 { compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; reg = <0x0209c000 0x4000>; interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; };当这个节点被解析成platform_device时:
reg属性会生成第一个resource,类型为IORESOURCE_MEM,起始地址0x0209c000,长度0x4000。interrupts属性会生成一个或多个资源类型为IORESOURCE_IRQ的struct resource。gpio-controller等自定义属性不会自动变成resource,它们保留在device_node的properties链表中,驱动需要通过of_property_read_u32等API来读取。
驱动侧获取资源也有两种风格。老式写法是:
struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO;现代推荐写法是:
void __iomem *base; base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); if (IS_ERR(base)) return PTR_ERR(base);devm_platform_get_and_ioremap_resource是内核提供的一个辅助函数,它把platform_get_resource和devm_ioremap_resource封装起来,一步到位返回映射后的虚拟地址。devm_前缀的意思是“设备管理资源”,在probe失败或remove时自动释放,不需要你手动调用iounmap。
3.4 不是所有设备树节点都能变成platform_device
最后重点提醒一个常见误区。在i.MX6ULL上,像I2C外设(例如触摸屏ft5426)虽然有compatible属性,但它在设备树中的位置是挂在i2c总线节点下面的:
&i2c1 { ft5426: touchscreen@38 { compatible = "edt,edt-ft5406"; reg = <0x38>; interrupt-parent = <&gpio1>; interrupts = <9 IRQ_TYPE_EDGE_FALLING>; }; };这种节点不会直接变成platform_device。它是由I2C控制器驱动在probe过程中,通过i2c_new_client_device或设备树解析接口创建为i2c_client,挂到i2c总线上。它的匹配走的是i2c_driver的id_table或of_match_table,跟platform机制无关。
很多教程在讲platform匹配时,拿I2C设备举例,容易让初学者张冠李戴。我建议你每次拿到一个外设时,先想清楚:它挂在什么总线上?如果是CPU内存总线,那大概率用platform;如果是I2C,那就是i2c_client;如果是SPI,那就是spi_device。总线不同,匹配机制有差异,虽然核心思想是相通的。
4. Platform驱动侧核心环节:从注册到probe的完整链路
4.1 驱动初始化:module_platform_driver背后发生了什么
上面我提到了module_platform_driver(my_led_driver)宏。为了吃透机制,有必要看一下它的展开逻辑(简化的内核源码逻辑):
#define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register, \ platform_driver_unregister) #define module_driver(__driver, __register, __unregister, ...) \ static int __init __driver##_init(void) \ { \ return __register(&(__driver)); \ } \ module_init(__driver##_init); \ static void __exit __driver##_exit(void) \ { \ __unregister(&(__driver)); \ } \ module_exit(__driver##_exit);所以当你insmod一个platform驱动模块时,实际调用的是platform_driver_register。这个函数内部会做几件事:
- 初始化
driver结构体,将它与platform_bus_type关联。 - 调用
driver_register(),把驱动加入bus的drivers链表。 - 触发bus的
match扫描:遍历总线上所有的设备,调用platform_match判断是否有匹配项。 - 对匹配成功的设备,调用
driver_bound(),最终执行probe。
这一系列动作,对用户来说只是insmod一条命令,内核却已经跑了不少代码。理解这个顺序,对排查“为什么probe没执行”至关重要——首先要看驱动有没有注册成功,其次看总线上有没有设备,最后看匹配条件是否满足。
4.2 probe函数的“上岗准备”:你都该做些什么
在i.MX6ULL上写一个简单的LED平台驱动,probe函数里通常要完成这些步骤:
static int my_led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct led_device *led; struct resource *res; int ret; led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led->reg_base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); if (IS_ERR(led->reg_base)) return PTR_ERR(led->reg_base); led->irq = platform_get_irq(pdev, 0); if (led->irq < 0) return led->irq; led->clk = devm_clk_get(dev, NULL); if (IS_ERR(led->clk)) return PTR_ERR(led->clk); platform_set_drvdata(pdev, led); ret = misc_register(&led->miscdev); if (ret) { dev_err(dev, "failed to register misc device\n"); return ret; } dev_info(dev, "my led probed successfully\n"); return 0; }这里面有几个细节值得注意:
第一,devm_kzalloc是设备管理内存分配,不用在remove里手动kfree,设备分离时自动释放。第二,platform_set_drvdata把驱动私有数据绑定到pdev上,这样在remove函数里可以通过platform_get_drvdata(pdev)拿回来。第三,我用了misc_register注册一个杂项设备,对于LED这种简单外设,比主设备号管理省事得多。
关于资源获取,I2C、SPI设备驱动的资源获取方式往往不是platform_get_resource,而是从各自的client结构体中取。但platform设备的资源获取路径,就是我上面写到的这两条:platform_get_resource和platform_get_irq。如果你设备树里定义了多个reg段,就可以用platform_get_resource(pdev, IORESOURCE_MEM, 1)来取第二段。
4.3 remove函数:留得干净,下次才能接着用
remove函数的职责是清理probe阶段申请的所有东西。用devm_*接口申请的资源可以不管,但有些东西还是需要手动释放:
static int my_led_remove(struct platform_device *pdev) { struct led_device *led = platform_get_drvdata(pdev); misc_deregister(&led->miscdev); // 如果用了gpio口,最好不要在这里释放gpio,而是交给devm机制 dev_info(&pdev->dev, "my led removed\n"); return 0; }一个大原则是:非devm申请的资源,必须手动释放;devm申请的资源,不用动。如果你混淆了,要么内存泄漏,要么use-after-free导致内核崩溃。调试时最典型的表现就是:反复insmod/rmmod几次后,/sys/bus/platform/devices/my_led节点下出现脏数据,或者内核报BUG: unable to handle kernel paging request。绝大多数情况下,问题出在remove函数里释放了不该释放的东西。
5. 匹配机制全解析:四种方式与优先级
5.1 platform_match:一切的入口
真正决定设备和驱动配对的,是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(设备树)匹配,最常用 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 尝试ACPI匹配(x86等平台) */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 尝试id_table匹配 */ if (pdrv->id_table) if (platform_match_id(pdrv->id_table, pdev) != NULL) return 1; /* 4. 最后尝试name匹配 */ if (strcmp(pdev->name, drv->name) == 0) return 1; return 0; }可以看到,匹配顺序是:OF匹配 → ACPI匹配 → id_table匹配 → name匹配。在i.MX6ULL这种ARM嵌入式平台上,ACPI基本用不到(那是x86/ARM64服务器场景),所以我们重点看OF匹配和id_table匹配。name匹配是老式机制,在纯设备树体系里很少作为首选,但在某些场景(比如用platform_device_register_simple静态创建的设备)依然有效。
5.2 of_match_table:设备树驱动的“一锤定音”
of_driver_match_device的实质是比较设备树节点的compatible字符串与of_device_id表中的compatible字符串。它内部实际调用of_match_device,取出设备的of_node(即设备树节点),再依次遍历驱动提供的of_device_id表。
在实际开发中,我最推荐的写法是:
static const struct of_device_id my_led_of_match[] = { { .compatible = "myvendor,myled" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match);需要特别强调,of_device_id数组的最后一个元素必须是空结构体(哨兵),否则内核遍历数组时会越界。这个问题有时候不会立即暴露,但在某些内核配置下会导致随机崩溃,很难排查。
在i.MX6ULL的imx6ull.dtsi中,你可以看到大量类似:
static const struct of_device_id imx6ull_uart_of_match[] = { { .compatible = "fsl,imx6ul-uart", }, { .compatible = "fsl,imx7d-uart", }, { /* sentinel */ } };厂商经常让一个驱动兼容多个SoC的同类设备,所以在of_match_table里写了多个compatible条目。匹配时,只要设备树节点的compatible与其中任意一个相同,就算匹配成功。
5.3 id_table:没有设备树时的Plan B
在设备树普及之前,平台设备是通过platform_device_register或platform_device_alloc在C代码中静态创建的。这时设备没有of_node,只能靠platform_device_id结构体来匹配:
static const struct platform_device_id my_led_id_table[] = { { "my_led", (kernel_ulong_t)&my_led_data }, { }, }; MODULE_DEVICE_TABLE(platform, my_led_id_table);匹配时,内核会比较pdev->name和id_table中的name条目。一旦匹配成功,pdev->id_entry会指向对应的id条目,驱动就可以用pdev->id_entry->driver_data来区分不同版本的硬件。
你可能会有疑问:既然有了of_match_table,为什么还需要id_table?我遇到过一种场景:一个驱动既支持设备树描述的硬件,又要兼容老的非设备树启动方式。这时驱动里两个表都放,probe里先判断dev->of_node是否存在,再决定走哪条数据路径。
还有一种常见场景:同一个驱动要支持多个设备型号,且各型号的行为差异较大。你可以通过id_table中的driver_data来传递每个型号的特定参数。在设备树下,这个值可以从of_device_id的data字段获取。
5.4 name匹配:最古老也最容易误解的机制
最后是简单的name匹配。它比较pdev->name和drv->driver.name。在设备树体系中,pdev->name通常由设备树节点名的<name>部分派生出来(例如节点myled@020c406c会派生出name为myled),而driver.name是你定义驱动时的字符串。
很多驱动不支持设备树匹配,或者忘记定义of_match_table,此时驱动在设备树环境下可能意外通过name匹配上某些设备。你可能会看到probe被调用,但platform_get_resource取不到任何资源,因为设备树解析的资源并没有正确关联。
我曾经在i.MX6ULL上调一个pwm背光驱动,现象是probe莫名其妙执行了两次。查了半天,发现驱动里同时定义了of_match_table和driver.name,而设备树node name恰好和driver.name相同,导致OF匹配和name匹配都成功了。内核虽然只会调用一次probe(同一设备同一驱动只会绑定一次),但调试日志里两种匹配路径都打出来了,很容易让人困惑。
所以我的建议是:在设备树时代,请专注of_match_table,driver.name只用来在sysfs中创建目录名,不要指望它做设备匹配。如果必须支持老式设备,再额外写id_table。
6. 实战:i.MX6ULL平台LED驱动完整流程
6.1 准备工作与设备树改造
这里我以i.MX6ULL的GPIO1_IO03控制一个LED为例,写一个完整的platform设备驱动。为什么用GPIO而不是直接操作寄存器?因为GPIO子系统的操作更贴近日常开发,而且更能体现platform资源获取的用法。
先在设备树中添加节点,通常放在根节点下:
/ { myled { compatible = "myvendor,myled"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_myled>; led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; }; };注意,这里我使用了led-gpio自定义属性,而不是reg。这是因为GPIO控制器的地址资源由pinctrl子系统管理,驱动只需要通过devm_gpiod_get来获取GPIO描述符即可。如果你想演示platform_get_resource,可以在节点里加reg = <0x020c406c 0x4>,然后通过devm_platform_get_and_ioremap_resource来映射GPIO方向寄存器和数据寄存器。
另外,为了pinctrl正常工作,还需要在iomuxc节点里定义引脚复用:
&iomuxc { pinctrl_myled: myledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 >; }; };6.2 驱动代码实现:一份可直接编译的模块
下面是简化但完整的LED平台驱动:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_gpio.h> #include <linux/gpio/consumer.h> #include <linux/miscdevice.h> #include <linux/fs.h> struct my_led_dev { struct gpio_desc *led_gpio; }; static int my_led_open(struct inode *inode, struct file *file) { return 0; } static ssize_t my_led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct my_led_dev *led = container_of(file->private_data, struct my_led_dev, miscdev); char kbuf[4] = {0}; if (count > 3) count = 3; if (copy_from_user(kbuf, buf, count)) return -EFAULT; if (kbuf[0] == '1') gpiod_set_value(led->led_gpio, 1); else if (kbuf[0] == '0') gpiod_set_value(led->led_gpio, 0); else return -EINVAL; return count; } static const struct file_operations my_led_fops = { .owner = THIS_MODULE, .open = my_led_open, .write = my_led_write, }; static int my_led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct my_led_dev *led; int ret; led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led->led_gpio)) return PTR_ERR(led->led_gpio); gpiod_set_consumer_name(led->led_gpio, "my led"); led->miscdev.minor = MISC_DYNAMIC_MINOR; led->miscdev.name = "mled"; led->miscdev.fops = &my_led_fops; led->miscdev.parent = dev; ret = misc_register(&led->miscdev); if (ret) { dev_err(dev, "failed to register misc device\n"); return ret; } platform_set_drvdata(pdev, led); dev_info(dev, "my led driver probed\n"); return 0; } static int my_led_remove(struct platform_device *pdev) { struct my_led_dev *led = platform_get_drvdata(pdev); misc_deregister(&led->miscdev); dev_info(&pdev->dev, "my led driver removed\n"); return 0; } static const struct of_device_id my_led_of_match[] = { { .compatible = "myvendor,myled", }, { /* 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_AUTHOR("Your Name"); MODULE_DESCRIPTION("i.MX6ULL platform led driver");这段代码中,发送echo 1 > /dev/mled亮灯,echo 0 > /dev/mled灭灯。
6.3 编译、加载与调试实录
在i.MX6ULL上,我一般用SDK里的交叉编译工具链编译模块。Makefile大致长这样:
obj-m := my_led.o KDIR := /path/to/your/kernel/source CROSS_COMPILE := arm-linux-gnueabihf- CC := $(CROSS_COMPILE)gcc all: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm clean编译好之后,拷贝到开发板,执行:
insmod my_led.ko dmesg | tail正常情况下应该能看到:
platform myled: my led driver probed同时,/sys/bus/platform/devices/目录下会出现myled节点,/sys/bus/platform/drivers/my_led/目录下会创建myled的符号链接:
ls -l /sys/bus/platform/devices/myled ls -l /sys/bus/platform/drivers/my_led/看到myled -> ../../../devices/platform/myled这样的符号链接,说明设备与驱动绑定成功。如果没有这个链接,说明匹配失败。
6.4 从sysfs验证总线匹配关系
我强烈建议你养成一个习惯:每次加载或卸载驱动后,先去看sysfs里的总线关系,而不是急着看dmesg。sysfs是内核设备模型的用户态投影,它直接反映了设备、驱动、总线的绑定状态。
以下几个检查点在调试时特别有用:
# 查看设备属性 cat /sys/bus/platform/devices/myled/modalias # 查看驱动是否绑定成功(有链接说明成功) ls -l /sys/bus/platform/drivers/my_led/ # 查看设备树节点信息是否正常 ls /sys/firmware/devicetree/base/myled/ cat /sys/firmware/devicetree/base/myled/compatible其中modalias文件在匹配驱动时至关重要。内核通过它生成设备别名,用户空间的udev/mdev会根据它自动加载驱动。如果modalias文件不存在,或者内容与你驱动的MODULE_ALIAS不一致,即使手动insmod成功,系统也无法实现自动加载。
7. 常见问题与排查技巧实录
7.1 probe不进:最经典的匹配失败现场
这是我在各种技术群被问到最多的问题。驱动insmod成功,但probe没执行。排查步骤我总结为一条链,按顺序检查:
- 驱动是否注册成功?执行
ls /sys/bus/platform/drivers/my_led/,如果目录不存在,说明platform_driver_register没成功。 - 设备是否存在?执行
ls /sys/bus/platform/devices/ | grep myled,如果看不到设备节点,说明设备树节点解析没生成platform_device。 - compatible是否一致?检查设备树节点的compatible和of_match_table里的字符串,注意大小写、逗号、下划线。这一步最容易被忽略。
- 是否被其他驱动抢占了?同一设备只能绑定一个驱动。如果你系统中另一个驱动的of_match_table也包含相同的compatible,而且先注册成功,你的驱动就匹配不上了。
我在一次调试中遇到一个特别隐蔽的情况:设备树节点的compatible是myvendor,myled,of_match_table里也是{ .compatible = "myvendor,myled" },但我在驱动里多写了一个空格,"myvendor, myled"。编译不报错,字符串比较却永远不相等。这类问题靠肉眼很难看出来,所以我在代码里写测试用例时,经常用of_device_is_compatible手动验证。
7.2 有多个设备节点时,probe会执行几次?
如果你的of_match_table包含多个compatible,而设备树里有多个不同compatible的节点都匹配上了,probe会执行多次,每次的pdev不同。同样,如果of_match_table里只有一个compatible,但设备树里有两个节点都使用这个compatible,probe也会执行两次。
这本身不是错误,但如果你在probe里用了全局变量保存设备状态,就要小心第二次probe覆盖第一次的状态。我建议把设备私有数据都放在struct my_led_dev里,用platform_set_drvdata绑定到对应pdev上,不要用全局变量。
7.3 设备树节点解析成platform_device失败怎么办?
如果你在设备树里加了节点,但/sys/bus/platform/devices/里找不到对应目录,先检查节点是否在platform总线的扫描范围内。根节点下的子节点通常没问题,但如果节点放在某个i2c节点内部,它就会变成i2c_client,不会出现在platform devices里。
还有一种情况是节点存在语法错误。比如忘了加status = "okay",或者reg属性格式不对。用以下命令验证设备树语法:
dtc -I dtb -O dts -o output.dts imx6ull.dtb或者在内核启动日志中搜索相关错误信息。i.MX6ULL的u-boot如果设备树加载失败,通常会打印Failed to pass device tree to kernel之类的提示。
7.4 OF匹配和id_table同时存在时,驱动数据怎么选
如果一个驱动同时定义了of_match_table和id_table,probe函数里可以通过以下方式获取匹配数据:
const struct of_device_id *of_id = of_match_device(my_led_of_match, dev); if (of_id) { /* 走设备树路径 */ my_data = of_id->data; } else if (pdev->id_entry) { /* 走id_table路径 */ my_data = (void *)pdev->id_entry->driver_data; }这个判断逻辑在“一个驱动支持多款硬件”的场合非常常见。比如I2C控制器驱动同时支持imx6ul和imx7d,两者的寄存器布局有差异,驱动就要根据匹配到的具体型号来选择不同的操作函数集。
7.5 不要忽略EPROBE_DEFER的等待机制
在复杂系统中,驱动A需要依赖驱动B提供的资源(如时钟、电源域),而B的初始化可能晚于A。如果A的probe在获取资源时失败,不要急着返回负数错误码,而是返回-EPROBE_DEFER。内核会把A的probe记录在延迟探测列表里,等B注册完成后再次调用A的probe。
在i.MX6ULL上,我实际观察到的合理现象是:新的platform_driver注册时,很多设备的probe首先会返回-EPROBE_DEFER,随后在相关时钟或pinctrl驱动就绪后,probe被重新调用一次。如果你在dmesg里看到如下日志:
platform myled: Driver my_led requests probe deferral platform myled: Retrying from deferred list不要慌,这是正常机制。但如果反复出现多次后仍失败,就要仔细检查依赖资源的准备工作了。
7.6 个人调试经验总结
最后分享几个实际操作中的心得,希望你少走弯路:
心得一:每次改设备树后,不要只insmod模块,一定要先确认设备树真的更新了。很多开发板从u-boot加载dtb时,会用环境变量指定dtb文件路径,如果你改了源码但没重新编译dtb,或者编译了但u-boot没加载新的,那怎么调都没用。我习惯在启动后执行cat /sys/firmware/devicetree/base/model看MODEL字符串,确认设备树版本。
心得二:在probe函数里多打日志不丢人。我见过很多人从网上抄驱动,probe里只有最后一句return 0,出问题后完全不知道执行到哪一步。我自己的习惯是资源获取的关键步骤后都跟一句dev_dbg或dev_info,调试时开ignore_loglevel或dynamic_debug,一眼就能定位到哪一步失败。
心得三:利用/sys/kernel/debug/devices_deferred查看延迟探测的设备。如果系统启动后有些驱动一直匹配不上,这个文件会列出所有处于deferred状态的设备。这是排查依赖关系问题的利器。
心得四:当你在i.MX6ULL上换用不同内核版本时,平台驱动的API可能发生变化。比如老内核里的platform_get_resource(pdev, IORESOURCE_IRQ, 0),新内核推荐platform_get_irq(pdev, 0)。编译报错不要硬改,先查内核里对应API的定义。我在从4.1内核移植到5.4内核时,光of_get_named_gpio换成devm_gpiod_get就花了不少时间。
8. 从Platform机制延伸出去的下一步
把platform匹配机制吃透之后,你会发现它像一张“血管网”,连接着嵌入式Linux驱动的方方面面。任何platform_driver的probe里,都能看到这套机制留下的痕迹。再往外走,input子系统、RTC子系统、LED子系统、PWM子系统,本质上都是在platform_driver.probe的基础上,再向各自的子系统框架注册节点。
我个人在实际调试中最深的体会是:驱动开发中90%的时间不是花在写代码上,而是花在确认“内核为什么没有按我预想的方式工作”上。设备树节点、compatible匹配、资源获取、总线扫描,任何一个环节出错都会让probe静默失败。而只要你对platform机制有了完整的认识,这些问题就能按图索骥地排查,而不是靠瞎猜和反复尝试。
如果你在i.MX6ULL上照着这篇文章走了一遍,建议再做几个小实验加深理解:把设备树里的compatible改错,看内核会报什么信息;去掉MODULE_DEVICE_TABLE宏,用modinfo看alias对比差异;写第二个驱动,故意匹配同一个设备节点,看系统如何选择。这些“故意搞坏”的实验,往往能帮助你更好地理解系统真正的工作方式。