坚持高质量原创,拒绝内容堆砌,喜欢的话点击上方星标,更新第一时间收到提醒,谢谢关注!
大家好我是沙师兄,从这一节开始,我们正式开始讲解 Linux 平台总线的内容。
按照我在上一章文件系统结尾说的计划,讲解顺序应该是先讲设备模型,但是设备模型涉及到非常底层的内核原理和设计模式。
如果对于内核和驱动理解不够深入,上来就讲kobj 、kset这些概念和实现,恐怕会过于抽象,大家也难以理解其设计的目的。
所以我还是决定继续秉承我们what - how - why 的流程,调整了一下顺序:先讲 Linux platform 框架,也就是平台总线,设备模型放到下一章。
这是大家在学习 BSP 驱动能直接接触到的,打开任何一个控制器驱动,大家应该都会看到platform_driver、probe()、compatible这些结构与函数。
这一章我们会完整的学习平台总线的使用、实现原理,还会动手继续在我们的qemu平台将之前的驱动移植到平台总线的框架中,最后还会完整的梳理驱动匹配与加载的流程与其底层实现原理。
等讲到最终的实现原理的时候大家就会发现,其中会涉及大量的设备模型的概念,所以我们会顺着这个流程将设备模型放到第七章中,去深入理解整个平台总线的底层的基石 -- 设备模型。
那接下来我们就开始平台总线的学习。
1. 为什么需要平台总线?
在第三章的时候,我们完整地讲过字符设备驱动:从 register_chrdev 到 alloc_chrdev_region + cdev_add,再到基于 QEMU 从零写一个 GPIO 驱动,把"用户态如何通过 /dev 设备节点访问到驱动"这条路径走通了。
但是正如开头所说,真正做 BSP 开发的时候发现驱动都是使用 probe、platform_driver这些函数和结构体,那平台总线为什么会出现,以及它和字符设备驱动到底是什么关系呢?
要讲清楚平台总线为什么存在,得先回到硬件本身。
在内核必修课3.1章那篇文章,我给过一个 BSP 视角的设备分类:从硬件角度看,设备分为片上总线设备、CPU 内部功能外设和片外设备,而 SoC 上的各个控制器都是通过 AMBA 总线连接到 CPU 核的。
I2C、SPI、UART、GPIO、RTC、WDT 这些控制器,都是集成在 SoC 内部、挂在 AMBA 总线上的片上设备。
这类片上设备有一个共同的特点:它们都没有办法自动枚举。
CPU 没有办法"扫描"出 AMBA 上到底挂了哪些控制器、每个控制器的寄存器基地址是多少、用的是哪一根中断线。
这些信息硬件不会主动上报,只能由我们提前"告知"内核。
更麻烦的是,SoC 之间千差万别:
每颗 SoC 集成的控制器种类不一样,地址映射也不一样;
可能在一个SoC 中包含同一个 IP 核多个控制器,例如RK3399 中有5个GPIO控制器,每一个的地址和中断都不同;
就算是同一颗 SoC,不同厂商开发的板子,外设的接法、用到哪些控制器也不一样
那内核怎么知道这些硬件的存在呢?
早期的做法是把这些板级信息直接硬编码进内核的 C 代码里,也就是所谓的板级文件(board file)。
每块板子对应一个文件,里面写死了这块板子上有哪些设备、地址是多少、中断是几号,大致是这个样子:
staticstructresourcei2c0_resources[] = { [0] = { .start = 0x10000000, .end = 0x10000fff, .flags = IORESOURCE_MEM }, [1] = { .start = 80, .end = 80, .flags = IORESOURCE_IRQ },};staticstructplatform_devicei2c0_device = { .name = "my-i2c", .id = 0, .resource = i2c0_resources, .num_resources = ARRAY_SIZE(i2c0_resources),};
一块板子这么写还好,问题是板子和 SoC 的数量在飞速增长。
ARM 平台尤其明显,arch/arm 下堆满了各家厂商的板级代码,同样的硬件在不同板子上被反复描述,改一处要动一大片。
十多年前,Linus 就公开批评过 ARM 这堆泛滥的板级代码,这也直接推动了后来设备树的普及。
这里的根本问题,是硬件描述和驱动逻辑耦合在了一起。
地址、中断这些属于硬件资源,被写进了内核代码,结果一份驱动没法跨 SoC、跨平台复用。换一块板子,就得改一次代码。
Linux 要解决的,就是这个平台兼容问题:让一份驱动能够适配不同的 SoC 和不同的板子,于是平台总线就出现了:
思路是把"板子上有什么硬件"从驱动里抽出来,单独描述(这就是设备树),驱动只保留通用逻辑,中间再用一条总线把"硬件描述"和"驱动逻辑"匹配起来
2. 什么是平台总线
平台总线(platform bus)是 Linux 为那些无法自动枚举的片上设备设计的一条"虚拟总线"。
它不对应任何真实的物理总线协议,不像 I2C、USB 那样有实际的电气信号,它纯粹是内核里的一层软件抽象。
平台总线上有三个角色:
platform_device:描述一个硬件实例,带着这个实例的资源,比如寄存器地址、中断号。现代 ARM64 BSP 中,它大多由设备树转换而来。
platform_driver:描述一类硬件的驱动逻辑,带着 probe() 回调和一张匹配表,告诉内核"我能驱动哪种设备"。
platform bus:维护着设备和驱动两份清单,负责把两边匹配起来。
可以平台总线理解成一个中介。
设备树相当于在中介这里登记了"有哪些房子、什么条件",每个 platform_driver 相当于登记了"我想租什么样的房子"。
平台总线这个中介则拿着两边的条件去撮合,一旦某个设备和某个驱动的条件对上了,就把它们拉到一起,通知驱动:你要找的硬件到了,来接手吧。
这个"通知驱动接手"的动作,就是调用驱动的 probe()。
这样一来,硬件描述(设备树)和驱动逻辑(platform_driver)就实现了解耦。
换一块板子,只改设备树里的地址和中断,驱动代码一行都不用动。
3. 平台总线与字符设备接口的关系
那是不是学习平台驱动以后,第三章讲过的字符设备是不是就被替代了?
答案是:不是的,它们负责的是驱动流程的不同阶段。
平台总线负责组织硬件资源和驱动,而字符设备接口用于将已经完成驱动初始化的硬件的接口暴露给用户。
具体关系可以看下面这个图:

平台总线负责把硬件实例和驱动组织起来,platform_device 记录硬件实例及其资源,platform_driver 提供匹配信息和驱动回调,两者匹配成功以后,内核就会调用驱动的 probe()。
对于设备接入过程来说,平台总线的主要工作到调用 probe() 就完成了。
至于怎样获取资源、怎样初始化硬件、最终注册到哪个内核框架,以及是否向用户态提供接口,都是具体驱动需要完成的工作。
平台驱动框架把硬件接入内核的过程整理成了一套固定结构,相当于 Linux 把驱动匹配流程变成了填空题。
BSP 工程师只需填写设备匹配表,实现 probe() 等回调;Linux 内核平台总线会负责完成匹配,并在匹配成功后调用这些入口。
对于驱动 Bringup 来说,probe() 是最主要的入口。驱动通常会在这里获取寄存器、中断、时钟、复位和 pinctrl 等资源,初始化硬件,申请中断,然后把设备提供的能力注册到相应的内核子系统。
我们之前学习的字符设备接口可以放在 probe函数中完成调用,实现驱动匹配加载后自动实现创建/dev节点以及对应的open、read、write接口实现。
然而大多数情况下不需要单独使用cdev,内核都提供了对应的接口,也就是我们常说的子系统。
这些子系统本身也就是以字符设备的形式出现的,但是并不是通过主动调用cdev接口实现的,例如:V4L2 提供 /dev/videoX,i2c-dev 提供 /dev/i2c-X。
I2C 控制器驱动通过 i2c_add_adapter() 注册到 I2C 子系统,SPI 控制器提供了 spi_register_controller() 接口,其他的GPIO 、 input子系统也都有对应的接口。
这些驱动不需要自己设计设备号,也不需要直接实现一套 cdev 和 file_operations。
只有当厂商自研的模块,内核中没有其合适的子系统,应用程序又确实需要通过 read()、write()、ioctl()、poll() 或 mmap() 直接操作设备,驱动才需要考虑自己注册 cdev 或 miscdevice。
因此,平台总线负责完成硬件实例与驱动的匹配,并把执行流程带到 probe();字符设备则是驱动完成硬件初始化以后,可以选择建立的一种用户态接口。
4. 本章内容安排
这一章的目标是建立一套完整的平台驱动开发思路。
我们会沿着“硬件怎样被描述、设备和驱动怎样完成匹配、驱动怎样进入 probe()”这条主线,逐步认识平台总线在 SoC 驱动中的位置。
理解平台总线的基本框架以后,我们还会学习平台总线的使用,并将之前实际开发过驱动移植到平台总线的框架中,建立从硬件描述到驱动正常工作的完整认识。
我们还会深入的梳理学习平台总线匹配加载的内核机制,理解probe调用的流程及其内部原理。学完这一章后,学会的不仅是概念和使用方法,还有其内核原理,而这些可能工作了很多年的 BSP 工程师都并没有完全掌握的。
5. 总结
这一节我们解释了 Linux 为什么需要平台总线:面对不同 SoC、不同板卡上的硬件差异,平台总线框架实现了把硬件描述和驱动逻辑解耦,让同一份驱动能够适配不同的硬件实例,从而提高了驱动的通用性。
在这个框架中,platform_device 记录硬件实例及其资源,platform_driver 提供匹配信息和驱动回调,platform bus 负责完成两者的匹配。
匹配成功以后,内核调用驱动的 probe(),驱动从这里开始接管硬件。
进入 probe() 以后,资源获取、硬件初始化以及后续接口注册都由具体驱动完成。
真实的控制器驱动通常会注册到所属的内核子系统,由子系统提供统一接口;只有确实需要直接向用户态提供文件式访问时,驱动才需要考虑注册字符设备。
因此,平台总线不会取代字符设备,字符设备也不是平台驱动的固定组成部分。
下一节我们会深入拆解平台总线的三个结构体,去进一步理解平台总线的框架。