
1. 从零上手i.MX6ULL 项目里为什么要先搞懂 Platform 机制拿到 i.MX6ULL 这块板子做 Linux 驱动开发第一道绕不过去的坎就是设备与驱动的匹配机制。很多新手上来就急着写字符设备驱动register_chrdev一把梭LED 也点亮了串口也打印了以为自己入门了。结果等你想把驱动写得规范一点、想接入设备树、想支持多个硬件版本的时候才发现 Linux 驱动模型根本不是那么玩的。i.MX6ULL 是 NXP 推出的 Cortex-A7 内核处理器主频 800MHz在工业控制、物联网网关、便携设备里用得非常多。它的外设资源很丰富GPIO、UART、I2C、SPI、PWM、ADC 一应俱全。但正因为外设多如果每个驱动都靠“硬编码”去指定寄存器地址、中断号、引脚复用那代码会变得极其难维护。Platform 机制就是为了解决这个问题而存在的。这套机制做的事情用大白话讲就一句话把“设备有什么资源”和“驱动怎么操作设备”彻底分开。设备侧只负责描述“我有哪些寄存器、哪些中断、哪些引脚”驱动侧只负责写“拿到这些资源之后怎么用”。两边通过一个匹配过程绑定在一起绑定成功之后驱动的probe函数就会被调用你真正干活的地方就在那里。这篇文章我会用 i.MX6ULL 的实际开发场景把 Platform 设备与驱动匹配机制从头到尾拆开讲。包含最核心的三种匹配方式、实际工程里的驱动框架怎么搭、设备树节点怎么写、匹配失败怎么办以及我在实际项目中踩过的那些坑。不管你是刚接触嵌入式 Linux 驱动开发还是已经写过几个驱动但一直没搞懂 Platform 内部逻辑这篇文章都能给你一个比较完整的答案。2. 先理解设备模型总线、设备、驱动三者之间的关系2.1 为什么需要 Platform 总线Linux 驱动模型里有个核心概念叫“总线”。我们最熟悉的物理总线是 I2C、SPI、USB、PCI 这些设备挂在上面的驱动也注册在这条总线上总线负责把设备和驱动配对。但问题来了i.MX6ULL 内部有很多外设比如 GPIO 控制器、UART 控制器、LCD 控制器它们并不是挂在某个物理总线上而是直接连着 CPU 的内部总线或者内存映射空间。在 Linux 的眼里这些设备没有一条现成的“物理总线”可以挂靠。你总不能给每个内部外设都虚拟一条 I2C 总线出来吧于是内核就搞了一条虚拟总线叫 Platform 总线。所有“直接挂在 CPU 内存映射空间上的设备”都统一挂到这条虚拟总线上。驱动也注册在这条总线上。总线的作用只有一个提供匹配规则。谁和谁匹配怎么匹配匹配上了之后调用哪个函数这一切都是由总线来调度的。这样一来Linux 的设备模型就统一了不管你是什么设备都逃不出“总线 设备 驱动”这个三角形结构。你要写一个 i.MX6ULL 上的外设驱动本质上就是往这个三角形里填充内容。2.2 设备侧有哪些东西在 Platform 机制里设备侧有两种表达方式。早期内核比如 Linux 3.x 之前用的是struct platform_device直接在 C 代码里定义一个设备结构体里面填上设备名字、编号、资源寄存器地址、中断号等然后通过platform_device_register注册到内核里。后来设备树Device Tree普及之后这种方式基本被取代了。现在我们在 i.MX6ULL 上做开发设备侧的表达方式是设备树里的一个节点。比如led { compatible mycompany,led; reg 0x020c406c 0x4; interrupts GIC_SPI 42 IRQ_TYPE_EDGE_RISING; pinctrl-names default; pinctrl-0 pinctrl_led; status okay; };这个节点描述的信息包括设备叫什么compatible、寄存器地址在哪reg、中断是哪个interrupts、引脚复用怎么配pinctrl。内核启动时会解析设备树为每个节点创建一个struct platform_device。也就是说设备树节点最后也会变成 platform_device只是我们不用自己写 C 结构体了。这里有个非常重要的认知设备树不是驱动它只是设备的“身份证”和“资源清单”。驱动还是得老老实实写 C 代码。2.3 驱动侧要做什么驱动侧对应的是struct platform_driver。你要做的事情就是定义一个这样的结构体填充匹配表实现probe和remove回调然后注册到内核。static const struct of_device_id led_of_match[] { { .compatible mycompany,led }, { /* sentinel */ } }; static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver);module_platform_driver这个宏展开之后会自动帮你处理module_init和module_exit注册和卸载都封装好了。这是目前写 Platform 驱动最标准、最简洁的写法。2.4 匹配成功的“化学反应”当设备树节点被解析成 platform_device 之后内核会把这个设备挂到 Platform 总线上。同时总线手里有一堆已经注册的 platform_driver。总线要做的就是拿设备的信息和每个驱动的匹配表逐一比对。一旦找到匹配项立刻调用驱动里的probe函数。probe是驱动真正开始工作的起点。在这个函数里你要完成从struct platform_device里获取寄存器地址、中断号等资源对寄存器进行映射ioremap或devm_ioremap_resource注册中断处理函数初始化硬件创建字符设备、注册 miscdevice 或 input 设备等所以你可以把probe理解为驱动开发的“主战场”。前面写的所有匹配表、设备树节点都是为了能把probe叫起来。3. 深度拆解Platform 三种匹配方式的原理与实战选择3.1 第一种方式设备树 compatible 匹配设备树 compatible 匹配是目前 i.MX6ULL 项目中最常用的方式没有之一。原因很简单几乎所有主流 BSP 和设备树都已经用这种方式组织了跟随主流就是最省事的选择。匹配原理非常直接设备树节点里的compatible属性是一个字符串列表驱动侧of_device_id表里也有一个compatible字符串。总线匹配时会拿设备节点的compatible字符串依次和驱动表里的字符串做字符串比较完全相同就匹配成功。比如设备树里有compatible mycompany,led;驱动里有static const struct of_device_id led_of_match[] { { .compatible mycompany,led }, { .compatible mycompany,led-rgb }, { /* sentinel */ } };只要有一项完全相等匹配即成功。这里有几个细节需要特别注意第一compatible字符串至少要有两个字段厂商前缀加设备名。比如mycompany,led前面的mycompany是厂商名后面的led是设备名。不要用纯单词比如直接写compatible led这不符合设备树规范也容易和别的驱动冲突。第二of_device_id表的最后一个元素必须是空结构体作为哨兵。{ /* sentinel */ }不是注释是有实际作用的结束标志。漏掉的话内核在遍历表时可能越界访问导致不可预知的行为。第三of_match_table的名字可以随意起但是结构体类型必须是struct of_device_id。这个表只负责匹配不负责传数据。在 i.MX6ULL 的实际开发中你通常会看到厂商 SDK 里的驱动大量使用这种匹配方式。比如 GPIO 按键驱动、LED 驱动、PWM 驱动全都是 compatible 匹配。因为这种方式和设备树天然结合硬件配置全部放在设备树里驱动代码不需要重新编译就能适配不同的引脚复用和寄存器地址。3.2 第二种方式platform_driver 名字匹配除了设备树 compatible 匹配还有一种比较传统的匹配方式通过platform_driver结构体里的driver.name和platform_device里的name字段进行字符串比较。这种方式在设备树出现之前非常常见。当年我们在 C 代码里定义 platform_devicestatic struct platform_device led_device { .name imx6ull-led, .id -1, .resource led_resources, .num_resources ARRAY_SIZE(led_resources), };驱动侧static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name imx6ull-led, }, };只要两边name字符串一致总线就会把设备和驱动配对。但问题在于用设备树之后这种方式就不太适用了。因为设备树中没有一个直接对应platform_device.name的字段。设备树节点生成 platform_device 时name会默认取节点名去掉地址部分。理论上你仍然可以让驱动侧的driver.name等于设备节点名来匹配但这种做法非常脆弱——设备节点名随时可能被人修改而且一个节点如果要支持多个驱动变体用名字匹配根本做不到。所以我的建议是在新项目里不要单独依赖这种方式。它更适合学习和理解不适合实战。3.3 第三种方式ACPI 匹配和设备树 ID 匹配ACPI 匹配主要用在 x86 平台上i.MX6ULL 这种 ARM 平台基本不涉及这里简单提一下让大家有个概念。ACPI 表里通过HIDHardware ID和CIDCompatible ID来标识设备驱动侧通过acpi_device_id表匹配。这个和嵌入式 Linux 关系不大不用细究。再就是 ID 表匹配即id_table字段。这个字段在老的 platform_driver 里很常见static const struct platform_device_id led_id_table[] { { imx6ull-led, 0 }, { /* sentinel */ } }; static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .id_table led_id_table, .driver { .name imx6ull-led, }, };platform_device_id里面可以带一个driver_data字段用来区分同系列设备的不同型号。比如你的驱动要同时支持imx6ull-led和imx6ul-led并且两者初始化参数不同就可以在 ID 表里给不同名字绑定不同的driver_data在 probe 里取出来做分支处理。不过要注意ID 表匹配和设备树 compatible 匹配是两套体系。在设备树环境下内核会优先使用of_match_table进行匹配。只有当of_match_table没匹配上时才会去看id_table。所以你在实际项目中通常只需要维护好of_match_table就够了id_table可以留空不设。3.4 三种方式对比与选型建议老规矩先把三种方式的核心差异整理成一张表匹配方式设备侧表达匹配依据适用场景实战推荐度设备树 compatible 匹配设备树节点 compatible 属性字符串完全相等所有基于设备树的项目包括 i.MX6ULL极高platform_driver 名字匹配platform_device.name 字段字符串完全相等无设备树的老内核、学习实验低ID 表匹配platform_device_id 名称字符串比较 driver_data老代码兼容、同系列设备复用驱动中ACPI 匹配ACPI 表 HID/CID字符串比较x86 平台不适用选型建议很简单只要是基于设备树启动的 Linux一律使用 compatible 匹配。不仅因为这是内核推荐的现代做法更因为你写的驱动要能通用设备树才是描述硬件的标准语言。3.5 匹配的优先级和内部过程你可能好奇内核到底是怎么把这几种匹配方式串起来的其实 platform 总线的匹配函数是platform_match它的内部匹配顺序大致如下如果of_match_table不为空尝试设备树 compatible 匹配成功则返回。如果 ACPI 匹配可用尝试 ACPI 匹配成功则返回。如果id_table不为空遍历 ID 表和设备名比较成功则返回。最后退回到driver.name和设备名比较。全部失败返回不匹配。这里有一个很多人会踩的坑如果你在驱动里同时设置了of_match_table和driver.name而且两边都写了字符串结果发现怎么都匹配不上那多半是设备树节点的 compatible 没写对或者驱动表里的字符串多打了空格。字符串比较是非常严格的差一个字符都不行。4. 手写一个完整的 i.MX6ULL Platform 驱动实例4.1 实例目标说明为了让前面的理论落地我写一个完整的示例一个基于 i.MX6ULL 的按键驱动。这个按键接在 GPIO 上按下时电平变化驱动通过 Platform 机制匹配设备树节点读取 GPIO 中断配置并在中断处理函数里上报事件。为什么选按键因为按键驱动涉及 GPIO、中断、设备树、Platform 匹配麻雀虽小五脏俱全非常适合用来演示整套流程。4.2 设备树节点设计在 i.MX6ULL 的设备树源码里我们可以往根节点下加一个自定义节点。假设按键接在 GPIO5_IO01 上对应 imx6ull 的 GPIO5 第 1 脚。设备树节点这样写/ { key { compatible mycompany,key; pinctrl-names default; pinctrl-0 pinctrl_key; key-gpio gpio5 1 GPIO_ACTIVE_LOW; status okay; }; };同时需要在iomuxc节点里添加引脚复用配置iomuxc { pinctrl_key: keygrp { fsl,pins MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x80000000 ; }; };这里我用了key-gpio属性来引用 GPIO而不是直接写中断号。这是比较推荐的做法因为 GPIO 驱动会帮你管理中断号的申请和释放。你在驱动里只需要用devm_gpiod_get获取struct gpio_desc再用gpiod_to_irq得到中断号根本不用关心具体的中断号是多少。status okay表示这个设备启用。如果你调试过程中想临时停用某个设备节点可以把 status 改成disabled内核就不会为它创建 platform_device 了。4.3 驱动代码完整实现#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/interrupt.h #include linux/miscdevice.h #include linux/uaccess.h struct key_dev { struct gpio_desc *gpio; int irq; int key_state; struct miscdevice mdev; }; static struct key_dev *g_key; static irqreturn_t key_isr(int irq, void *dev_id) { struct key_dev *dev dev_id; dev-key_state gpiod_get_value(dev-gpio); printk(KERN_INFO key state: %d\n, dev-key_state); return IRQ_HANDLED; } static ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { struct key_dev *dev container_of(file-private_data, struct key_dev, mdev); char val dev-key_state 0; if (copy_to_user(buf, val, 1)) return -EFAULT; return 1; } static const struct file_operations key_fops { .owner THIS_MODULE, .read key_read, }; static int key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct key_dev *key; int ret; key devm_kzalloc(dev, sizeof(*key), GFP_KERNEL); if (!key) return -ENOMEM; key-gpio devm_gpiod_get(dev, key, GPIOD_IN); if (IS_ERR(key-gpio)) { dev_err(dev, failed to get key gpio\n); return PTR_ERR(key-gpio); } key-irq gpiod_to_irq(key-gpio); if (key-irq 0) { dev_err(dev, failed to get irq\n); return key-irq; } ret devm_request_irq(dev, key-irq, key_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, key, key); if (ret 0) { dev_err(dev, failed to request irq\n); return ret; } key-mdev.minor MISC_DYNAMIC_MINOR; key-mdev.name key; key-mdev.fops key_fops; ret misc_register(key-mdev); if (ret 0) { dev_err(dev, failed to register misc device\n); return ret; } g_key key; platform_set_drvdata(pdev, key); dev_info(dev, key driver probed successfully\n); return 0; } static void key_remove(struct platform_device *pdev) { struct key_dev *key platform_get_drvdata(pdev); misc_deregister(key-mdev); g_key NULL; } static const struct of_device_id key_of_match[] { { .compatible mycompany,key }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, key_of_match); static struct platform_driver key_driver { .probe key_probe, .remove key_remove, .driver { .name key, .of_match_table key_of_match, }, }; module_platform_driver(key_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL key driver using platform mechanism);代码不算长但每一部分都有讲究。我挑几个关键点展开解释。第一个关键点是devm_gpiod_get。这个函数会解析设备树里的key-gpio属性返回一个 GPIO 描述符。devm前缀表示资源由设备管理设备移除时自动释放。用devm_*系列函数可以极大简化错误处理路径不用在每一个错误分支里手动释放之前申请的资源。这是现代内核驱动开发的推荐实践。第二个关键点是gpiod_to_irq。它把 GPIO 描述符转换为 Linux 中断号。这样设备树里就不用写死中断号GPIO 控制器驱动会帮你完成映射。如果你的设备树里直接写interrupts gpio5 1 IRQ_TYPE_EDGE_BOTH那驱动侧也可以直接用platform_get_irq(pdev, 0)获取中断号两种方式都可行。我推荐用 GPIO 描述符方式因为更抽象、更可移植。第三个关键点是MODULE_DEVICE_TABLE(of, key_of_match)。这个宏看起来不起眼实际上非常重要。它会把匹配表信息写入模块的编译产物中这样当你把编译好的 .ko 文件拷贝到板子上用modprobe加载时内核可以根据模块自带的设备表信息判断这个模块是否匹配当前系统中的设备。如果你漏掉这个宏modprobe可能加载失败提示「modprobe: module not found」之类的问题。第四个关键点是misc_register。我用了 miscdevice 来创建设备节点而不是完整的字符设备注册。miscdevice 是 Linux 提供的一种简化的杂项设备框架适合那些不需要主设备号的简单设备。它的主设备号统一是 10次设备号动态分配。对于按键这种只有 read 一个接口的设备来说miscdevice 是最高效的选择。4.4 编译与加载步骤在 i.MX6ULL 的 SDK 环境下通常你会有一个编译内核模块的 Makefileobj-m : key.o KERNELDIR : /path/to/your/kernel/source PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- clean编译出key.ko后拷贝到板子上执行insmod key.ko正常情况下你会看到内核打印类似这样的日志key driver probed successfully如果设备树节点没有正确匹配你会看到key: probe of platform 20c8000.key failed with error -ENODEV这里20c8000.key是设备树节点自动生成的 platform device 名称-ENODEV表示没有可用的设备或 GPIO 获取失败。这个日志是排查问题的重要线索。卸载驱动rmmod key卸载时会调用key_remove释放 misc 设备和相关资源。4.5 验证功能加载驱动之后检查设备节点是否创建成功ls -l /dev/keycat 一下读取按键状态cat /dev/key按键按下或释放时dmesg里能看到中断处理函数打印的key state: 0/1信息。到这一步一个完整的 Platform 设备与驱动匹配流程就闭环了。5. 匹配失败排查从日志到设备树的逐层定位法5.1 常见失败现象与原因匹配失败是驱动开发里最让人头疼的问题。我根据经验整理了 i.MX6ULL 场景下最常见的几种失败现象和对应原因现象常见原因排查方向insmod 后没有任何 probe 日志compatible 字符串不匹配检查设备树和驱动表字符串probe 被调用但返回 -ENOMEM内存申请失败检查 devm_kzalloc 大小、系统内存probe 被调用但返回 -EINVAL参数无效检查 GPIO 属性、中断配置probe 被调用但返回 -ENODEV设备不存在或资源获取失败检查设备树 status、reg、gpio 属性modprobe 提示 module not found缺少 MODULE_DEVICE_TABLE添加 of 设备表宏5.2 第一步确认设备树节点是否生成了 platform_device在板子上执行ls /sys/bus/platform/devices/你会看到一大堆设备目录。找一个名字和你的节点相关的比如20c8000.key。如果找不到说明你的设备树节点没有被解析成 platform_device可能原因有设备树编译失败或没打包进 dtb节点 status 设置为 disabled节点挂在了一个本身被 disabled 的总线节点下此时需要回到设备树源码仔细检查节点放的位置。我在实际项目里就遇到过把自定义节点放在了soc节点下但soc节点被status disabled了结果设备直接消失。后来把节点挪到根节点下才解决。5.3 第二步确认驱动是否注册成功查看已注册的 platform 驱动ls /sys/bus/platform/drivers/你应该能看到key目录。如果加载了模块但这里没有说明platform_driver_register可能失败了。可以检查dmesg看是否有错误信息。module_platform_driver宏正常情况下是不会失败的因为注册失败也会打印一条错误日志。所以如果驱动目录看不到大概率是模块根本没加载成功先用insmod时的返回值判断。5.4 第三步手动触发匹配验证如果你怀疑 compatible 不匹配可以临时在驱动里加上static int key_probe(struct platform_device *pdev) { dev_info(pdev-dev, compatible: %s\n, pdev-dev.of_node-full_name); return -ENODEV; }强行把 probe 拉起来打印出设备节点的路径。然后用of_device_is_compatible(pdev-dev.of_node, mycompany,key)来验证字符串是否匹配。这个方法能快速定位字符串差异问题。另一个更直接的排查方法是看/sys/firmware/devicetree/base目录ls /sys/firmware/devicetree/base/这里反映了实际加载的设备树内容。找到你的 key 节点cat /sys/firmware/devicetree/base/key/compatible输出应该是mycompany,key。如果这里和你驱动里的字符串有差异问题就在这里。5.5 关于 GPIO 和中断的常见匹配陷阱很多时候 probe 成功调用了但中断或 GPIO 初始化失败驱动加载后看不到功能。这里有几个高频坑第一个坑GPIO 编号和复用冲突。i.MX6ULL 的 GPIO 引脚复用是由 IOMUXC 控制的。如果你在设备树里写了pinctrl-0但 pinctrl 配置里的引脚和按键想要的引脚不一致GPIO 驱动可能拿到一个复用了其他功能的引脚。这时候devm_gpiod_get可能返回成功但 gpiod_get_value 读到的电平是乱的。第二个坑GPIO_ACTIVE_LOW的语义。设备树里key-gpio gpio5 1 GPIO_ACTIVE_LOW表示按键按下时的有效电平是低电平。如果你在驱动里用gpiod_get_value读取内核会自动根据 active low 属性反转电平。也就是说按键按下时读到的是 1释放时读到的是 0。如果你手动使用gpio_get_value这类老接口不会做这个反转导致逻辑判断混乱。建议统一使用新的gpiod_*API。第三个坑共享中断。GPIO 中断在 Linux 中很多是共享的同一个 GPIO 控制器下面的多个引脚中断可能共享同一个中断号。如果你在devm_request_irq时没有设置IRQF_SHARED而另一个驱动已经占用了这个中断号请求就会失败。排查方法是看dmesg里有没有IRQ handler type mismatch之类的日志。这类问题在按键、触摸屏、编码器等外设驱动中经常出现我在实际项目中遇到过一次排查了很久。5.6 终极排查手段打开内核的 device model debug当你实在找不出问题时可以打开内核的 debug 信息。在 kernel cmdline 里加dyndbgfile drivers/base/platform.c p或者直接打开CONFIG_DEBUG_DRIVER然后重新编译内核。这样内核会在匹配过程中打印大量调试信息包括比较的设备名、驱动名、匹配结果。虽然日志量大但在复杂问题面前非常有效。6. 工程化实践驱动里几个容易忽略的细节6.1 设备树属性和驱动 API 的对应关系在 i.MX6ULL 的实际开发中经常需要在设备树里自定义一些属性来传递配置参数。比如你有一个 LCD 驱动想传递屏幕分辨率、像素格式等参数。设备树里可以写成display { compatible mycompany,lcd; width 800; height 480; bpp 32; };驱动里用device_property_read_u32读取u32 width; ret device_property_read_u32(dev, width, width); if (ret 0) { dev_err(dev, failed to read width\n); return ret; }这个 API 是设备属性读取的通用接口不管底层是设备树还是 ACPI 都能工作。推荐优先使用这类通用 API而不是专门用of_property_read_u32。因为通用 API 封装了更多的细节代码可移植性更好。6.2 probe 失败后一定要看返回错误码许多新手在 probe 函数里遇到错误就直接return -1。这是非常不好的习惯。内核的 error code 约定是有意义的返回负数错误码比如-ENOMEM、-EINVAL、-ENODEV不仅方便你自己调试也方便内核日志记录。很多总线核心代码会根据 probe 返回值调整匹配策略比如针对-EPROBE_DEFER的特殊处理。6.3 EPROBE_DEFER 的妙用这里必须单独提一下-EPROBE_DEFER。在 i.MX6ULL 这种复杂 SoC 上设备之间有严格的依赖关系。比如你的按键驱动依赖 GPIO 控制器驱动如果 GPIO 控制器驱动还没加载你的devm_gpiod_get就会失败。传统做法是返回-ENODEV然后系统永远不会再尝试绑定这个设备。Linux 提供了-EPROBE_DEFER这种特殊错误码。当 probe 返回这个错误码时内核会把这个设备放到“待处理”队列等系统里其他相关设备注册完成后再重新尝试 probe。所以规范的写法应该是key-gpio devm_gpiod_get(dev, key, GPIOD_IN); if (IS_ERR(key-gpio)) { ret PTR_ERR(key-gpio); if (ret -EPROBE_DEFER) return ret; dev_err(dev, failed to get key gpio\n); return ret; }当然devm_gpiod_get本身在 GPIO 控制器还没就绪时就会返回-EPROBE_DEFER所以你需要把这个错误码原样传出去而不是吃掉它或改成其他错误码。这个细节在处理多个外设依赖的项目里特别重要能避免很多莫名其妙的“probe 失败后怎么重试”等问题。6.4 platform_set_drvdata 和 platform_get_drvdata 的使用在实际项目中你可能需要在其他函数里访问这个设备对应的私有数据结构。platform_set_drvdata可以把自定义结构体指针存在struct device里platform_get_drvdata再取出来。注意platform_set_drvdata的第一个参数是struct platform_device *不是struct device *很多从旧代码移植过来的人容易搞混。platform_set_drvdata(pdev, key); void func(struct platform_device *pdev) { struct key_dev *key platform_get_drvdata(pdev); }6.5 关于设备树的 reg 属性使用如果你的设备是纯内存映射设备比如 i.MX6ULL 的 ECSPI、UART、I2C 控制器那么设备树里通常用reg属性描述寄存器地址和长度驱动侧用platform_get_resource(pdev, IORESOURCE_MEM, 0)获取资源然后devm_ioremap_resource做映射。struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; regs devm_ioremap_resource(dev, res); if (IS_ERR(regs)) return PTR_ERR(regs);如果你用的是 GPIO、PWM、IIO 这类子系统提供的设备树属性就不需要自己处理 reg 和 ioremap直接用对应的子系统和 API 接口即可。这也体现了 Linux 驱动模型的抽象能力不同种类的设备用不同的子系统管理Platform 总线只是把这套机制串起来。7. 常见问题速查表与排查思路整理在实际开发与社区答疑中我整理了一些出现频率非常高的 Platform 驱动问题。这里做成速查表方便大家直接对照排查。问题描述排查重点常用修复方式probe 一直不被调用compatible 不匹配 / 设备树节点未生成检查设备树 status、compatible 字符串probe 被调用但返回 -EPROBE_DEFER依赖的设备没准备好检查 gpio controller、clock 等是否已注册probe 被调用但返回 -ENODEV资源获取失败 / 节点 disabled检查 reg、interrupts、gpio 属性gpiod_get 返回错误pinctrl 配置冲突 / 引脚被占用检查 iomuxc 配置和 pinmux 状态request_irq 失败共享中断冲突 / 中断号无效添加 IRQF_SHARED 或检查 GPIO 转中断设备节点 /dev/key 不存在misc 注册失败 / udev 规则问题检查 dmesg检查 misc_register 返回值加载模块时提示 Unknown symbol内核版本不匹配 / 依赖模块未加载检查符号依赖modprobe 先加载依赖系统启动时设备树报错设备树语法错误 / 标点符号错误使用 dtc 编译验证检查逗号分号probe 成功但中断不触发pinctrl 配置成了 GPIO 输出检查 pinmux 方向配置排错思路三步走先确认设备树节点存在再确认驱动注册成功最后确认匹配表字符串无误。这三步走完90% 的问题都能解决。剩下 10% 的疑难杂症再上 dyn debug 和 ftrace。8. 从一个驱动到一套体系理解 Platform 机制的长远价值学 Platform 匹配机制表面上是为了写一个能用的驱动实际上是为理解整个 Linux 设备模型打基础。你把这个机制搞清楚了后面再学 I2C 子系统、SPI 子系统、Regmap、Pinctrl、GPIO 子系统逻辑上有大量相通之处。比如 I2C 设备驱动核心结构是struct i2c_driver它也有probe回调也有一套匹配规程。SPI 驱动同理。它们的区别只是挂在不同的总线上匹配规则略有差异。而 Platform 总线是这一切的起点因为它足够简单没有协议层面的东西只有纯粹的设备和驱动配对。i.MX6ULL 这款芯片之所以适合学习是因为它不算复杂外设数量适中芯片手册清晰官方 SDK 也比较成熟。你在上面把 Platform 机制吃透后面去写 RK3399、全志、ST、兆易创新等平台的驱动底层的思维完全一致。换的只是芯片寄存器、设备树写法、BSP 版本等细节。另外很多人在学习时容易陷入一个误区认为只要 flash 一下别人的 .ko 或者改改设备树节点就能交差。这种思路在快速验证时没问题但一旦涉及长期维护、多版本适配、内核升级就会痛苦不堪。真正能让你从容面对这些问题的不是记住某个 API 的调用方式而是理解这套匹配机制的底层逻辑设备怎么描述、驱动怎么声明、总线怎么配对、probe 怎么串联资源。这些才是经得起时间考验的东西。如果这篇文章能帮你把这些逻辑串起来那它就算达到目的了。剩下的就是在板子上不断试错、看日志、改设备树、重新编译反反复复之中这些知识才会真正长在你自己身上。