资讯动态

Linux Platform总线机制详解:设备树匹配、probe调用与驱动开发实战

发布时间:2026/9/9 12:00:55 来源:尧图企业网站定制
1. 为什么Linux驱动里会冒出一个Platform机制先聊一个很多初学者绕不过去的弯子一提到Linux驱动开发很多人第一反应是字符设备、file_operations、中断、内核定时器这些“看得见摸得着”的知识点但真正决定一个驱动能不能被系统自动识别、能不能正确加载的往往是一个藏在底层的机制——Platform总线也叫平台总线。我在刚开始接触i.MX6ULL驱动开发的时候其实并不太明白为什么写一个LED驱动、按键驱动明明只是操作几个GPIO寄存器却非要先注册一个platform_driver还要配设备树里的compatible属性搞得一套流程下来比操作寄存器本身复杂得多。直到后来被几个诡异的问题折磨过才真正搞明白这套机制的价值。先说一个最直白的类比Linux驱动模型里有I2C总线、SPI总线、PCI总线这些物理总线它们的特点是设备挂在真正的物理线路上总线控制器可以枚举出“谁在线上”设备可以被主动发现。但ARM SoC里的大量外设像GPIO控制器、UART、网卡MAC、DMA控制器它们不是挂在一条可枚举的物理总线上的而是直接挂在CPU的内存地址映射空间里。系统上电时根本不知道“你这个板子上到底接了一个什么样的LED、用哪个引脚、高电平点亮还是低电平点亮”。这时候就需要一种“软件总线”把它们统一管理起来Platform总线就是干这个事的。它把“设备”和“驱动”两边通过约定好的匹配规则拉在一起一旦匹配成功内核就自动调用驱动的probe函数把设备资源交给驱动使用。这套机制设计的核心目的有三个解耦设备信息放在设备树或板级文件里驱动代码只关注“怎么操作硬件”两边可以独立修改。自动匹配系统启动时设备树被解析成一个个platform_device驱动注册时内核自动查找配对无需人工干预。可移植同样的驱动代码只要设备树描述不同就能适配不同的板卡引脚连接。所以如果你做i.MX6ULL这类嵌入式Linux开发几乎遇到的每一个外设驱动都会跟Platform机制打交道。搞懂它的匹配原理相当于打通了Linux设备驱动模型的主干脉络。2. 设备驱动模型的三件套device、driver、bus2.1 三条核心数据结构Linux设备模型用三个核心结构体来描述一个设备struct device设备、struct device_driver驱动、struct bus_type总线。Platform机制无非是这套通用模型的实例化。在Platform框架下它们被封装成struct platform_device { const char *name; // 设备名字 int id; // 设备编号-1表示只有一个 struct device dev; // 内嵌通用device结构体 struct resource *resource; // 资源数组寄存器地址、中断号等 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 state); int (*resume)(struct platform_device *); struct device_driver driver; // 内嵌通用driver结构体 const struct platform_device_id *id_table; // 设备ID匹配表 };这里注意一个关键点这两个结构体都“内嵌”了通用设备模型的结构体而不是用指针。这种C语言里的继承写法保证platform_device本身就是一个device可以被内核统一管理。驱动模型最核心的动作是注册设备侧device_register()或platform_device_register()被调用设备对象进入总线。驱动侧driver_register()或platform_driver_register()被调用驱动对象进入总线。总线侧每次新设备或新驱动加入总线的瞬间都会触发一次匹配检查。2.2 匹配时到底在比什么Platform总线的匹配逻辑是整个机制的心脏。在drivers/base/platform.c里的platform_match函数依次按照以下优先级进行比对优先级匹配方式比较内容1OF设备树匹配设备树节点的compatible属性与驱动of_match_table里列出的compatible字符串2ACPI匹配ACPI表里的HID/CID与驱动acpi_match_table比对嵌入式ARM一般不涉及3ID表匹配platform_device.name与platform_driver.id_table里的name字段逐一比对4名字直接匹配platform_device.name与platform_driver.driver.name直接strcmp很多初学者以为驱动和设备只要“名字一样”就能匹配其实在i.MX6ULL这种设备树全面普及的平台上走的几乎都是第一条OF匹配路线。设备树里每个节点都有一个compatible属性比如led_test { compatible mycompany,board-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio1 4 GPIO_ACTIVE_LOW; };驱动这边用of_match_table声明static const struct of_device_id myled_of_match[] { { .compatible mycompany,board-led }, { }, }; static struct platform_driver myled_driver { .driver { .name myled, .of_match_table myled_of_match, }, .probe myled_probe, .remove myled_remove, };只要设备树里的compatible字符串和of_match_table里任意一项相同匹配就成功probe函数被调用。这里有个实操细节内核匹配OF表用的是of_device_id里的compatible和DT节点里的compatible逐字节比较大小写和标点都要完全一致少一个逗号都不行。我自己调试时因为dts里漏写了一个逗号浪费了整整一下午看dmesg结果是probe根本没被调用。2.3 总线连接的“隐形中介”还有一个经常被忽略的点设备注册时用的name在OF匹配成功之后会被覆盖为compatible字符串的节点名。在内核源码里of_device_is_available、of_match_node这个过程会把对应of_device_id里的name赋值给platform_device的name。所以如果调试时用“与设备树节点同名的字符串”去匹配反而可能找不到刚上手的人很容易在这个地方踩坑。打个比方Platform总线就像一家婚介所。设备是一个人带着一份“自我描述表”设备树里的compatible属性、资源信息驱动是另一个人带着一份“择偶条件表”of_match_table里的compatible列表、id_table。婚介所登记双方信息后只要条件匹配上就安排见面——也就是调用probe。见面合适了两人绑定过日子有一天驱动卸载或设备移除就触发remove“离婚程序”。3. 资源传递probe到底能拿到什么东西3.1 三类资源获取方式匹配成功只是第一步驱动最终目的是操作硬件。i.MX6ULL的外设资源无外乎三类寄存器物理地址、中断号、DMA通道。这些信息都在设备树节点里描述内核对每个platform_device会做两件事分配resource数组设置dev.platform_data或dev.of_node。驱动侧通过以下三类接口获取资源第一类寄存器地址获取struct resource *res; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; reg_base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(reg_base)) return PTR_ERR(reg_base);第二类中断号获取irq platform_get_irq(pdev, 0); if (irq 0) return irq;第三类GPIO等自定义属性获取需要借助devm_gpiod_get之类的函数比如led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW);这里ioremap的语义值得展开一下。ARM处理器上外设寄存器被映射到内存地址空间但Linux内核运行在虚拟地址环境里物理地址不能直接访问必须通过ioremap建立页表映射。devm_ioremap_resource相比传统ioremap做了两件事检查资源合法性并绑定devm生命周期管理驱动卸载时自动释放映射省去手动iounmap的烦恼。这也是现在驱动开发里推荐使用devm系列接口的原因。3.2 设备树如何翻译成platform_device再往深一层i.MX6ULL的dtb文件被内核解析时start_kernel一路调用到unflatten_device_tree把dtb里二进制格式的设备树转成struct device_node的树状结构。然后platform代码遍历所有节点把满足条件的节点转换成platform_device。转换规则大致是根节点下直接挂的设备节点且带compatible属性。有compatible属性且的节点。simple-bus一层简单内存总线其子节点也满足转换条件。这个过程中reg属性被翻译成IORESOURCE_MEM类型资源interrupts属性被翻译成IORESOURCE_IRQ类型资源。比如下面这个设备树片段ecspi3 { compatible fsl,imx6ul-ecspi; reg 0x02010000 0x4000; interrupts GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH; dmas sdma 7 8 0, sdma 8 8 0; dma-names rx, tx; };在i.MX6ULL上ECSPI3的寄存器基地址是0x02010000长度0x4000中断号对应GIC中断33。转换成platform_device后platform_get_resource(pdev, IORESOURCE_MEM, 0)返回reg描述platform_get_irq(pdev, 0)返回中断号。有一点我必须强调在设备树Context下资源数组里的index不是reg的第几段而是所有reg段和interrupts按类型分开排序后的序号。MEM资源按reg出现的顺序从0开始编号IRQ资源按interrupts顺序从0开始编号读取时先看类型再看序号别搞混。4. 实操示例手写一个完整的platform驱动4.1 从零搭建驱动骨架理论说再多不如直接写一个能跑的驱动来得实在。下面这个示例基于i.MX6ULL控制一个GPIO点灯完整覆盖了platform_driver注册、设备树匹配、GPIO请求、miscdevice字符设备注册的全流程。复制下来可以直接编译验证。首先是驱动的头文件和基本结构体定义#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/miscdevice.h #include linux/fs.h #include linux/uaccess.h #include linux/delay.h #define LED_ON 0 #define LED_OFF 1 struct myled_dev { struct gpio_desc *led_gpio; struct miscdevice miscdev; };这里注意LED_ON和LED_OFF的定义普通GPIO点灯经常遇到“逻辑电平烫手”的问题有的板子GPIO输出高电平灯亮有的是低电平灯亮。设备树里可以用GPIO_ACTIVE_LOW标志来声明有效电平然后驱动侧统一用逻辑值1表示点亮0表示熄灭由gpiolib自动完成电平反转。这样驱动代码就彻底屏蔽了硬件接线的差异。接着是probe和remove函数整个驱动的重头戏static int myled_probe(struct platform_device *pdev) { struct myled_dev *dev; int ret; dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev-led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (IS_ERR(dev-led_gpio)) { dev_err(pdev-dev, Failed to get led gpio\n); return PTR_ERR(dev-led_gpio); } gpiod_direction_output(dev-led_gpio, LED_OFF); gpiod_set_value(dev-led_gpio, LED_OFF); dev-miscdev.name myled; dev-miscdev.minor MISC_DYNAMIC_MINOR; dev-miscdev.fops myled_fops; ret misc_register(dev-miscdev); if (ret) { dev_err(pdev-dev, Failed to register misc device\n); return ret; } platform_set_drvdata(pdev, dev); dev_info(pdev-dev, myled probed successfully\n); return 0; }代码里的platform_set_drvdata把自定义结构体挂到pdev上这样remove里可以随时取回设备私有数据这个操作在复杂驱动里几乎成了一种惯例。remove函数负责注销字符设备static int myled_remove(struct platform_device *pdev) { struct myled_dev *dev platform_get_drvdata(pdev); misc_deregister(dev-miscdev); gpiod_set_value(dev-led_gpio, LED_OFF); dev_info(pdev-dev, myled removed\n); return 0; }4.2 设备树与驱动匹配缺一不可驱动侧还需要声明match表并注册platform_driverstatic const struct of_device_id myled_of_match[] { { .compatible mycompany,board-led }, { } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_platform_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .of_match_table myled_of_match, }, }; module_platform_driver(myled_platform_driver);这里使用的module_platform_driver宏做了两件事在module_init里调用platform_driver_register在module_exit里调用platform_driver_unregister。这个宏几乎是所有platform驱动标配的写法比手写module_init/module_exit少几行样板代码也避免了注册顺序的疏漏。配套的设备树节点在imx6ull对应的dts文件中添加led_test { compatible mycompany,board-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpios gpio1 4 GPIO_ACTIVE_LOW; status okay; };同时要在iomuxc节点里补充引脚复用配置iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x10b0 ; }; };这里0x10b0是IOMUX控制寄存器里设置的电气属性包含上下拉、驱动能力等参数。它的具体含义在i.MX6ULL参考手册里可以查到但如果你只是点个灯大多数情况下列表里既有参考值直接抄就行。关键是要确保pad被复用成GPIO功能否则后面gpiod_get即使成功电平也翻不出去。4.3 应用层验证驱动状态驱动加载成功后可以在开发板shell里执行# 加载模块 insmod myled.ko # 查看platform总线上的设备挂载情况 ls /sys/bus/platform/devices/ # 查看匹配的driver ls /sys/bus/platform/drivers/myled/ # 打开字符设备写入“1”点灯 echo 1 /dev/myled echo 0 /dev/myled这里echo操作会走到file_operations里的write回调。很多人会有疑问echo后面的换行符怎么办实际上write回调接收到的缓冲区会包含一个换行符\n如果驱动里直接用缓冲区第一个字符判断没问题但如果用了kstrtoint去解析需要注意换行符会变成格式错误。我习惯的做法是使用simple_strtoul或对缓冲区做一次首字符判断忽略多余内容。这个驱动示例虽然简单但五脏俱全platform驱动注册、OF匹配、资源获取、GPIO操作、misc字符设备拿出来可以直接迁移到按键、蜂鸣器、继电器控制等场景改改probe里的检测逻辑而已。5. 匹配失败的常见原因与排查手段5.1 probe没被调用先别怀疑人生Platform驱动开发中最典型的故障现象是insmod之后dmesg里什么日志都没有probe根本没执行。这种情况下优先级最高的排查方向不是驱动代码本身而是设备树。我归纳了一下probe不被调用主要由以下几个原因造成故障现象可能原因排查方式设备树节点没生成platform_device节点缺少compatible或节点在simple-bus之外检查/sys/bus/platform/devices/下有无对应设备compatible字符串不一致dts和of_match_table里字符串不同逐字符对比特别注意大小写、下划线、逗号节点状态为disableddts里status属性没改成okay搜索设备树片段确认pinctrl解析失败pinctrl-0属性引用的pinctrl不存在查看/boot目录下实际dtb反编译结果有一个经验丰富的调试技巧是在板卡启动后查看用户空间的设备列表来判断内核是否真的生成了platform_devicels /sys/bus/platform/devices/正常匹配成功前设备会以一种类似“led_test”的形式出现在这个列表里。如果设备都没出现说明设备树转设备的环节就出了问题跟驱动代码毫无关系这时候再纠结platform_driver里的注册流程就没有意义。另一个排查手段是把设备树编译并反编译出来查看实际生效内容。我经常用的命令# 反编译实际加载的dtb dtc -I dtb -O dts -o decompiled.dts /boot/imx6ull-myboard.dtb反编译后grep关键字能直接看到SD卡里实际运行的设备树到底是什么样避免“我改了设备树但没重新烧写”这种低级错误。5.2 GPIO获取失败与资源越界还有一类常见问题probe确实执行了但在获取资源时返回错误。比如devm_gpiod_get返回-ENOENT或-EINVAL大概率是设备树里gpios属性写法不对。比如led-gpios gpio1 4 GPIO_ACTIVE_LOW;如果写成led-gpio漏了sdevm_gpiod_get就会因为找不到对应的“led”后缀属性而报错。gpiod系列接口自动在属性名后拼接一个s这个细节很容易让人莫名其妙。检查设备树里到底是gpio还是gpios非常关键。再比如platform_get_resource返回NULL常见于reg属性没有配置或者配置格式不匹配。i.MX6ULL的reg写法标准格式是reg 0x0209C000 0x4000;两个数值分别代表起始地址和地址长度。如果只写了一个数值resource的end会被设为0进而导致devm_ioremap_resource失败。5.3 Resource Managed接口的内存泄漏问题写驱动时尽可能统一使用devm前缀接口比如devm_kzalloc、devm_gpiod_get、devm_ioremap_resource。这些接口会在设备卸载时自动释放资源。但有个前提devm资源是挂在struct device生命周期上的如果你在probe里用了devm接口申请资源却在其他模块里导入导出了对应指针要注意设备remove时这些资源会自动消失外部指针变成悬空指针。我的习惯是字符设备用传统接口misc_register私有数据用devm_kzallocGPIO和ioremap也全部用devm。这样驱动代码里基本不需要手动释放逻辑出错概率大幅下降。在写remove函数时只需要注销字符设备其他资源让内核自动回收。6. 多设备共用驱动的匹配策略与扩展技巧6.1 同一驱动匹配多个设备实际项目里经常遇到这种情况一块板子上同型号的LED接了4路或者两个同型号的ADC芯片挂在同一个I2C总线上。平台驱动可以用同一个probe函数服务多个设备。关键在于设备树里每个节点都有独立的compatible而驱动只需要声明一次of_match_table。比如两款板卡一个LED设备树节点是led_red { compatible mycompany,board-led; led-gpios gpio1 5 GPIO_ACTIVE_LOW; };另一个LED在别的主控上节点是led_green { compatible mycompany,board-led; led-gpios gpio1 9 GPIO_ACTIVE_LOW; };驱动侧完全不用改同一个probe会被调用两次。但要注意每个platform_device拥有独立的私有数据所以probe里不能用全局变量保存设备状态所有状态都要用platform_set_drvdata/pdev-dev私有数据来管理。这是驱动设计里很重要的一个意识否则两个设备会互相覆盖状态。6.2 无设备树环境下的ID表匹配在i.MX6ULL这种设备树大环境下官方推荐使用of_match_table但仍有部分老内核或非设备树平台使用id_table匹配。id_table里的字段匹配逻辑非常直白static const struct platform_device_id myled_id_table[] { { myboard-led, (kernel_ulong_t)led_data }, { }, }; MODULE_DEVICE_TABLE(platform, myled_id_table); static struct platform_driver myled_driver { .probe myled_probe, .driver { .name myled, }, .id_table myled_id_table, };id_table里的数据指针可以携带平台信息probe执行时通过platform_get_device_id(pdev)拿回对应表项从而区分同一驱动在不同板卡上的行为差异。这种方法在纯板级文件注册设备的项目里比较常见比如老款三星、瑞芯微平台。还有一种匹配方式是platform_device.name直接等于platform_driver.driver.name。这种方式最古老灵活性最差但在一些极简驱动里还能看到。比如注册platform_device时直接指定name为myled驱动driver.name也是myled就能匹配。前提是系统里没人注册同名设备否则会冲突。这种模式我一般只在写学习示例时用产品代码里不建议。6.3 probe顺序与依赖关系的处理多个驱动之间存在依赖关系时会遇到顺序问题。比如一个GPIO按键驱动需要等GPIO控制器驱动先加载完成pinctrl子系统就绪后才能正常请求GPIO。如果两个驱动都写成模块自动加载可能会出现按键驱动先probeGPIO子系统还没ready导致devm_gpiod_get失败。解决办法有三种使用deferred probe机制让probe在资源不足时返回-EPROBE_DEFER内核会稍后自动重试probe。这是最推荐的方式。通过module_init的调用顺序控制但这在模块化编译时不可靠。在驱动里主动查询依赖是否ready处理起来比较繁琐不推荐。deferred probe在platform驱动全家桶里几乎是标配。如果你在probe里遇到“GPIO控制器未就绪”之类的错误直接返回-EPROBE_DEFER内核会在依赖设备注册完成后再次触发probe这在驱动手册里不会写得特别显眼但实际项目里非常管用。7. 调试与验证的实用技巧总结文章最后我再分享几个自己在i.MX6ULL开发中沉淀下来的调试经验这些不是教材里能随便翻到的。第一个技巧注册platform_driver时设置了name但设备树匹配成功后/sys/bus/platform/drivers/myled/目录下会生成一个符号链接指向匹配到的设备。你可以通过这个链接快速确认“驱动已经认领了哪个设备”。如果设备没有被认领drivers目录下是空的这时候怀疑点应该放在of_match_table上。第二个技巧临时验证驱动时可以用dmesg配合ftrace查看probe函数有没有执行。简单点用echo 1 /sys/kernel/debug/tracing/tracing_on echo function_graph /sys/kernel/debug/tracing/current_tracer echo myled_probe /sys/kernel/debug/tracing/set_ftrace_filter cat /sys/kernel/debug/tracing/trace不过嵌入式开发板上ftrace配置稍微麻烦一般dmesg里加打印就够用了。我习惯在probe入口加dev_info在错误出口加dev_err宁可多打印几条日志也不要靠猜。第三个技巧当驱动编译成模块时模块卸载再加载不会重新解析设备树。设备树只在系统启动时被解析一次platform_device的register动作只发生在设备树初始化阶段。所以如果修改了设备树要重启系统才能生效。如果只是改了驱动代码重新编译insmod即可。这两者的耦合关系如果没搞清会浪费很多重启时间。第四个技巧使用of_property_read_string、of_property_read_u32等接口直接读取设备树自定义属性。比如在dts里定义myboard,led-blink-delay 500;驱动里通过如下代码读取u32 delay; struct device_node *np pdev-dev.of_node; if (!of_property_read_u32(np, myboard,led-blink-delay, delay)) { dev_info(pdev-dev, blink delay is %d ms\n, delay); }注意属性名最好加上厂商前缀尤其在一个团队共同开发、设备树会被多个驱动复用的场景下能避免属性名冲突。最后总结一句个人体会Linux驱动开发里能操作寄存器固然是基本功但真正决定一个驱动“能不能活下来”的是你能不能把它嵌入到内核的设备模型里。Platform机制就是那把钥匙。把匹配规则、资源传递、生命周期管理这些概念理顺了再看i.MX6ULL上游内核里那些驱动代码基本能做到一目十行。这套能力不仅限于i.MX6ULL换成i.MX8M、RK3568、全志H3思路完全一样只是寄存器地址和设备树写法略有差异。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价