驱动程序是Linux中重要的一个部分,其大部分代码位于Linux源代码的"drivers
"目录下,其代码行数占据了Linux总代码的一半左右。但是事实上,真正的驱动模型的核心代码总行数并不是大多。驱动程序那么多的代码,都是建立在驱动模型核心代码的基础上,为了支持各种各样不同的具体设备而编写的。而当掌握了驱动模型之后,编写驱动程序就像是搭积木一样,只需要在别人搭好的框架内填充一些硬件相关的代码而已。当然,这个过程会用到操作系统其他模块提供的接口,从这个角度来讲,实际上驱动程序是基于内核核心模块之上的程序。因此,有人也将驱动程序戏称为Linux kernel的“应用程序”。从其结构上来看,驱动程序是相对独立且简单的,正是由于这种特性,选择从驱动程序作为切入点去学习和理解内核相对来讲是比较容易的。因此,这个系列的文章就先从驱动程序谈起。
一、什么是驱动程序
驱动程序是控制并使硬件能够正常工作的程序,同时,它提供了设备节点,以供应用程序通过访问这些节点控制设备或从设备中获取数据。。显然,理解硬件工作的原理是必须的,为了方便大家理解,这节我们从最简单的LED灯谈起,带大家认识学习一些硬件知识。
1.1 LED控制器
根据最基本的物理知识,我们知道要让一个发光二极管(后面简称LED
)正常工作(即点亮),只需要将合适的电源接在LED两端。如果要想控制LED的亮灭,可以在这个通路中添加一个物理开关。
而在嵌入式系统中,为了能够使用程序来控制这个灯的亮灭,需要把物理开关替换成一个可以用软件控制的器件(如上图中红色虚线框所示),CPU执行不同的代码,即可以控制这个器件,从而使电路实现通断,最终实现控制开关的亮灭。那么,控制这个器件的代码就可以认为是这个LED灯的驱动程序,而这个器件就称之为LED控制器
(注意,控制器件的代码适是执行在CPU上的)。
在实际绘制原理图的过程中,由于电路系统的复杂性和电源的多样性,一般不会直接画出电源,而是使用Vcc
表示电压源,GND
表示接地,从而保证两者之前的器件存在电压。而所谓的LED控制器
,这里实际上就是使用一个GPIO加上一个MOS管来实现——GPIO的高低电平控制MOS管的通断,从而控制LED灯两端的电压有无,最终达到控制LED灯的目的。
1.2 控制器(Controller)
类似于LED控制器,如果想使用程序控制一个温度传感器,就也需要一个相应的控制器;如果想控制一个摄像头,就需要一个camera的控制器;如果想控制一个USB的鼠标就需要一个USB控制器。但是显然,如果我们针对每个器件都做一个控制器的话,是不太现实的,因为各种各样的器件实在是太多了。其实,很多器件的接口(即所使用的通信协议)都是一样的,没有必要针对每一个器件做一个控制器,而只针对使用相同接口的一类设备做一个通用的控制器,不管外面连接了什么设备,只要它们的接口(使用的协议)相同,就能够使用同一个控制器从设备中读取或向设备写入数据。
举个例子,HDC1080是一款温湿度传感器,它使用了标准的i2c协议进行通信;而BMP280是一款大气压力传感器,同样使用了标准的i2c协议进行通信。因此,可以设计一款控制器能发送或者接收标准的i2c协议的数据。这样,就可以通过这个控制器和这两款芯片通信了,这种控制器就称为i2c控制器。类似的,我们可以为各种协议设计一个控制器,比如,用于SPI通信的SPI控制器,用于USB设备通信的USB控制器,用于PCI通信的PCI控制器等等,从而使用一个控制器就可以和一类设备通信。
1.3 SoC
在现代计算机系统中,CPU是核心,而上述的这些控制器获取的数据最终都通过系统总线传输给了CPU来处理。所以,为了便于芯片使用,同时更加高效、快捷的传输数据,CPU生产商会把CPU和各种控制器集成到一个芯片内部,从而组成了一个计算机系统,这种集成电路被称作SoC
(System on a Chip
)。其示意图如下:
但是在实际使用中情况往往比这个示意图复杂的多,SOC往往整合了各种各样的控制器和外设,甚至于还把其他类型的CPU也整合到了同一个芯片上,从而形成了功能复杂的计算机系统。下图是高通一款名为APQ8016E SGE
SoC的框图,来源于高通官网。
图中标注有APQ8016E浅蓝色的框就是这款SOC,其内部根据各部分的作用不同,又分为以下几个部分:
- “
Processors
”就是指CPUs
,在这个SoC中有两个CPU。它们分别是:- 负责处理主要应用的
App Processor
——Arm Quad Cortex A53
。这里需要注意的是,它是一个CPU,但是却有4个ARM核心(core
)。这就是我们通常所说的“CPU是双核的,还是4核的”。 - 另外一个是负责整个SoC电源管理的RPM处理器,它同样也是一颗ARM架构的CPU。
- 负责处理主要应用的
- “
Memory support
”指的是用于存储的控制器,比如内存控制器,emmc控制器。 - “
Air interface
”包括GNSS
(导航相关的控制器)和WCN
(无线设备相关的控制器,比如,wifi、bluetooth和FM)。 - “
Multimedia
”指的是多媒体相关的控制器,比如,camera sensor控制器,用于显示的MIPI控制器,Audio的控制器codec。 - “
Connectivity
”指的就是用于连接外部设备的控制器。比如,常见的i2c 控制器,SPI控制器,SD卡控制器,USB控制器等等。 - “
GPIOs / PWR & GND
”指的是GPIO控制器和芯片上的一些其他资源。 - “
Internal functions
”指的SoC内部的一些功能,比如clock发生器,温度传感器等等 -
“
Chipset & RFFE I/Fs
”指的射频和基带相关的部分控制电路。综上所述,SoC主要就是CPU和一些控制器组成,而这些控制器的作用就是辅助CPU传输数据,控制外部设备。因此,他们都需要对应的驱动程序。
1.4 驱动程序
在上一篇文章中,我们介绍了Linux的哲学之一——“一切都是文件”。意思是,对于Linux来讲,所有的IO(input/output
)资源都是文件。前面也提到设备可以通过相应的控制器给CPU提供数据,显然设备也属于IO资源,也就是说,设备在Linux中也被抽象成了文件,那么是谁完成了这一动作?对,就是设备驱动程序。
设备驱动程序不仅将设备抽象成了设备文件,隐藏了设备工作细节,还向应用程序提供了一组操作函数,使得用户能够通过标准的文件系统访问接口和这组操作函数来控制该设备。更为灵活的是驱动程序也可以像Linux的其他模块一样,在运行时动态的“插入”到内核,从而实现设备对应的功能。实际上,编写驱动程序就是在Linux模块的框架下,加入驱动硬件正常工作的代码,并使用统一的Linux接口对外提供操作访问硬件的接口。
综上,编写驱动程序首先要求对相应的硬件功能和操作有一定的了解,其次,要熟悉内核的很多知识,比如中断管理,并发和竞争,进程的调度。由于内核高度的模块化,因此,在编写驱动程序中使用到其他内核机制,我们甚至不需要去理解其具体实现,仅了解其用法就能写出完整的驱动程序。这也是我选择从驱动程序开始理解Linux内核的原因。在熟练使用这些机制的基础上,再去了解机制背后具体的实现方法及其框架就会有事半功倍的效果。
二、本系列的主要内容
学习Linux驱动程序最核心的部分是掌握Linux设备模型,进而理解Linux如何管理所有的设备及其依赖关系。另一方面,设备驱动程序根据硬件工作的原理,抽象出了通用的接口供应用程序来访问设备数据或者控制设备,因此,对于编写驱动程序首要的任务是理解该硬件的工作原理,且不同的硬件工作原理也大不相同。比如,编写USB设备驱动需要对USB协议有一定的了解,而编写sensor的驱动,可能需要根据其接口不同,掌握不同串行通信协议,如i2c、spi等。
这个系列的文章会从设备驱动程序的最基础框架——Linux的模块机制谈起,根据SoC组织设备的方式引入Device tree,从而讨论如何从Device tree中解析出系统中所有的device,这些device 又是如何被管理的。然后,我们会讨论driver是如何注册并管理的,device和driver如何bind并probe的。基于此编写一个最简单的字符设备驱动,从而对设备驱动程序建立一个直观的印象。然后,我们会研究并分析几个典型的设备驱动程序,进而抽象出设备驱动模型;有时间的话,我们会在这个基础上探究一些更为复杂的设备驱动,比如,usb设备驱动,display driver等。在这个过程中,我们还会学习一些驱动中常用的Linux机制,比如,为了处理并发和竞态而引入的各种锁、Linux的中断机制等知识。
总之,Linux是一个复杂而精细的系统,研究的越透彻,愈能发现其魅力所在。你准备好了吗?让我们开始吧!