ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

i.MX6ULL驱动开发:Platform总线设备与驱动匹配机制详解

2026/9/6 8:08:03 拓冰建站 浏览量
i.MX6ULL驱动开发:Platform总线设备与驱动匹配机制详解 i.MX6ULL这块芯片在嵌入式Linux圈子里算是入门级经典了Cortex-A7内核性价比高资料也多。但很多人反映裸机例程跑得飞起一上Linux驱动就懵了。尤其是Platform总线这套机制网上文章不少但大多是贴代码讲API真正把“设备是怎么和驱动对上眼的”讲透的不多。这篇文章我打算换个思路不直接贴大段大段的驱动代码而是把Platform设备与驱动的匹配机制掰开揉碎了讲。我先说清楚这套机制解决了什么问题再带你走一遍内核里匹配的完整流程接着用我们实际项目里的一个按键驱动作为案例把我们调试过程中遇到的坑和排查思路都交代清楚。如果你正在学嵌入式Linux驱动或者准备用i.MX6ULL做产品这篇文章应该能帮你省不少时间。1. 为什么需要Platform总线机制先解决“设备”和“驱动”怎么认识的问题1.1 从最原始的驱动写法说起我们刚开始学驱动的时候最常见的写法是什么直接在一个file_operations里操作寄存器地址。比如操作GPIO直接ioremap一个物理地址然后写寄存器。static int __init gpio_demo_init(void) { gpio_base ioremap(GPIO1_BASE, 0x4000); writel(0x5, gpio_base 0x0); // 设置复用 writel(0x1, gpio_base 0x4); // 设置方向 return 0; }这种写法的问题是驱动和硬件地址、中断号、时钟频率这些信息是硬编码在代码里的。如果换了板子哪怕只是改了一个GPIO引脚都得重新编译内核。这种驱动不具备可移植性而且代码里的所有硬件细节没法被系统统一管理。1.2 设备树出现后的变化后来设备树Device Tree引入了硬件信息被描述成一个个节点驱动不再需要知道具体地址。驱动通过of_property_read_u32、of_iomap这些API从设备树节点里动态获取硬件资源。这一步确实解决了硬件描述的问题硬件资源从代码里解放出来变成了树形结构的数据。但这只解决了“资源怎么描述”的问题还没有解决“设备和驱动怎么匹配”的问题。1.3 Platform总线把两者串起来这个时候Platform总线机制的定位就清晰了它是连接设备device和驱动driver的桥梁。它定义了一套统一的规则和匹配流程让操作系统能够自动地、可靠地完成“设备”与“驱动”的配对。你可以把Platform总线理解成“红娘”。设备侧提交一份“征婚启事”设备节点驱动侧提交一份“择偶标准”驱动匹配表Platform总线在系统启动或驱动加载时拿着两侧的资料做匹配。配对成功就调用驱动的probe函数代表联姻成功。这套机制带来的好处特别明显驱动代码可以做成模块动态加载和卸载不用每次改动都重新编译内核一个驱动可以支持多个同类型的设备只要它们描述的资源不同设备和驱动解耦产品迭代时替换硬件只需要修改设备树驱动层基本不用动2. 匹配机制的完整拆解从接口到内核源码一个细节都不放过2.1 两个核心结构体device和driver既然Platform总线是红娘那得先看看“征婚启事”和“择偶标准”长什么样。在Linux内核里这两份资料分别对应struct platform_device和struct platform_driver。// 设备侧 struct platform_device { const char *name; // 设备名 int id; // 设备编号 struct device dev; // 内嵌通用设备结构体 u32 num_resources; // 资源数量 struct resource *resource; // 资源数组IO/中断等 const struct platform_device_id *id_entry; // 匹配到的ID表项 };对应驱动侧的结构体很关键与匹配规则直接相关// 驱动侧 struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t); int (*resume)(struct platform_device *); struct device_driver driver; // 内嵌通用驱动结构体 const struct platform_device_id *id_table; // ID匹配表 };这里最重要的是struct platform_driver内嵌的struct device_driver因为真正的匹配规则其实定义在这个结构体里struct device_driver { const char *name; struct bus_type *bus; const struct of_device_id *of_match_table; // 设备树匹配表 int (*probe)(struct device *dev); int (*remove)(struct device *dev); ... };注意了probe函数的指针在struct device_driver里是struct device指针在struct platform_driver里是struct platform_device指针。内核在调用时会把struct device转换成struct platform_device使用的是container_of宏。2.2 四种匹配方式源码级解读内核里真正执行匹配的函数是platform_match位于drivers/base/platform.c。这个函数的匹配顺序是这样的第一关of_driver_match_device。如果驱动的of_match_table不为空就用设备树节点的compatible属性与of_match_table里的compatible字符串逐一比较。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); /* 第一关设备树匹配 */ if (pdrv-driver.of_match_table of_driver_match_device(dev, drv)) return 1; /* 第二关ACPI匹配 */ if (pdrv-driver.acpi_match_table acpi_driver_match_device(dev, drv)) return 1; /* 第三关id_table匹配 */ if (pdrv-id_table) { const struct platform_device_id *id; for (id pdrv-id_table; id-name[0]; id) { if (strcmp(pdev-name, id-name) 0) { pdev-id_entry id; return 1; } } } /* 第四关设备名与驱动名匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }仔细看这个函数顺序是有讲究的。设备树匹配优先级最高因为它最精确能同时比较多个compatible属性。ACPI主要用在x86/ARM服务器平台嵌入式里很少用。id_table匹配和驱动名匹配是兼容老式写法的。在我们i.MX6ULL的开发中最常用的是第一关设备树匹配。这是唯一推荐使用的匹配方式因为设备树已经描述了硬件资源驱动只需要声明自己支持哪些compatible字符串即可。如果of_match_table为空就跳过设备树匹配用设备名与驱动名去比较。这种方式是旧式平台无设备树的做法。在现代内核中只要用了设备树of_match_table就是推荐的标准做法。2.3 of_match_table深挖compatible属性的真正作用of_match_table里放的是struct of_device_id数组这个结构体在include/linux/mod_devicetable.h中有定义struct of_device_id { char name[32]; // 节点名一般不填 char type[32]; // 节点type属性一般不填 char compatible[128]; // compatible属性这是重点 const void *data; // 私有数据指针 };在内核源码的include/linux/mod_devicetable.h中可以看到完整的struct of_device_id定义。设备树里的compatible属性格式是“厂商,型号”比如“fsl,imx6ull-evk-board”。这个字符串在驱动里必须原样写下包括逗号和大小写。内核在匹配时会严格比较字符串用的是of_device_is_compatible函数。有一个细节值得注意of_device_id数组的最后一个元素必须全零填充。这个句柄在内核中非常重要被称作“终止符”。内核通过它判断数组是否遍历完毕。写驱动时漏掉终止符是最常见的错误之一。2.4 匹配成功后的调用链probe函数是如何被调用的当匹配成功之后总线核心会调用device_release_driver_internal最终会执行drv-probe(dev)。但这里还有一个重要的前置条件设备在注册到内核时状态是“unbound”未绑定驱动。当驱动注册时总线会遍历所有设备查找匹配项。当设备注册时总线会遍历所有驱动查找匹配项。这个过程叫做“总线扫描”。扫描完成后匹配成功的设备会与驱动绑定系统会在sysfs中创建符号链接。以i.MX6ULL上某个设备为例/sys/bus/platform/devices/xxx.uart/ /sys/bus/platform/drivers/xxx-serial/在devices目录下可以看到设备drivers目录下可以看到驱动。驱动目录里有一个名为“bind”的文件可以手动强制绑定设备和驱动。这里分享一个调试技巧如果你不确定设备与驱动是否绑定直接查看/sys/bus/platform/drivers/下的目录看设备名是否出现在驱动目录列表中。3. i.MX6ULL按键驱动实战跑一遍完整的设备与驱动匹配流程3.1 设备侧编写dts设备树节点以i.MX6ULL开发板上的一个按键为例。按键接在GPIO1_IO18上我们需要一个设备节点描述这个按键。打开设备树源文件arch/arm/boot/dts/imx6ull-evk.dts在根节点下添加/ { mykey { compatible fsl,imx6ull-mykey; pinctrl-names default; pinctrl-0 pinctrl_mykey; gpios gpio1 18 GPIO_ACTIVE_LOW; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; status okay; }; };同时在iomuxc节点下添加引脚复用配置iomuxc { pinctrl_mykey: mykeygrp { fsl,pins MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x8000 ; }; };注意事项i.MX6ULL的GPIO1_IO18对应的是UART1_CTS_B引脚这个宏定义在arch/arm/boot/dts/imx6ul-pinfunc.h中。不同开发板的引脚对应关系不同要参考原理图。配置完成后重新编译设备树make dtbs烧录dtb到开发板重启后在/sys/firmware/devicetree/base/目录下就能看到mykey节点ls /sys/firmware/devicetree/base/mykey如果这个目录存在说明设备树解析成功设备节点已经注册到了内核。3.2 驱动侧编写完整Platform驱动代码接下来编写驱动。这个驱动要做的事情是注册platform_driver声明of_match_table在probe函数里获取GPIO信息并注册中断实现按键上报功能。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio.h #include linux/interrupt.h #include linux/miscdevice.h #include linux/uaccess.h static int key_irq; static int key_gpio; static int key_value 1; static irqreturn_t key_isr(int irq, void *dev_id) { key_value gpio_get_value(key_gpio); /* 上报事件用户态可通过read读取 */ return IRQ_RETVAL(IRQ_HANDLED); } static ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { int ret; ret copy_to_user(buf, key_value, sizeof(key_value)); if (ret) { return -EFAULT; } return sizeof(key_value); } static const struct file_operations key_fops { .owner THIS_MODULE, .read key_read, }; static struct miscdevice key_miscdev { .minor MISC_DYNAMIC_MINOR, .name mykey, .fops key_fops, }; static const struct of_device_id mykey_of_match[] { { .compatible fsl,imx6ull-mykey }, { /* 终止符必须为零 */ } }; MODULE_DEVICE_TABLE(of, mykey_of_match); static int mykey_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; int ret; /* 从设备树获取GPIO */ key_gpio of_get_named_gpio(np, gpios, 0); if (!gpio_is_valid(key_gpio)) { dev_err(dev, invalid gpio\n); return -EINVAL; } /* 申请GPIO并配置为输入 */ ret devm_gpio_request(dev, key_gpio, mykey); if (ret) { dev_err(dev, gpio request failed\n); return ret; } gpio_direction_input(key_gpio); /* 获取中断号 */ key_irq gpio_to_irq(key_gpio); if (key_irq 0) { dev_err(dev, gpio_to_irq failed\n); return key_irq; } /* 注册中断 */ ret devm_request_irq(dev, key_irq, key_isr, IRQF_TRIGGER_FALLING, mykey, NULL); if (ret) { dev_err(dev, request_irq failed\n); return ret; } /* 注册misc设备 */ ret misc_register(key_miscdev); if (ret) { dev_err(dev, misc_register failed\n); return ret; } dev_info(dev, mykey probed, gpio%d, irq%d\n, key_gpio, key_irq); return 0; } static int mykey_remove(struct platform_device *pdev) { misc_deregister(key_miscdev); return 0; } static struct platform_driver mykey_driver { .probe mykey_probe, .remove mykey_remove, .driver { .name mykey, .of_match_table mykey_of_match, }, }; module_platform_driver(mykey_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name);3.3 编译与加载全过程一步步验证匹配是否成功驱动写成模块用交叉编译工具链编译。对于i.MX6ULL一般是arm-linux-gnueabihf-gcc配合内核源码树。我们用的编译命令是export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make -C /path/to/kernel M$(pwd) modules这里有一个非常重要的前提内核源码必须已经配置过并且在编译驱动前需要先编译内核否则缺少Module.symvers文件驱动编译时会报错。编译完成后得到mykey.ko。把模块拷贝到开发板可以用NFS挂载也可以用scp执行加载insmod mykey.ko模块加载日志mykey mykey: mykey probed, gpio18, irq74当看到这一行日志时说明probe函数被成功调用设备和驱动匹配成功了。调试时在probe里加dev_info打印这是最直观的验证方法。验证设备节点ls -l /dev/mykey cat /sys/bus/platform/devices/mykey/在驱动加载前设备名是“mykey”在驱动加载后系统会自动在设备与驱动之间创建关联。3.4 如果匹配失败无probe日志的现场排查思路有时候驱动编译加载都没有报错但就是没有probe日志。这时需要按以下顺序排查先查设备树节点是否正确解析。进入/sys/firmware/devicetree/base/mykey/目录查看compatible属性的内容cat /sys/firmware/devicetree/base/mykey/compatible输出应该是fsl,imx6ull-mykey注意cat命令不会自动在末尾添加换行符所以输出后面直接跟#提示符是正常的。再查驱动是否注册成功cat /sys/bus/platform/drivers/mykey/如果驱动注册成功这里会显示驱动目录。如果这两个条件都满足但probe没被调用那就要检查of_match_table里的compatible字符串与设备树里的compatible是否完全一致了。包括空格的差异都会导致匹配失败。4. 从源码角度解读我调试匹配机制时用到的几个关键函数4.1 of_driver_match_device与of_device_is_compatibleof_driver_match_device是匹配流程中最核心的函数它调用of_device_is_compatible。我们看看of_device_is_compatible的源码逻辑int of_device_is_compatible(const struct device_node *device, const char *compat) { const char *cp; int cplen; int i; cp of_get_property(device, compatible, cplen); if (cp NULL) return 0; while (cplen 0) { if (of_compat_cmp(cp, compat, strlen(compat)) 0) return 1; i strlen(cp) 1; cp i; cplen - i; } return 0; }这个函数做的事情很简单遍历设备树节点compatible属性中所有字符串一个节点可以有多个compatible属性值逐一与驱动提供的字符串比较。注意of_compat_cmp就是strcmp的封装只是做了一个大小写敏感性设置的宏封装默认区分大小写。从这段代码可以得出一个关键词完全匹配。驱动里的“fsl,imx6ull-mykey”和设备树里的“fsl,imx6ull-mykey”必须一字不差。4.2 bus_probe_device与really_probe函数系统的大致流程是当设备树被解析后of_platform_bus_probe会遍历设备树节点为每个节点创建platform_device然后调用bus_probe_device。bus_probe_device位于drivers/base/bus.c会触发设备与驱动之间的匹配匹配成功则调用really_probe。真正执行probe的是really_probe路径在drivers/base/dd.c。really_probe中有一段代码值得关注static int really_probe(struct device *dev, struct device_driver *drv) { ... /* 如果设备已经有驱动绑定则不再执行 */ if (dev-driver) { ret -EBUSY; goto done; } ... /* 调用驱动总线的probe函数 */ if (dev-bus-probe) { ret dev-bus-probe(dev); if (ret) goto probe_failed; } else if (drv-probe) { ret drv-probe(dev); if (ret) goto probe_failed; } ... }platform_bus的probe函数是platform_probe它做的事情是把struct device转换为struct platform_device然后调用platform_driver的probe函数。搞清楚了这一整套链路你在调试设备与驱动相关问题时就能有的放矢。比如probe没有被调用问题大概率出在匹配阶段probe被调用了但报错问题就出在probe函数内部。4.3 设备树里compatible属性的多个值与匹配顺序设备树里compatible属性可以写多个字符串这在产品开发中很有用。例如compatible fsl,imx6ull-evk, fsl,imx6ull;内核匹配时会优先与第一个字符串比较。如果驱动只声明了“fsl,imx6ull”也能匹配成功因为of_device_is_compatible会遍历所有compatible值。这个机制的意义在于一个通用的驱动可以匹配多个厂商的硬件描述。比如你可以写一个通用的LCD驱动compatible写“boe,evk-screen”“innolux,evk-screen”这样同一个驱动就能兼容多个屏幕厂家。与之相反同一个驱动如果写了多个of_device_id项就可以匹配多个设备节点。这在驱动设计中被称为“多设备兼容”。比如一个GPIO按键驱动可以匹配多个按键节点每个节点的资源不同。5. 常见问题与排查技巧速查表这些坑我都替你踩过了5.1 模块加载报错Unknown symbol / Overlap加载驱动时如果看到mykey: Unknown symbol of_get_named_gpio说明内核没有导出这个符号或者模块编译时的内核版本与目标内核不一致。解决方法是重新配置内核编译完成后确认Module.symvers中存在对应符号。如果看到mykey: module verification failed: signature and/or required key missing这是内核开启了模块签名校验加载模块时需要用insmod配合modprobe的配置或者在内核配置中关闭CONFIG_MODULE_SIG。开发阶段一般直接关闭。5.2 驱动加载成功但probe未调用按顺序自查我最常遇到的场景是insmod后dmesg只有“module init”日志probe函数根本没执行。按照3.4节的排查思路列出自查清单检查项命令/方法成功标志设备树节点存在ls /proc/device-tree/mykey目录存在compatible属性正确cat /proc/device-tree/mykey/compatible内容与驱动匹配表一致驱动注册成功cat /sys/bus/platform/drivers/mykey/目录存在设备未绑定其他驱动cat /sys/bus/platform/devices/mykey/无driver链接其中有一点容易被忽略设备的dts节点里status属性。如果status disabled虽然节点会出现在设备树中但不会被创建为platform_device。我们在调试时习惯把status写成“okay”但量产配置里可能会通过bootargs动态修改status这一点要留意。5.3 probe函数返回负数资源申请失败如果probe被调用了但日志显示mykey mykey: gpio request failed这个情况通常不是GPIO被占用就是设备树里的gpios属性格式错误。检查gpio编号是否符合i.MX6ULL的GPIO编号规则GPIO1的IO18对应的全局编号是18。ipg ((gpio_bank - 1) * 32) pin_indexGPIO1_IO18 (1-1)*32 18 18 GPIO5_IO09 (5-1)*32 9 137如果gpio编号超过了合理范围应该检查设备树里gpios gpio1 18 GPIO_ACTIVE_LOW中的gpio1引用是否正确。有些默认的设备树头文件里gpio1节点序号可能不是从0开始需要看i.MX6ULL的gpio控制器宏定义。5.4 中断不工作用devm_request_irq代替request_irq在实际项目中我见过不少驱动在probe里用request_irq申请中断但没有在remove或错误路径里释放中断。一旦probe失败中断资源就泄露了。推荐的做法是使用devm_request_irq。这个API是managed版本驱动卸载或probe失败时内核会自动释放资源。同理gpio申请也不要再用gpio_request直接用devm_gpio_request。这类资源管理的API在嵌入式Linux驱动开发中非常实用。probe函数里多个资源申请如果中间某一步失败只需要return错误码内核会帮你把前面已经成功申请的资源全部回收不用手动写一堆goto错误处理。5.5 设备树修改了却不生效dtb缓存的坑这个坑非常经典。开发阶段经常是修改了dts之后重新编译dtb烧录重启发现改动完全没有生效。排查思路是确认u-boot加载的是哪个dtb。有些板子的u-boot环境变量里fdt_file指向的是另一个dtb文件或者u-boot会默认加载设备树分区里的dtb而不是你烧录的那个。在u-boot控制台里执行printenv fdt_file查看当前使用的设备树文件名。如果不如预期可以通过setenv fdt_file imx6ull-14x14-evk.dtb saveenv重新指定设备树文件。另外如果板子上有多个dtb分区确认烧录命令是否正确指定了目标分区。非常常见的坑是你以为烧的是dts编译出来的新dtb实际上烧到了另一个分区启动时加载的还是旧文件。6. 现网排查实录一次由compatible拼写问题导致的匹配失败全过程6.1 现象描述按键驱动无声无息讲一个实际操作中的案例。我们给客户做的一块板子基于i.MX6ULL定制有一个GPIO按键客户要求做成Linux输入子系统上报事件。驱动代码是我们提供的参考我们自己的参考板是能正常工作的。但是客户反馈在你们的板子上完全没有反应。我们拿到板子后串口登录执行insmod facebook_key.kodmesg无任何probe日志。第一反应就是匹配失败。执行cat /sys/bus/platform/drivers/facebook_key/发现驱动目录存在说明platform_driver注册成功。执行ls /sys/firmware/devicetree/base/发现设备树里没有我们要求的key节点名。这就有意思了我们内核里明明有节点。6.2 排查过程逐层深入第一个怀疑点设备树没烧录正确。查看u-boot fdt_file确认使用的是我们编译的dtb没问题。第二个怀疑点dts源文件里节点被某个宏开关包含编译时没展开。检查发现代码里节点是在“iomuxc”下的而不是根节点下。DTS文件里节点位置不同编译结果是完全不同的。我们原来的参考板用的是/ { facebook_key { compatible fsl,imx6ull-facebook-key; ... }; };客户板子使用的是他们自己维护的dts他们加的位置是iomuxc { facebook_key { compatible fsl,imx6ull-facebook-key; ... }; };这就有本质区别了。根节点下的节点会被of_platform_default_populate遍历并创建platform_device。而iomuxc子节点不会被默认遍历除非iomuxc驱动自己定义并注册子设备。这个问题浪费了我们将近一个小时。最终修改dts把facebook_key移回根节点下重新编译dtb启动后模块probe成功。6.3 根因分析与总结这个问题的根因对设备树节点位置与platform设备自动创建的规则理解不透彻。不是所有节点都会自动创建为platform_device只有根节点下的节点或者特定bus下的节点才会被遍历。在内核启动早期of_platform_bus_probe会遍历根节点下的节点为它们创建platform_device。而对于根节点下的simple-bus节点内核也会递归遍历其子节点。iomuxc节点不是simple-bus它的子节点不会被自动遍历。所以不管你在iomuxc里添加多少个自定义节点都不会自动生成platform_deviceprobe自然不会被调用。这个案例告诉我们写驱动前先搞清楚两个问题设备树节点放在哪里选择什么compatible属性放在哪个父节点下面这直接决定了系统能否正确创建设备。7. 调试Platform匹配机制的几个实战工具帮你少走弯路7.1 sysfs用几行命令快速判断绑定状态sysfs是调试驱动的第一利器。对于platform设备最常用的查看方法ls /sys/bus/platform/devices/ ls /sys/bus/platform/drivers/如果你看设备与驱动是否绑定看设备目录下有没有driver符号链接readlink /sys/bus/platform/devices/mykey/driver如果输出类似../../../../bus/platform/drivers/mykey说明绑定成功。如果提示No such file or directory说明设备节点存在但没有匹配到驱动。在驱动目录下还有两个文件bind和unbind。这两个文件可以手动强制绑定和解绑设备在公司产品调试中非常实用echo mykey /sys/bus/platform/drivers/mykey/unbind echo mykey /sys/bus/platform/drivers/mykey/bind这个操作在驱动升级测试时特别有用不用重启系统就能重新执行probe。7.2 ftrace追踪匹配流程的完整链路如果想知道匹配过程在哪个环节出了问题可以用ftrace动态追踪函数调用。比如echo function_graph /sys/kernel/debug/tracing/current_tracer echo platform_match /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on然后cat /sys/kernel/debug/tracing/trace查看调用过程。这个工具在嵌入式Linux调试中很强大但很多入门开发者不太会用。这里分享几个常用的跟踪点platform_match匹配入口of_driver_match_device设备树匹配函数platform_probe总线probe函数really_probe设备与驱动绑定的核心函数这些函数在drivers/base/platform.c和drivers/base/dd.c中查看调用过程就能知道匹配是走到了哪一步失败的。7.3 dev_dbg与动态调试开关很多内核驱动里都有dev_dbg调试打印只是默认不输出。开启方法echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control或者更直接的在驱动里用dev_info代替dev_dbg。不过在产品发布时建议用dev_dbg加上动态调试开关这样不用重新编译内核就能控制调试输出级别。7.4 一个额外的排查技巧检查设备节点是否被正确转换为设备有时候设备树节点存在但内核没有为它创建设备。这时可以通过of_platform_populate机制来判断。在/sys/firmware/devicetree/base/目录下的节点并不一定都会在/sys/bus/platform/devices/下存在。这里有一条经验法则设备树下的节点要生成platform_device要么节点在根节点下且visible要么父节点是simple-bus要么驱动通过of_platform_populate主动创建子设备。如果节点不在这些条件内就需要在父驱动里显式注册platform_device。8. 匹配机制的进阶用法与后续扩展思路8.1 同一驱动匹配多个设备如何区分不同设备在实际产品中经常会遇到这种情况一块板子上有两个同类型的按键或传感器硬件资源不同但驱动逻辑是一样的。这时of_match_table里写一组compatible就能匹配多个节点因为每个节点都是独立的platform_device。在probe函数里通过pdev-dev.of_node拿到当前设备节点of_get_named_gpio、of_property_read_string等API获取当前设备的专属资源就能区分不同设备。static int mykey_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; int gpio; gpio of_get_named_gpio(np, gpios, 0); dev_info(pdev-dev, gpio %d\n, gpio); ... }同一个platform_driver会被多次调用probe每次传入不同的pdev因此probe里一定不能使用全局变量保存设备特定数据。如果要保存私有数据应该使用platform_set_drvdata和platform_get_drvdata或者devm_kzalloc申请动态内存。8.2 从Platform到Input子系统按键驱动的完整形态对于按键这类设备驱动一般不会直接暴露misc设备给用户态而是注册为Linux input子系统设备。这样用户态通过/dev/input/eventX统一读取事件。刚才的例子中我们用miscdevice实现了一套简化的读取接口。更好的做法是在probe里申请input_dev在中断里上报按键事件这样系统中的其他服务比如系统按键监听、电源管理都能收到事件。static irqreturn_t key_isr(int irq, void *dev_id) { struct mykey_dev *key dev_id; input_report_key(key-input, KEY_ENTER, 1); input_sync(key-input); /* 硬件消抖后上报释放 */ ... }这种设计的好处是驱动只上报标准输入事件用户态的Qt界面、命令行工具都可以直接复用不必为每个按键单独定制接口。8.3 匹配机制在不同内核版本间的差异内核版本不同platform_match的实现可能会有细微差异。比如编译内核时开启CONFIG_OF后of_node不为空时的优先级最高。但有些老版本内核在of_match_table为空时如果设备树节点存在仍然可以匹配成功。以我们开发中常用的Linux 4.1.15NXP官方BSP和Linux 5.4主线内核为例两者在drivers/base/platform.c的实现基本一致但一些API有变动。比如of_get_named_gpio在5.4版本中已被标记为deprecated推荐使用gpiod_getplatform_get_resource第3个参数在部分版本中有差异设备树中gpio属性与gpio phandle的解析方式在gpiod接口下更简洁这个差异告诉我们遇到问题先确认内核版本再查找对应版本的源码。网络上的博客文章很多是基于老版本内核写的照搬过来可能编译都无法通过。8.4 从设备树匹配到其他总线匹配机制了解Platform总线匹配机制后再看其他总线的匹配机制就能举一反三。比如I2C总线它的匹配机制是遍历i2c_device_id和of_match_table优先级与Platform类似。SPI总线也是类似的设计。可以说内核的设备模型是一个统一的框架bus_type, device, device_driver三个角色构成了整个驱动模型的核心。Platform只是其中最基础、最常用的一种专用总线。理解了Platform匹配机制再去学I2C、SPI、USB等驱动就会感觉处处熟悉。因为万变不离其宗都是设备模型那一套无非是多了些特定总线的匹配字段和传输协议。我在实际带新人时都会要求他们先把Platform匹配机制彻底搞明白。这就像学编程先搞懂函数调用栈一样。逻辑通了后面学什么都是时间问题。这套机制虽然看起来繁琐但它带来的设备与驱动解耦能力在真实产品迭代中的价值是无法替代的。你在自己的板子上跑通一遍匹配流程再对照这篇博文看一下细节应该就能形成自己的理解了。