Android本质上是基于Linux内核的系统,也就是说Android就是一种Linux操作系统。只不过大多数时候都会运行在ARM架构的设备上,例如,Android手机、平板等。Android驱动实际上就是Linux驱动,只是这里使用Android深度探索(卷1):安装C/C++交叉编译环境 介绍的交叉编译器将Linux驱动编译成了ARM架构的,所以驱动可以安装在Android模拟器、Android手机(需要root)或平板上(这些设备都要使用给予ARM架构的CPU),当然,使用传统的GCC也可以编译成X86架构的驱动(并不需要修改代码),这样也可以在Ubuntu Linux上安装Linux驱动。
本文及后面几篇文章主要介绍如何利用Android模拟器和S3C6410开发板开发给予ARM架构的Linux驱动,当然,测试的环境是Android,而不是我们通常使用的Ubuntu Linux等X86架构的系统。最后会介绍通过多种方式测试这个驱动,测试方法包括命令行、NDK、Android程序(Java代码)等,当然,在最最后还会介绍如果将驱动嵌入到LInux内核中,这样Android在启动是就自动拥有了这个驱动。
想学习Android底层开发的童鞋可以通过本文完全掌握开发基于Android的LInux驱动的完整步骤。在《Android深度探索(卷1):HAL与驱动开发》随书光盘上有完整的实验环境(VMWare Ubuntu Linux12.04LTS),如果嫌自己配置麻烦,可以从光盘中复制该虚拟环境,虚拟文件太大(3.6G),传不上去,只能发文章了!
一、Linux驱动到底是个什么东西
对于从未接触过驱动开发的程序员可能会感觉Linux驱动很神秘。感觉开发起来会很复杂。其实这完全是误解。实际上Linux驱动和普通的LinuxAPI没有本质的区别。只是使用Linux驱动的方式与使用Linux API的方式不同而已。
在学习Linux驱动之前我们先来介绍一下Linux驱动的工作方式。如果读者以前接触过Windows或其他非Unix体系的操作系统,最好将它们的工作方式暂时忘掉,因为这些记忆会干扰我们理解Linux底层的一些细节。
Linux驱动的工作和访问方式是Linux的亮点之一,同时受到了业界的广泛好评。Linux系统将每一个驱动都映射成一个文件。这些文件称为设备文件或驱动文件,都保存在/dev目录中。这种设计理念使得与Linux驱动进行交互就像与普通文件进行交互一样容易。当然,也比访问LinuxAPI更容易。由于大多数Linux驱动都有与其对应的设备文件,因此与Linux驱动交换数据就变成了与设备文件交换数据。例如,向Linux打印机驱动发送一个打印命令,可以直接使用C语言函数open打开设备文件,再使用C语言函数ioctl向该驱动的设备文件发送打印命令。
当然,要编写Linux驱动程序还需要更高级的功能。如向打印机驱动写入数据时,对于打印机驱动来说,需要接收这些被写入的数据,并将它们通过PC的并口、USB等端口发送给打印机。要实现这一过程就需要Linux驱动可以响应应用程序传递过来的数据。这就是Linux驱动的事件,虽然在C语言里没有事件的概念,但却有与事件类似的概念,这就是回调(callback)函数。因此,编写Linux驱动最重要的一步就是编写回调函数,否则与设备文件交互的数据将无法得到处理。图6-1是应用软件、设备文件、驱动程序、硬件之间的关系。
二、编写Linux驱动程序的步骤
Linux驱动程序与其他类型的Linux程序一样,也有自己的规则。对于刚开始接触Linux驱动开发的读者可能对如何开发一个LInux驱动程序还不是太了解。为了解决这部分读者的困惑,本节给出了编写一个基本的Linux驱动的一般步骤。读者可以按着这些步骤循序渐进地学习Linux驱动开发。
第1步:建立Linux驱动骨架(装载和卸载Linux驱动)
任何类型的程序都有一个基本的结构,例如,C语言需要有一个入口函数main。Linux驱动程序也不例外。Linux内核在使用驱动时首先需要装载驱动。在装载过程中需要进行一些初始化工作,例如,建立设备文件,分配内存地址空间等。当Linux系统退出时需要卸载Linux驱动,在卸载的过程中需要释放由Linux驱动占用的资源,例如,删除设备文件、释放内存地址空间等。在Linux驱动程序中需要提供两个函数来分别处理驱动初始化和退出的工作。这两个函数分别用module_init和module_exit宏指定。Linux驱动程序一般都都需要指定这两个函数,因此包含这两个函数以及指定这两个函数的两个宏的C程序文件也可看作是Linux驱动的骨架。
第2步:注册和注销设备文件
任何一个Linux驱动都需要有一个设备文件。否则应用程序将无法与驱动程序交互。建立设备文件的工作一般在第1步编写的处理Linux初始化工作的函数中完成。删除设备文件一般在第1步编写的处理Linux退出工作的函数中完成。可以分别使用misc_register和misc_deregister函数创建和移除设备文件。
第3步:指定与驱动相关的信息
驱动程序是自描述的。例如,可以通过modinfo命令获取驱动程序的作者姓名、使用的开源协议、别名、驱动描述等信息。这些信息都需要在驱动源代码中指定。通过MODULE_AUTHOR、MODULE_LICENSE 、MODULE_ALIAS 、MODULE_DESCRIPTION等宏可以指定与驱动相关的信息。
第4步:指定回调函数
Linux驱动包含了多种动作,也可称为事件。例如,向设备文件写入数据时会触发“写”事件,Linux系统会调用对应驱动程序的write回调函数,从设备文件读数据时会触发“读”事件,Linux系统会调用对应驱动程序的read回调函数。一个驱动程序并不一定要指定所有的回调函数。回调函数会通过相关机制进行注册。例如,与设备文件相关的回调函数会通过misc_register函数进行注册。
第5步:编写业务逻辑
这一步是Linux驱动的核心部分。光有骨架和回调函数的Linux驱动是没有任何意义的。任何一个完整的Linux驱动都会做一些与其功能相关的工作,如打印机驱动会向打印机发送打印指令。COM驱动会根据传输数率进行数据交互。具体的业务逻辑与驱动的功能有关。业务逻辑可能有多个函数、多个文件甚至是多个Linux驱动模块组成。具体的实现读者可以根据实际情况而定。
第6步:编写Makefile文件
Linux内核源代码的编译规则是通过Makefile文件定义的。因此编写一个新的Linux驱动程序必须要有一个Makefile文件。
第7步:编译Linux驱动程序
Linux驱动程序可以直接编译进内核,也可以作为模块单独编译。
第8步:安装和卸载Linux驱动
如果将Linux驱动编译进内核,只要Linux使用该内核,驱动程序就会自动装载。如果Linux驱动程序以模块单独存在,需要使用insmod或modprobe命令装载Linux驱动模块,使用rmmod命令卸载Linux驱动模块。
上面8步中的前5步是关于如何编写Linux驱动程序的,通过后3步可以使Linux驱动正常工作。
三、编写Linux驱动程序前的准备工作
本例的Linux驱动源代码并未与linux内核源代码放在一起,而是单独放在一个目录。首先使用下面的命令建立存放Linux驱动程序的目录。
# mkdir –p /root/drivers/ch06/word_count
# cd /root/drivers/ch06/word_count
然后使用下面的命令建立驱动源代码文件(word_count.c)
# echo '' > word_count.c
最后编写一个Makefile文件,实际上这是6.2节介绍的编写Linux驱动程序的第6步。当熟悉编写Linux驱动程序的步骤后可以不按6.2节介绍的顺序来编写Linux驱动。
# echo 'obj-m := word_count.o' > Makefile
其中obj-m表示将Linux驱动作为模块(.ko文件)编译。如果使用obj-y,则将Linux驱动编译进Linux内核。obj-m或obj-y需要使用“:=”赋值。如果obj-m或obj-y的值为word_count.o,表示make命令会把Linux驱动源代码目录中的word_count.c或word_count.s文件编译成word_count.o文件。如果使用obj-m,word_count.o会被连接进word_count.ko文件,然后使用insmod或modprobe命令装载word_count.ko。如果使用obj-y,word_count.o会被连接进built-in.o文件,最终会被连接进内核。其中built-in.o文件是连接同一类程序的.o文件生成的中间目标文件。例如,所有的字符设备驱动程序会最终生成一个built-in.o文件。读者可以在<Linux内核源代码目录>/drivers/char目录找到一个built-in.o文件。该目标文件包含了所有可连接进Linux内核的字符驱动(通过make menuconfig命令可以配置每一个驱动及其他内核程序是否允许编译进内核,关于配置Linux内核的技术详见4.2.4节介绍)。
如果Linux驱动依赖其他程序,如process.c、data.c。需要按如下方式编写Makefile文件。
obj-m := word_count.o
word_count-y := process.o data.o
其中依赖文件要使用module-y或module-objs指定。module表示模块名,如word_count。
四、编写Linux驱动程序的骨架
现在编写Linux驱动程序的骨架部分,也就是前面介绍的第1步。骨架部分主要是Linux驱动的初始化和退出函数,代码如下:
#include <linux/module.h>#include <linux/init.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/miscdevice.h>#include <asm/uaccess.h>// 初始化Linux驱动static int word_count_init(void){ // 输出日志信息 printk("word_count_init_success\n"); return 0;}// 退出Linux驱动static void word_count_exit(void){ // 输出日志信息 printk("word_count_init_exit_success\n");}// 注册初始化Linux驱动的函数module_init(word_count_init);// 注册退出Linux驱动的函数module_exit(word_count_exit);
在上面的代码中使用了printk函数。该函数用于输出日志信息(关于printk函数的详细用法将在10.1节详细介绍)。printk函数与printf函数的用法类似。有的读者可能会有疑问,为什么不用printf函数呢?这里就涉及到一个Linux内核程序可以调用什么,不可以调用什么的问题。Linux系统将内存分为了用户空间和内核空间,这两个空间的程序不能直接访问。printf函数运行在用户空间,printk函数运行在内核空间。因此,属于内核程序的Linux驱动是不能直接访问printf函数的。就算包含了stdio.h头文件,在编译Linux驱动时也会抛出stdio.h文件没找到的错误。当然,运行在用户空间的程序也不能直接调用printk函数。那么是不是用户空间和内核空间的程序就无法交互了呢?答案是否定的。否则这两块内存不就成了孤岛了吗。运行在这两块内存中的程序之间交互的方法很多。其中设备文件就是一种主要的交互方式(在后面的章节还会介绍/proc虚拟文件的交互方式)。如果用户空间的程序要访问内核空间,只要做一个可以访问内核空间的驱动程序,然后用户空间的程序通过设备文件与驱动程序进行交互即可。
看到这可能有的读者疑问更大了。Linux驱动程序无法直接访问运行在用户空间的程序,那么很多功能就都得自己实现了。例如,在C语言中会经常使用malloc函数动态分配内存空间,该函数在Linux驱动程序中是无法使用的。那么如何在Linux驱动程序中动态分配内存空间呢?解决类似的问题也很简单。既然Linux驱动无法直接调用运行在用户空间的函数,那么在Linux内核中就必须要提供替代品。读者可以进入<Linux内核源代码>/include目录,该目录的各个子目录中包含了大量的C语言头文件。这些头文件中定义的函数、宏等资源就是运行在用户空间的程序的替代品。运行在用户空间的函数库对应的头文件在/usr/include目录中。刚才提到的malloc函数在内核空间的替代品是kmalloc(需要包含slab.h头文件,#include <linux/slab.h>)。
注意:用户空间与内核空间完成同样或类似功能的函数、宏等资源的名称并不一定相同,有的名称类似,如malloc和kmalloc,有的完全是两个不同的名字:如atoi(用户空间)和simple_strtol(内核空间)、itoa(用户空间)和snprintf(内核空间)。读者在使用内核相关资源时要注意在一点。
如果读者想看看前面编写的程序的效果,可以使用下面的命令编译Linux驱动源代码(X86架构)。
# make -C /usr/src/linux-headers-3.0.0-15-generic M=/root/drivers/ch06/word_count
在测试Linux驱动未必一定在Android设备上完成。因为Android系统和Ubuntu Linux以及其他Linux发行版本都是基于Linux内核的,大多数Linux驱动程序可以在Ubuntu Linux或其他Linux发行版上测试完再重新用交叉编译器编译成基于ARM架构的目标文件,然后再安装到Android上即可正常运行。由于编译Linux内核源代码需要使用Linux内核的头文件。为了在Ubuntu Linux上测试驱动程序,需要使用-C命令行参数指定Linux内核头文件的目录(/usr/src/linux-headers-3.0.0-15-generic)。其中linux-headers-3.0.0-15-generic目录是Linux内核源代码目录,在该目录中只有include子目录有实际的头文件,其他目录只有Makefile和其他一些配置文件,并不包含Linux内核源代码。该目录就是为了开发当前Linux内核版本的驱动及其他内核程序而提供的(因为在编译Linux驱动时生成目标文件只需要头文件,在进行目标文件链接时只要有相关的目标文件即可,并不需要源代码文件)。如果以模块方式编译Linux驱动程序,需要使用M指定驱动程序所在的目录(M= root/drivers/ch06/word_count)。
注意:如果读者使用的Linux发行版采用了其他Linux内核,需要为-C命令行参数设置正确的路径。
执行上面的命令后,会输出如图6-2所示信息。从这些信息可以看出,已经将word_count.c文件编译成了Linux驱动模块文件word_count.ko。
使用ls命令列出/root/drivers/ch06/word_count目录中的文件后发现,除了多了几个.o和.ko文件,还多了一些其他的文件,如图6-3所示。这些文件是有编译器自动生成的,一般并不需要管这些文件的内容。
本文编写的Linux驱动程序虽然什么实际的功能都没有,但已经可以作为驱动程序安装在Linux内核空间了。读者可以使用下面的命令安装、查看、卸载Linux驱动,也可以查看由驱动程序输出的日志信息(执行下面命令时需要先进入word_count目录)。
安装Linux驱动
# insmod word_count.ko
查看word_count是否成功安装
# lsmod | grep word_count
卸载Linux驱动
# rmmod word_count
查看由Linux驱动输出的日志信息
# dmesg | grep word_count | tail –n 2
执行上面的命令后,如果输出如图6-4所示的信息说明读者已成功完成本节的学习,可以继续看下一节了。
dmesg命令实际上是从/var/log/messages(Ubuntu Linux 10.04)或/var/log/syslog(Ubuntu Linux11.10)文件中读取的日志信息,因此也可以执行下面的命令获取由Linux驱动输出的日志信息。
# cat /var/log/syslog | grep word_count | tail –n 2
执行上面的命令后会输出更多的信息,如图6-5所示。
本文节选至《Android深度探索(卷1):HAL与驱动开发》,接下来几篇文章将详细阐述如何开发ARM架构的Linux驱动,并分别利用android程序、NDK、可执行文件测试Linux驱动。可在ubuntu Linux、Android模拟器和S3C6410开发板(可以选购OK6410-A开发板,需要刷Android)