Linux Platform驱动匹配机制详解:从设备树到probe触发全流程

Linux Platform驱动匹配机制详解:从设备树到probe触发全流程 从事嵌入式Linux驱动开发的朋友早晚都要跟Platform驱动打交道。哪怕你现在写的是I2C、SPI、USB外设驱动底层也基本都挂在Platform总线上或者至少绕不开这套设备与驱动的匹配逻辑。今天这篇东西我结合i.MX6ULL这颗NXP的Cortex-A7处理器把Platform设备与驱动匹配机制从头到尾拆一遍重点回答几个大家初学时常困惑的为什么为什么我们用device_tree描述了硬件还要写一个platform_driverprobe函数到底是什么时候被调用的compatible字符串到底是怎么匹配上的这篇内容的定位是给有一定C语言和Linux基础、正在入门嵌入式Linux驱动开发的朋友目标是让读完的人能自己捋清设备树、platform驱动、总线匹配三者之间的关系并能独立在i.MX6ULL上跑通一个最简单的Platform驱动实验。i.MX6ULL是NXP i.MX6ULL系列里非常经典的一颗芯片基于ARM Cortex-A7核心主频可以跑到800MHz在工业控制、物联网网关、人机界面这些方向都有大量应用。很多开发板比如正点原子的ALPHA/Mini系列、野火的i.MX6ULL板子都围绕这颗芯片做了完整的Linux驱动教程生态所以拿它来学习Platform驱动匹配机制资料多、例程全你自己实验起来也方便。1. 为什么需要Platform总线先看懂Linux设备模型这盘棋要理解Platform匹配机制得先明白一个底层问题Linux内核为什么非要搞出一条虚拟的Platform总线出来嵌入式SoC片上系统里大量控制器并不是像PCI设备那样可以枚举发现的。PCI设备有标准的配置空间系统可以通过扫描总线号、设备号、功能号主动发现硬件而SoC里的GPIO控制器、UART控制器、I2C控制器它们的寄存器地址和中断号都是SoC设计时固定死的Linux内核不可能像PCI那样去探测它们。这些设备在系统里是静态存在的需要有人告诉内核这里有一个外设它的硬件资源寄存器基地址、中断号、时钟等在哪里。Platform总线就是为解决这个问题而生的。它是一条虚拟总线用来统一管理那些挂在CPU内存映射空间上的设备。所谓Platform设备本质就是一组硬件资源信息的抽象内核通过platform_device结构体来描述它对应的写驱动的人通过platform_driver结构体来声明我能驱动什么样的设备。总线的作用就是把左边的设备和右边的驱动牵线搭桥让它们互相找到对方。在老的、没有设备树的内核版本里平台设备是通过代码静态定义的比如在arch/arm/mach-xxx/board-xxx.c里用platform_device结构体一个个注册。而现在绝大部分ARM平台都切换到设备树(DT, Device Tree)方式平台设备不再用C代码定义而是由内核在启动时解析设备树二进制(DTB)针对每个匹配的节点自动生成platform_device。这一点非常关键你在设备树里写了一个节点内核启动后就会把它变成一个platform_device挂到虚拟总线上。而你写的platform_driver则是在驱动模块加载或内核启动时向总线注册自己。当总线上设备和驱动两边都就位了匹配机制就会触发匹配成功后就调用驱动里的probe函数。所以现代ARM Linux驱动开发的本质工作变成了两件事第一在设备树里把硬件描述正确描述设备的资源第二写一个platform_driver把硬件驱动起来实现驱动的逻辑。而连接这两者的桥梁就是Platform总线的匹配机制。2. 搭建你的第一个Platform驱动实验基于i.MX6ULL的LED点灯光讲理论容易晕我们先在i.MX6ULL上搭建一个真实的Platform驱动实验把整个过程跑通后面再回过头去细看匹配机制的源码实现。我用的环境如下开发板正点原子Alpha i.MX6ULL开发板底板核心板交叉编译工具链arm-linux-gnueabihf-gccLinaro 7.5或映泰提供的都可以内核源码4.1.15正点原子出厂内核版本或者5.x系列内核都可以原理一致编译环境Ubuntu 18.04/20.04 x64虚拟机在做这个实验之前确保你的开发板已经能正常进入Linux系统并且内核源码已经在Ubuntu上编译通过。这块儿不展开是前置条件。2.1 设备树里面如何进行硬件描述在i.MX6ULL的设备树源文件里以正点原子开发板为例客户板级文件是arch/arm/boot/dts/imx6ull-14x14-evk.dts。我们在这个文件里追加一个自定义的LED节点。先看一下i.MX6ULL的GPIO引脚使用情况。在核心板上LED通常连接到GPIO1_IO03或者GPIO5_IO03这样的引脚上。正点原子Mini开发板上有一个RGB灯这里我们简化处理用一个常规GPIO来控制LED。在imx6ull-14x14-evk.dts文件的根节点下也就是/ { ... };内部添加这样一个节点/ { myled { compatible my_company,my-led; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };在iomuxc节点里我们要把GPIO1_IO03这个引脚复用为GPIO功能并且设置好上下拉和驱动能力。在iomuxc { ... };节点内部补充如下内容iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 ; }; };这段描述的含义是把GPIO1_IO03这个pad复用为普通的GPIO功能不是UART或别的外设功能0x10B0是pad的电气配置——使能下拉、设置压摆率、配置驱动强度等具体的位含义可以查Reference Manual里IOMUXC章节的Pad Control Register定义。有了这个设备树节点内核启动的时候就会为这个myled节点生成一个platform设备。注意我们这里用了compatible my_company,my-led自定义的字符串后面驱动里的of_match_table要跟它完全一致否则匹配不上。2.2 驱动模块的完整代码实现接下来写驱动代码。新建一个my_platform_led.c文件完整代码如下#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/delay.h static struct gpio_desc *led_gpio; static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; // 1. 从设备树中获取GPIO描述符 led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, Failed to get led gpio: %ld\n, PTR_ERR(led_gpio)); return PTR_ERR(led_gpio); } // 2. 初始化时先灭灯这取决于你的硬件极性 gpiod_set_value(led_gpio, 1); dev_info(dev, my led platform driver probed successfully\n); return 0; } static int my_led_remove(struct platform_device *pdev) { dev_info(pdev-dev, my led platform driver removed\n); return 0; } static const struct of_device_id my_led_of_match[] { { .compatible my_company,my-led, }, { /* 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(A simple platform driver for i.MX6ULL LED);这个驱动的逻辑很清晰用module_platform_driver宏最终展开为一个平台驱动的注册函数模块加载时调用platform_driver_register。驱动结构体里指定了probe和remove回调函数。probe在设备与驱动匹配成功后被调用。of_match_table里写了compatible字符串数组这是匹配的核心。probe函数里通过devm_gpiod_get(dev, led, ...)从设备树节点获取GPIO这里第二个参数led对应的是设备树里的led-gpio属性名。2.3 Makefile与编译过程驱动编译要用内核的模块编译机制。Makefile这样写KERNELDIR : /home/user/linux-imx-4.1.15 CURRENT_PATH : $(shell pwd) obj-m : my_platform_led.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) cleanKERNELDIR换成你自己的内核源码路径。注意内核源码必须预先配置好使用开发板对应的默认配置文件并完成编译否则模块编译时找不到Module.symvers和linux/autoconf.h这类文件。编译命令make ARCHarm CROSS_COMPILEarm-linux-gnueabihf-编译完会生成my_platform_led.ko。把它拷贝到开发板文件系统里用nfs挂载或者scp都行。2.4 实验验证与现象分析在开发板命令行执行insmod my_platform_led.ko正常情况下你会看到类似这样的日志输出my_led myled: my led platform driver probed successfully同时LED灯状态会发生变化。如果你提前在probe里做了硬件操作LED会跟着动作。当执行rmmod my_platform_led时会调用remove回调输出移除日志。这个简单的实验背后已经完整体现了Platform匹配机制的核心流程设备树节点生成platform设备 → 驱动注册 → 总线上设备与驱动匹配 → 匹配成功触发probe函数 → probe函数里访问硬件资源。把这整条链路吃透了后面看内核源码就不懵。3. 匹配机制深度拆解compatible、id_table与name的三角关系上面实验跑通后接下来重点回答几个核心问题Platform总线的匹配顺序是什么匹配到底比对了哪些字段为什么我们常写的compatible字符串和驱动中的platform_driver结构体里的name并不一样3.1 platform_bus_match到底在比对什么在Linux内核里Platform总线的匹配逻辑在drivers/base/platform.c里的platform_match函数。调用路径是总线子系统遍历设备链表时对每个未绑定的设备调用总线的match回调。我们来看这个函数的核心逻辑以内核4.1.x版本为例后续版本略有调整但核心思路稳定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_match_table则优先尝试设备树方式匹配 */ if (pdrv-driver.of_match_table) if (of_driver_match_device(dev, drv-driver.of_match_table)) return 1; /* 2. 如果驱动指定了id_table则进行id_table匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 3. 兜底匹配比较platform_driver.driver.name与platform_device.name */ return (strcmp(pdev-name, drv-name) 0); }这个顺序非常重要。实际执行时优先级是of_match_tableid_tablename。现代设备树方案下绝大多数驱动都走第一条路径也就是of_driver_match_device。这条路径最终会去比较设备树节点的compatible属性与of_match_table数组里每个元素的compatible字段。of_driver_match_device的调用链最终会走到__of_match_node函数它会对of_match_table里的每个条目进行比较先比对compatible字符串如果某条目的compatible与节点的compatible属性相等就匹配成功。如果of_match_table里的条目指定了.type或.name字段还会额外比对device_type和name属性但绝大多数驱动只设置compatible我们这里也只看compatible。3.2 id_table匹配的底层逻辑platform_match_id函数的比对逻辑是static const struct platform_device_id *platform_match_id( struct platform_device *pdev, struct platform_device_id *id) { while (id-name[0]) { if (strcmp(pdev-name, id-name) 0) { pdev-id_entry id; return id; } id; } return NULL; }也就是说它会逐个比较platform_device的name与id_table数组里每个条目platform_device_id的name。这里的platform_device.name在老式无设备树的平台里指的就是C代码里定义platform_device时给的name在设备树方式下platform_device的name取自设备树节点的node name注意不是compatible是节点名比如我们前面例子里的myled。这样你就明白为什么有的驱动既写了of_match_table又写了id_table——它们面向两种不同的设备描述来源。of_match_table面向设备树id_table面向传统无设备树时的ACPIx86平台或静态注册的platform设备。3.3 既有设备树又手写platform_device会发生什么这是很多初学者容易踩坑的地方。假设你在设备树里放了一个节点又在C代码里用platform_device_register注册了一个同名platform设备这时候系统里就有两个platform_device。你的probe会被调用两次吗答案是如果of_match_table和id_table都配置了可能确实会被匹配两次但往往第二次会因资源冲突而失败或者因为设备树节点的compatible与of_match_table匹配而正常probe而C代码注册的设备通过id_table匹配也会probe。这在实际项目中是容易出问题的地方。我记得最开始入门时在一个项目里既想用设备树描述设备又为了测试在板级文件里加了platform_device结果发现probe被调用了两次硬件寄存器被操作了两遍LED闪烁频率都不对。排查了很久才意识到是重复注册。正确的做法是设备树方式下绝对不要在C代码里再手动注册同个设备。两者二选一现代内核一律用设备树。3.4 匹配成功后内核做了哪些事匹配成功后内核会走到really_probe函数drivers/base/dd.c它做这几件事调用driver-probe(dev)之前先执行dev-bus-probe如果有定义的话。对platform总线来说这个是platform_probe它会把platform_device的resource资源与设备树解析的硬件信息准备好。如果设备树节点里有pinctrl-0属性会在这时候配置引脚的复用和电气属性。这就是为什么probe函数里执行devm_gpiod_get之前GPIO引脚已经处于正确的复用状态。调用驱动里提供的probe函数。probe返回0则驱动绑定成功返回负值则绑定失败内核会继续尝试匹配其他驱动如果没有其他驱动设备就处于unbound状态。probe成功后设备和驱动的指针互相引用设备挂到驱动名下的设备链表里驱动也记录着自己管理的设备数量。此时/sys/bus/platform/devices/和/sys/bus/platform/drivers/下能看到绑定关系。4. 源码追读从drivers/base/platform.c看注册与注销全流程理解机制的最高效方式是直接去读内核源码。我们以4.1.15内核为例把platform_driver_register和platform_device_add这两条路线的关键代码走一遍。4.1 platform_driver_register的展开与调用链我们驱动里用的module_platform_driver(my_led_driver)宏展开后是static int __init my_led_driver_init(void) { return platform_driver_register(my_led_driver); } module_init(my_led_driver_init); static void __exit my_led_driver_exit(void) { platform_driver_unregister(my_led_driver); } module_exit(my_led_driver_exit);platform_driver_register源码如下drivers/base/platform.cint platform_driver_register(struct platform_driver *drv) { drv-driver.bus platform_bus_type; if (drv-probe) drv-driver.probe platform_drv_probe; if (drv-remove) drv-driver.remove platform_drv_remove; ... return driver_register(drv-driver); }这里做了两个重要动作一是把驱动的总线类型指定为platform_bus_type二是把驱动里我们写的probe包装成了platform_drv_probe。为什么需要包装因为平台总线的probe回调需要先从通用device结构体拿到platform_device再传递给驱动自己的probestatic int platform_drv_probe(struct device *_dev) { struct platform_driver *drv to_platform_driver(_dev-driver); struct platform_device *dev to_platform_device(_dev); ret dev_pm_domain_attach(_dev, true); if (ret ! -EPROBE_DEFER) { ret drv-probe(dev); ... } return ret; }你看到的这层包装就是Linux设备模型里总线驱动隔离的具体体现。写驱动的人只需要实现与platform_device打交道的probe签名参数是platform_device *而不需要关心底层通用设备模型如何调用它。driver_register最终会把驱动挂到platform_bus_type的驱动链表里然后触发总线上的设备与驱动匹配扫描也就是bus_probe_device。4.2 设备树生成platform_device的启动流程设备树方式下platform_device并不是我们手动创建的。在ARM平台启动早期内核会调用of_platform_populate函数遍历设备树根节点下的所有节点对每一个设置了compatible属性的节点都会创建一个platform_device前提是内核没有为它匹配到其他更具体类型的总线比如I2C设备节点会被挂到i2c总线而不是platform总线。of_platform_populate的下游调用链是of_platform_populate - of_platform_bus_create - of_platform_device_create_pdata - of_device_alloc // 分配platform_device解析reg、interrupts等资源 - platform_device_add // 把platform_device添加到系统中其中platform_device_add函数drivers/base/platform.c是这个流程的核心它的任务有初始化设备的名称和ID。把设备树解析到的资源reg寄存器地址、中断号等绑定到platform_device的resource数组。调用device_add把设备挂到platform总线的设备链表上。触发设备与驱动的匹配尝试也就是bus_probe_device。这一步非常关键设备一旦add到总线内核就会立刻尝试为它寻找驱动。如果在设备注册时驱动已经加载好了马上就会probe如果驱动还没有加载设备会暂时挂在总线上处于unbound状态等驱动注册时再回头匹配。这也是我们insmod驱动模块之后probe会立即被调用的原因。4.3 模块加载时总线如何响应当你在开发板执行insmod my_platform_led.ko时模块初始化函数里调用platform_driver_register最终执行driver_register。driver_register会把驱动的device_driver结构体挂到platform总线的driver链表。遍历总线上的设备链表对每个还没有绑定驱动的设备尝试调用match去匹配。这个遍历匹配过程是在bus_add_driver里通过driver_attach完成的。对于每个设备最终尝试绑定device_bind_driver并调用probe。所以insmod后的时序是驱动注册 → 总线遍历设备 → compatible匹配成功 → probe执行 → 硬件初始化和相关操作。整个过程一气呵成看起来就像模块一加载硬件瞬间被接管了。4.4 卸载驱动的反向流程执行rmmod时模块退出函数调用platform_driver_unregister它最终会调用driver_unregister。内核会遍历该驱动名下的设备对每个设备执行一次remove流程。从platform总线角度看最终调用我们驱动里的remove回调。这里有几个常见疑问为什么不调用probe里devm申请的资源释放函数因为devm系列APIdevice managed是设备模型自动管理的。设备与驱动解绑时内核会释放devm机制管理的所有资源——包括GPIO描述符、时钟、中断等。所以remove回调里通常只需要做必要的硬件关闭动作不必手动释放devm申请的资源。remove回调执行完设备重新变为unbound状态等待下一个驱动来匹配。驱动模块虽然卸载了但设备树里的节点还在所以/sys/bus/platform/devices/myled这个目录仍然存在只是里面的driver符号链接消失了。5. 常见踩坑与调试手段为什么我的probe没有被调用写驱动初期最让人头疼的问题就是模块加载了dmesg没有任何输出led也不亮probe函数好像根本没执行。这类问题我遇到过很多次也帮人排查过很多次归纳下来无非是以下几个原因按出现频率排序。5.1 compatible字符串不一致这是最常见的问题。驱动里的of_match_table写的是my_company,my-led设备树节点里写的却是my_company,my_led下划线vs横线或者写成my-led少了厂商前缀都会导致匹配失败。检查方法很简单在开发板执行cat /sys/bus/platform/devices/myled/of_node/compatible这条命令会输出设备树节点的compatible属性实际值。如果输出为空说明of_node目录不存在设备没有创建成功如果输出和驱动里的of_match_table不一致就是这里的匹配问题。还有一种情况你在开发板使用的dtb文件是旧版本的设备树改动后忘记重新编译dtb并烧录。明明源码里已经改了compatible板子上跑的却还是旧dtb。这个用上面的cat命令一查便知。5.2 设备树节点没有成功生成platform设备设备树节点要生成platform设备一个必要条件是节点必须有compatible属性而且不能是simple-bus这一类的bus节点这类节点是作为总线容器处理的不会生成直接的platform设备也不能被更高优先级的子系统如I2C、SPI所匹配。如果节点本身没有出现在设备树里同样不会生成设备。检查方法ls /sys/bus/platform/devices/看有没有myled。如果没有再检查设备树节点是不是写在了正确的dts文件里并且使用的是你实际编译的那个dts。i.MX6ULL有多个dts文件14x14和min等板型不同文件也不同别改错文件了。5.3 驱动没有加载成功或模块加载报错有时候probe没调用其实是驱动模块加载就失败了。在开发板执行insmod后立即看dmesg | tail如果出现类似Unknown symbol或者module verification failed的提示说明模块编译有问题驱动根本没注册成功。特别说明一下内核版本校验的问题4.1.15内核默认开启了模块版本校验MODVERSIONS如果你编译模块用的内核源码和开发板实际运行的内核不是同一个版本哪怕小版本不同insmod会报版本不匹配。解决办法是把编译好的内核镜像和模块同步部署或者编译内核时关闭CONFIG_MODVERSIONS。5.4 使用of_match_table却漏了MODULE_DEVICE_TABLE有朋友在调试时发现模块编译时提示WARNING: modpost: missing MODULE_DEVICE_TABLE()。这行警告值得重视。MODULE_DEVICE_TABLE(of, my_led_of_match)的作用是在模块文件里生成一个符号表告诉系统这个驱动支持的设备列表。对于设备树驱动来说它主要是为了让内核的自动加载机制udev/mdev知道这个驱动该匹配哪些compatible字符串。少了它手动insmod时probe还是能执行的因为of_match_table还在但是在交叉编译时modpost会给出警告而且当模块被静态编译进内核时设备树匹配会受影响。所以即使不是致命错误也建议规范写上。这也是驱动代码仓库审查时经常被review出来的问题。5.5 利用sysfs和dmesg逐级排查当你完全不清楚问题出在哪一步时建议按照下面的顺序排查先确认设备存在ls /sys/bus/platform/devices/myled没有就回到设备树排查。再确认驱动存在ls /sys/bus/platform/drivers/my_led没有就回到驱动加载排查。确认是否绑定ls -l /sys/bus/platform/devices/myled/driver如果有指向驱动的软链接说明绑定成功没有则说明匹配失败。查看绑定关系详情cat /sys/bus/platform/devices/myled/uevent里面会显示设备名称和of节点compatible等信息。全程搭配dmesg | tail查看内核日志重点看有没有platform驱动注册的提示、有没有匹配失败或probe失败的错误。这套sysfs排查的方法在内核开发社区是非常通用的手法建议养成习惯。每次写新驱动先在sysfs里确认设备和驱动都就位再去调试probe内部逻辑效率会高很多。6. 进阶resource获取与probe里常用的GPIO操作姿势匹配机制打通之后probe函数里就是真正的驱动逻辑了。这一节我结合实际经验聊几个probe里高频使用的资源获取方式以及在i.MX6ULL上做GPIO操作时的一些细节。6.1 从设备树获取寄存器资源platform_get_resource与devm_platform_ioremap_resource第一个常用API是platform_get_resource它从platform_device管理的资源数组里取出指定类型的资源。比如设备树节点里写了reg 0x020C406C 0x4;驱动里可以这样获取struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENOENT;拿到resource之后通常需要映射到虚拟地址空间旧式写法是void __iomem *base; base ioremap(res-start, resource_size(res));现在更推荐的写法是用devm_platform_ioremap_resource一步到位void __iomem *base; base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(base)) return PTR_ERR(base);这个函数内部帮你做了平台资源获取、ioremap、以及devm资源跟踪probe失败或驱动卸载后自动释放映射不用手动ioremap/iounmap配对。对于I2C、SPI这类外设寄存器控制我们都应该优先使用这种devm版本。对于中断资源可以用int irq platform_get_irq(pdev, 0);拿到irq后配合devm_request_irq或request_threaded_irq注册中断处理。这也是设备树方式下获取中断的标准姿势。6.2 GPIO操作从gpio_request到GPIO descriptor API在老的GPIO接口体系中驱动里是这么获取引脚的int gpio of_get_named_gpio(dev-of_node, led-gpio, 0); gpio_request(gpio, led); gpio_direction_output(gpio, 0); gpio_set_value(gpio, 1);这条链路在4.1.15内核里能用在5.x内核里也还没完全删除但内核社区已经明确推荐GPIO descriptor API。新版代码建议使用devm_gpiod_getstruct gpio_desc *desc; desc devm_gpiod_get(dev, led, GPIOD_OUT_LOW);这个方法的好处自动解析设备树里的led-gpio属性。自动处理GPIO引脚是否有效、是否被占用。devm跟踪生命周期probe失败自动释放。返回的是GPIO descriptor指针不直接暴露全局编号。使用注意devm_gpiod_get的第二个参数led要与设备树属性去掉-gpio后缀的名字一致。比如设备树属性叫led-gpio这里就传led如果设备树属性叫enable-gpio这里传enable。如果设备树里用的是标准属性名gpios不带前缀就直接传NULL即可。GPIO极性问题是i.MX6ULL相关板子上比较容易踩的坑。有的开发板LED阳极接3.3V阴极接GPIO引脚这时GPIO输出低电平LED亮输出高电平LED灭对应设备树里的GPIO_ACTIVE_LOW。用GPIO descriptor API时需要把设备树里的flags设置为GPIO_ACTIVE_LOW然后代码里语义上调用gpiod_set_value(desc, 1)表示点亮就足够了底层驱动会自动反转电平。这样硬件的电平极性与逻辑语义分离代码可读性和可移植性都更好。6.3 pinctrl与device tree的关系再强调probe函数执行前pinctrl系统已经根据设备树节点里的pinctrl-0属性把引脚复用配置好了。这个时序非常重要意味着你在probe里访问寄存器或者使用GPIO时引脚已经处于正确的复用状态不需要手动去操作IOMUXC寄存器。但也因此有个调试盲区如果设备树里pinctrl配置错误比如复用成了UART功能你在probe里操作GPIO时硬件不会按预期工作却没有任何报错。这时候就要回头检查pinctrl子节点的配置或者用/sys/kernel/debug/pinctrl/下的调试信息确认引脚当前状态。在i.MX6ULL上pinctrl配置由pinctrl-imx6ul.c驱动和dts里的iomuxc节点共同完成。常见的pad配置值0x10B0这类数字来自i.MX6ULL参考手册里IOMUXC_SW_PAD_CTL寄存器的定义具体bit的含义是bit 0-3驱动强度、bit 4-5压摆率、bit 6-11上下拉和保持等建议写驱动前把对应位定义翻一下手册避免反复试错调不出正确的电气参数。7. 从单设备到多个同类设备平台的id_table与多个led实例管理前面例子只管理了一个LED。真实项目中板子上往往有多个同类型设备比如两个LED、四个UART、多个GPIO按键。这种情况下Platform驱动怎么写这就要用到id_table和platform_device的编号机制了。7.1 多个同compatible设备节点时的处理最简单的场景是设备树里有多个相同compatible的节点。比如myled0: myled0 { compatible my_company,my-led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; reg 0; }; myled1: myled1 { compatible my_company,my-led; led-gpio gpio1 4 GPIO_ACTIVE_LOW; reg 1; };这种情况下of_match_table配好compatible后驱动注册时会对这两个节点生成的两个platform_device都执行probe。probe函数里的devm_gpiod_get会根据各自的of_node去解析不同的led-gpio属性所以天然就支持多设备实例。这就是设备树方式最大的优势——不需要在驱动里为每个LED硬编码gpio号。7.2 platform_device_id与不同硬件版本的适配再看另一种场景同一个驱动要兼容多个不同版本的硬件这些硬件的寄存器布局或操作方式有细微差异。这时可以用of_device_id里的data字段或platform_device_id里的driver_data字段来区分。以平台设备id为例static const struct platform_device_id my_led_ids[] { { .name my-led-v1, .driver_data (kernel_ulong_t)v1_config }, { .name my-led-v2, .driver_data (kernel_ulong_t)v2_config }, { } }; MODULE_DEVICE_TABLE(platform, my_led_ids);在probe里const struct platform_device_id *id platform_get_device_id(pdev); if (id) { const struct my_led_config *cfg (const struct my_led_config *)id-driver_data; // 使用cfg里的差异配置 }这种用法在维护多个硬件版本时非常实用比如同一颗芯片做了两个产品LED的控制寄存器地址不一样通过driver_data区分probe函数里读不同的配置结构体代码复用率高且逻辑清楚。7.3 驱动内部如何区分多个同类设备对于多个LEDprobe每个实例时需要把各自的gpio描述符、名称、当前状态等信息保存起来。常见做法是自定义一个私有结构体用platform_set_drvdata关联到platform_device上struct my_led_dev { struct gpio_desc *desc; struct device *dev; int id; }; static int my_led_probe(struct platform_device *pdev) { struct my_led_dev *led; led devm_kzalloc(pdev-dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led-dev pdev-dev; led-id pdev-id; // 对于设备树节点pdev-id通常为-1可以用别名或reg属性代替 led-desc devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); ... platform_set_drvdata(pdev, led); return 0; }这样在remove或sysfs回调里都能通过platform_get_drvdata(pdev)拿到私有数据对每个设备实例独立操作。需要留意一个细节设备树方式生成的platform_devicepdev-id值通常为-1称为PLATFORM_DEVID_NONE设备树节点里的reg属性不会自动变成pdev-id。如果需要区分多个设备物理地址不同可以使用platform_get_resource(pdev, IORESOURCE_MEM, 0)的地址值来区分业务层面可以在设备树里增加自定义属性比如myled0 { compatible my_company,my-led; reg 0; label led0; };驱动里用of_property_read_string读取label属性。当然最简单粗暴的就是直接用reg的索引值看个人习惯了。8. 写在最后几个让我印象深刻的教训最后分享几个我实际开发中踩过的坑和经验希望能帮看到这篇文章的朋友少走弯路。第一个教训是设备树里改完compatible后经常忘记重新编译dtb。我在一个项目里调试一个新的LCD驱动时dts里改了好几轮compatible但板子上跑的一直是旧dtb每次insmod后都提示匹配失败。当时花了大半天时间排查驱动代码最后才发现是dtb没更新。从那以后我每次修改dts后第一件事就是确认dtb已经烧录进板子用cat /proc/device-tree/my-led/compatible这种命令去确认运行时实际加载的设备树内容。第二个体会是devm资源管理真的是个好东西但也别滥用。devm系API让资源释放变简单了可一旦你用了devm_kzalloc分配内存后又额外在堆上申请了手动内存就得手动管理好释放时机不能因为probe里有devm就把所有资源管理都扔给内核。内核文档里也明确说devm主要面向probe后长期持有的资源临时使用的资源用普通的kmalloc/kfree更合适。第三个经验是关于定位问题的顺序。我现在调试驱动时有个固定套路模块加载前先确认设备在sysfs里存在加载后再看驱动的driver目录有没有出现然后用dmesg抓probe的日志输出。如果probe返回了错误码dmesg里通常会明确打印是哪一步失败。内核日志里常见的-EPROBE_DEFER提示说明驱动依赖的某个resource通常是另一个驱动提供的时钟、pin controller等还没准备好内核会自动推迟probe等依赖就绪后再次尝试这种情况不用担心也不是驱动逻辑错误。第四个经验是建议初学朋友一定要自己动手把platform_match的源码读一遍再把of_match_table的展开过程在源码里追一遍。设备模型的核心代码其实不多读明白drivers/base/platform.c和drivers/base/dd.c这两个文件比背十篇网上的教程都管用。内核版本迭代很快API会变但设备模型的基本原理非常稳定把这套逻辑吃透了你从4.x迁移到5.x或者将来用6.x内核都不会有太大障碍。Platform匹配机制是Linux驱动开发的地基设备树、platform驱动、总线匹配这三个概念串起来之后再看其他类型的驱动I2C、SPI、USB、MTD等会发现套路都差不多设备侧描述硬件信息驱动侧实现操作逻辑总线负责牵线搭桥。把今天这篇里的链路彻底理解了后面学什么都快。