
1. 从一颗LED开始理解Platform机制很多人学Linux驱动开发第一个练手项目往往是GPIO点灯或者读取按键。等到驱动能跑起来、灯能亮了心里还挺高兴。但紧接着就会遇到一个绕不开的问题驱动代码里写死了寄存器地址、写死了中断号换个板子或者换个引脚就得改源码重新编译。这种写法在单片机时代没问题但在Linux内核里属于典型的“反面教材”。为什么这么说因为Linux要面对的是成千上万种硬件平台和板卡如果每个驱动都跟具体硬件绑死内核变成一锅粥只是时间问题。而且现代嵌入式开发几乎都离不开设备树Device Tree驱动和设备的关系从“一对一的绑定”变成了“一对多的匹配”。这里的核心机制就是今天要讲的Platform总线平台总线上的设备与驱动匹配机制。我以i.MX6ULL为例来讲原因很简单这颗芯片是Cortex-A7内核在工业控制、物联网网关、手持设备里用得非常多而且NXP官方的SDK和常见的开发板资料比如正点原子、野火等都是基于设备树来组织代码的非常适合用来做驱动开发的入门到进阶。它的外设控制器比如UART、I2C、SPI、GPIO、定时器等等绝大部分都挂在Platform总线上理解了这一套机制等于拿到了阅读整个内核驱动代码的钥匙。本文适合正在学嵌入式Linux驱动的人也适合已经会写hello world驱动、但还没搞懂设备树和驱动之间是怎么“对上号”的同学。我会把Platform设备、Platform驱动、设备树三者之间的关系讲清楚并基于i.MX6ULL给出可以动手实操的完整例子。看完之后你不仅能看懂开发板自带的驱动为什么长那样还能自己写出一套规范的、可复用的驱动代码。2. 为什么Linux要搞出一套Platform机制2.1 从物理总线到虚拟总线的演进先看一个最朴素的场景PCI设备、USB设备它们天然有总线。设备插上总线总线枚举设备驱动去匹配设备的Vendor ID、Device ID匹配上了就bind绑定。这套流程是硬件总线自己定好的驱动开发者不需要额外操心。但嵌入式SoC里的大量设备比如i.MX6ULL的GPIO控制器、UART控制器它们是什么总线呢答案是没有总线。它们只是SoC内部的一些寄存器块物理上直接挂在ARM内部的AHB/APB总线上严格来说连“总线枚举”的过程都没有。如果每个设备都自己写一套探测流程那内核就会被各种“野路子”代码填满。这时候内核开发者想了一个非常巧妙的办法既然没有物理总线那就虚拟出一条总线出来把所有没有总线归属的设备都挂到这条虚拟总线上。这条总线就是Platform总线。它做的事情很简单维护一个设备链表一个驱动链表然后不断进行“配对”尝试。驱动注册时总线会拿出所有设备来匹配一遍设备注册时总线也会拿出所有驱动来匹配一遍。匹配成功就调用驱动的probe函数驱动正式接管这个设备。这样一搞好处立刻显现出来设备代码和驱动代码完全解耦。设备是谁硬件长什么样、寄存器在哪、中断号是多少——这些信息放在设备侧。驱动是谁逻辑怎么处理、数据怎么收发、中断怎么响应——这些逻辑放在驱动侧。两者通过Platform总线这个“红娘”牵线互不干扰各自可以独立演进。2.2 设备树加入后事情开始变得规范在设备树出现之前Platform设备是直接在C代码里定义的用platform_device结构体注册进去。这种方式有个致命问题Linux内核是通用的而板级硬件信息五花八门每换一块板子就要改内核源码里的设备列表然后重新编译整个内核。维护成本高得离谱。设备树出现之后硬件描述从C代码里剥离出来变成了一个树状的文本文件。板子上有哪些Platform设备、它们的寄存器基地址是多少、中断接到哪个引脚、时钟用哪一个——这些全部在设备树节点里描述清楚。内核启动时解析设备树把每个节点变成一个platform_device挂到Platform总线上。驱动呢只需要声明自己能匹配哪些设备剩下的事情交给总线。对i.MX6ULL来说这个优势特别明显。同一颗芯片可能被用在几百种不同配置的板卡上。A板用了UART2B板把UART2的引脚复用成了GPIO这在设备树里改一下就行驱动完全不用动。我在实际项目中见过很多次这种情况硬件改版之后BSP工程师只需要调整设备树驱动代码一行不改系统照样跑得稳稳当当。2.3 匹配机制是整个体系的“心脏”Platform机制听着简单但真正决定这套体系能否可靠工作的是“匹配”这一步。匹配规则不清楚设备找不到驱动驱动找不到设备probe不执行后面的所有操作就全是空中楼阁。内核里platform_match函数就是干这件事的它按照一套既定的优先级顺序依次尝试不同的匹配方式。我在调试中看过无数次这个函数的调用路径每一次都感叹内核作者把各种历史包袱兼容得如此之好。从最早期的driver.name直接比较到后来引入的id_table、of_match_table、ACPI匹配一层一层叠加上来形成了今天完整的匹配逻辑。3. Platform匹配机制的完整拆解从源码看匹配优先级3.1 platform_match函数到底做了什么我先把这个函数的核心逻辑用相对直白的语言描述出来不贴冗长的源码但会让你清楚每一步的顺序和条件。内核每次要判断一个platform_driver和一个platform_device是否能配对时会进入platform_match函数。它的判断顺序是这样的第一步如果驱动的driver结构体里设置了of_match_table并且设备有对应的of_node也就是设备树节点就尝试用设备树兼容性匹配。这是目前主流的匹配方式i.MX6ULL设备树驱动几乎全靠它。第二步如果第一步不行看驱动有没有acpi_match_table试试ACPI匹配。需要说明的是ACPI主要用于x86平台和部分ARM服务器i.MX6ULL这种嵌入式场景基本用不到所以绝大多数情况下这步会直接跳过。第三步看驱动的id_table。这是一个platform_device_id数组里面可以定义名字字符串。如果设备的名字和其中任何一个名字相同就算匹配成功。第四步如果驱动没有id_table那就直接比较驱动的driver.name和设备的名字。这个是最传统的匹配方式遗留代码里很常见。这个顺序是内核设计者精心安排的新机制优先老机制兜底。既保证了新代码可以使用强大的设备树匹配又不至于让老代码全部失效。我刚开始学的时候只知道设备树匹配这一种方式遇到老驱动就觉得奇怪怎么没有compatible也能probe后来看了源码才明白原来是走到了第四步。3.2 四种匹配方式的适用场景与对比我给你整理了一张对比清单方便以后写驱动时对照选择匹配方式判断依据适用场景i.MX6ULL里常见吗of_match_table设备树节点的compatible属性一切使用设备树描述硬件的平台极其常见几乎所有新驱动都用它acpi_match_tableACPI表中的HID/CIDx86/ARM服务器等ACPI平台基本见不到id_tableplatform_device_id.name无设备树时的传统写法或某些特定子系统偶尔见到老驱动居多driver.namedevice.name和driver.name直接比较最古老的兜底方式历史遗留代码里能看到需要特别注意一个容易踩坑的细节如果驱动同时设置了of_match_table和id_table设备树匹配会优先执行。也就是说即使设备树节点的compatible里没写设备的名字也能和id_table匹配上但内核根本不会走到那一步——它在第一步就根据of_node存在与否选择了设备树匹配这条路径。一旦of_match_table里的所有条目都不匹配函数直接返回失败根本不会继续用id_table去试。这个行为和很多人想象的不一样。3.3 设备树compatible匹配的底层逻辑设备树匹配是当前的主流我再单独展开讲一讲。设备树节点里有一个compatible属性比如i.MX6ULL的设备树里GPIO控制器节点长这样gpio1: gpio0209c000 { compatible fsl,imx6ull-gpio, fsl,imx6ul-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH; ... };注意compatible是一个字符串数组可以写多个值。前面的是“更具体的兼容型号”后面的是“泛化的兼容型号”。驱动的of_match_table里则需要使用of_device_id结构体来声明static const struct of_device_id my_gpio_of_match[] { { .compatible fsl,imx6ul-gpio, }, { .compatible fsl,imx6ull-gpio, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_gpio_of_match);匹配过程就是依次拿设备节点的compatible里的每一个字符串去和of_match_table里的每一个compatible字符串做字符串比较只要有任意一对相等就算匹配成功。这里有个小细节compatible字符串里的芯片型号排序有讲究。一般会把精确型号放前面比如fsl,imx6ull-gpio后面跟兼容型号fsl,imx6ul-gpio。这样做的意思是这个IP核在i.MX6UL和i.MX6ULL上是相同的驱动对两者都兼容。反过来如果你在驱动里只声明了fsl,imx6ul-gpio它也能匹配上i.MX6ULL的节点因为节点里包含了这个字符串。我在实际项目中就是这么干的——多个型号共用一个驱动大大减少了代码量。4. 在i.MX6ULL上动手写一套完整的Platform驱动4.1 需求设定与开发环境准备理论知识讲完了如果不实际操作一遍印象始终是浮的。我这里设计一个完整的例子写一个Platform驱动去控制i.MX6ULL上一个自定义的“伪设备”——就用GPIO模拟一个简单的硬件。这个例子麻雀虽小五脏俱全包含设备树节点定义、platform驱动注册、probe/remove流程、文件操作接口完整展示了匹配机制从设备树到驱动的全过程。开发环境我用的是最常见的组合一台Ubuntu主机做交叉编译目标板是i.MX6ULL开发板内核版本4.1.15NXP官方BSP常用版本我身边很多人用的也是这个。你需要准备好交叉编译工具链arm-linux-gnueabihf-以及对应版本的内核源码树。注意内核源码必须先编译过一遍生成了Module.symvers和头文件才能用来编译外部驱动模块。4.2 第一步编写设备树节点设备树节点的位置通常在arch/arm/boot/dts/imx6ull-14x14-evk.dts这个板级文件里。我建议不要直接改这个文件而是新建一个my_platform_dev.dtsi文件然后在板级文件里include进来这样后续维护更方便。节点内容如下/ { my_platform_dev { compatible mycompany,mydev; reg 0x020c406c 0x4; interrupts GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH; status okay; }; };这里解释一下各字段的含义。compatible是匹配的关键驱动里的of_match_table必须有一项和它完全一致。reg字段表示这个设备用到的寄存器地址0x020c406c是i.MX6ULL的GPIO1_DR寄存器地址不同资料可能略有差异以你的参考手册为准。interrupts声明设备使用的中断号。对于纯软件模拟的设备这些值不一定要真实有效但为了体现完整实践我按真实硬件的风格写出来。写好之后重新编译设备树make dtbs然后把生成的imx6ull-14x14-evk.dtb拷贝到开发板boot分区替换原来的设备树文件重启系统。验证设备树是否生效可以在开发板的/proc/device-tree/目录下查看ls /proc/device-tree/my_platform_dev/ cat /proc/device-tree/my_platform_dev/compatible如果能看到节点信息说明设备树解析正常后面就可以进行驱动侧的工作了。4.3 第二步编写Platform驱动模板接下来是重头戏写一个完整的Platform驱动。我把代码分成两个文件一个是驱动核心逻辑一个是模块装载入口。先看驱动的核心文件my_platform_drv.c#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/io.h #include linux/interrupt.h #include linux/fs.h #include linux/miscdevice.h #include linux/uaccess.h #define MYDEV_NAME my_platform_dev static void __iomem *gpio1_dr_base; static int mydev_irq; /* 设备树匹配表必须和dts里的compatible严格一致 */ static const struct of_device_id mydev_of_match[] { { .compatible mycompany,mydev, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydev_of_match); /* 读取寄存器模拟读取硬件状态 */ static ssize_t mydev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned int val; if (!gpio1_dr_base) { return -ENXIO; } val readl(gpio1_dr_base); if (copy_to_user(buf, val, sizeof(val))) { return -EFAULT; } return sizeof(val); } /* 写寄存器模拟控制硬件输出 */ static ssize_t mydev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { unsigned int val; unsigned int tmp; if (count sizeof(val)) { return -EINVAL; } if (copy_from_user(val, buf, sizeof(val))) { return -EFAULT; } tmp readl(gpio1_dr_base); tmp ~0x1; /* 把bit0清零 */ tmp | (val 0x1); /* 把bit0设为用户传入的值 */ writel(tmp, gpio1_dr_base); return sizeof(val); } static const struct file_operations mydev_fops { .owner THIS_MODULE, .read mydev_read, .write mydev_write, }; static struct miscdevice mydev_miscdev { .minor MISC_DYNAMIC_MINOR, .name mydev, .fops mydev_fops, }; /* 匹配成功后由总线框架调用的probe函数 */ static int mydev_probe(struct platform_device *pdev) { struct resource *res; int ret; dev_info(pdev-dev, mydev probe success\n); /* 从设备树节点中解析reg属性获得寄存器物理地址 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get memory resource\n); return -ENXIO; } gpio1_dr_base devm_ioremap(pdev-dev, res-start, resource_size(res)); if (!gpio1_dr_base) { dev_err(pdev-dev, failed to ioremap\n); return -ENOMEM; } /* 从设备树节点中解析中断号 */ mydev_irq platform_get_irq(pdev, 0); if (mydev_irq 0) { dev_err(pdev-dev, failed to get irq\n); return -EINVAL; } /* 注册misc设备在/dev下生成mydev节点 */ ret misc_register(mydev_miscdev); if (ret) { dev_err(pdev-dev, failed to register misc device: %d\n, ret); return ret; } dev_info(pdev-dev, mydev resource: reg%pa, irq%d\n, res-start, mydev_irq); return 0; } static int mydev_remove(struct platform_device *pdev) { misc_deregister(mydev_miscdev); if (gpio1_dr_base) { iounmap(gpio1_dr_base); gpio1_dr_base NULL; } dev_info(pdev-dev, mydev removed\n); return 0; } static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name MYDEV_NAME, .of_match_table mydev_of_match, }, }; module_platform_driver(mydev_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple platform driver for i.MX6ULL);再写一个模块加载入口my_platform_drv_main.c#include linux/module.h #include linux/platform_device.h static struct platform_device my_platform_device { .name my_platform_dev, .id -1, .dev { .release my_platform_device_release, }, }; static void my_platform_device_release(struct device *dev) { /* release回调不能为空否则内核在注销设备时会崩溃 */ pr_info(my_platform_device released\n); } static int __init my_platform_init(void) { return platform_device_register(my_platform_device); } static void __exit my_platform_exit(void) { platform_device_unregister(my_platform_device); } module_init(my_platform_init); module_exit(my_platform_exit); MODULE_LICENSE(GPL);这里需要提醒一个关键点platform_device的dev.release回调不能为空。如果你不提供这个回调设备注册和注销的流程里一旦走到device_release内核直接触发空指针异常。这个问题我在初学的时候踩过当时整个系统直接崩溃花了半天才定位到原因。很多网上的例子为了省事不写release这在x86平台上也许侥幸能过但在i.MX6ULL这种严格的内核配置下几乎是必崩的。4.4 第三步编写Makefile并编译驱动模块的Makefile比普通的应用复杂一点因为要告诉内核构建系统去哪里找头文件和编译规则。obj-m : my_platform_drv.o my_platform_drv-objs : my_platform_drv_main.o my_platform_drv.o KDIR : /home/user/linux-imx-4.1.15 CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc all: $(MAKE) -C $(KDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) modules clean: $(MAKE) -C $(KDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) clean注意KDIR要改成你实际的内核源码路径CROSS_COMPILE改成你的交叉编译工具链前缀。执行make如果一切顺利会生成my_platform_drv.ko文件。编译过程中如果出现找不到Module.symvers之类的报错说明内核源码树没有先编译过。解决办法是回到内核源码目录先执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage生成必要的符号文件后再来编译驱动模块。4.5 第四步设备树匹配全流程实测验证把编译好的.ko文件拷贝到开发板开始验证。第一步先确认设备树节点已经存在ls /proc/device-tree/my_platform_dev/如果能看到包含compatible、reg、interrupts等二进制属性文件说明设备树解析成功。第二步加载驱动模块insmod my_platform_drv.ko正常情况下你会看到类似这样的输出my_platform_dev my_platform_dev: mydev probe success my_platform_dev my_platform_dev: mydev resource: reg0x20c406c, irq72看到probe success就说明整个匹配链路走通了。此时在/sys/bus/platform/devices/目录下应该能看到设备节点在/sys/bus/platform/drivers/目录下能看到驱动节点。更关键的是/sys/bus/platform/devices/my_platform_dev/driver这个软链接会指向驱动目录这就是“绑定成功”的标识。第三步测试设备文件echo 1 /dev/mydev dd if/dev/mydev bs4 count1 | hexdump如果一切正常写操作会操作GPIO1_DR的bit0。实际硬件上这个寄存器可能还和其他外设复用所以不建议在开发板上真的去动它我只是用它来演示读写路径。如果只是想验证机制可以换成一段简单的内核内存区域来模拟效果是一样的。4.6 动态匹配不修改设备树也能触发probe除了设备树静态匹配Platform总线还支持一种非常便于调试的机制手动绑定。很多时候你改设备树要重新编译、烧写、重启太麻烦了。如果只是想验证驱动代码逻辑可以用/sys/bus/platform/drivers_autoprobe控制自动匹配或者用bind/unbind手工绑定。方法很简单。驱动加载之后如果因为设备树没写对应节点导致probe没执行你可以手动创建一条绑定关系echo my_platform_dev /sys/bus/platform/drivers/my_platform_dev/bind这个操作其实是模拟了platform_match的完整流程如果驱动和设备能匹配上就执行probe匹配不上会返回错误。我在调试共享中断、电源管理等复杂驱动时经常用bind/unbind来反复测试probe和remove路径比频繁重启效率高了一个量级。还有一种更彻底的手动方式就是直接在代码里动态注册一个platform_device也就是前面my_platform_init里干的事。这种方式适合开发早期硬件设备树还没准备好的阶段。5. 调试实录匹配失败的常见原因与排查思路5.1 compatible不匹配最容易犯的错误我见过最多的匹配失败原因就是compatible字符串不一致。设备树里写的和驱动of_match_table里写的看起来一样实际上可能差了一个字符、一个空格、甚至大小写不同。字符串比较是严格精确的mycompany,mydev和mycompany,mydev 末尾多了一个空格都匹配不上。排查这类问题有一个非常高效的路径。在开发板上确认设备树里的compatible值xxd /proc/device-tree/my_platform_dev/compatible输出的十六进制字节流能看到完整的字符串内容包括不可见的空白字符。然后对比驱动里的.compatible字符串确保两者逐字节完全一致。另外要注意of_match_table数组必须以空的sentinel元素结尾也就是{ /* sentinel */ }这一项。漏掉它内核在遍历匹配表时就会越界访问轻则匹配失败重则内核崩溃。这种崩溃往往没有任何有意义的内核日志非常难排查。5.2 驱动注册成功但probe不执行的典型场景驱动加载没有报错/sys/bus/platform/drivers/下也能看到驱动目录但probe就是不执行。这种情况通常有几种可能。第一种设备树节点没有产生platform_device。我遇到过的情况是节点放在了错误的位置。设备树里/根节点下的子节点和有compatible属性的节点比如/soc总线下的节点它们生成设备的路径是不一样的。如果你的节点嵌套在某个已有总线下而这个总线的驱动没有实现of_platform_populate之类的人口函数设备就不会被注册到Platform总线上。最简单的验证方法还是去看/proc/device-tree/和/sys/bus/platform/devices/下有没有对应条目。第二种驱动被of_match_table和id_table的特殊关系误导了。前面讲过设备树节点存在时只走of_match_table匹配。你可以检查一下驱动加载时内核有没有打出OF: fsl,imx6ull-gpio ...之类的of匹配日志这些日志通常在drivers/of/base.c的of_match_device函数里。开启内核动态调试后能看到完整的匹配过程。第三种设备其实已经绑定到了另一个驱动上。一个platform_device只能绑定一个platform_driver如果之前已经有另一个驱动抢先把设备绑走了你的驱动自然probe不了。查看当前绑定关系ls -l /sys/bus/platform/devices/my_platform_dev/driver cat /sys/bus/platform/devices/my_platform_dev/ueventuevent文件里能看到设备当前的驱动匹配信息如果driver_override被设置过也会在这里显示。5.3 name匹配与id_table匹配的混淆在没有设备树的老代码里经常看到这样的写法驱动设置.driver.name xxx设备设置.name xxx然后两者就匹配上了。这种方式背后走的是第四步也就是driver.name和device.name直接比较。但在有设备树的i.MX6ULL上很多新手会犯一个错设备树节点里的compatible写的是mycompany,mydev驱动里的.driver.name也写成了mydev以为这样就能匹配。实际上完全不行。有设备树节点的设备匹配逻辑绕过了driver.name直接看compatible。如果of_match_table不存在或者没匹配上直接就返回失败根本不会拿driver.name去和设备名字比较。结论很简单如果走设备树这条路径就老老实实把compatible写对如果走老式路径就不要在设备树里加节点。两条路不要混着来。我见过有人把两种方式混在一起写结果驱动在别的平台能跑到了i.MX6ULL上就莫名其妙probe不了最后发现是of_match_table里的compatible少写了一个型号。5.4 用sysfs快速定位一套百试百灵的排查命令最后分享一套我在调试Platform驱动时几乎必用的排查命令组合方便你在遇到问题时能快速缩小范围。先看设备是否存在、以及当前状态ls /sys/bus/platform/devices/ | grep my_platform cat /sys/bus/platform/devices/my_platform_dev/uevent再看驱动是否注册成功ls /sys/bus/platform/drivers/ | grep my_platform ls -l /sys/bus/platform/drivers/my_platform_dev/手动触发绑定和解绑echo my_platform_dev /sys/bus/platform/drivers/my_platform_dev/unbind echo my_platform_dev /sys/bus/platform/drivers/my_platform_dev/bind查看内核完整日志dmesg | tail -50 dmesg | grep -i platform\|of_match\|mydev这一套下来90%的匹配问题都能定位。如果日志里没有任何有效信息再回去检查设备树编译和烧写过程。我之前碰到过一次非常诡异的现象改了设备树重启后/proc/device-tree/里的内容确实是新节点但驱动就是匹配不上。后来发现是uboot启动时加载的还是老版本的dtb文件根本没用到新编译出来的dtb。这类问题往往和设备树机制本身无关纯粹是启动流程的问题排查思路要转到uboot环境变量上。5.5 一个真实的匹配失败排查案例去年我在帮一个客户调i.MX6ULL的UART扩展驱动遇到一个很有意思的问题。驱动在NXP官方EVK板卡上一切正常换到客户自己设计的板卡上probe函数就是不执行。常规排查手段全过了一遍dts里compatible没问题节点结构没问题/proc/device-tree/能看到节点/sys/bus/platform/devices/下也有设备。最后我用echo 1 /sys/module/kernel/parameters/initcall_debug开启内核初始化流程调试发现设备树在板级初始化的时候自定义节点的status属性被覆盖成了disabled。原因是客户在设计设备树时在/soc节点下定义了一个同名的覆盖节点把status disabled写进去了。设备树节点的合并规则会以后出现的属性为准于是原本status okay的节点被覆盖成了disabled。内核在创建platform_device时会检查status属性disabled的设备直接跳过注册。这就是为什么设备树里能看到节点但总线上始终没有设备。这类问题纯靠看代码和sysfs很难发现因为节点内容看起来都是对的。我的经验是当所有常规检查都正常但设备就是不出现在总线上时一定要检查status属性以及设备树里是否有同名节点的覆盖。6. 从匹配机制看Linux驱动的设计哲学回头再看整个Platform匹配机制其实贯彻了一个思想把“硬件长什么样”和“驱动怎么工作”彻底分开。设备树负责回答第一个问题驱动代码负责回答第二个问题Platform总线负责在两者之间牵线搭桥。这种解耦带来的直接好处是同一个驱动可以不经任何修改就支持几十种不同板卡的同类设备同一块板卡换个驱动实现就能改变设备的行为逻辑而硬件描述完全不用动。我在实际项目里最直观的感受是驱动开发变成了“对表”工作。拿到一块新板子第一步看原理图第二步看设备树第三步看驱动。只要设备树里的compatible能和某份驱动匹配上大概率这个设备就能工作。如果匹配不上优先怀疑设备树节点写错了而不是驱动写错了。这套流程在i.MX6ULL上尤其顺畅因为NXP的BSP把大部分外设的驱动都写好了你真正需要亲自动手的往往是那些自定义的、非标准的设备——点灯、读按键、控制某个IO扩展芯片、挂一个自定义的SPI从设备等等。把这套机制彻底搞懂之后再看Linux内核里的其他虚拟总线比如I2C总线、SPI总线、USB总线会有一种豁然开朗的感觉。它们的匹配机制总体思路是一样的只是具体的匹配键不同I2C用i2c_device_id和of_match_tableSPI用spi_device_id和of_match_tableUSB用usb_device_id。掌握了Platform这套认知框架再去迁移到其他总线学习成本会大幅降低。7. 一个高效的开发建议用模块化思路组织驱动代码最后分享一个我个人非常受用的经验。在i.MX6ULL上写驱动我习惯把代码分成三个层次第一个层次是“匹配层”。这一层只负责声明of_match_table、id_table、driver.name这些匹配信息以及probe和remove函数的基本框架。这层代码非常薄大多是对外设驱动的通用封装。第二个层次是“逻辑层”。所有真正的驱动逻辑包括寄存器读写、中断处理、数据解析、状态机管理都放在这里。这部分代码不感知设备树的存在它只通过probe传入的参数比如寄存器基地址、中断号来工作。第三个层次是“接口层”。这部分面向用户空间可以是misc设备、字符设备、sysfs属性、proc接口。它负责把内核逻辑暴露给应用程序。这样分层之后好处非常明显设备树改了只动第一层硬件行为变了只动第二层用户接口变了只动第三层。三层互不干扰测试和排查问题的时候思路也清晰很多。我在维护一个多平台适配的项目时同一套驱动逻辑在i.MX6ULL、i.MX8M Mini、甚至STM32MP1上都能跑区别只是第一层的of_match_table条目不同。这就是Platform机制带给嵌入式Linux开发者的最大红利。写到这里核心内容其实已经全部覆盖了。如果你正在学i.MX6ULL的驱动开发建议不要停留在“能编译能加载”的阶段一定要亲手验证一遍匹配流程把/sys/bus/platform/devices/和/sys/bus/platform/drivers/下的每一个软链接含义都搞清楚。这套机制理解到位后面学input子系统、RTC驱动、网络驱动都会轻松很多。