
i.MX6ULL 这块芯片在嵌入式圈子里几乎快成“Linux 驱动入门标配”了NXP 的文档和原厂 BSP 都相当全资料多到新手反而容易看花眼。不过很多初学者卡住的第一个坎往往不是 GPIO 怎么配、中断怎么写而是搞不清楚设备和驱动到底是怎么“走到一起”的。裸机开发里你想操作一个外设拿地址、配寄存器就完事了到了 Linux 下同样是一个 LED、一个按键背后却藏着一套完整的设备模型而 Platform 总线机制就是这套模型的骨架。这篇就以 i.MX6ULL 为例把 Platform 设备与驱动的匹配机制从原理到代码、再到底层源码一层层拆开讲清楚。1. 整体设计思路为什么 Linux 非要搞出个 Platform 总线很多人一开始不理解Linux 内核里明明已经有 PCI、USB、I2C 这些总线了为什么还要单独搞一个 Platform 总线这个问题想明白了后面的代码看起来就会顺很多。1.1 那些“没有总线”的设备怎么办I2C 设备挂在 I2C 控制器上USB 设备挂在 USB 控制器上它们都有实实在在的物理总线设备枚举、地址分配、热插拔这些事都由总线硬件和协议栈帮忙搞定了。但芯片内部大量的外设——比如 i.MX6ULL 上的 GPIO、UART、SPI 控制器、LCD 控制器、SDIO 控制器——它们直接挂在 SoC 内部总线上虽然地址是固定的但并没有一套像 USB 那样的“物理枚举”流程在启动时自动发现它们。内核总得有一套统一的方式来管理这些设备设备树里描述了一段寄存器地址说明这儿有个 UART3驱动代码里写了一个 compatible 字符串说“我能驱动这个 UART3”。两者怎么对应上Platform 总线就是干这个的。它不是物理存在的总线纯粹是内核软件层抽象出来的虚拟总线专门用来挂载那些不依附于传统物理总线的设备。1.2 Platform 总线在设备模型里的位置Linux 设备模型的核心是 kobject往上依次抽象出 device、driver、bus 三个概念。bus 是连接 device 和 driver 的纽带它负责维护两条链表一条挂设备一条挂驱动然后不停地进行匹配尝试。Platform 总线只是 bus 的一个具体实例类型是 platform_bus_type。i.MX6ULL 的板子启动后你可以通过 /sys/bus/platform/devices 看到一大堆设备/sys/bus/platform/drivers 下面则挂着一堆驱动这些就是设备模型在当前系统里的真实投影。搞懂了 Platform 总线的匹配机制你基本就理解了 Linux 设备模型的核心一半。1.3 为什么 i.MX6ULL 特别适合用 Platform 机制i.MX6ULL 是 Cortex-A7 内核跑 Linux 是它的家常便饭。相比 STM32 那种 MCUA 系列的芯片外设更丰富设备树的使用也更彻底。NXP 原厂 BSP 里几乎所有的外围驱动都是以 Platform 驱动的方式写的包括 gpio-leds、pinctrl、uart、i2c 控制器、spi 控制器等等。所以你在 i.MX6ULL 上做驱动开发绕不开 Platform。另一个原因是 i.MX6ULL 的寄存器映射和外设分布相对规整用它来讲 Platform 机制不会因为芯片特殊性产生太多干扰。你在它上面理解了这套机制之后换到 RK、全志或者树莓派的 BCM 芯片思路完全通用只是设备树 compatible 字符串和寄存器配置不同而已。2. 匹配机制核心原理从 device 和 driver 的“相亲”说起打个比方驱动和设备的关系就像招聘设备是岗位需求驱动是求职者。岗位说“我需要一个会 C 语言、懂 ARM 的”求职者简历写“精通 C、熟悉 Cortex-A 系列”HR 拿着两边信息一比对能对上就录用。Platform 总线的 match 函数就是这个 HR。2.1 谁来调用 match 函数在内核启动流程里总线注册时会挂上 platform_match 函数指针。后续每次注册一个 platform_driver 或者 platform_device总线核心都会触发一次匹配流程。匹配成功之后驱动里的 probe 函数就会被调用设备就“激活”了。这个流程由内核的 driver core 统一调度和具体的芯片平台无关。不管是 i.MX6ULL 还是其它 ARM 芯片匹配机制本身是一模一样的区别只在于你的设备树里写了什么样的 compatible、驱动里声明了什么样的 of_match_table。2.2 platform_match 的四种匹配方式打开内核源码找到 drivers/base/platform.c 里的 platform_match 函数它的匹配逻辑依次分四种情况第一种是 compatible 匹配这也是设备树时代最常用、优先级最高的一种。内核会比较 device 的 of_node 里的 compatible 属性值和 driver 的 of_match_table 里每一个 of_device_id 的 compatible 字符串只要有一个相等就算匹配成功。第二种是 ACPI 匹配但在 i.MX6ULL 这种嵌入式 ARM 平台上基本用不到x86 平台上才会走这条路径。第三种是 id_table 匹配比较 platform_device 的 name 和 platform_driver 的 id_table 中任何一个名字是否相同。这是老式、非设备树时代的匹配方式在没有设备树的平台或者某些 MFD 子设备里还在用。第四种是 platform_device.name 和 platform_driver.driver.name 直接比较作为最兜底的一种方式很多时候经常被忽略但实际工程里还时不时会遇到。2.3 匹配成功之后干什么一旦匹配成功总线核心就会调用 driver 的 probe 方法。probe 里你可以拿到设备树节点、读取寄存器地址、申请中断号、注册字符设备、初始化硬件等等。这就解释了为什么驱动开发里大家常说“真正的初始化在 probe 里做而不是在 module_init 里做”。module_init 里的 platform_driver_register 只是把驱动“登记”到总线上真正的硬件操作要等总线确认“有设备匹配上了”才执行。这里也顺便解释了另外一个经典问题为什么驱动先加载还是后加载都不影响最终结果。驱动比设备先注册没问题总线会记着设备后来了再触发一次匹配即可反过来设备先注册、驱动后加载同样也能匹配上。3. 从零手写一个 Platform 驱动基于 i.MX6ULL 的完整实现前面的原理听起来不复杂但写代码时还是有很多细节容易踩坑。这里用 i.MX6ULL 开发板上最常见的 LED 来控制举例手写一个完整的 Platform 驱动从设备树到驱动代码全部过一遍。3.1 设备树端的写法在 i.MX6ULL 的设备树里一般是在根节点下添加自己的自定义节点。以艾为、正点原子这类常见的开发板为例设备树文件通常是 imx6ull-14x14-evk.dts你在根节点范围里追加一个 led 节点/ { myled { compatible example,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };compatible 是设备树和驱动之间最重要的约定example,myled这种“厂商,型号”的格式是规范推荐写法一定要带上厂商前缀避免和其它设备冲突。pinctrl 用来配置引脚复用led-gpio 属性用来告诉驱动“我用的哪个 GPIO”这样驱动代码就不需要硬编码引脚了。对应的 pinctrl 节点定义一般放在 iomuxc 节点下面iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 是 NXP 提供的宏展开后是一串描述引脚 mux 模式的整数0x10b0 是引脚电气属性配置具体值参考芯片手册里的 IOMUX 章节不同引脚不同场景要适当调整。3.2 驱动代码的整体框架Platform 驱动的标准结构包括一个 platform_driver 结构体、一个 of_match_table、probe 和 remove 函数以及 module_init/module_exit 入口。下面是完整的驱动代码精简掉了一些错误处理保留主逻辑#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/of_device.h static struct gpio_desc *led_gpio; static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, failed to get led gpio\n); return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); dev_info(dev, led probe success\n); return 0; } static void my_led_remove(struct platform_device *pdev) { gpiod_set_value(led_gpio, 0); dev_info(pdev-dev, led remove\n); } static const struct of_device_id my_led_of_match[] { { .compatible example,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);这个代码里有几个细节值得注意。devm_gpiod_get用的是设备树里 led-gpio 属性属性名里的 “led-” 前缀会自动去掉所以你传参时写的是led。gpiod_get 系列接口是 GPIO descriptor 的推荐方式相比老的 of_get_named_gpio 和 gpio_request 组合devm 版本有很多优势自动资源管理不用手动释放probe 失败时内核会帮你清理接口更简洁不容易出错。3.3 module_platform_driver 展开后是什么module_platform_driver 是一个宏展开后就两件事定义 module_init 调用 platform_driver_register定义 module_exit 调用 platform_driver_unregister。你可以展开看一下代码它会同时处理内置编译和模块编译两种模式有些地方还会生成一个__platform_driver_register(pdrv, THIS_MODULE)的调用。注意如果驱动要支持内置编译编进 zImage就不能用 MODULE_LICENSE 之外的差异来区分但 module_platform_driver 宏已经考虑到了这一点所以平时无脑用这个宏简化代码就行。3.4 驱动的Makefile 与编译加载如果你把源码放在内核源码树外面的目录需要写一个独立 Makefileobj-m my_led.o KDIR : /path/to/your/kernel/source all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean编译完成后得到 my_led.ko在开发板上用 insmod 加载或者直接放进根文件系统开机自动加载。加载后去 /sys/bus/platform/drivers/my_led 目录看一眼如果能看到设备节点说明匹配成功dmesg里应该能看到 “led probe success” 的打印。4. 深入设备树匹配compatible、of_match_table 与节点属性的联动前面代码里已经出现了 of_match_table但很多人会问of_match_table 里的 compatible 和设备树节点里的 compatible 到底是怎么对应上的中间有没有什么坑这一节专门把这个联动关系讲透。4.1 compatible 匹配的完整路径当内核注册一个 platform_device 时如果这个设备来自设备树device 结构体里的 of_node 会指向对应的 device_node。在 platform_match 函数中首先会调 of_driver_match_device它遍历 driver 的 of_match_table 中的每一个 of_device_id和设备节点的 compatible 数组中的每一个字符串逐一比较。设备树节点 compatible 是一个字符串数组可以写多个值compatible example,myled, gpio-leds;这种情况下驱动 of_match_table 里只要有一个 compatible 能和其中任意一个匹配就算匹配成功。这个特性在实际项目中很有用——做兼容设计时新板子的设备树可以保留旧板的 compatible这样旧驱动不加改动就能继续用。匹配优先级上内核会比较 compatible 在数组里的顺序排在越前面的优先级越高。如果你希望新驱动优先于旧驱动匹配就把新的 compatible 写在最前面。4.2 data 字段的妙用of_device_id 结构体除了 compatible还有一个 data 字段。这个字段是 void 指针类型可以在匹配时给 probe 传递平台相关的私有数据。static const struct of_device_id my_led_of_match[] { { .compatible example,myled, .data myled_v1_data }, { .compatible example,myled-v2, .data myled_v2_data }, { /* sentinel */ } };probe 函数里可以通过of_device_get_match_data(dev)拿到这个指针然后根据不同的硬件版本做不同的初始化。这是一种非常优雅的扩展方式避免了在 probe 里用字符串比较来区分硬件版本。4.3 设备树里没有 compatible 怎么办并不是所有 platform_device 都来自设备树。比如某些 MFD 子设备、或者通过 platform_device_register 动态注册的设备它们没有 of_node也就没有 compatible 属性。这种情况下匹配会落到 id_table 或者 name 匹配。对应地platform_driver 里可以不提供 of_match_table而是提供 id_tablestatic const struct platform_device_id my_led_ids[] { { .name my_led, .driver_data 0 }, { } }; MODULE_DEVICE_TABLE(platform, my_led_ids);这种写法在多平台兼容的时候比较常用。i.MX6ULL 的设备树驱动里不太会用到但你要知道有这么个机制否则哪天拿到一个非设备树注册的设备可能还会傻眼。5. 实际调试记录匹配失败时的常见问题与排查方法写驱动最烦的事情是什么就是 insmod 之后一脸懵没有打印、没有报错、probe 根本没进来。这一节把我在 i.MX6ULL 上实际遇到过的匹配失败问题整理成速查表附带排查步骤。5.1 常见问题速查表现象可能原因排查方法probe 没有被调用compatible 字符串不一致检查设备树编译后的 dtb 中该节点的 compatible 实际值设备树节点没生成节点 status disabled 或父节点禁用确认设备树节点 status 为 okay驱动加载报 unknown symbol依赖的符号未导出或未编译检查内核配置和相关模块是否加载probe 里获取 GPIO 失败pinctrl 配置错误或 gpio 已被占用检查 pinctrl 节点、gpio 是否被其它驱动使用匹配的是奇怪驱动id_table 或 name 与别的驱动撞车用 /sys/bus/platform/drivers 下目录比对5.2 如何确认设备树编译后的实际 compatible设备树编译成 dtb 之后字符串是二进制存储的直接在源码里搜不一定能看出问题。推荐两种方式确认实际生效的值一种是在板子启动后到 /proc/device-tree 下找到对应节点cat compatible 文件输出的字符串就是实际生效的值。另一种是使用 dtc 工具反编译 dtbdtc -I dtb -O dts -o output.dts xxx.dtb然后搜你的节点名。常见的问题之一是设备树源码里 compatible 写成了example,myled驱动里 of_match_table 写成了example,myled 多了一个空格或者大小写不一致这种错误肉眼很难发现但字符串比较就是会失败。5.3 排查 probe 未执行的具体步骤第一步先确认驱动是否成功注册。insmod 之后看ls /sys/bus/platform/drivers/my_led是否存在。如果连这个目录都没有说明 platform_driver_register 本身没成功常见原因是对应模块没编进内核、或者 module_init 函数没跑。第二步确认设备是否成功注册。看ls /sys/bus/platform/devices/下有没有对应的设备目录名字一般是设备节点名加上基地址比如myled或者类似的形式。如果设备节点都不存在问题出在设备树那边先检查 dtb 是否更新到板子上。第三步如果驱动和设备都存在但 probe 没被调用手动触发一次匹配尝试。有时因为deferred probe 机制probe 会因为某个依赖资源没准备好而被延后。查看dmesg | grep -i probe如果看到deferred字样等一会儿再查一次或者检查依赖的外设驱动是否正常加载。5.4 deferred probe 这个隐藏坑i.MX6ULL 上跑设备树驱动deferred probe延期探测是个常见现象。比如你的驱动在 probe 里用了某个时钟或 GPIO而这个资源对应的驱动还没加载完成内核会把你的设备放回到待匹配队列末尾等依赖条件满足后再重新尝试 probe。这本身是内核的优雅处理机制但也容易让人抓狂看着设备节点和驱动都在probe 就是不进来。排查手段是查看/sys/kernel/debug/devices_deferred文件里面会列出所有处于 deferred 状态的设备以及原因。这个文件要挂载 debugfs 之后才能看到mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/devices_deferred6. 进阶扩展从 platform 到其他总线的迁移与复用熟悉了 Platform 总线匹配机制之后你会发现这套思维可以平移到其他总线。I2C 总线的 i2c_driver 有 id_tableSPI 总线的 spi_driver 也有 of_match_table匹配方式大同小异。所以搞懂 Platform本质上就是搞懂 Linux 设备模型的一套通用范式。6.1 I2C 驱动与 Platform 驱动的对比I2C 驱动的基本结构static const struct of_device_id my_i2c_of_match[] { { .compatible example,myi2c }, { } }; MODULE_DEVICE_TABLE(of, my_i2c_of_match); static struct i2c_driver my_i2c_driver { .probe my_i2c_probe, .remove my_i2c_remove, .id_table my_i2c_ids, .driver { .name my_i2c, .of_match_table my_i2c_of_match, }, }; module_i2c_driver(my_i2c_driver);除了总线类型和注册宏不同整体框架几乎一样。所以你在 i.MX6ULL 上把 Platform 机制学扎实后写触摸屏驱动I2C接口、写 SPI 显示屏驱动都能够快速上手学习成本比想象中低很多。6.2 多个设备共用一个驱动的场景处理实际项目中经常会遇到一个驱动要管理多个硬件实例的情况。比如板子上有 4 个 LED都通过 GPIO 控制你会希望只写一个驱动、设备树里挂 4 个节点。这种情况下probe 会被调用 4 次每次传入的 platform_device 结构体代表一个具体实例你可以通过 device 的 of_node 区分当前是哪一个。设备树写法/ { led1: led1 { compatible example,myled; label led1; led-gpio gpio1 3 GPIO_ACTIVE_LOW; }; led2: led2 { compatible example,myled; label led2; led-gpio gpio1 4 GPIO_ACTIVE_LOW; }; };probe 里每注册一次 gpiod_get就用 devm_ 接口管理内部会自动关联到对应 device。多个设备调用同一个驱动、各自维护独立状态这是 Linux 设备模型里非常常见的设计。你还可以把每个实例的 name 通过设备树属性传进来probe 里用of_property_read_string读取然后动态创建 sysfs 属性节点实现类似/sys/class/leds/led1/brightness这样的标准接口。6.3 从匹配机制看设备树的本质设备树的本质就是“硬件描述语言”它把过去硬编码在 C 代码里的寄存器地址、中断号、引脚配置全部搬到了 dts 文件里让同一份内核镜像可以适配不同硬件。Platform 总线机制则是让这种描述能够被内核动态解析的关键环节。你在写设备树时其实是在回答三个问题这个设备是什么compatible它在哪里reg / 引脚它怎么用属性。而驱动代码回答的是我能匹配什么of_match_table匹配上了怎么初始化probe卸载时怎么清理remove。两边各司其职界面清晰这就是 Linux 设备模型设计的精妙之处。7. 个人项目经验小结写 Platform 驱动的三点关键心得最后分享几条我在实际项目里攒下来的个人体会不算什么高深理论但确实能少走弯路。第一点设备树里 compatible 的命名为“厂商,型号”格式不是形式主义。曾经在项目里图省事全部自定义为 “led” 这种简单字符串结果别的驱动里也有一个 “led” 的 compatible两者碰撞导致两个设备互相同步匹配失败。正规命名从一开始就能避开这种隐藏冲突。第二点probe 里尽量全部用 devm_ 系列接口。很多驱动新手习惯在 probe 里手动 gpio_request、ioremap、request_irq然后写一堆 goto err 处理代码冗长且容易漏释放。devm 接口devm_gpiod_get、devm_ioremap_resource、devm_request_irq配合 device 生命周期管理probe 失败时资源自动释放remove 时不用手动清理代码量能减少三分之一而且不容易出内存泄漏。在 i.MX6ULL 这种资源不算富裕的平台上内核的自动资源管理机制值得充分信任。第三点多利用 sysfs 和 debugfs 观察设备模型。匹配不上、probe 没进来的时候与其瞎改代码反复 insmod不如先花两分钟看看 /sys/bus/platform/devices 和 /sys/bus/platform/drivers 下的目录情况再查一下 dmesg。设备模型的状态在 sysfs 里几乎是全透明的会用这些“望远镜”调试效率能翻倍。这也是我从最开始看字符设备那头雾水到后来能快速定位问题的一个重要转折点。