设备驱动概述

1.1节讲解了设备驱动的概念和作用。
1.2节和1.3节分别讲述无操作系统和有操作系统情况下设备驱动的设计方法,通过分析讲解设备驱动与硬件和操作系统的关系。
1.4节对Linux操作系统的设备驱动进行了概要性的介绍,讲解设备驱动与系统软硬件的关系,分析了Linux设备驱动的重点难点和学习方法。
最后给出了一个设备驱动的“Hello World”实例,即最简单的LED驱动在无操作系统情况下和Linux操作系统下的实现。

1.1 设备驱动的作用

任何一个计算机系统的运行都是系统中软硬件协作的结果,没有硬件的软件是空中楼阁,而没有软件的硬件则只是一堆废铁。硬件是底层基础,是所有软件得以运行的平台,代码最终会落实为硬件上的组合逻辑和时序逻辑;软件则实现了具体应用,它按照各种不同的业务需求而设计,满足了用户的需求。硬件较固定,软件则很灵活,可以适应各种复杂多变的应用。可以说,计算机系统的软硬件互相成就了对方。
但是,软硬件之间同样存在着悖论,那就是软件和硬件不应该互相渗透到对方的领地。为了尽可能快速地完成设计,应用软件工程师不想也不必关心硬件,而硬件工程师也难有足够的闲暇和能力来顾及软件。例如,应用软件工程师在调用套接字发送和接收数据包的时候,他不必关心网卡上的中断、寄存器、存储空间、I/O端口、片选以及其他任何硬件词汇;在使用printf()函数输出信息的时候,他不用知道底层究竟是怎样把相应的信息输出到屏幕或者串口。
也就是说,应用程序工程师需要看到一个没有硬件的纯粹的软件世界,硬件必须被透明地呈现给他们。谁来实现硬件对应用软件工程师的隐形?这个艰巨的任务就落到了驱动工程师的头上。
对设备驱动最通俗的解释就是“驱使硬件设备行动”。设备驱动与底层硬件直接打交道,按照硬件设备的具体工作方式读写设备寄存器,完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射,最终使通信设备能够收发数据,使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。
由此可见,设备驱动充当了硬件和应用软件之间的纽带,它使得应用软件只需要调用系统软件的应用编程接口(API)就可让硬件去完成要求的工作。在系统中没有操作系统的情况下,工程师可以根据硬件设备的自行定义接口。如对串口定义SerialSend()、SerialRecv();对LED定义LightOn()、LightOff();以及对Flash定义FlashWrite()、FlashRead()等。而在有操作系统的情况下,设备驱动的架构则由相应的操作系统定义,驱动工程师必须按照相应的架构设计设备驱动,这样,设备驱动才能良好地整合到操作系统地内核中。
驱动程序沟通着硬件和应用软件,而驱动工程师则沟通着硬件工程师和软件工程师。随着通信、电子行业的迅速发展,全世界每天都会有大量的新芯片被生产,大量的新电路板被设计,因此也会有大量设备驱动需要开发。这些设备驱动,或运行在简单的单任务环境中,或运行在VxWorks、Linux、Windows等多任务操作系统环境中,发挥着不可替代的作用。

1.2 无操作系统时的设备驱动

并不是任何一个计算机系统都一定要运行操作系统,在许多情况下操作系统是不必要的。对于功能比较单一、控制并不复杂的系统,如公交车刷卡机、电冰箱、微波炉、简单的手机和小灵通等,并不需要多任务调度、文件系统、内存管理等复杂功能,用单任务完全可以很好地支持它们的工作。一个无限循环中夹杂对设备中断的检测或者对设备的轮询是这种系统中软件的典型架构,如代码清单所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc,char *argv[])
{
while(1)
{
if(serialInt == 1)//有串口中断
{
ProcessSerialInt();//处理串口中断
serialInt = 0;//中断标志清零
}

if(keyInt == 1)//有按键中断
{
ProcessKeyInt();//处理按键中断
keyInt = 0;//中断标志变量清零
}

status = CheckXXX();
switch(status)
{
...
}
...
}
}

在这样的系统中,虽然不存在操作系统,但是设备驱动是必须存在的。一般情况下,对每一种设备驱动都会定义为一个软件模块,包含.h文件和.c文件,前者定义该设备驱动的数据结构并声明外部函数后,后者进行设备驱动的具体实现。下列代码定义了一个串口的驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
serial.h 文件
*/
extern void SerialInit(void);
extern void SerialSend(const char *buf,int count);
extern void SerailRecv(char *buf,int count);

/*
serial.c 文件
*/
//串口初始化
void SerialInit(void)
{
...
}
//串口发送
void SerialSend(const char *buf,int count)
{
...
}
//串口接收
void SerailRecv(char *buf,int count)
{
...
}
//串口中断处理函数
void SerialIsr(void)
{
...
serailInt = 1
}

其他模块需要使用这个设备的时候,只需要包含设备驱动的头文件serial.h,然后调用其中的外部接口函数即可。如我们要从串口上发送字符串“Hello World”,使用函数SerialSend(“Hello World”,11)即可;
由此可见,在没有操作系统的情况下,设备驱动的接口被直接提交给了应用软件工程师,应用软件没有跨越任何层次就直接访问到了设备驱动的接口。设备驱动包含的接口函数也与硬件的功能直接吻合,没有任何附加功能。下图所示为无操作系统情况下硬件、设备驱动与应用软件的关系。
img not found
有的工程师把单任务系统设计成了如下图所示的结构,即设备驱动和具体的应用软件模块出于同一层次,这显然是不合理的,不符合软件设计中高内聚低耦合的要求。
img not found
另一种不合理的设计是直接在应用中操作硬件的寄存器,而不单独设计驱动模块,如下图所示。这种设计意味着系统中不存在或未能充分利用可被重用的驱动代码。
img not found

1.3 有操作系统时的设备驱动

1.2节中的设备驱动直接运行在硬件之上,不与任何操作系统关联。当系统中包含操作系统后,设备驱动会变得怎样?
首先,无操作系统时设备驱动的硬件操作工作仍然是必不可少的,没有这一部分,设备驱动不可能与硬件打交道。
其次,我们还需要将设备驱动融入内核。为了实现这种融合,必须在所有的设备驱动中设计面向操作系统内核的接口,这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。
由此可见,当系统中存在操作系统的时候,设备驱动变成了连接硬件和内核的桥梁。如下图所示,操作系统的存在势必要求设备驱动附加更多的代码和功能,把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API,不再给应用软件工程师直接提供接口。
img not found
有了操作系统之后,设备驱动反而变得复杂,那要操作系统干什么?
首先,一个复杂的软件系统需要处理多个并发的任务,没有操作系统,想完成多任务并发是很困难的。
其次,操作系统给我们提供内存管理机制。一个典型的例子是,对于多数含MMU的处理器而言,Windows、Linux等操作系统可以让每个进程都独立地访问4GB的内存空间。
上述优点似乎并没有体现在设备驱动上,操作系统的存在给设备驱动究竟带来了什么好处呢?
简而言之,操作系统通过给设备驱动制造麻烦来达到给上层应用提供便利的目的。如果设备驱动都按照操作系统给出的独立于设备的接口而设计,应用程序将可使用统一的系统调用接口来访问各种设备。对于类Unix的VxWorks、Linux等操作系统而言,应用程序通过write()、read()等函数读写文件就可以访问各种字符设备和块设备,而不用管设备的具体类型和工作方式,是非常方便的。

1.4 Linux设备驱动

1.4.1 设备的分类及特点

计算机系统的硬件主要由CPU、存储器和外设组成。随着IC制造工艺的发展,目前,芯片的集成度越来越高,往往在CPU内部就集成了存储器和外设适配器。ARM、PowerPC、MIPS等处理器都集成了UART、I2C控制器、USB控制器、SDRAM控制器等,有的处理器还集成了片内RAM和Flash。
驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设),而不是针对CPU核。Linux将存储器和外设分为3个基础大类:

  1. 字符设备;
  2. 块设备;
  3. 网络设备。

字符设备是指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁盘驱动器、鼠标等。块设备可以用任意顺序进行访问,以块为单位进行操作,如硬盘、软驱等。字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲。但是,字符设备和块设备并没有明显的界限,如Flash设备符合块设备的特点,但是我们仍然可以把它作为一个字符设备来访问。
字符设备和块设备的驱动设计呈现出很大的差异,但是对于用户而言,他们都使用文件系统的操作接口open()、close()、read()、write()等函数进行访问。
在Linux系统中,网络设备面向数据包的接收和发送而设计,它并不对应文件系统的节点。内核与网络设备的通信和内核与字符设备、块设备的通信方式完全不同。
另外,TTY驱动、I2C驱动、USB驱动、PCI驱动、LCD驱动等本身大体可归纳入3个基础大类,但是对于这些复杂的设备,Linux系统还定义了独特的驱动体系结构。

1.4.2 Linux设备驱动与整个软硬件系统的关系

如下图所示,除网络设备之外,字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()等函数即可访问字符设备和块设备。所有的字符设备和块设备都被统一地呈现给用户。块设备比字符设备更复杂,在它上面会首先建立一个磁盘/Flash文件系统,如FAT、Ext3、YAFFS、JFFS等。FAT、Ext3、YAFFS、JFFS规范了文件和目录在存储介质上的组织。
应用程序可以使用Linux的系统调用接口编程,也可以使用C库函数,出于代码可移植性的考虑,后者更值得推荐。C库函数本身也通过系统调用接口而实现,如C库函数中的fopen()、fwrite()、fread()、fclose()分别会调用操作系统API的open()、write()、read()、close()函数。
img not found

1.4.3 编写Linux设备驱动的技术基础

Linux设备驱动的学习是一项浩大的工程,读者需要首先掌握以下基础。

  1. 编写Linux设备驱动要求工程师具有良好的硬件基础,懂得SRAM、Flash、SDRAM、磁盘的读写方式,UART、I2C、USB等设备的接口,轮询、中断、DMA的原理,PCI总线的工作方式以及CPU的内存管理单元(MMU)等。
  2. 编写Linux设备驱动要求工程师具有良好的C语言基础,能灵活地运用C语言的结构体、指针、函数指针及内存动态申请和释放等。
  3. 编写Linux设备驱动要求工程师具有一定的Linux内核基础,虽然并不要求工程师对内核各个部分有深入的研究,但至少要了解设备驱动和内核的接口,尤其是对于块设备、网络设备、Flash设备、串口设备等复杂设备。
  4. 编写Linux设备驱动要求工程师具有良好的多任务并发控制和同步的基础,因为在设备驱动中会大量使用自旋锁、互斥、信号量、等待队列等并发与同步机制。

1.4.4 Linux设备驱动的学习方法

1.5 设备驱动的Hello World:LED驱动

1.5.1 无操作系统时的LED驱动

在嵌入式系统的设计中,LED一般直接由CPU的GPIO控制。GPIO一般由两组寄存器控制,即一组数据寄存器和一组控制寄存器。控制寄存器可设置GPIO口的工作方式为输入或输出。当引脚被设置为输出时,向数据寄存器的对应位写入1和0会分别在引脚上产生高电平和低电平;当引脚设置为输入时,读取数据寄存器的对应位可获得引脚上相应的电平信号。
在本例子中,我们屏蔽具体CPU的差异,假设在GPIO_REG_CTRL物理地址处的控制寄存器处的第n位写入1可设置GPIO为输出,在GPIO_REG_DATA物理地址处的数据寄存器的第n位写入1或0可在引脚上产生高或低电平,则在无操作系统的情况下,设备驱动代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define reg_gpio_ctrl *(volatile int *)(ToVirtual(GPIO_REG_CTRL))
#define reg_gpio_data *(volatile int *)(ToVirtual(GPIO_REG_DATA))

//初始化LED
void LightInit(void)
{
reg_gpio_ctrl |= (1 << n);//设置GPIO为输出
}

//点亮LED
void LightOn(void)
{
reg_gpio_data |= (1 << n);//在GPIO上输出高电平
}

//熄灭LED
void LightOff(void)
{
reg_gpio_data &= ~(1 << n);//在GPIO上输出低电平
}

上述程序中的LightInit()、LightOn()、LightOff()等函数都将作为LED驱动提供给应用程序的外部接口函数。程序中ToVirtual()等函数的作用是当系统启动了硬件MMU之后,根据物理地址和虚拟地址的映射关系,将寄存器的物理地址转化为虚拟地址。

1.5.2 Linux系统下的LED驱动

在Linux操作系统下编写LED设备的驱动时,操作硬件的LightInit()、LightOn()、LightOff()这些函数仍然需要,但是,需要遵循Linux编程的命名习惯,重新将其命名为light_init()、light_on()、light_off()。这些函数将被LED驱动中独立于设备的针对内核的接口进行调用,下列代码给出了Linux系统下的LED驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include ...//包含内核中的多个头文件

//设备结构体
struct light_dev
{
struct cdev cdev; //字符设备cdev结构体
unsigned char value; //LED亮时为1,熄灭时为0,用户可读写此值
};

struct light_dev *light_devp;
int light_major = LIGHT_MAJOR;

MODULE_AUTHOR("Song Baohua");
MODULE_LICENSE("Dual BSD/GPL");

//打开和关闭函数
int light_open(struct inode *inode,struct file *filp)
{
struct light_dev *dev;

//获得设备结构体指针
dev = container_of(inode->i_cdev,struct light_dev,cdev);

//让设备结构体作为设备的私有信息
filp->priavte_data = dev;
return 0;
}

int light_release(struct inode *inode,struct file *filp)
{
return 0;
}

//写设备:可以不需要
ssize_t light_read(struct file *filp,char __user *buf,size_t count,loff_t *fops)
{
struct light_dev *dev = filp->private_data;//获得设备结构体

if(copy_to_user(buf,&(dev->value),1))
{
return -EFAULT;
}
return 1;
}

ssize_t light_write(struct file *filp,const char __user *buf,size_t count,loff_t *fops)
{
struct light_dev *dev = filp->private_data;//获得设备结构体

if(copy_from_user(&(dev->value),buf,1))
{
return -EFAULT;
}

//根据写入的值点亮和熄灭LED
if(dev->value == 1)
light_on();
else
light_off();

return 1;
}

//ioctl函数
int light_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
{
struct light_dev *dev = filp->private_data;//获得设备结构体

switch(cmd)
{
case LIGHT_ON:
dev->value = 1;
light_on();
break;
case LIGHT_OFF:
dev->value = 0;
light_off();
break;
default:
//不能支持的命令
return -ENOTTY;
}
return 0;
}

struct file_operations light_fops =
{
.owner = THIS_MODULE,
.read = light_read,
.write = light_write,
.ioctl = light_ioctl,
.open = light_open,
.release = light_release,
};

//设置字符设备cdev结构体
static void light_setup_cdev(struct light_dev *dev,int index)
{
int err,devno = MKDEV(light_major,index);

cdev_init(&dev->cdev,&light_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &light_fops;
err = cdev_add(&dev->cdev,devno,1);
if(err)
printk(KERN_NOTICE"Error %d adding LED%d",err,index);
}

//模块加载函数
int light_init(void)
{
int result;
dev_t dev = MKDEV(light_major,0);

//申请字符设备号
if(light_major)
result = register_chrdev_region(dev,1,"LED");
else
{
result = alloc_chrdev_region(&dev,0,1,"LED");
light_major = MAJOR(dev);
}

if(result < 0)
return result;

//分配设备结构体的内存
light_devp = kmalloc(sizeof(struct light_dev),GFP_KERNEL);
if(!light_devp)//分配失败
{
result = -ENOMEM;
goto fail_malloc;
}
memset(light_devp,0,sizeof(struct light_dev));
light_setup_cdev(light_devp,0);
light_init();
return 0;

fail_malloc:
unregister_chrdev_region(dev,light_devp);
return result;
}

//模块卸载函数
void light_cleanup(void)
{
cdev_del(&light_devp->cdev);//删除字符设备结构体
kfree(light_devp);//释放在light_init中分配的内存
unregister_chrdev_region(MKDEV(light_major,0),1);//删除字符设备
}

module_init(light_init);
module_exit(light_cleanup);

上述代码比上上份代码多了很多,除了硬件操作函数仍然需要外,上述代码还包含了大量读者陌生的元素,如结构体file_operations、cdev,Linux内核模块声明用的MODULE_AUTHOR、MODUL_LICENSE、module_init、module_exit,以及用于字符设备注册、分配和注销用的函数register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region()等。此外设驱动中也增加了light_init()、light_cleanup()、light_read()、light_write()等这样的函数。
此时,我们只需要有一个感性认识,那就是,上述元素都是Linux驱动与内核的接口。Linux对各类设备的驱动都定义了类似的数据结构和函数。