
开头说句实在话在i.MX6ULL上写Linux驱动最绕不开的就是Platform这套东西。我第一次接触它时一上来就去看probe函数怎么写、platform_driver_register怎么调代码是照着例程敲出来了可一加载就懵了——驱动根本不进probe设备也不知道去哪了。后来老老实实打开/sys/bus/platform/devices/目录一个个对把设备树、platform_device、platform_driver三者之间的关系捋清楚才总算开窍。这篇文章就把我对i.MX6ULL下Platform设备与驱动匹配机制的理解整理一遍重点讲清楚三件事为什么Linux内核需要platform这套虚拟总线设备树节点是怎么一步步变成platform_device的以及probe函数最终是如何被调用起来的。整个过程结合了我实际调试中用到的命令、代码和踩过的坑面向的是刚把字符设备驱动流程跑通、打算往设备模型方向深入的嵌入式Linux开发者。你要是已经在i.MX6ULL上编译过内核、会写简单的module读这篇文章不会觉得吃力如果你完全没接触过设备树那我建议先花半小时把dts的基本语法过一遍再回来看整体效果会好很多。1. 先理解Platform到底是什么为什么需要一条“虚拟总线”1.1 设备模型里的三类角色内核把驱动体系抽象成了几个非常朴素的对象设备device、驱动driver、总线bus。总线负责把设备和驱动拉到一起它维护两条链表一条挂设备一条挂驱动。每当有新的device注册进来总线就去驱动链表里找有没有匹配的driver反过来有新的driver注册进来总线就去设备链表里找有没有合适的device。找到之后总线调用驱动里的probe函数让驱动去初始化设备。这个机制在PCI、USB、I2C这些总线上表现得非常顺畅因为设备是“真”挂在硬件总线上的可以通过硬件枚举来发现。比如USB设备插上去主机控制器就能读到它的厂商ID和设备ID内核据此找到对应驱动完成匹配。整个过程是自发的、动态的。问题来了i.MX6ULL这类SoC内部集成的外设UART、GPIO控制器、以太网MAC、SDIO控制器、I2C控制器等等它们并不挂在PCI或者USB这类可枚举的硬件总线上而是芯片设计阶段就固定焊接在某个内存地址上寄存器地址、中断号都由芯片手册规定好了不会因为插拔而变化。这种设备在硬件上没有任何“总线”可以枚举Linux内核又不能对它们视而不见所以干脆虚拟出一条总线把这些“板级设备”都挂上去。这条虚拟总线就是platform_bus挂在上面的设备叫platform_device驱动程序则叫platform_driver。本质上它就是一个软件层面的归类容器让SoC内部外设也能享受到设备模型带来的“设备和驱动自动匹配、自动probe”的便利。很多初学者觉得platform总线神秘其实把它理解成内核给“焊接在板子上的设备”准备的一个临时停车场就行跟PCIe的枚举没有本质区别只是没有物理电气信号参与。1.2 传统字符驱动为什么撑不住你如果写过最原始的字符设备驱动一定熟悉这种模式module_init里直接register_chrdev然后立刻ioremap一片物理地址再request_irq申请中断。代码短平快跑起来也没问题一个led驱动大概几十行就能搞定。但一旦外设多起来这种写法的弊端就非常明显。假设板子上接了三个不同的外设每个驱动里都直接写死了物理地址和中断号换一块板子地址变了、中断变了就得改源码重新编译。驱动和设备的信息完全耦合在一起驱动根本无法复用。更麻烦的是如果两个驱动同时ioremap了同一块地址内核不会立刻报错只会在实际访问时出现各种诡异现象调试起来非常痛苦。Platform机制要解决的问题就是把“设备有什么资源、设备是什么型号”这些硬件信息从驱动代码中剥离出来。驱动只需要声明自己支持哪类设备、需要哪些资源至于具体地址是多少、中断号是几由内核在匹配成功后通过resource结构交给驱动。驱动拿到资源再ioremap、request_irq这样同样的驱动源码可以适配不同板卡只改设备树就行代码本身不用动。1.3 匹配机制的核心逻辑platform总线匹配的核心动作说白了就是“对暗号”。系统中有两个关键结构体platform_device包含name、id、num_resources、resource、dev内含of_node等platform_driver则是一个device_driver的封装里面包含probe、remove、shutdown、suspend、resume等回调函数还有id_table和driver.of_match_table。总线在匹配时优先级大概是这样先看驱动里的of_match_table与设备的设备树compatible属性是否匹配如果驱动没有of_match_table再看id_table里填的name是否跟设备name一致最后还会尝试直接拿platform_driver.name跟platform_device.name比较。三关只要过了一关总线就认为配对成功马上调用驱动的probe。在这个链条里probe被调用不等于万事大吉它只是代表驱动和设备“看对眼了”真正让设备工作起来的硬件初始化代码都写在probe里。理解probe为什么被调用、为什么不被调用是理解整个匹配机制的关键后面我会专门讲排查方法。2. 设备树节点如何变成platform_device2.1 从dts到device的完整链路在i.MX6ULL的内核里设备树文件.dts和.dtsi经过dtc编译后变成.dtb引导阶段由bootloader通常是U-Boot加载到内存并把地址传给内核。内核启动时会解析dtb把每个节点变成一个struct device_node挂在设备树全局链表里。然后在init阶段内核会遍历device_node根据节点的compatible属性和根节点的compatible判断该节点对应的设备属于哪种总线。判断规则并不复杂如果节点的compatible值与某个平台驱动声明的of_match_table匹配且这个设备不在I2C、SPI、PCI等具体总线的“管辖范围”内内核就会为这个节点创建一个platform_device。换句话说设备树里大部分节点除了挂载在具体控制器下的子节点比如i2c客户端、spi从设备最终都会变成platform_device。像i.MX6ULL上的GPIO控制器、UART、以太网控制器这些节点全部通过这个路径变成platform_device。这里有一个很重要的细节值得注意设备节点被创建成platform_device的过程并不会自动去匹配驱动。platform_device只是先挂到了platform总线上真正的“对暗号”要等platform_driver注册进来总线才会去scan所有挂着的设备。所以驱动加载顺序并不影响最终能否probe先加载设备还是先注册驱动结果是一样的这也是platform总线机制设计得比较优雅的地方之一。2.2 一个典型的设备树节点长什么样以i.MX6ULL常见的LED灯设备为例设备树节点大体是这样/ { myled: myled { compatible myvendor,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 4 GPIO_ACTIVE_LOW; status okay; }; };注意几个关键属性compatible是匹配驱动的主钥匙格式一般为“厂商,型号”后面必须跟驱动的of_match_table里的字符串严格一致大小写、逗号都不能马虎pinctrl-0指向外设引脚复用配置这是i.MX6ULL上非常容易出问题的点如果引脚复用没配置对GPIO就操作不了led-gpio是自定义属性用来指定GPIO编号具体值由驱动自行解析statusokay表示节点使能如果写成disabled内核就不会把它变成platform_device。设备树的编写看似简单实际上非常考验对芯片手册的理解。同一个物理引脚既可以复用为GPIO也可以复用为UART_TXD如果pinctrl配置和后端驱动期望的功能不一致硬件初始化会在很底层的地方卡住而且内核不一定报错排查起来很痛苦。在i.MX6ULL上我习惯先到芯片手册的MUX控制章节把所有引脚复用选项查一遍再写pinctrl节点避免反复改设备树重编。2.3 reg、interrupt等资源如何交给驱动设备树节点里用reg描述寄存器地址范围用interrupt描述中断号用interrupt-parent指定中断控制器。内核把节点变成platform_device时这些信息会被逐一转换成struct resource数组存放在platform_device的resource字段中。比如一个简单的片内外设节点myuart: myuart02020000 { compatible myvendor,myuart; reg 0x02020000 0x4000; interrupts GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH; interrupt-parent intc; clocks uart_clk; clock-names uart; };reg被解析成IORESOURCE_MEM类型的resource中断被解析成IORESOURCE_IRQ。驱动在probe里通过platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到寄存器物理地址再用devm_ioremap_resource做映射中断则通过platform_get_irq(pdev, 0)获取之后request_irq申请。整个过程中驱动源码与具体地址完全解耦换一块地址不同的板子只需改设备树。这里我特别想说一下devm_ioremap_resource和ioremap的区别。devm版本是“设备管理资源”机制的一部分probe成功申请的资源不需要在remove函数里手动释放设备注销时内核会自动清理。用devm系列API能少写很多错误处理路径配合devm_kzalloc、devm_request_irq等一起用代码会干净得多。我在i.MX6ULL上写驱动时几乎无脑devm除非有特殊需求才用裸版本。3. 驱动侧如何写好匹配逻辑3.1 of_match_table设备和驱动的第一道暗号在驱动代码里声明支持哪些设备主要通过of_device_id数组。来看一个实际例子static const struct of_device_id myled_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver);MODULE_DEVICE_TABLE宏的作用是让modpost工具把这些字符串提取出来写进模块文件的modinfo段。这一点在模块方式加载驱动时意义重大如果板子上某个设备没有驱动用户执行modprobe时内核可以根据设备的compatible属性自动找到对应的模块并加载实现“设备插上、驱动自动来”。of_match_table匹配时内核会比较设备树节点的compatible属性值和of_device_id数组里每个元素的compatible值。只要有一个相等就匹配成功。注意这里的匹配是字符串精确相等如果你在设备树里写的是“myvendor,myled”驱动里写的是“myvendor,myled1”哪怕只差一个字符也匹不上这一点我觉得怎么强调都不为过因为我自己就吃过这个亏在设备树里多打了一个空格排查了半天才看到dmesg报的Unknown device。3.2 id_table和name匹配的适用场景of_match_table是设备树时代的首选匹配方式但platform驱动还保留着另外两条匹配路径id_table和name匹配。id_table的结构是platform_device_id数组里面包含name字段和driver_data字段。匹配逻辑相对简单粗暴拿id_table[i].name与platform_device的name字段比较字符串相等就匹配成功。这个机制在没有设备树的时代很常见因为platform_device的name是驱动注册时或BSP代码里手动填写的。在有设备树的情况下如果一个驱动想同时支持多个型号的设备也可以通过id_table配合driver_data来区分不同型号probe时直接返回driver_data指出的型号ID避免逐个字符串比较。name匹配是最古老的策略直接拿platform_driver.driver.name与platform_device.name比较。这个方式非常不推荐在新代码里使用一来name冲突的可能性大二来不直观三来跟设备树生态不太兼容。而且这种匹配方式里platform_device.name通常要在代码中手动指定设备树根本无法控制写出来的代码基本不可移植。在i.MX6ULL的驱动开发中主线趋势就是of_match_table大量外部子系统驱动也都通过of_match_table匹配新人学的时候把这条路走通就足够应付80%的场景。3.3 probe函数的核心职责当匹配成功后platform总线调用驱动的probe并传入platform_device指针。probe函数的核心任务就是“接住”这个device的资源然后完成硬件初始化并注册字符设备、中断处理函数或内核子系统接口。常见代码骨架如下static int myled_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct resource *res; void __iomem *base; int irq, ret; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq platform_get_irq(pdev, 0); if (irq 0) return irq; ret devm_request_irq(dev, irq, myled_isr, 0, myled, base); if (ret) return ret; /* 读取自定义属性比如GPIO引脚号 */ ret of_property_read_u32(dev-of_node, led-gpio, gpio_num); if (ret) return ret; /* 注册字符设备 */ ret misc_register(myled_miscdev); return ret; }这段代码里有一个细节值得停下来想一想platform_get_resource返回的struct resource里面的start就是设备树reg里填的起始地址。devm_ioremap_resource完成两件事——先检查地址是否已被其他驱动占用然后建立内核虚拟地址到物理地址的映射。如果reg地址填错了或者和别的设备重叠devm_ioremap_resource会打印错误并返回ERR_PTR这个报错信息在dmesg里很显眼属于平台驱动里最友好的错误提示之一。probe函数的返回值也很关键。返回0表示初始化成功返回负数比如-ENOMEM、-EIO内核会认为驱动和设备匹配失败解除绑定关系后续remove不会执行。换句话讲probe失败并不会导致内核panic但外设等于不可用。调试时要多留意probe返回了什么错误码这是很多“驱动加载成功却无反应”问题的真正原因。4. 一次完整的Platform驱动开发实操4.1 实验环境准备我手头用的是一块基于i.MX6ULL的开发板主芯片为Cortex-A7单核运行Linux 4.19内核。开发主机是Ubuntu 18.04交叉编译工具链为arm-linux-gnueabihf-gcc。为了减少干扰我把驱动编译成模块设备树单独编译后打包进镜像通过U-Boot的tftp方式加载内核和dtbrootfs使用NFS挂载。如果你用的是其他开发板步骤完全一致只是工具链路径和设备树文件名不同。强烈建议先把内核编译环节跑通能正常启动到控制台再碰驱动。上来就写驱动是不可取的因为一旦编译报错你根本分不清是驱动代码的问题、内核配置的问题还是交叉编译环境的问题。4.2 新增设备树节点的完整步骤设备树源码一般位于arch/arm/boot/dts/imx6ull-xxx.dts。我这次拿一个“自定义LED”来做示例文件里新增/ { myled { compatible myvendor,myled; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 4 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x10b0 ; }; };这里pinctrl节点放置在iomuxc节点下通过fsl,pins指定引脚复用功能为GPIO1_IO04并配置电气属性。0x10b0是DSE、HYS等电气参数组合不同板子要求不同。这一步出错的话GPIO虽然能申请成功但引脚电平控制不了现象是“驱动正常加载灯就是不亮”所以遇到这种问题优先排查pinctrl。改完dts进入内核源码目录重新编译dtbmake imx6ull_xxx_defconfig make dtbs如果只需要单独编译dtb可以指定具体目标文件比如make arch/arm/boot/dts/imx6ull-xxx.dtb编译成功后把新的dtb替换掉SD卡或者tftp目录里的同名文件重启开发板用如下命令确认设备树节点已经生效# ls /proc/device-tree/myled/ compatible led-gpio name pinctrl-0 pinctrl-names status看到compatible和status文件就说明设备树解析成功并且节点被转换成了platform_device挂上了platform总线。4.3 驱动模块代码的骨架下面是一份完整可编译的platform驱动示例重点展示匹配和probe流程#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/miscdevice.h #include linux/gpio.h static int led_gpio; static int myled_open(struct inode *inode, struct file *filp) { return 0; } static long myled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case 0: gpio_set_value(led_gpio, 0); break; case 1: gpio_set_value(led_gpio, 1); break; default: return -EINVAL; } return 0; } static const struct file_operations myled_fops { .owner THIS_MODULE, .open myled_open, .unlocked_ioctl myled_ioctl, }; static struct miscdevice myled_miscdev { .minor MISC_DYNAMIC_MINOR, .name myled, .fops myled_fops, }; static int myled_probe(struct platform_device *pdev) { struct device *dev pdev-dev; enum of_gpio_flags flags; int ret; led_gpio of_get_named_gpio_flags(dev-of_node, led-gpio, 0, flags); if (!gpio_is_valid(led_gpio)) { dev_err(dev, invalid led gpio: %d\n, led_gpio); return -EINVAL; } ret devm_gpio_request_one(dev, led_gpio, GPIOF_OUT_INIT_LOW, myled); if (ret) return ret; ret misc_register(myled_miscdev); if (ret) return ret; dev_info(dev, myled probed, gpio%d\n, led_gpio); return 0; } static int myled_remove(struct platform_device *pdev) { misc_deregister(myled_miscdev); return 0; } static const struct of_device_id myled_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform driver demo);代码本身不复杂但其中有不少细节值得说明。第一module_platform_driver是一个宏展开后就是platform_driver_register和platform_driver_unregister的组合分别作为module_init和module_exit的回调。除非你需要在module_init里做别的初始化否则直接用它即可。第二devm_gpio_request_one比gpio_request更安全remove时无需手动释放GPIO。现在新内核里gpio描述符接口devm_gpiod_get等更受推荐但为了贴近很多旧教程和i.MX6ULL BSP环境这里保留gpio整数接口理解思路更直接。第三misc_register注册的是杂项字符设备主设备号固定为10次设备号动态分配。用misc设备的好处是省去手动分配主设备号的麻烦适合这种简单的平台驱动示例应用层通过/dev/myled访问。4.4 编译、加载、验证全流程驱动的Makefile写法如下obj-m myled.o KERN_DIR : /path/to/kernel/source all: make -C $(KERN_DIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: make -C $(KERN_DIR) M$(PWD) ARCHarm clean编译完成后会生成myled.ko。将模块拷贝到开发板执行# insmod myled.ko如果一切正常dmesg里应该出现这一行myled: myled probed, gpio4这行日志来自probe里的dev_info说明驱动和设备已在platform总线上完成匹配probe被调用成功。继续验证设备节点# ls -l /dev/myled # echo 1 /dev/myled # 如果设备支持write则需要自行实现 # ./test_myled 1通过ioctl控制LED亮灭的实验目录可以直观看到驱动确实把硬件操作跑通了。这里我建议你自己写一个十几行的测试程序调用open和ioctl把GPIO拉高拉低亲眼看灯的亮灭比干看日志有成就感也能增强对驱动链路整体性的把握。再看sysfs里的对应关系# ls /sys/bus/platform/devices/myled/ driver driver_override modalias of_node power subsystem uevent # ls /sys/bus/platform/drivers/myled/ bind myled uevent unbinddevices目录下出现myled说明platform_device存在drivers目录下出现myled说明platform_driver已经注册。两者之间通过driver目录可以互相链接。手动执行以下命令还可以强制解绑再绑定# echo myled /sys/bus/platform/drivers/myled/unbind # echo myled /sys/bus/platform/drivers/myled/bind这两个操作非常有助于调试probe执行流程我就经常在修改驱动代码后先卸载模块再重新加载模块观察dmesg里的新日志确认修改生效。5. 匹配失败与probe不调用的排查套路5.1 常见的匹配失败原因驱动模块加载了但probe没执行这是几乎所有platform驱动新手都经历过的阶段。我把这些年遇到的原因整理成一个表供对照排查现象可能原因排查方法modprobe/insmod无任何输出probe没日志compatible字符串不一致比较设备树compatible和of_match_table内容用strings命令查看ko里的modinfodmesg报“Unknown device”设备树节点statusdisabled检查dts节点status属性改为okay并重编dtbprobe执行了但LED不工作pinctrl配置错误或GPIO号错误查看dmesg有无pinctrl相关日志手动echo调试GPIO加载驱动报“Device or resource busy”ioremap区域被其他驱动占用检查reg地址是否与其他节点重叠确认IO资源占用情况设备树改了但内核还是旧行为dtb没编译或没替换成功确认启动日志里的dtb加载路径用/proc/device-tree核对内容compatible不一致是最常见的问题。调试时我建议先手动检查设备树解析结果。在开发板控制台执行# cat /proc/device-tree/myled/compatible myvendor,myled再用modinfo查看模块内置的匹配表# modinfo myled.komodinfo输出中会有alias字段包含of:NmyledT myvendor,myled之类的信息。设备树里的compatible必须跟modinfo里的字符串逐字节相等连个斜杠都不能差。我在现场排查过不少问题最后都发现是设备树文件里的tab键和空格键混用导致字符串被拆成了两半这基本只能靠十六进制dump才能看出来。5.2 用sysfs和debugfs定位问题sysfs在platform驱动调试中价值非常大。当设备节点正常存在而驱动没匹配时可以查看# ls /sys/bus/platform/devices/myled/这个目录始终存在只代表设备存在真正的关键在driver链接。如果设备目录下没有driver软链接说明驱动未匹配成功。进一步查看uevent文件# cat /sys/bus/platform/devices/myled/uevent OF_NAMEmyled OF_FULLNAME/myled OF_COMPATIBLE_0myvendor,myled MODALIASof:NmyledTCmyvendor,myledMODALIAS字段显示了内核为这个设备生成的module别名而驱动侧MODULE_DEVICE_TABLE生成的alias必须与之对应。两者对不上modprobe就自动拉不起驱动即使手动insmodprobe也不会被调用。内核如果开启了debugfs也可以查看platform总线下的驱动绑定情况# mount -t debugfs none /sys/kernel/debug # cat /sys/kernel/debug/driver_probe在driver_probe文件里能看到每个platform_driver的probe执行情况因为它记录了大量总线匹配细节输出信息非常详细比只看dmesg高效太多。还有一个小技巧/sys/bus/platform/drivers_autoprobe文件控制着新驱动注册时是否自动匹配设备。如果它是0驱动注册不会触发任何probe行为。极少数情况下某些发行版把这个值写成0就会导致“驱动明明加载了probe就是不动”的假象。检查方法很简单# cat /sys/bus/platform/drivers_autoprobe 1如果输出是0执行echo 1 /sys/bus/platform/drivers_autoprobe恢复即可。5.3 我给新人的一些避坑心得第一点modprobe和insmod有区别。modprobe要依赖模块依赖关系并加载相关alias匹配到的模块这对platform驱动尤其重要。如果你明确知道模块里没有依赖其他模块用insmod更方便但如果你希望测试“设备自动匹配驱动”的完整链路用modprobe更能反映真实的板级集成效果。第二点不要在probe函数里做耗时操作。平台总线在匹配设备时往往是串行调用的如果某个驱动的probe里加了msleep或者长时间自旋会拖慢整个内核启动。我的习惯是probe只做必要的资源申请和硬件基本初始化耗时任务放到工作队列或线程里。第三点dev_err里面尽量带上设备信息。写日志时用dev_err而不是printk这样日志会自带设备名多个设备多个实例时不会看花眼。比如dev_err(dev, failed to request irq %d\n, irq);这段日志会比裸printk更容易锁定问题点尤其是在同时挂载多个相同控制器的情况下一眼就能看出是哪个设备出了错。第四点注意module_platform_driver宏挥发的平台驱动在module_exit时会执行platform_driver_unregister此时内核会自动解除所有匹配设备的绑定并调用remove回调。如果驱动在probe里申请了但没释放的资源remove就会触发各种问题。优先用devm系列API因为remove时可以自动释放大部分资源。6. 扩展到I2C、SPI子系统的匹配规律弄懂了platform总线的匹配机制后再去看I2C、SPI这些真实总线上的驱动匹配会感觉特别亲切。i2c_driver里有id_table也有of_match_table匹配逻辑本质上跟platform_driver一模一样只是总线换成了真实的I2C控制器。SPI子系统同样如此。这种架构的优雅之处在于内核把“设备发现、匹配、probe”这套核心流程抽象得极其统一。你只需要搞清楚“设备节点怎么声明设备树、驱动怎么声明of_match_table、总线怎么调用probe、remove回调”任何外设驱动的开发套路都能套用。i.MX6ULL里I2C控制器本身是一个platform_driverI2C总线上挂的传感器又遵循I2C驱动模型两条线并行不悖。设备树里I2C总线的child节点虽然不会变成platform_device但它们的compatible属性在I2C子系统里又承担了与of_match_table匹配的职责。从“匹配机制”这个高度去理解会发现整个内核驱动框架就像一张大的关系网总线只是连接设备和驱动的那条绳子。我在实际项目里很多时候是同时改多个驱动一个platform驱动负责初始化某个片内外设控制器另一个i2c驱动负责挂在这个控制器下面的传感器。两个驱动各自拥有独立的匹配路径但共享同一条设备树描述协作起来非常流畅。理解platform总线的同时其实也就理解了整个内核设备模型的运转规则。7. 写在最后的调试建议我个人觉得i.MX6ULL上的platform驱动匹配机制是整个嵌入式Linux驱动学习过程中性价比最高的一部分。它不涉及特别复杂的硬件时序也不需要你去啃芯片手册里几百页寄存器描述主要考察的就是对设备模型的理解和排查问题的动手能力。掌握好它后续学中断子系统、时钟框架、pinctrl子系统都会顺利很多因为这些组件和platform驱动的协同几乎是所有BSP开发的基石。最后分享一个我实际工作中很受用的习惯每次遇到probe不进或者设备不工作的怪问题我永远不急着改代码先打开dmesg、再看/sys/bus/platform/devices/目录下目标设备是否挂上、driver软链接是否存在、MODALIAS能否对上按这三步走完80%的问题都能定位出方向。剩下的再结合设备树、引脚复用和寄存器读值去深挖。按照这套排查顺序我在i.MX6ULL上解决过的驱动适配问题没有一百也有八十希望你也能少走一些弯路。