嵌入式Linux设备驱动开发思想进阶之驱动分层与驱动分离
duyiwuer11
duyiwuer11 Lv.5
2017年11月03日 10:03:06
只看楼主

我们在学习I2C 、 USB 、 SD 驱动时,大家有没有发现一个共性,就是在驱动开发时,每个驱动都分层三部分,由上到下分别是: 1 、 XXX 设备驱动 2 、 XXX 核心层 3 、 XXX 主机控制器驱动 而需要我们编写的主要是设备驱动部分,主机控制器驱动部分也有少量编写,二者进行交互主要时由 核心层 提供的接口来实现;这样结构清晰,大大地有利于我们的驱动开发,这其中就是利用了

我们在学习I2C USB SD 驱动时,大家有没有发现一个共性,就是在驱动开发时,每个驱动都分层三部分,由上到下分别是:
1 XXX 设备驱动
2 XXX 核心层
3 XXX 主机控制器驱动
而需要我们编写的主要是设备驱动部分,主机控制器驱动部分也有少量编写,二者进行交互主要时由 核心层 提供的接口来实现;这样结构清晰,大大地有利于我们的驱动开发,这其中就是利用了Linux 设备驱动开发中两个重要思想,今天我们来仔细解析一下。
一、设备驱动的分层思想
在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类中的函数。如果对于继承的这个事物而言,其某函数的实现与基类一致,那它就可以直接继承基类的函数;相反,它可以重载之。这种面向对象的设计思想极大地提高了代码的可重用能力,是对现实世界事物间关系的一种良好呈现。
Linux 内核完全由 C 语言和汇编语言写成,但是却频繁用到了面向对象的设计思想。在设备驱动方面,往往为同类的设备设计了一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,它可以重载之。举个例子:
[cpp] view plain copy
1. return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
2. {
3. if (bottom_dev->funca)
4. return bottom_dev->funca(param1, param2);
5. /* 核心层通用的 funca 代码 */
6. ...
7. }
上述core_funca 的实现中,会检查底层设备是否重载了 funca() ,如果重载了,就调用底层的代码,否则,直接使用通用层的。这样做的好处是, 核心层的代码可以处理绝大多数该类设备的funca() 对应的功能,只有少数特殊设备需要重新实现 funca()
再看一个例子:
[cpp] view plain copy
1. return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
2. {
3. /* 通用的步骤代码 A */
4. ...
5. bottom_dev->funca_ops1();
6. /* 通用的步骤代码 B */
7. ...
8. bottom_dev->funca_ops2();
9. /* 通用的步骤代码 C */
10. ...
11. bottom_dev->funca_ops3();
12. }
上述代码假定为了实现funca() ,对于同类设备而言,操作流程一致,都要经过 通用代码 A 、底层 ops1 、通用代码 B 、底层 ops2 、通用代码 C 、底层 ops3” 这几步,分层设计明显带来的好处是,对于通用代码 A B C ,具体的底层驱动不需要再实现,而仅仅只关心其底层的操作 ops1 ops2 ops3 。图 1 明确反映了设备驱动的核心层与具体设备驱动的关系,实际上,这种分层可能只有 2 层(图 1 a ),也可能是多层的(图 1 b 信盈达嵌入式要领吧五六领悟四五吧
这样的分层化设计在Linux input RTC MTD I2 C SPI TTY USB 等诸多设备驱动类型中屡见不鲜。
二、主机驱动和外设驱动分离思想
主机、外设驱动分离的意义
Linux 设备驱动框架的设计中,除了有分层设计实现以外,还有分隔的思想。举一个简单的例子,假设我们要通过 SPI 总线访问某外设,在这个访问过程中,要通过操作 CPU XXX 上的 SPI 控制器的寄存器来达到访问 SPI 外设 YYY 的目的,最简单的方法是:
[cpp] view plain copy
1. return_type xxx_write_spi_yyy(...)
2. {
3. xxx_write_spi_host_ctrl_reg(ctrl);
4. xxx_ write_spi_host_data_reg(buf);
5. while(!(xxx_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
6. ...
7. }
如果按照这种方式来设计驱动,结果是对于任何一个SPI 外设来讲,它的驱动代码都是 CPU 相关的。也就是说,当然用在 CPU XXX 上的时候,它访问 XXX SPI 主机控制寄存器,当用在 XXX1 的时候,它访问 XXX1 SPI 主机控制寄存器:
[cpp] view plain copy
1. return_type xxx1_write_spi_yyy(...)
2. {
3. xxx1_write_spi_host_ctrl_reg(ctrl);
4. xxx1_ write_spi_host_data_reg(buf);
5. while(!(xxx1_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
6. ...
7. }
这显然是不能接受的,因为这意味着外设YYY 用在不同的 CPU XXX XXX1 上的时候需要不同的驱动。那么,我们可以用如图的思想对主机控制器驱动和外设驱动进行分离。这样的结构是, 外设a b c 的驱动与主机控制器 A B C 的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用的 API 进行数据传输,主机和外设之间可以进行任意的组合。
如果我们不进行上图的主机和外设分离,外设a b c 和主机 A B C 进行组合的时候,需要 9 个不同的驱动。设想一共有 m 个主机控制器, n 个外设,分离的结果是需要 m+n 个驱动,不分离则需要 m*n 个驱动。
Linux SPI I2C USB ASoC ALSA SoC )等子系统都典型地利用了这种分离的设计思想。

免费打赏

相关推荐

APP内打开