网上聊i.MX6ULL Linux驱动开发的帖子不少,但大多一上来就甩代码,很少有人把Platform总线这个地基讲透。我自己带过几批做嵌入式的新人,发现一个特别典型的现象:驱动insmod成功,dmesg也没有任何报错,但probe函数就是不执行,设备节点明明也能在/sys下看到,就是死活绑定不上。折腾半天,最后发现是对Platform设备与驱动匹配机制的理解出了问题。这篇文章就围绕i.MX6ULL上的Platform机制,把设备树、platform_driver、platform_device三者到底怎么对上号这件事讲明白,适合刚接触设备树驱动开发的初学者,也适合那些调试时经常卡在probe不触发的人。
1. 为什么嵌入式驱动离不开Platform总线
1.1 从“驱动裸写硬件”到“设备与驱动分家”
早期Linux驱动开发有一种很原始的做法:在驱动的init函数里,直接把寄存器物理地址、中断号、GPIO编号全部硬编码进去。比如我要操作某个外设,就在代码里写:
#define MY_DEV_BASE 0x020C4000 #define MY_IRQ_NUM 32然后ioremap、request_irq一条龙。这种代码在特定板子上能跑,但一旦换了板子,或者同型号芯片在不同开发板上引脚变了,就得回头改驱动源码重新编译。更麻烦的是,如果系统里有两颗相同的外设IP,但硬件配置不同,你几乎没法用一份驱动代码同时管理。
Platform机制就是为解决这个问题诞生的。它的核心思想是“设备”和“驱动”分离:驱动只描述“我会操作这种硬件”,设备只描述“我这里有哪些硬件资源”。两者都注册到系统里,由内核去匹配,匹配成功就调用驱动的probe,把设备资源交给驱动使用。驱动不需要在代码里写死任何物理地址,它只需要在probe时从设备结构体里把资源取出来。
这个设计对你写驱动的直接影响是:你写的platform_driver代码,在i.MX6ULL上能用,换一块i.MX8MM的板子,只要设备树描述得对,驱动几乎不用改。这就是解耦的价值。
1.2 设备、驱动、总线三者的关系
Linux设备模型里有三个核心对象:device(设备)、device_driver(驱动)、bus(总线)。可以这么理解:设备是插座板上的插座孔,驱动是插头,总线是那块插座板。设备说“我有这些引脚和功能”,驱动说“我能驱动这类插座”,而总线的职责就是当任何一个插座孔或插头出现时,立刻检查能不能插上。
对应到Platform机制:platform_device是设备,platform_driver是驱动,platform_bus_type是它们共同挂载的总线。在i.MX6ULL上,系统启动过程中,设备树里描述的外设节点会被解析,转换成platform_device注册到platform总线;而我们的驱动通过platform_driver_register也注册到这条总线上。内核的匹配时机很聪明,设备先来也好,驱动先来也好,只要双方碰面,总线就会调用match函数判断是否匹配。
bus_type结构体里的match回调就是整个匹配机制的灵魂,它对platform总线来说就是platform_match函数。这个函数内部做了什么,决定了你的probe能不能被调用。
1.3 i.MX6ULL上Platform机制的现实意义
i.MX6ULL这颗芯片是NXP的Cortex-A7单核处理器,在工业控制、物联网网关、入门级Linux开发板(正点原子、野火等)上用得非常多。它内部集成了大量外设控制器:GPIO、UART、I2C、SPI、ECSPI、LCD控制器、以太网MAC等等。这些外设控制器地址都是固定的内存映射地址,不依赖PCIe或USB这类可枚举热插拔总线,所以它们天然就是Platform设备。
在实际的i.MX6ULL开发中,基本流程是:先在设备树里描述硬件资源和引脚复用,然后写一个platform_driver,通过of_match_table里的compatible字符串与设备树节点匹配。匹配成功后,内核自动调用probe,你在probe里拿资源、注册字符设备、申请中断。如果这个流程没走通,后面的一切都无从谈起,这就凸显了理解匹配机制的重要性。
2. 一次完整的Platform驱动注册:以i.MX6ULL点灯为例
2.1 设备树侧:先把硬件描述清楚
以i.MX6ULL开发板上最常见的LED为例。假定LED接在GPIO1_IO03上,低电平点亮(LED阳极接3.3V,阴极经电阻接到GPIO引脚)。设备树里需要做两件事:配置引脚复用和添加LED设备节点。
引脚复用一般在iomuxc节点里添加pinctrl子节点:
&iomuxc { pinctrl_gpioled: gpioledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 >; }; };然后添加LED设备节点。在imx6ull-14x14-evk.dts这类板级dts的根节点下添加:
gpioled { compatible = "fsl,imx6ull-led"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_gpioled>; led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; };这里最关键的就是compatible属性。设备树节点的compatible相当于给内核看的一张“身份证”,驱动端of_match_table里的compatible必须和它完全一致,才能匹配上。后面我会专门讲这个字符串有多容易出错。
另一个值得注意的点是gpio1在哪里来的。i.MX6ULL的GPIO1控制器本身在imx6ull.dtsi里已经定义好了:
gpio1: gpio@0209c000 { compatible = "fsl,imx6ull-gpio", "fsl,imx6q-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>; };也就是说,我们自定义的LED节点引用了gpio1这个已存在的GPIO控制器节点,通过&gpio1让设备树知道GPIO控制器在哪里。这里体现的是设备树的分层描述能力,也解释了为什么我们自己的LED节点不需要写GPIO控制器的物理地址。
2.2 驱动侧:of_match_table与probe的准备
设备树描述完硬件,接下来写驱动。一个标准的最小platform驱动模板长这样:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/gpio/consumer.h> static const struct of_device_id led_of_match[] = { { .compatible = "fsl,imx6ull-led" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static int led_probe(struct platform_device *pdev) { struct gpio_desc *led_gpio; led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio); pr_info("led probe success\n"); return 0; } static int led_remove(struct platform_device *pdev) { pr_info("led remove\n"); return 0; } static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "gpioled", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name");两个地方要特别解释一下。第一个是of_match_table,它指向of_device_id数组,数组里每个元素定义一种兼容型号,最后必须以空结构体作为哨兵结束。内核遍历这个数组时,就是靠这个哨兵来判断边界的,少了它会出问题。第二个是MODULE_DEVICE_TABLE宏,它会把of_device_id数组导出到模块的别名信息里,当设备树中出现对应compatible的节点时,udev/mdev可以根据modalias自动加载驱动模块,省去手动insmod。
module_platform_driver宏本质上是:
module_init(xxx_init); module_exit(xxx_exit);其中xxx_init调用platform_driver_register,xxx_exit调用platform_driver_unregister。它帮我们省掉了一堆样板代码,这是内核推荐的标准写法。
2.3 加载测试:怎么确认匹配真的发生了
交叉编译后,把.ko拷贝到板子上,insmod加载:
# insmod gpioled.ko # dmesg | tail [ 1234.567890] led probe success看到这行打印,说明匹配和probe都成功了。如果没看到,先在/sys/bus/platform/下检查设备与驱动各自的注册情况:
# ls /sys/bus/platform/devices/ gpioled ... # ls /sys/bus/platform/drivers/gpioled/ bind gpioled module uevent unbind注意drivers/gpioled/目录下有个名字叫gpioled的子目录或链接,说明设备已经绑定到了驱动。如果驱动注册了但没绑定设备,这里就看不到那个设备条目。这个现象在排查问题时非常关键,下面第四章会详细展开。
3. 匹配链路的源码级拆解:从platform_match谈起
3.1 platform_match的五种匹配途径
理解匹配机制,必须直接看内核源码。在Linux 4.1.15(NXP官方BSP和大部分i.MX6ULL开发板内核版本)里,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); /* When driver_override is set, only bind to the matching driver */ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* 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(pdrv->id_table, pdev) != NULL; /* fall-back to driver name match */ return (strcmp(pdev->name, drv->name) == 0); }匹配是有严格优先级顺序的,按下面的顺序依次尝试,命中即返回成功:
| 匹配方式 | 触发条件 | 适用场景 |
|---|---|---|
| driver_override | 设备节点设置了driver_override属性 | 用户态强制指定驱动绑定 |
| OF匹配 | 设备树节点存在,且compatible/type/name命中 | 设备树环境,嵌入式主流 |
| ACPI匹配 | ACPI表存在 | x86/ARM服务器场景,ARM板级设备树基本走不到 |
| id_table匹配 | platform_driver设置了id_table | 传统板级文件/多设备名适配 |
| name字符串比较 | 以上均失败时的兜底 | 老代码、简单设备 |
有个细节值得注意:id_table匹配和name兜底的顺序。只有在驱动没有设置id_table时,才会走到name字符串比较。也就是说,如果你的driver.name和设备的pdev->name完全一致,但你在driver里设置了id_table且里面没有匹配项,那么probe照样不会被调用。这个坑比较隐蔽。
3.2 设备树compatible匹配的具体流程
OF匹配最终调用的是of_match_device,它会遍历设备节点的compatible属性列表,对每一个compatible字符串,再遍历驱动的of_device_id数组,逐个比较。比较的内容包括三个维度,of_device_id结构体如下:
struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };- compatible匹配:dts节点compatible属性里任意一个字符串等于of_device_id里任意一个compatible,即命中。
- name匹配:dts节点的name字段等于of_device_id.name。老式设备树用得多,现代dts基本上不靠这个。
- type匹配:dts节点的device_type属性等于of_device_id.type。这个更古老,现在几乎不关注。
当前i.MX6ULL的设备树开发中,基本只靠compatible一条路径。设备树里可以写多个compatible值,比如:
compatible = "fsl,imx6ull-gpio", "fsl,imx6q-gpio";内核依次尝试,只要of_match_table里命中任意一个就匹配成功。所以驱动端可以只写一个兼容值,设备树端可以列一串,这设计的好处是:同一驱动可以向后兼容多个芯片型号。
还有一个经验:compatible字符串里的厂商前缀不要乱省略,比如“fsl,imx6ull-led”中的fsl代表NXP(飞思卡尔时期遗留的前缀)。虽然内核匹配时只是纯字符串比较,并没有强制校验前缀,但遵守这个命名规范能避免不同厂商的兼容值冲突,也方便阅读者一眼看出设备来源。
3.3 设备树之外的id_table和name匹配
老式非设备树环境下,platform_device通常在内核启动时通过板级文件静态注册,例如:
static struct platform_device gpio_led_device = { .name = "gpioled", .id = -1, .dev = { .platform_data = &led_pdata, }, };此时没有of_node,匹配只能走id_table或name兜底。platform_device_id结构体定义是这样的:
struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };多个设备名共用一个驱动时,用id_table比较方便:
static const struct platform_device_id led_id_table[] = { { "gpioled", 0 }, { "panel-led", 0 }, { } }; MODULE_DEVICE_TABLE(platform, led_id_table);name兜底则是直接比较pdev->name和drv->name。这里有个和“设备树场景”强相关的坑:设备树下platform_device的name字段取自设备树节点的name,也就是去掉@地址后缀的节点名。比如设备树节点叫gpioled,那么pdev->name就是“gpioled”;如果节点叫gpioled@20a0000,那么pdev->name仍然是“gpioled”。所以如果你打算用name兜底来匹配,.driver.name要填节点名,而不是compatible字符串。曾经见过有人把driver.name写成“fsl,imx6ull-led”,然后把of_match_table删掉,怎么都匹配不上,折腾半天发现pdev->name其实是“gpioled”,确实容易懵。
4. 实际开发中匹配失败的排查思路
4.1 先确认设备与驱动是否“同在总线上”
平台驱动不工作,第一步不是改代码,而是确认设备和驱动的“在场状态”。我自己的调试习惯是依次做这几件事:
第一,确认设备树节点是否被解析成了platform_device:
# ls /sys/bus/platform/devices/ | grep gpioled gpioled如果这一步就找不到设备,说明设备树节点根本没有被实例化,常见原因是节点status为disabled,或者dts没编译/没生效。
第二,确认驱动是否注册:
# ls /sys/bus/platform/drivers/gpioled/第三,确认是否绑定。在上面的drivers/gpioled目录下,如果出现了设备的软链接(比如gpioled -> ../../devices/platform/gpioled),说明匹配成功且probe已走通。如果没有这个链接,说明设备在总线上、驱动也在总线上,但匹配环节出了问题。
第四,直接看设备树解析出来的实际值:
# cat /sys/bus/platform/devices/gpioled/of_node/compatible fsl,imx6ull-led这个命令能看到内核实际解析出来的compatible列表,拿它跟驱动源码里的of_match_table逐字符对比,往往一眼就能发现问题。
4.2 从设备树到匹配成功的常见坑
结合我见到过的案例,匹配失败的原因集中在以下几类:
compatible字符串不一致。这是最高频的问题。dts里写成“fsl,imx6ull-led”,驱动里写“fsl,imx6ul-led”,少一个x;或者dts里字符串末尾带了空格;或者编辑器自动把逗号改成了全角逗号。字符串比较是精确匹配,一个字符不对就是不行。
修改dts后没重新编译或没更新dtb。很多人改了dts,make完内核忘记单独编译dtb,或者编译了dtb但烧录时用的还是旧分区里的文件。可以通过cat /proc/device-tree/gpioled/compatible检查板子上实际生效的值。
节点status为disabled。设备树里如果写了
status = "disabled",这个节点默认不会被实例化成platform_device。有些SoC的dtsi里默认把外设节点设为disabled,板级dts里才改为okay,容易忽略。依赖的父设备没起来。如果在probe里获取GPIO、时钟等资源时失败,并且返回的不是-EPROBE_DEFER,那么这次probe就算失败了。资源依赖问题非常隐蔽,后面5.2节细讲。
模块没加载或加载失败。检查modprobe有没有因为依赖问题而静默失败,dmesg里有没有模块加载的报错。
4.3 一个典型的compatible踩坑复盘
讲一个我实际带人时遇到的案例。驱动是自己的LED模块,交叉编译、insmod都正常,dmesg没有任何输出,probe就是没跑。当时的排查过程是这样的:
先看设备:
# ls /sys/bus/platform/devices/ gpioled设备在。再看驱动:
# ls /sys/bus/platform/drivers/ gpioled驱动也在。但drivers/gpioled/目录下没有设备链接,说明没绑定。接着cat设备树解析出来的compatible:
# cat /sys/bus/platform/devices/gpioled/of_node/compatible fsl,imx6ull-led然后打开驱动源码,of_match_table里写的是:
static const struct of_device_id led_of_match[] = { { .compatible = "fsl,imx6ul-led" }, { } };发现问题了:“imx6ull”和“imx6ul”,少了一个字母。这种问题之所以难排查,是因为Linux在设备树匹配失败时不会打印任何错误信息,整个过程安安静静,看起来就像驱动代码本身有问题。修正compatible字符串后重新编译加载,dmesg立刻出现probe打印。
这个案例给的建议是:不要凭肉眼对比dmesg和代码,直接借助sysfs导出的设备树属性和调用of_match_table来对照,效率高得多。也可以在probe里临时加一行pr_info,确认它到底有没有被进入;再加一个module_init里的打印,确认驱动注册本身是成功的,这样能快速二分定位问题所在的环节。
5. probe之后的资源获取与生命周期
5.1 在probe里拿寄存器、中断、GPIO
匹配成功后probe被调用,这时才真正开始“取资源”。设备树里怎么描述,这里就怎么取。三种最常见的资源对应三种API:
内存映射寄存器,设备树里用reg描述,驱动里用platform_get_resource加devm_ioremap:
struct resource *res; void __iomem *base; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); if (IS_ERR(base)) return PTR_ERR(base);中断,设备树里用interrupts描述:
int irq; irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; devm_request_irq(&pdev->dev, irq, my_isr, 0, "my_device", dev);GPIO,设备树里用*-gpios属性描述,驱动里用devm_gpiod_get:
struct gpio_desc *led_gpio; led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);注意devm_gpiod_get的第二个参数是“led”,它会自动查找设备树里的led-gpios属性,非常方便。设备树里写的led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>,驱动里拿到的gpio_desc就已经包含了GPIO控制器、引脚号和有效电平信息。GPIO_ACTIVE_LOW这个标志的意义是:调用gpiod_set_value(desc, 1)时,内核会在底层自动翻转电平,输出低电平点亮LED,驱动逻辑不需要关心硬件电平极性,这是新式GPIO子系统的优点。
5.2 remove与错误处理的正确姿势
probe要负责申请资源,remove就要负责释放。但大量使用devm_*系列API之后,remove里几乎什么都不用做,因为devm(device managed)机制会在设备unbind时自动释放资源和打印对应信息。我的习惯是remove里只做用户态可见的清理,比如注销字符设备、删除设备节点、关闭电源等。
还有一个必须掌握的返回值:-EPROBE_DEFER。当probe因为依赖的设备(比如GPIO控制器、时钟控制器、复位控制器)还没注册而失败时,正确做法不是直接返回错误码,而是返回-EPROBE_DEFER。内核拿到这个特定返回值,会把这个设备放到等待队列,等后续有驱动注册时再次尝试probe。比如在i.MX6ULL上,如果GPIO控制器驱动没加载完成,devm_gpiod_get会返回-EPROBE_DEFER,你直接用return PTR_ERR(led_gpio)就会导致设备probe失败且不会重试,最后表现为设备一直没法使用。正确的写法是:
led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW); if (PTR_ERR(led_gpio) == -EPROBE_DEFER) return -EPROBE_DEFER; if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);这就是“设备有依赖时,告诉内核晚点再试我”的沟通方式。
5.3 关于“到底要不要自己写platform驱动”的建议
最后说点个人经验。很多初学者会问:我写一个纯字符设备驱动,没有硬件资源,是不是也要套platform?我的建议是:在i.MX6ULL这种设备树平台上,凡是跟实际硬件引脚相关的驱动,都按“设备树+platform_driver”的套路来;但纯软件逻辑模块,比如一个内核线程、一个sysfs接口控制某个算法开关,没有对应硬件外设,那直接用module_init注册就好,没必要硬套platform。
判断标准很简单:你的驱动有没有自己的设备树节点?有没有需要从设备树解析的资源?如果有,就选platform_driver;如果没有,常规模块即可。设备树里定义的每个外设子节点,在系统初始化时都会经过of_platform_populate流程被实例化,而根节点下那些compatible命名的独立节点,最终也会被注册为platform_device。所以大部分实际外设驱动在i.MX6ULL里天然就应该走platform。
还有个小提示:不要混淆platform总线和I2C、SPI总线。设备树里挂在i2c1节点下的子设备是i2c_client,挂在spi总线下的子设备是spi_device,它们不归platform总线管。只有那些直接挂在SoC内存映射空间上的外设控制器,或者根节点下独立描述的设备,才走platform机制。这一点在阅读dts时务必分清楚,否则你会用platform_driver去匹配一个i2c设备节点,必然失败。
回到匹配机制本身,我在实际开发中最大的体会是:Linux的驱动模型是一个“先连接、后通信”的框架。设备和驱动通过总线建立连接是第一步,probe是连接成功后的第一个回调。连接不成功,后面的一切都是空谈。调试platform驱动时,先确认sysfs下设备和驱动是否绑定,远比反复检查probe内部代码要快的多。这个习惯帮我节省了大量时间,也希望对你有所帮助。