Linux开发学习记录(8):Linux 驱动的分离与分层思想

成元 2026-03-22 { 教程, 笔记 } [ Linux, C, Device Tree ]

前言

上一篇笔记简单记录了设备树的基本用法,而这一篇笔记则更进一步,尝试从整体上梳理 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架构),常见的硬件描述方式主要有两类:

  1. 使用C语言。使用Linux内核提供的C语言结构体进行描述,并使用对应的接口进行注册。
  2. 使用设备树。Linux内核在启动时自动读取设备树文件,将其内容自动转为合适的格式并进行注册。

对于现代 ARM/ARM64 嵌入式 Linux 平台,社区通常更推荐第二种方案,也就是设备树,原因主要包括:

  1. 硬件描述本质上更接近数据,而不是程序逻辑,适合用一种统一的格式来描述而非程序来执行。
  2. 板级连接信息变化时,通常只需修改设备树,而不必像以前一样既要改动驱动代码,又要重新编译整个内核
  3. 将繁杂的,有很多代码重复的初始化程序转为少量的设备树文件(设备树可以通过 .dtsi 复用 SoC 级公共描述,减少重复)。
  4. 将驱动逻辑与硬件描述分开后,代码结构更清晰,也更利于社区维护(提交.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 一方面通过总线和子系统机制,将控制器驱动、设备驱动以及上层应用之间的关系解耦;另一方面又通过设备树等硬件描述机制,将驱动逻辑与板级硬件信息分离开来。这样做的结果就是,驱动开发不再总是围绕某一块具体开发板展开,而是逐渐走向通用化、模块化和可复用化

参考文章