资讯详情 Linux platform总线深度解析:设备驱动模型中的虚拟总线与匹配机制
📅 2026/10/21 19:54:44
开头做嵌入式Linux驱动开发或者其他跟内核打交道的活儿几乎绕不开一个名字platform总线。我在跟刚入行的同事聊驱动框架时十有八九他们会问“这个platform总线到底是什么是不是一条物理上的线”其实它是一个纯软件层面的概念但理解透了它你再看内核里任何一个设备驱动都会有种豁然开朗的感觉。platform总线也叫平台总线它不会像USB或PCI那样有物理的“线”去连接设备它的作用是把SoC片上系统内部那些无法通过标准总线枚举的外设比如UART控制器、I2C控制器、GPIO控制器、DMA控制器等统一纳入Linux设备驱动模型来管理。本质上是给“没有总线”的设备虚构出了一条总线让设备与驱动能够以标准方式匹配、对接。无论你是要读内核源码还是要自己写一个板级驱动这篇文章都适合你先从platform总线入手。我尽量用大白话配合代码讲清楚它的来龙去脉。1. 为什么会有platform总线先搞清楚设备模型的三件套1.1 Linux设备模型的基本骨架device、driver、busLinux内核里设备驱动模型最核心的三个概念就是设备device、驱动driver和总线bus。我习惯把它想象成一个“红娘系统”设备是单身用户驱动是单身用户总线就是红娘。红娘负责撮合match撮合成功之后两者就“绑定”到一起驱动开始为用户提供服务。这里面的关键是总线的本质是“匹配规则”的载体。每种总线定义自己的一套匹配方式。比如PCI总线通过vendor ID厂商ID和device ID设备ID来匹配USB总线通过USB的VID/PID来匹配。内核里每一种物理总线背后都对应一个bus_type结构体这里面写死了该总线的匹配规则、热插拔处理逻辑、电源管理回调等等。1.2 物理总线缺失下的困境SoC内部外设怎么“挂”问题来了CPU内部的UART控制器、GPIO控制器它们没有PCI那样的总线去连接也没有USB那种枚举机制但它们又实实在在存在着需要被驱动起来。早期内核处理这类设备比较原始直接在板级文件里创建一个平台设备指定它的资源寄存器地址、中断号然后注册一个平台驱动两者靠名字去匹配。这种“无总线设备”越来越多之后内核开发者发现不如干脆把这种匹配方式抽象成一种总线于是就有了platform_bus_type。它虽然看不见摸不着但注册在内核里设备模型把SoC内部外设统统挂到它上面。简单说platform总线的存在是为了让设备驱动模型能够覆盖那些没有物理枚举机制、但又真实存在于硬件上的设备。1.3 platform总线的现代含义不只是SoC内部外设现在来看凡是你没法用PCI、USB、I2C作为物理总线设备挂载、SPI这些具体总线去枚举的设备基本都可以用platform总线来承载。比如SoC内部集成外设UART/SPI/I2C控制器、时钟控制器、中断控制器等直接焊接在板卡上、地址映射到CPU内存空间的设备比如板载网卡芯片、板载音频芯片一些简单的无总线设备比如GPIO按键、LED在设备树Device Tree时代系统启动时会扫描设备树节点把compatible兼容性属性匹配到的节点统统创造成platform_device再等着注册进来的platform_driver来匹配。理解了这一层你再读内核启动日志里那大片的platform ...打印就不会觉得莫名其妙了。2. platform总线的核心工作机理match是灵魂2.1 platform_bus_type的结构与match函数platform总线在内核中的实体是platform_bus_type定义在drivers/base/platform.c里。它的核心是match回调函数也就是红娘的关键动作。我直接给你精神实质驱动代码里platform_match的匹配顺序大概是这样的简化版如果驱动和设备都有设备树节点优先比较compatible属性看驱动of_match_table里的compatible字符串是否和设备树节点里的compatible一致。如果没有设备树或者设备树没匹配上就看驱动的id_table一个platform_device_id数组比较name字段。如果还没有就退回到最原始的方式直接比较驱动的driver.name和设备的name。这个顺序极其重要。项目中我见过很多人因为设备树里写了一个compatible驱动id_table里也写了一个结果改动设备树后驱动不生效查了半天发现是两边的compatible大小写不一致。2.2 驱动与设备的注册时机无论谁先到总能动态匹配platform总线还有一个让人安心的特性驱动和设备只要注册到总线上无论谁先谁后只要符合匹配规则probe函数一定会被调用。因为总线在注册新设备时会遍历所有已注册驱动注册新驱动时会遍历所有已注册设备没有时序上的“先有鸡还是先有蛋”的困扰。这份动态特性来自bus_type的probe、uevent等一系列机制。我们在调试时经常会用“先insmod驱动模块、再动态创建设备节点”的方式来验证驱动逻辑就是因为platform总线会在设备出现的那一刻立刻触发匹配把probe函数拉起来。写驱动入门时利用这个特性可以快速验证自己的probe逻辑。2.3 uevent与sysfs从用户空间如何观察platform总线platform总线在sysfs中暴露为/sys/bus/platform。下面会有devices和drivers两个目录。devices里能看到所有挂在platform总线上的设备drivers里是所有平台驱动。通过ls -l去看driver下的设备软链接甚至能直观看到哪个设备被哪个驱动绑定。这是我在调试时最喜欢的检查手段之一。3. 解剖一个platform驱动从注册到probe3.1 struct platform_driver里藏了什么写platform驱动你的核心工作就是填充一个struct platform_driver。别被它吓着其实你需要关心的就是一个driver子结构、一个probe、一个remove还有对应的id_table或of_match_table。static const struct of_device_id mydev_of_match[] { { .compatible vendor,my-device, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydev_of_match); static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name my-device, .of_match_table mydev_of_match, }, }; module_platform_driver(mydev_driver);注意module_platform_driver这个宏它等于注册和注销函数的一体化封装。内核模块加载时调用platform_driver_register卸载时调用platform_driver_unregister。省得你手动写init/exit了。3.2 probe函数到底在干什么probe被调用说明设备匹配成功。接下来你要做的就是把“设备和驱动正式绑定”的后续动作做完获取硬件资源寄存器物理地址、中断号、DMA通道等不要自己瞎写地址要用platform_get_resource系列函数去拿这是规范做法。将寄存器物理地址映射为内核可直接读写的虚拟地址ioremap或者用devm_ioremap_resource。注册字符设备、中断处理函数、/proc或/sysfs接口等。初始化硬件状态关闭看门狗、设置时钟、配置GPIO等。反过来remove函数要做的是清理工作注销字符设备、释放中断、iounmap。这个函数在设备被移除时调用如果模块卸载却不执行remove资源泄漏就会出现。3.3 资源获取的黄金搭档devm_与platform_get_resource现代内核驱动越来越推荐使用devm_开头的资源管理接口。比如devm_kzalloc、devm_platform_ioremap_resource它们的好处是“自动释放”probe失败或remove时不用你一个个手动清理内核会按依赖关系自动释放资源。static int mydev_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(base)) return PTR_ERR(base); return 0; }devm_接口写出来的代码错误路径处理简单得多。不经意的细节里这些API是十几年来内核开发者踩坑后沉淀下来的经验结晶跟着用总没错。4. 实操从头搭建一个platform驱动并验证匹配4.1 设备端怎么制造一个platform设备在没有设备树的环境下如果你要手动造一个platform设备可以用platform_device_register_simplestatic struct resource test_resources[] { { .start 0x10000000, .end 0x1000001f, .flags IORESOURCE_MEM, }, { .start 38, .end 38, .flags IORESOURCE_IRQ, }, }; static struct platform_device test_device { .name test-device, .id -1, .num_resources ARRAY_SIZE(test_resources), .resource test_resources, }; platform_device_register(test_device);而在设备树开启的现代内核里大多数情况我们不需要手动注册设备设备树节点会自动变成platform设备。比如设备树里写my_device: my-device10000000 { compatible vendor,my-device; reg 0x0 0x10000000 0x0 0x20; interrupts 0x0 38 0x4; };内核在启动时会解析这个节点对应生成一个platform设备资源从reg和interrupts属性自动转换。4.2 驱动端完整示例一个尽量简单的平台驱动把设备树和驱动结合起来看一份最小可工作的平台驱动大概是这个样子。它拿到寄存器地址后映射成虚拟地址在此不展开具体业务逻辑只把骨架清晰呈现。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/io.h static int demo_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(base)) return PTR_ERR(base); irq platform_get_irq(pdev, 0); if (irq 0) return irq; dev_info(pdev-dev, demo probed: base%pa irq%d\n, res-start, irq); return 0; } static void demo_remove(struct platform_device *pdev) { dev_info(pdev-dev, demo removed\n); } static const struct of_device_id demo_of_match[] { { .compatible vendor,my-device }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo, .of_match_table demo_of_match, }, }; module_platform_driver(demo_driver); MODULE_LICENSE(GPL);4.3 编译、装载与验证如何确认我写的驱动已被绑定把以上文件放到drivers/misc/或者其他目录改好Kconfig和Makefile就可以编成模块。如果只是自己实验也可以单独建一个外部模块目录用内核的Makefile框架编译出.ko文件。装载之后最关键的是看系统日志insmod demo.ko dmesg | tail -10你会看到类似这样一行demo 10000000.my-device: demo probed: base0x10000000 irq38如果没有看到这行说明匹配有问题。此时去/sys/bus/platform/devices/下找有没有10000000.my-device这样的目录。如果设备目录存在但没绑定再查驱动的of_match_table和设备树里的compatible是不是完全一致。很多时候问题就出在一个字符的大小写上。5. 常见问题与排查技巧实录5.1 probe不调用就像相亲没看对眼这是我被问得最多的问题驱动明明加载了sysfs里也有设备但probe就是没被调用。排查步骤我建议顺一下先确认设备树节点确实被解析成了platform设备。在/sys/bus/platform/devices/找找有没有相应的节点名。再看驱动是否成功注册。/sys/bus/platform/drivers/下有没有你驱动的目录。查匹配表。用compatible时注意设备树里的属性值和驱动of_match_table必须完全一致包括大小写和厂商前缀。查内核是否把匹配用的CONFIG_OF打开了。早期移植内核时很多人忘了开这个选项结果of_match_table根本没参与匹配。在我实际项目里遇到最多的是第3种尤其是复制粘贴帮忙改设备树时把vendor,my-device写成了vendor,mydevice少一个短横线亲测就会匹配失败。5.2 resource获取失败中断号也可能是坑platform_get_resource获取IRQ失败时往往不是真没有中断资源而是设备树里interrupts属性格式不对。中断属性还依赖中断控制器比如interrupt-parent指向哪个GICcells是多少。另一个容易被忽视的点是设备树里interrupts和reg解析失败时内核不会报错很明确通常直接表现为probe里platform_get_irq返回负值。一个常用的调试技巧是查看/proc/interrupts看设备的中断号是否在系统里出现了以及对应的设备名是否注册成功。中断资源这地方我建议多花点时间在设备树语法上因为三五个字段拼错一个表现出的症状就是“驱动代码没问题中断却始终拿不到”。5.3 设备树里明明有节点为什么没生成platform设备有几种情况比较隐蔽。比如status “disabled”会让节点直接不生成设备这在调试休眠或测试功能时非常常见。再比如节点挂在某个i2c或spi总线节点下面那它就归i2c或spi子系统管不会成为platform设备。遇到这种情况从/sys/bus/platform/devices/里找不到节点是正常的因为它压根不在这个总线上。my_device: my-device10000000 { compatible vendor,my-device; reg 0x0 0x10000000 0x0 0x20; status disabled; /* 这个会让设备“隐身” */ interrupts 0x0 38 0x4; };看到disabled先把节点拿掉或改成okay再说。设备的使能状态是设备树使用中最容易忽略的暗坑。5.4 如何快速在系统里查到platform设备与驱动的绑定情况我最常用的三板斧# 查看所有platform设备 ls /sys/bus/platform/devices/ # 查看某个设备的驱动绑定情况 ls -l /sys/bus/platform/devices/10000000.my-device/driver # 查看某个驱动是否注册成功 ls /sys/bus/platform/drivers/demo/如果driver软链接存在说明绑定成功。这种查看方式比dmesg更直观尤其在多个设备共用一个驱动时你能一眼看出哪些设备归属哪个驱动。调试时还可以结合echo 10000000.my-device /sys/bus/platform/drivers/demo/unbind来模拟解绑再写回bind来重新绑定理论上不用重启就能验证remove和probe逻辑。5.5 驱动卸载时崩溃的排查remove回调里如果释放了不该释放的资源或者顺序错了极容易出现卸载时内核报错甚至直接重启。我在自研项目里就遇到过一次在remove里先iounmap之后又访问了寄存器地址结果虚拟地址已经是悬空的系统直接挂了。排查时先看一眼代码里有没有在iounmap之后再访问寄存器。devm_接口可以有效规避这类问题因为内核自动处理的顺序通常比我们手写更稳。5.6 平台总线的热插拔语义它真的“热”吗platform总线从设计上没有严格的热插拔语义但内核实现了unbind/bind机制。用户空间可以手动将一个设备从驱动中解绑或重新绑定这在驱动调试时非常有用。比如你想模拟设备移除时的行为并不需要真正拔掉硬件# 解绑 echo 10000000.my-device /sys/bus/platform/drivers/demo/unbind # 重新绑定 echo 10000000.my-device /sys/bus/platform/drivers/demo/bind如果remove函数写得没问题解绑后设备状态应该恢复到初始状态再绑定时probe会被重新调用。这个机制对做驱动热升级验证特别方便。6. 我的一些经验总结与扩展小技巧platform总线本身只是一个框架但它串起了设备模型、设备树、资源管理、热插拔模拟等一大串内核知识。我从实际项目中得到的体会是驱动死活不生效时九成问题出在设备/驱动匹配环节其次就是资源获取与设备树格式代码逻辑本身反而很少掉链子。所以新写一个platform驱动我会先花半小时确认设备树节点和of_match_table的一致性再动手写业务逻辑这样能省掉一堆排查时间。最后再分享一个小技巧如果你暂时没有真实硬件也可以用内核的虚拟平台设备进行驱动调试。内核里有不少测试驱动比如利用gpio-mockup这种虚拟设备来做GPIO驱动验证原理上就是一个platform设备。在你理解了platform总线之后这类虚拟设备的代码实际上是很容易读懂的。顺着这些测试驱动源码去理解总线匹配、资源管理比单啃文档效率高得多。平台总线的内容肯定不止一篇文章能讲透但把“虚拟总线、匹配规则、设备资源、probe流程”这四件事搞明白了之后再去看I2C、SPI、USB这些具体驱动你会发现自己已经在以大模型设备模型的视角去理解内核了。动手写一个最小平台驱动装上系统看一眼日志比死记硬背代码有效得多这是我想给你的最终建议。