设备驱动程序是计算机硬件与应用程序的接口,是软件系统与硬件系统沟通的桥梁。如果没有设备驱动程序,那么硬件设备就只是一堆废铁,没有什么功能。本章将对Linux驱动开发进行简要的概述,使读者理解一些常见的概念。
刚刚接触Linux设备驱动的朋友,会对Linux设备驱动中的一些基本概念不太理解。这种不理解,会导致继续学习的困难,所以本节集中讲解一些Linux设备驱动的基本概念,为进一步学习打下良好的基础。
设备驱动程序(Device Driver),简称驱动程序(Driver)。它是一个允许计算机软件(Computer Software)与硬件(Hardware)交互的程序。这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面。CPU经由主板上的总线(Bus)或其他沟通子系统(Subsystem)与硬件形成连接,这样的连接使得硬件设备(Device)之间的数据交换成为可能。
依据不同的计算机架构与操作系统平台差异,驱动程序可以是8位、16位、32位,64位。不同平台的操作系统需要不同的驱动程序。例如32位的Windows系统需要32位的驱动程序,64位的Windows系统,需要64位的驱动程序;在Windows 3.11的16位操作系统时代,大部分的驱动程序都是16位;到了32位的Windows XP,则大部分是使用32位驱动程序;至于64位的Linux或是Windows 7平台上,就必须使用64位驱动程序。
设备驱动程序是一种可以使计算机与设备进行通信的特殊程序,可以说相当于硬件的接口。操作系统只有通过这个接口,才能控制硬件设备的工作。假如某设备的驱动程序未能正确安装,便不能正常工作。正因为这个原因,驱动程序在系统中所占的地位十分重要。一般,当操作系统安装完毕后,首要的便是安装硬件设备的驱动程序。并且,当设备驱动程序有更新的时候,新的驱动程序比旧的驱动程序有更好的性能。这是因为新的驱动程序对内存、IO等进行了优化,使硬件能够到达更好的性能。
但是,大多数情况下,并不需要安装所有硬件设备的驱动程序。例如硬盘、显示器、光驱、键盘和鼠标等就不需要安装驱动程序,而显卡、声卡、扫描仪、摄像头和Modem等就需要安装驱动程序。不需要安装驱动程序并不代表这些硬件不需要驱动程序,而是这些设备所需驱动已经内置在操作系统中。另外,不同版本的操作系统对硬件设备的支持也是不同的,一般情况下,版本越高所支持的硬件设备也越多。
设备驱动程序用来将硬件本身的功能告诉操作系统(通过提供接口的方式),完成硬件设备电子信号与操作系统及软件的高级编程语言之间的互相翻译。当操作系统需要使用某个硬件时,例如让声卡播放音乐,它会先发送相应指令到声卡的某个I/O端口。声卡驱动程序从该I/O端口接收到数据后,马上将其翻译成声卡才能听懂的电子信号命令,从而让声卡播放音乐。所以简单地说,驱动程序是提供硬件到操作系统的一个接口,并且协调二者之间的关系。而因为驱动程序有如此重要的作用,所以人们都称“驱动程序是硬件的灵魂”、“硬件的主宰”,同时驱动程序也被形象地称为“硬件和系统之间的桥梁”。
计算机系统的主要硬件由CPU、存储器和外部设备组成。驱动程序的对象一般是存储器和外部设备。随着芯片制造工艺的提高,为了节约成本,通常将很多原属于外部设备的控制器嵌入到CPU内部。例如Intel的酷睿i5 3450处理器,就内置有GPU单元,配合“需要搭配内建GPU的处理器”的主板,就能够起到显卡的作用。相比独立显卡,性价比上有很大的优势。所以现在的驱动程序应该支持CPU中的嵌入控制器。Linux将这些设备分为3大类,分别是字符设备、块设备和网络设备。
1. 字符设备
字符设备是指那些能一个字节一个字节读取数据的设备,如LED灯、键盘和鼠标等。字符设备一般需要在驱动层实现open()、close()、read()、write()、ioctl()等函数。这些函数最终将被文件系统中的相关函数调用。内核为字符设备对应一个文件,如字符设备文件/dev/console。对字符设备的操作可以通过字符设备文件/dev/console来进行。这些字符设备文件与普通文件没有太大的差别,差别之处是字符设备一般不支持寻址,但特殊情况下,有很多字符设备也是支持寻址的。寻址的意思是,对硬件中一块寄存器进行随机地访问。不支持寻址就是只能对硬件中的寄存器进行顺序的读取,读取数据后,由驱动程序自己分析需要哪一部分数据。
2. 块设备
块设备与字符设备类似,一般是像磁盘一样的设备。在块设备中还可以容纳文件系统,并存贮大量的信息,如U盘、SD卡。在Linux系统中,进行块设备读写时,每次只能传输一个或者多个块。Linux可以让应用程序像访问字符设备一样访问块设备,一次只读取一个字节。所以块设备从本质上更像一个字符设备的扩展,块设备能完成更多的工作,例如传输一块数据。
综合来说,块设备比字符设备要求更复杂的数据结构来描述,其内部实现也是不一样的。所以,在Linux内核中,与字符驱动程序相比,块设备驱动程序具有完全不同的API接口。
3. 网络设备
计算机连接到互联网上需要一个网络设备,网络设备主要负责主机之间的数据交换。与字符设备和块设备完全不同,网络设备主要是面向数据包的接收和发送而设计的。网络设备在Linux操作系统中是一种非常特殊的设备,其没有实现类似块设备和字符设备的read()、write()和ioctl()等函数。网络设备实现了一种套接字接口,任何网络数据传输都可以通过套接字来完成。
Linux操作系统与设备驱动之间的关系如图1.1所示。用户空间包括应用程序和系统调用两层。应用程序一般依赖于函数库,而函数库是由系统调用来编写的,所以应用程序间接地依赖于系统调用。
图 1.1 设备驱动程序与操作系统的关系
系统调用层是内核空间和用户空间的接口层,就是操作系统提供给应用程序最底层的API。通过这个系统调用层,应用程序不需要直接访问内核空间的程序,增加了内核的安全性。同时,应用程序也不能访问硬件设备,只能通过系统调用层来访问硬件设备。如果应用程序需要访问硬件设备,那么应用程序先访问系统调用层,由系统调用层去访问内核层的设备驱动程序。这样的设计,保证了各个模块的功能独立性,也保证了系统的安全。
系统调用层依赖内核空间的各个模块来实现。在Linux内核中,包含很多实现具体功能的模块。这些模块包括文件系统、网络协议栈、设备驱动、内核调度、内存管理、进程管理等,都属于系统内核空间,也就是这些模块是在内核空间实现的。
最底层是硬件层,这一层是实际硬件设备的抽象。设备驱动程序的功能就是驱动这一层硬件。设备驱动程序可以工作在有操作系统的情况下,也可以工作在没有操作系统的情况下。如果只需要实现一些简单的控制设备操作,那么可以不使用操作系统。如果嵌入式系统完成的功能比较复杂,则往往需要操作系统来帮忙。例如,单片机程序,就不需要操作系统,因为其功能简单,内存、处理器能力弱,不能也没有必要为其开发操作系统。
Linux驱动程序的开发与应用程序的开发有很大的差别。这些差别导致了编写Linux设备驱动程序与编写应用程序有本质的区别,所以对于应用程序的设计技巧很难直接应用在驱动程序的开发上。最经典的例子是应用程序如果错误可以通过try catch等方式,避免程序的崩溃,驱动程序则没有这么好的处理方式。本节将对Linux驱动程序的开发进行简要的讲解。
Linux操作系统分为用户态和内核态。用户态处理上层的软件工作。内核态用来管理用户态的程序,完成用户态请求的工作。驱动程序与底层的硬件交互,所以工作在内核态。
简单来说,内核态大部分时间在完成与硬件的交互,比如读取内存,将硬盘上的数据读取到内存中,调度内存中的程序到处理器中运行等。相对于内核态,用户态则自由得多,其实,用户态中的用户可以狭隘的理解为应用程序开发者,他们很少与硬件直接打交道,他们经常的工作是编写Java虚拟机下的应用程序,或者.NET框架下的应用程序。即使他们的编程水平很初级,经常出现程序异常,那么充其量是将Java虚拟机搞崩溃,想把操作系统搞崩溃还是很难的。这一切都归功于内核态对操作系统有很强大的保护能力。
另一方面,Linux操作系统分为两个状态的原因主要是,为应用程序提供一个统一的计算机硬件抽象。工作在用户态的应用程序完全可以不考虑底层的硬件操作,这些操作由内核态程序来完成。这些内核态程序大部分是设备驱动程序。一个好的操作系统的驱动程序对用户态应用程序应该是透明的,也就是说,应用程序可以在不了解硬件工作原理的情况下,很好地操作硬件设备,同时不会使硬件设备进入非法状态。Linux操作系统很好的做到了这一点。在Linux编程中,程序员经常使用open()方法来读取磁盘中的数据,在调用这个方法的时候,并不需要关心磁盘控制器是怎么读取数据,并将其传到内存中的。这些工作都是驱动程序完成的,这就是驱动程序的透明性。
一个值得注意的问题是,工作在用户态的应用程序不能因为一些错误而破坏内核态的程序。现代处理器已经充分考虑了这个问题。处理器提供了一些指令,分为特权指令和普通指令。特权指令只有在内核态下才能使用;普通指令既可以在内核态使用,也可以在用户态使用。通过这种限制,用户态程序就不能执行只有在内核态才能执行的程序了,从而起到保护的作用。
另一个值得注意的问题是,用户态和内核态是可以互相转换的。每当应用程序执行系统调用或者被硬件中断挂起时,Linux操作系统都会从用户态切换到内核态。当系统调用完成或者中断处理完成后,操作系统会从内核态返回用户态,继续执行应用程序。
模块是可以在运行时加入内核的代码,这是Linux一个很好的特性。这个特性使内核可以很容易地扩大或缩小,一方面扩大内核可以增加内核的功能,另一方面缩小内核可以减小内核的大小。
Linux内核支持很多种模块,驱动程序就是其中最重要的一种,甚至文件系统也可以写成一个模块,然后加入内核中。每一个模块由编译好的目标代码组成,可以使用insmod(insert module的缩写)命令将模块加入正在运行的内核,也可以使用rmmod(remove module的缩写)命令将一个未使用的模块从内核中删除。试图删除一个正在使用的模块,将是不允许的。对Windows熟悉的朋友,可以将模块理解为DLL文件。
模块在内核启动时装载称为静态装载,在内核已经运行时装载称为动态装载。模块可以扩充内核所期望的任何功能,但通常用于实现设备驱动程序。一个模块的最基本框架代码如下:
#include#include #include int __init xxx_init(void) { /*这里是模块加载时的初始化工作*/ return 0; } void __exit xxx_exit(void) { /*这里是模块卸载时的销毁工作*/ } module_init(xxx_init); /*指定模块的初始化函数的宏*/ module_exit(xxx_exit); /*指定模块的卸载函数的宏*/
目前,Linux操作系统有七、八百万行代码,其中驱动程序代码就有四分之三左右。所以对于驱动开发者来说,学习和编写设备驱动程序都是一个漫长的过程。在这个过程中,读者应该掌握如下一些知识:
1. 驱动开发人员应该有良好的C语言基础,并能灵活地应用C语言的结构体、指针、宏等基本语言结构。另外,Linux系统使用的C编译器是GNU C编译器,所以对GNU C标准的C语言也应该有所了解。
2. 驱动开发人员应该有良好的硬件基础。虽然不要求驱动开发人员具有设计电路的能力,但也应该对芯片手册上描述的接口设备有清楚的认识。常用的设备有SRAM、Flash、UART、IIC和USB等。
3. 驱动开发人员应该对Linux内核源代码有初步的了解。例如一些重要的数据结构和函数等。
4. 驱动开发人员应该有多任务程序设计的能力,同时驱动中也会使用大量的自旋锁、互斥锁和信号量等。
本书的大部分知识基本包括了这些方面,读者在阅读本书的过程中应该能快速掌握这些知识。
大部分程序员都比较熟悉应用程序的编写,但是对于驱动程序的编写可能不是很熟悉。关于应用程序的很多编程经验不能直接应用到驱动程序的编写中,下面给出编写驱动程序的一些注意事项,希望引起读者注意。
在Linux上的程序开发一般分为两种,一种是内核及驱动程序开发,另一种是应用程序开发。这两种开发种类对应Linux的两种状态,分别是内核态和用户态。内核态用来管理用户态的程序,完成用户态请求的工作;用户态处理上层的软件工作。驱动程序与底层的硬件交互,所以工作在内核态。
大多数程序员致力于应用程序的开发,少数程序员则致力于内核及驱动程序的开发。相对于应用程序的开发,内核及驱动程序的开发有很大的不同。最重要的差异包括以下几点:
内核及驱动程序开发时不能访问C库,因为C库是使用内核中的系统调用来实现的,而且是在用户空间实现的。驱动程序只能访问有限的系统调用,或者汇编程序。
内核及驱动程序开发时必须使用GNU C,因为Linux操作系统从一开始就使用的是GNU C,虽然也可以使用其他的编译工具,但是需要对以前的代码做大量的修改。需要注意的是,32位的机型和64位的机型在编译时,也有一定的差异。
内核支持异步中断、抢占和SMP,因此内核及驱动程序开发时必须时刻注意同步和并发。
内核只有一个很小的定长堆栈。
内核及驱动程序开发时缺乏像用户空间那样的内存保护机制。稍不注意,就可能读写其他程序的内存。
内核及驱动程序开发时浮点数很难使用,应该使用整型数。
内核及驱动程序开发要考虑可移植性,因为对于不同的平台,驱动程序是不兼容的。
GUN C语言最早起源于一个GUN计划,GUN的意思是GUN is not UNIX。GUN计划开始于1984年,这个计划的目的是开发一个类似UNIX并且软件自由的完整操作系统。这个计划一直在进行,到Linus开发Linux操作系统时,GNU计划已经开发出来了很多高质量的自由软件,其中就包括著名的GCC编译器,GCC编译器能够编译GUN C语言。Linus考虑到GUN计划的自由和免费,所以选择了GCC编译器来编写内核代码,之后的很多开发者也使用这个编译器,所以直到现在,驱动开发人员还使用GUN C语言来开发驱动程序。
与用户空间的应用程序不同,内核不能调用标准的C函数库,主要的原因在于对于内核来说完整的C库太大了。一个编译的内核大小可以是1MB左右,而一个标准的C语言库大小可能操作5MB。这对于存储容量较小的嵌入式设备来说,是不实用的。缺少标准C语言库,并不是说驱动程序就只能做很好的事情了。因为标准C语言库是通过系统调用实现的,驱动也是通过系统调用等实现的,两者都有相同的底层,所以驱动程序不需要调用C语言库,也能实现很多的功能。
大部分常用的C库函数在内核中都已经实现了。比如操作字符串的函数就位于内核文件lib/string.c中。只要包含
注意:内核程序中包含的头文件是指内核代码树中的内核头文件,不是指开发应用程序时的外部头文件。在内核中实现的库函数中的打印函数printk(),它是C库函数printf()的内核版本。printk()函数和printf()函数有基本相同的用法和功能。
当一个用户应用程序由于编程错误,试图访问一个非法的内存空间,那么操作系统内核会结束这个进程,并返回错误码。应用程序可以在操作系统内核的帮助下恢复过来,而且应用程序并不会对操作系统内核有太大的影响。但是如果操作系统内核访问了一个非法的内存,那么就有可能破坏内核的代码或者数据。这将导致内核处于未知的状态,内核会通过oops错误给用户一些提示,但是这些提示都是不支持、难以分析的。
在内核编程中,不应该访问非法内存,特别是空指针,否则内核会忽然死掉,没有任何机会给用户提示。对于不好的驱动程序,引起系统崩溃是很常见的事情,所以对于驱动开发人员来说,应该非常重视对内存的正确访问。一个好的建议是,当申请内存后,应该对返回的地址进行检测。
用户空间的程序可以从栈上分配大量的空间存放变量,甚至用栈存放巨大的数据结构或者数组都没问题。之所以能这样做是因为应用程序是非常驻内存的,它们可以动态地申请和释放所有可用的内存空间。内核要求使用固定常驻的内存空间,因此要求尽量少地占用常驻内存,而尽量多地留出内存提供给用户程序使用。因此内核栈的长度是固定大小的,不可动态增长的,32位机的内核栈是8KB;64位机的内核栈是16KB。
由于内核栈比较小,所以编写程序时,应该充分考虑小内核栈问题。尽量不要使用递归调用,在应用程序中,递归调用4000多次就有可能溢出,在内核中,递归调用的次数非常少,几乎不能完成程序的功能。另外使用完内存空间后,应该尽快地释放内存,以防止资源泄漏,引起内核崩溃。
对于用户空间的应用程序来说,可移植性一直是一个重要的问题。一般可移植性通过两种方式来实现。一种方式是定义一套可移植的API,然后对这套API在这两个需要移植的平台上分别实现。应用程序开发人员只要使用这套可移植的API,就可以写出可移植的程序。在嵌入式领域,比较常见的API套件是QT。另一种方式是使用类似Java、Actionscript等可移植到很多操作系统上的语言。这些语言一般通过虚拟机执行,所以可以移植到很多平台上。
对于驱动程序来说,可移植性需要注意以下几个问题:
考虑字节顺序,一些设备使用大端字节序,一些设备使用小端字节序。Linux内核提供了大小端字节序转换的函数。
#define cpu_to_le16(v16) (v16)
#define cpu_to_le32(v32) (v32)
#define cpu_to_le64(v64) (v64)
#define le16_to_cpu(v16) (v16)
#define le32_to_cpu(v32) (v32)
#define le64_to_cpu(v64) (v64)
即使是同一种设备的驱动程序,如果使用的芯片不同,也应该写不同的驱动程序,但是应该给用户提供一个统一的编程接口。
尽量使用宏代替设备端口的物理地址,并且可以使用ifdefine宏确定版本等信息。
针对不同的处理器,应该使用相关处理器的函数。
随着嵌入式技术的发展,使用Linux的嵌入式设备也越来越多,特别是现在的Android设备。同样地,工业上对Linux驱动的开发也越来越重视。本节将对Linux驱动的发展做简要的介绍。
Linux和嵌入式Linux软件在过去几年里已经被越来越多的IT、半导体、嵌入式系统等公司所认可和接受,它已经成为一个可以替代微软的Windows和众多传统的RTOS的重要的操作系统。Linux内核和基本组件及工具已经非常成熟。面向行业、应用和设备的嵌入式Linux工具软件和嵌入式Linux操作系统平台,是未来发展的必然趋势。符合标准,遵循开放是大势所趋,人心所向,嵌入式Linux也不例外。
使嵌入式Linux不断发展的一个核心问题,是提供大量的稳定和可靠的驱动程序。每天都有大量的芯片被生产出来,芯片的设计和原理不一样,那么驱动程序就不一样。这样,就需要大量的驱动程序开发人员开发驱动程序,可以说,Linux驱动程序的发展前景是很光明的。
计算机系统已经融入到了各行各业、各个领域;计算机系统在电子产品中无处不在,从手机、游戏机、冰箱、电视、洗衣机等小型设备,到汽车、轮船、火车、飞机等大型设备都有它的身影。这些设备都需要驱动程序使之运行,可以说驱动程序的运用前景是非常广泛的。每天都有很多驱动程序需要编写,所以驱动程序开发人员的前途是无比光明的。
学习Linux设备驱动程序,仅仅只学习理论是不够的,还需要亲自动手写各种设备的驱动程序。编写驱动程序不仅需要软件知识,还需要硬件知识。在这里,笔者推荐一些国内外优秀的驱动开发网站,希望对读者的学习有所帮助。同时,笔者也计划开通一个学习网站(http://www.zhengxiaoqiang.com),里面将记录一些工作心得和总结,希望对大家有所帮助。
1. Linux内核之旅网站:http://www.kerneltravel.net/;
2. 知名博客:http://www.lupaworld.com/26540;
3. Linux中国:http://www.linux-cn.com/;
4. 一个不错的Linux中文社区:http://www.linux-cn.com/;
5. csdn内核驱动研究社区:http://topic.csdn.net/s/Linux_Dirver/0.html;
6. Linux伊甸园:http://bbs.linuxeden.com/index.php。
本章首先对Linux设备驱动程序的基本概念进行了详细的讲述,并且讲述了设备驱动程序的作用;接着讲述了设备驱动程序的分类、特点及与操作系统之间的关系等;然后讲述了驱动程序开发的一些重要知识和一些注意事项;最后讲述了Linux驱动程序的发展趋势。通过本章的学习,读者可以对Linux设备驱动程序的开发有一个大概的了解。
随着嵌入式设备的迅猛出现,有越来越多的驱动程序需要程序员去编写,所以学习驱动程序的开发对个人的进步是非常有帮助的。本章作为驱动程序开发的入门,希望能够引起读者的学习兴趣。