Linux 设备驱动的软件架构思想

发布于:2025-03-24 ⋅ 阅读:(40) ⋅ 点赞:(0)

1. 总线、设备与驱动模型

Linux不是为了某单一电路板而设计的操作系统,它可以支持约30种体系结构下一定数量的硬件,因此,它的驱动架构很显然不能像RTOS下或者无操作系统下那么小儿科的做法。

Linux设备驱动非常重视软件的可重用和跨平台能力。譬如,如果我们写下一个DM9000网卡的驱动,Linux的想法是这个驱动应该最好一行都不要改就可以在任何一个平台上跑起来。为了做到这一点(看似很难,因为每个板子连接DM9000的基地址,中断号什么的都可能不一样)。很久以前是这么写的:

#ifdefBOARD XXX
#define DM9000 BSE x0000
#define 9000 IR 
#elif defined(BoARD YYY)
#define D9000 BSE20000
#define DM9000 IRO 7
#elif defined(BOARD ZZ)
#define D9000 BASE0x30000
#define DM9000 IRQ 9
...
#endif

上述代码主要有如下问题:

  • 代码重复性高:使用大量的if-else语句来处理不同板子的情况,代码冗余且难以维护,每次新增板子都需要手动添加新的if-else分支。
  • 扩展性差:对于多网卡的情况,需要定义多个宏(如DM9000BASE1DM9000BASE2等),当网卡数量增加时,之前的代码无法复用,需要重新修改。
  • 不符合多硬件平台目标:依赖于make menuconfig选择的项目来编译内核,通过BOARD XXXBOARD YYY等选项决定代码逻辑,这使得一个映像难以适用于多个硬件平台

我们按照上面的方法编写代码的时候,相信自己编着编着也会觉得奇怪,闻到了代码里不好的味道。这个时候,请停下你飞奔的脚步,等一等你的灵魂。我们有没有办法把设备端的信息从驱动里面剥离出来,让驱动以某种标准方法拿到这些平台信息呢?

Linux总线、设备和驱动模型实际上可以做到这一点,驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息,这样,驱动就可以放之四海而皆准了,如图 12.1所示。

2. 分层与分离

2.1 分层的思想

2.1.1分层思想介绍

Linux的字符设备驱动需要编写file_operations 成员函数,并负责处理阻塞、非组塞、多路复用、SIGIO等复杂事物。但是,当我们面对一个真实的硬件驱动时,假如要编写一个按键的驱动,作为一个“懒惰”的程序员,你真的只想做最简单的工作,譬如,收到一个按键中断、汇报一个按键值,至于什么file_operations 、几种 I/O 模型,那是 Linux 的事情,为什么要我管?Linux也是程序员写出来的,因此,程序员怎么想,它必然要怎么做。于是,这里就衍生出来了一个软件分层的想法,尽管file_operations 、IO 模型不可或缺,但是关于此部分的代码,全世界恐怕所有的输人设备都是一样的,为什么不提炼一个中间层出来,把这些事情搞定,也就是在底层编写驱动的时候,搞定具体的硬件操作呢?

将软件进行分层设计应该是软件工程最基本的一个思想,如果提炼一个input的核心层出来,把跟 Linux 接口以及整个一套 input 事件的汇报机制都在这里面实现,如图 12.2所示,显然是非常好的。这也就是Linux中的input 子系统

图12.5明确反映了设备驱动的核心层与具体设备驱动的关系,实际上,这种分层可能只有两层(见图12.5a),也可能是多层的(图12.5b)。

这样的分层化设计在 Linux的input、RTC、MTD、C、SPI、tyy、USB 等诸多类型设备驱动中屡见不鲜。

2.1.2面向对象的设计思想

在分层设计的时候,Linux内核大量使用了面向对象的设计思想,在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类中的函数。

  • 如果对于继承的这个事物而言,某成员函数的实现与基类一致,那它就可以直接继承基类的函数
  • 相反,它也可以重写(0veriding),对父类的函数进行重新定义。若子类中的方法与父类中的某方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。

这种面向对象的“多态”设计思想极大地提高了代码的可重用能力,是对现实世界中事物之间关系的一种良好呈现,

Linux内核完全是由C语言和汇编语言写成,但是却频繁地用到了面向对象的设计思想。在设备驱动方面,往往为同类的设备设计了一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,也可以重写。举个例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在上述core_funca的实现中,会检查底层设备是否重写了funca(),如果重写了,就调用底层的代码,否则,直接使用通用层的。这样做的好处是,核心层的代码可以处理绝大多数与该类设备的funca()对应的功能,只有少数特殊设备需要重新实现funca()

2.2 分离的思想

2.2.1 主机驱动与外设驱动分离的思想

举一个简在Linux设备驱动框架的设计中,除了有分层设计以外,还有分隔的思想单的例子,假设我们要通过SPI总线访问某外设,假设CPU的名字叫XXXI,SPI外设叫YYY1。在访问 YYY1外设的时候,要通过操作 CPUXXX1上的 SPI控制器的寄存器才能达到访问SPI外设YYY1的目的,最简单的代码逻辑是:

cpu xxxl spi reg write{
cpu xxx1 spi reg read()
spi client yyyl work1()
epu xxx1 spi reg write{
cpu xxx1 spi reg read()
spi client yyyl work2()

如果按照这种方式来设计驱动,结果对于任何一个SPI外设来讲,它的驱动代码都是与CPU相关的。也就是说,当代码用在CPUXXX1上的时候,它访问XXX1的SPI主机控制寄存器,当用在XXX2上的时候,它访问XXX2的SPI主机控制存器:

cpu_xxx2_spi_reg_write()
cpu xxx2 spi reg read()
spi client yyyl workl()
cpu xxx2 spi reg write(
epu xxx2 spi reg read()
spi client yyyl work2()

这显然是不被接受的,因为这意味着外设YYY1用在不同的CPUXXX1和XXX2上的时候需要不同的驱动。同时,如果CPUXXX1除了支持YYY1以外,还要支持外设YYY2、YYY3、YYY4等,这个XXX的代码就要重复出现在 YYY1,YYY2、YYY3,YYY4的驱动里面:

按照这样的逻辑,如果要让N个不同的YYY在M个不同的CPUXXX上跑起来,需要MN份代码。这是一种典型的强耦合,不符合软件工程**“高内聚、低耦合”和“信息隐蔽”的基本原则**这种软件架构是一种典型的网状耦合,网状耦合一般不太适合人类的思维逻辑,会把我们的思维搞乱。对于网状耦合的M:N,我们一般要提炼出一个中间“1”,让M与“1”耦合,N也与这个“1”耦合,如图 12.3 所示

那么,我们可以用如图12.4所示的思想对主机控制器驱动和外设驱动进行分离。这样的结果是,外设 YYY1、YYY2、YYY3、YYY4的驱动与主机控制器XXX1、XXX2、XXX3、XXX4的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用API进行数据传输,主机和外设之间可以进行任意组合。IIC、SPI等子系统也是采用类似的设计思想,通过核心层提供通用接口,使得不同的外设驱动可以通过这些接口与主机控制器进行通信。

![<img src="C:\Users\zz237\AppData\Roaming\Typora\typora-user-images\image-20250323102632731.png" alt="image-20250323102632731" style="zoom:50%;"

书上是这么说的分离思想,但我认为这里仍然也是分层的思想,提取出核心层,主机控制器与各个具体的外设之间通过核心层来进行交互。

2.2.2 驱动与设备资源的分离

驱动与设备资源的分离是指将驱动程序的逻辑与设备的具体资源(如寄存器、内存等)分离。这种分离可以通过以下几种方式实现:

  1. 资源管理:内核负责管理设备资源,驱动程序通过内核提供的API来访问这些资源。
  2. 抽象接口:内核提供抽象接口,驱动程序通过这些接口与设备进行交互,而不需要关心设备的具体实现细节。
  3. 模块化设计:驱动程序被设计为模块化的,可以根据需要加载或卸载,而不影响系统的其他部分。