这个内容是我观看《Linux设备驱动开发解读》的学习笔记,虽然书上面是先讲了关于驱动的好多的基础知识,之后再讲驱动的软件构架。而且我近来深深地痴迷于自顶向上的学习逻辑,所以准备先对整个驱动有了框架以后,再带着这个学习的过程中的疑问与思维去学习基础。
老师是基于globalmem和globalfifo两个虚拟得驱动开始讲解的,所以先说一下这两个是哪些?
**globalmem意味着“全局显存”,**在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的显存空间,并在驱动中提供针对该片显存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片显存的内容。实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的便捷而陡然制造的设备。
globalfifo,如今我们给globalmem降低这样的约束:把globalmem中的全局显存弄成一个FIFO,只有当FIFO中有数据的时侯(即有进程把数据讲到这个FIFO并且没有被读进程读空),读进程能够把数据读出linux设备驱动开发,并且读取后的数据会从globalmem的全局显存中被拔掉;只有当FIFO不是满的时(即还有一些空间未被写,或写满后被读进程从这个FIFO中读出了数据),写进程能够往这个FIFO中写入数据。
开整:
1、Linux驱动的软件构架
Linux不是为了某单一电路板而设计的操作系统,它可以支持约30种体系结构下一定数目的硬件,因而,它的驱动构架很似乎不能像RTOS下或则无操作系统下这么小儿科的做法。(曾经玩单片机的时侯,可惜了空闲时间全拿去搞应用层了,仍然跑的都是裸板,没有玩儿操作系统)
Linux设备驱动特别注重软件的可重用和跨平台能力。例如,假如我们写下一个DM9000网卡的驱动,Linux的看法是这个驱动应当最好一行都不要改就可以在任何一个平台上跑上去。为了做到这一点(看似很难,由于每位板子联接DM9000的基地址,中断号哪些的都可能不一样),驱动中势必会有类似这样的代码:
#ifdef BOARD_XXX
#define DM9000_BASE 0x10000
#define DM9000_IRQ 8
#elif defined(BOARD_YYY)
#define DM9000_BASE 0x20000
#define DM9000_IRQ 7
#elif defined(BOARD_ZZZ)
#define DM9000_BASE 0x30000
#define DM9000_IRQ 9…
#endif
上述代码主要有如下问题:
1)此段代码看上去面目低贱,假如有100个板子,就要if/else100次,到了第101个板子,又得重新加if/else。代码进行着简单的“复制—粘贴”,“复制—粘贴”式的简单重复一般意味着代码编撰者的水平很差。
2)特别难做到一个驱动支持多个设备,假如某个电路板上有两个DM9000网卡,则DM9000_BASE这个宏就不够用了,此时势必要定义下来DM9000_BASE1、DM9000_BASE2、DM9000_IRQ1、DM9000_IRQ2类的宏;定义了DM9000_BASE1、DM9000_BASE2后,假若又有第3个DM9000网卡加到板子上,上面的代码就又不适用了。
3)依赖于makemenuconfig选择的项目来编译内核,为此,在不同的硬件平台下要依赖于所选择的BOARD_XXX、BOARD_YYY选项来决定代码逻辑。这不符合ARMLinux3.x一个映像适用于多个硬件的目标。实际上linux定时关机命令,我们可能同时选择了BOARD_XXX、BOARD_YYY、BOARD_ZZZ。
我们根据前面的方式编撰代码的时侯,相信自己编着编着也会感觉奇怪,嗅到了代码里不好的口感。这个时侯,请停下你狂奔的步伐,等一等你的灵魂。我们有没有办法把设备端的信息从驱动上面剥离下来,让驱动以某种标准方式领到这种平台信息呢,Linux总线、设备和驱动模型实际上可以做到这一点,**驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,**而驱动则以标准途径领到板级信息,这样,驱动就可以放之四海而皆准了,如图12.1所示。
Linux的字符设备驱动须要编撰file_operations成员函数,并负责处理阻塞、非组塞、多路复用、SIGIO等复杂事物。
然而,当我们面对一个真实的硬件驱动时,如果要编撰一个按钮的驱动,作为一个“懒惰”的程序员,你真的只想做最简单的工作,例如,收到一个按钮中断、汇报一个按钮值,至于哪些file_operations、几种I/O模型,那是Linux的事情,为何要我管Linux也是程序员写下来的,因而linux设备驱动开发,程序员如何想,它必然要如何做。
于是,这儿就衍生下来了一个软件分层的看法,虽然file_operations、I/O模型不可或缺,然而关于此部份的代码,全世界估计所有的输入设备都是一样的,为何不提炼一个中间层下来,把这种事情搞定,也就是在底层编撰驱动的时侯,搞定具体的硬件操作呢?
将软件进行分层设计应当是软件工程最基本的一个思想,假如提炼一个input的核心层下来,把跟Linux插口以及整个一套input风波的汇报机制都在这儿面实现,如图12.2所示,其实是十分好的。
(确实,我先前实现一个字符设备的时侯,对于好多的功能如open、read这种函数进行了自定义挂钩子,然而框架是直接复用的)
在Linux设备驱动框架的设计中,不仅有分层设计以外,还有分隔的思想。
举一个简单的事例,假定我们要通过SPI总线访问某外设,假定CPU的名子叫XXX1,SPI外设叫YYY1。在访问YYY1外设的时侯,要通过操作CPUXXX1上的SPI控制器的寄存器能够达到访问SPI外设YYY1的目的,最简单的代码逻辑是:
cpu_xxx1_spi_reg_write()
cpu_xxx1_spi_reg_read()
spi_client_yyy1_work1()
cpu_xxx1_spi_reg_write()
cpu_xxx1_spi_reg_read()
spi_client_yyy1_work2()
若果根据这些方法来设计驱动,结果对于任何一个SPI外设来讲,它的驱动代码都是与CPU相关的。也就是说,当代码用在CPUXXX1上的时侯,它访问XXX1的SPI主机控制寄存器,当用在XXX2上的时侯,它访问XXX2的SPI主机控制寄存器:
cpu_xxx2_spi_reg_write()
cpu_xxx2_spi_reg_read()
spi_client_yyy1_work1()
cpu_xxx2_spi_reg_write()
cpu_xxx2_spi_reg_read()
spi_client_yyy1_work2()
这其实是不被接受的,由于这意味着外设YYY1用在不同的CPUXXX1和XXX2上的时侯须要不同的驱动。同时,假如CPUXXX1不仅支持YYY1以外,还要支持外设YYY2、YYY3、YYY4等硬盘安装linux,这个XXX的代码就要重复出现在YYY1、YYY2、YYY3、YYY4的驱动上面:
cpu_xxx1_spi_reg_write()
cpu_xxx1_spi_reg_read()
spi_client_yyy2_work1()
cpu_xxx1_spi_reg_write()
cpu_xxx1_spi_reg_read()
spi_client_yyy2_work2()…
根据这样的逻辑,假如要让N个不同的YYY在M个不同的CPUXXX上跑上去,须要M*N份代码。这是一种典型的强耦合,不符合软件工程“高内聚、低耦合”和“信息隐蔽”的基本原则。
这些软件构架是一种典型的网状耦合,网状耦合通常不太适宜人类的思维逻辑,会把我们的思维搞乱。对于网状耦合的M∶N,我们通常要提炼出一个中间“1”,让M与“1”耦合,N也与这个“1”耦合,如图12.3所示。
这么,我们可以用如图12.4所示的思想对主机控制器驱动和外设驱动进行分离。
这样的结果是,外设YYY1、YYY2、YYY3、YYY4的驱动与主机控制器XXX1、XXX2、XXX3、XXX4的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用API进行数据传输,主机和外设之间可以进行任意组合。
假如我们不进行如图12.4所示的主机和外设分离,外设YYY1、YYY2、YYY3和主机XXX1、XXX2、XXX3进行组合的时侯,须要9个不同的驱动。构想一共有m个主机控制器,n个外设,分离的结果是须要m+n个驱动,不分离则须要m*n个驱动。由于,m个主机控制器,n个外设的驱动都可以被充分地复用了。