前言
上一篇笔记简单记录了设备树的基本用法,而这一篇笔记则更进一步,尝试从整体上梳理 Linux 驱动框架的设计思路,以及设备树在其中所扮演的角色。
在初学 Linux 驱动时,很多知识点看起来是零散的:总线、设备、驱动、设备树、子系统、probe、remove……这些概念彼此交织,很容易让人只记住接口,却不清楚它们为什么会被设计成现在这个样子。实际上,这些机制背后都围绕着几个很重要的目标,例如降低耦合、提高复用性,以及将程序逻辑与硬件描述分离。
因此,这篇笔记不打算一开始就陷入具体 API 的细节,而是先从整体框架入手,记录 Linux 驱动中几个重要的设计思想,包括驱动与驱动之间的分隔、驱动逻辑与板级硬件描述的分离,以及子系统所体现出的分层思想。在理解了这些内容之后,再回头看具体的驱动接口和代码实现,往往会更容易抓住主线。
Linux驱动的分隔与分离思想
驱动与驱动的分隔
不知道你是否有过这样的经历,在学习STM32时,我写过大量IIC设备的驱动代码。后来转到ESP32等平台时,我发现了一个问题——每个平台底层IIC控制器的驱动通常都不一样。如果按照传统的一对一模型来编写驱动,那么对于同一个设备,我几乎每个平台都要重新写一份驱动。这就导致大量的代码重复,如下所示。
传统的IIC设备驱动模型
对于图中的 MPU6050 而言,初始化时需要写入的寄存器值、算法等,基本上在每个平台上都是一样的,不同的只是底层 IIC 控制器的驱动代码。如果按照一对一的模型来编写,那么前面那些部分就都成了无效的重复代码。
不过,没有什么是加中间层解决不了的,我们可以加一个"接口"层,构成如下的模型。
现代的IIC设备驱动模型
现在MPU6050驱动与IIC控制器驱动通过一个统一的IIC接口连接起来了,一份面向IIC接口的MPU6050驱动代码可用于好几个平台,只要提供对应平台面向IIC接口的IIC控制器驱动。 我们可以说做到了控制器驱动与设备驱动的分隔。
驱动与板级硬件描述的分离
此外,无论是控制器驱动还是设备驱动,都需要用到一些硬件相关信息,例如引脚、中断、时钟、电源以及设备连接关系等。假如 PCB 重新设计,或者同一 SoC 存在多套开发板,导致这些板级连接信息发生变化,那么对于同一个驱动而言,就可能不得不复制出多份几乎相同的代码,仅仅为了适配不同的硬件连接方式。这同样会造成大量重复代码。
为了解决这个问题,就像上面的上面驱动与驱动的分隔一样,Linux 新加了一个"中间层",将“驱动逻辑”和“板级硬件描述”尽量分离。驱动负责实现设备的控制逻辑,而设备的地址、中断、GPIO、时钟、电源等资源信息,则通过额外的硬件描述机制提供给内核,再由驱动在 probe 函数中获取和使用。Linux 中常见的模型如下图所示。
Linux总线、驱动和设备模式
现在驱动逻辑与板级硬件描述实现了解耦。驱动通常通过总线框架和固件描述接口获取设备所需的硬件资源,而不必把这些板级连接信息硬编码在驱动中。
那么我们如何编写Linux可以读取的硬件信息了?在嵌入式 Linux 场景中(尤其是ARM架构),常见的硬件描述方式主要有两类:
- 使用C语言。使用Linux内核提供的C语言结构体进行描述,并使用对应的接口进行注册。
- 使用设备树。Linux内核在启动时自动读取设备树文件,将其内容自动转为合适的格式并进行注册。
对于现代 ARM/ARM64 嵌入式 Linux 平台,社区通常更推荐第二种方案,也就是设备树,原因主要包括:
- 硬件描述本质上更接近数据,而不是程序逻辑,适合用一种统一的格式来描述而非程序来执行。
- 板级连接信息变化时,通常只需修改设备树,而不必像以前一样既要改动驱动代码,又要重新编译整个内核。
- 将繁杂的,有很多代码重复的初始化程序转为少量的设备树文件(设备树可以通过 .dtsi 复用 SoC 级公共描述,减少重复)。
- 将驱动逻辑与硬件描述分开后,代码结构更清晰,也更利于社区维护(提交.c就是程序逻辑,提交.dts就是硬件信息)。诸如Linus之类的大佬更喜欢看程序逻辑,分析算法等等,而不是乱七八糟的硬件描述,这也是Linus等大佬主推第二种方案并严厉批判第一种方案的原因之一。
小结
驱动与驱动的分隔以及驱动与板级硬件描述的分离极大的方便了Linux驱动程序的开发。
现如今装载Linux系统的平台在上电复位启动后,驱动管理的大概的流程流程可以参考下面的示意图。
+----------------------+
| 内核启动,解析设备树 |
| of/platform 初始化 |
+---------+------------+
驱动模块初始化 / 内建驱动初始化 |
| | 根据设备树节点
| platform_driver_register() | 创建并注册 platform_device
v v
+----------------------+ +----------------------+
| 已注册的平台驱动列表 | | 已注册的平台设备列表 |
+-----------+----------+ +-----------+----------+
| |
| |
└-----------------+----------------┘
|
|
| 总线进行匹配
| platform_match()
| 常见依据:
| 1. of_match_table/compatible
| 2. id_table
| 3. name
v
+----------------------+
| 匹配成功 |
+----------+-----------+
|
| 调用驱动 probe()
v
+----------------------+
| 驱动获取资源并初始化 |
| reg/irq/gpio/clock等 |
+----------+-----------+
|
| 创建设备节点/注册子设备/
| 初始化中断和硬件
v
+----------------------+
| 设备正常工作 |
+----------+-----------+
|
+-----------------+-----------------+
| |
| 驱动卸载 | 设备移除/节点失效
v v
+----------------------+ +----------------------+
| 注销 platform_driver | | 注销 platform_device |
+----------+-----------+ +----------+-----------+
| |
+-----------------+----------------+
|
| 若此前已绑定
| 则调用 driver remove()
v
+----------------------+
| 释放资源,关闭硬件 |
+----------------------+
Linux驱动的分层思想
在计算机系统中,分层是一种很常见的设计方法。它的核心好处在于:每一层只需要完成自己的职责,并通过统一接口与上下层交互,而不必关心对方的具体实现。这样可以降低耦合、提高复用性,也更方便维护和扩展。
Linux 驱动框架也采用了类似的思想。以内核中的输入设备为例,键盘、鼠标、触摸屏、游戏手柄等虽然硬件类型不同、底层总线不同,但它们都属于“输入设备”。为此,Linux 设计了input 子系统,对这些设备进行统一管理。
对于下层驱动而言,input 子系统提供了统一的注册和上报接口。驱动只需要告诉内核“我是什么输入设备”和“我产生了什么输入事件”,而不需要自己去处理用户空间如何读取这些数据。对于上层应用而言,也不需要关心底层设备到底是键盘控制器、I2C 触摸屏还是 USB 鼠标,只需要通过统一的输入事件接口读取数据即可。
流程图可以完善成这样:
+----------------------+
| 用户应用 |
| 例如按键测试程序 |
+----------+-----------+
|
| read / poll / ioctl
v
+----------------------+
| /dev/input/eventX |
| 字符设备接口层 |
| (如 evdev) |
+----------+-----------+
|
| 统一输入事件接口
v
+----------------------+
| input 子系统核心层 |
| 设备注册、事件管理 |
+----------+-----------+
|
| input_report_key()
| input_report_abs()
v
+----------------------+
| 具体输入设备驱动 |
| 键盘/鼠标/触摸屏驱动 |
+----------+-----------+
|
| I2C / SPI / USB /
| platform 等总线
v
+----------------------+
| 具体硬件设备 |
+----------------------+
总结
总的来说,Linux 驱动框架之所以显得复杂,并不是因为它刻意追求繁琐,而是因为它试图解决一个非常现实的问题:如何让同一套驱动代码尽可能适配更多硬件平台,同时又保持良好的可维护性。
为此,Linux 一方面通过总线和子系统机制,将控制器驱动、设备驱动以及上层应用之间的关系解耦;另一方面又通过设备树等硬件描述机制,将驱动逻辑与板级硬件信息分离开来。这样做的结果就是,驱动开发不再总是围绕某一块具体开发板展开,而是逐渐走向通用化、模块化和可复用化。
参考文章
- Linux 驱动开发学习日记:分离的思想和总线设备驱动模型
- 【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1