Android深度探索(卷1):HAL与驱动开发

978-7-115-29802-7
作者: 李宁
译者:
编辑: 张涛

图书目录:

详情

本书作为学习Android驱动的第一本书,将对Android以及Linux驱动做一个总体的介绍,以便使读者了解开发Android驱动和开发Linux驱动的联系和区别,并更好地掌握学习Android驱动和移植的方法和技巧。

图书摘要

Android深度探索(卷1) HAL与驱动开发
李宁 编著
人民邮电出版社

北京

前言

为什么要写这本书

由于 Android 底层开发在最近几年开始逐渐升温,很多读者迫切希望出现更符合实际需求的Android 底层开发的技术书籍。也有很多读者给我发 E-mail,希望多写一些关于 Android 和 Linux底层开发的书。尽管目前已经有一些这方面的书籍,但这方面的书中理论讲的多一些,这对于很多初学者,学习的第一步搭建试验环境都很困难,而且书中并未提供完整的代码,这对于新手来说,上机试验的困难比较大。而对于程序设计这门学科来说,实践是成为高手的唯一途径。所以,这些书一般只适合有一定Android底层开发基础的程序员阅读,而对于一些初学者,由于本身知识有限,再加上一开始搭建试验环境可能会失败,从而大大失去了继续学习的欲望和信心。

还有一些Linux 底层开发的书涉及的版本比较低,有些书的部分内容甚至仍然为Linux2.4.x,或Linux2.6.x的较低版本。这对于目前学习Linux 底层开发的读者造成了一定的困惑。学习低版本的Linux内核实际上用处不大,因为现在很多系统(如Android)都使用了较高版本的Linux内核。所以这部分读者急切需要一本可以结合Android和Linux内核,并且使用较高Linux内核版本的技术书籍。

为了满足读者的上述需求,笔者特意结合目前很常用的Android和Linux底层开发技术编写了这本《Android深度探索(卷1):HAL与驱动开发》。这本书主要介绍了Linux驱动的驱动开发,以及如何使用HAL(硬件抽象层)与Linux驱动交互,并使用NDK和Java技术通过HAL与Linux驱动交互。而且使用了较新的Linux内核版本(Linux2.6.36和Linux3.x)。

本书从实践角度出发,每一个技术点都提供了详细的例子,除此之外,还提供了一个完整的虚拟环境(随书光盘中)供读者进行测试。这个虚拟环境是基于VMWare的Ubuntu Linux12.04LTS。虚拟环境的配置与笔者使用的开发环境完全一样,而且虚拟环境中包含了本书所有的源代码和相应的Linux内核源代码。读者可以直接在虚拟环境中测试本书所有的例子代码。

使用《Android深度探索(卷1):HAL与驱动开发》学习Android和Linux底层开发技术完全没有因不具备试验环境而无法试验的后顾之忧。而且书中所有的知识点都提供了完整的例子代码,读者在试验时并不需要自己来输入代码,从而大大提高了学习的效率。

本书的特点

1.本书几乎所有的知识点都提供了详细的例子代码,读者可以在自己配置的开发环境或虚拟环境中学习和测试这些代码。

2.提供了VMWare虚拟环境,同时在虚拟环境中包含了本书所有的例子代码和相应的Linux内核源代码。读者不再需要自己搭建试验环境,大大提高了学习效率。

3.结合了实际的S3C6410 开发板学习嵌入式Linux驱动开发和Android HAL开发,目前是市面上唯一一本结合真实开发板、Android和Linux内核的底层开发书籍。

4.大多数例子(有些例子只能在某一个或两个环境中运行)都提供了Ubuntu Linux、Android虚拟机和S3C6410开发板3种平台的运行脚本程序,执行build.sh脚本文件后可以选择在不同环境中安装和运行程序,使学习和测试代码更方便。

5.本书除了涉及大量的C/C++和Java代码外,还涉及一些Shell脚本的知识,这些Shell脚本主要用于自动编译和运行例子代码,以及选择安装和运行平台。本书会对这些Shell脚本的核心代码进行详细解释。

6.由于测试环境可能会连接多个 Android 设备,这时操作某一个设备就需要指定该设备的Device ID。为了方便读者操作这些设备。在虚拟环境中提供了若干个Shell 脚本(pull.sh、push.sh、shell.sh 等),允许读者选择操作的设备,而不是输入 Device ID。这样可以大大缩短读者操作这些Android设备的时间。

7.本书分析了大量的Linux驱动和Android底层的代码,而且使用多个版本进行对比的方式进行分析,例如,音频驱动分别分析了Android2.3.4、Android4.x与Linux驱动交互的差异。

8.本书采用了较新的Linux内核(Linux2.6.36和Linux3.x)进行讲解和代码分析。

9.本书还介绍了大量的工具的使用。这些工具在开发的过程中起到了很大的作用,例如,通过串口调试开发板的minicom,搜素源代码的工具等。

读者对象

从事Android应用开发,但想进入Android 底层开发领域的程序员。

想从事Android 底层开发的在校或即将毕业的大学生。

有C/C++基础,想进入Android 底层开发领域的程序员。

想进一步提高Android 底层开发技术和实践能力的程序员。

开设Android底层开发课程的大专院校和培训机构。

想进一步了解Linux 常用驱动实现原理的程序员。

有一定的Android和Linux底层开发理论基础,想进一步增加实战经验的程序员。

所有对Android 底层技术感兴趣的读者。

如何阅读本书

本书分为4篇,主要内容如下。

第1篇为入门准备篇,在这一部分主要为后面的内容搭建开发环境。包括Android开发环境和Linux驱动开发环境。其中包括JDK、AndroidSDK、Android NDK、Eclipse、交叉编译器以及一些工具的安装和使用。

第2篇为Android 和Linux 底层开发的入门篇。主要讨论了Linux 驱动的开发,Android HAL开发,Android应用程序如何与Linux驱动交互和Linux驱动开发的调试技术。

第3篇为开发Linux驱动需要掌握的高级技术篇。包括并发控制、阻塞和非阻塞I/O、异步编程、Linux中断和底半部、时间管理、内存管理和I/O访问。

第4篇为Linux中典型驱动的源代码篇,包括RTC驱动、LCD驱动、音频驱动、块设备驱动、网络设备驱动和USB驱动。

这4篇通过由易到难的顺序系统地介绍了AndroidHAL和Linux驱动开发的各种技术。如果读者是初学者,建议从本书的第1章开始阅读。如果读者的机器上已经配置了相应的开发环境,可以跳过第1篇,直接从第2篇开始学习。当然,对于有一定Android和Linux底层开发经验的读者,可以直接阅读后两篇的内容。

在随书光盘的sources文件夹中包含了本书的所有例子代码,建议读者将该目录复制到硬盘中进行测试,也可以用Eclipse导入其中的Eclipse工程(包括Android工程和C/C++工程)。在随书光盘中还提供了一个虚拟环境(VMWareUbuntuLinux12.04LTS),也包含了例子代码(/root/drivers)。如果读者由于某种原因不想或不能配置开发环境,可以在Windows、Linux或MacOSX中通过VMWare打开虚拟机文件来学习本书提供的例子代码,并结合书中介绍的理论知识,学习的效果会更好。关于虚拟环境的用法会在“如何使用虚拟环境”中详细介绍。

如何使用虚拟环境

本书的随书光盘中带了一个Ubuntu Linux12.04LTS的VMWare虚拟环境,以虚拟文件的形式提供(用7z压缩),登录用户名:root,登录密码:12345678。该虚拟环境可以在Windows、Mac OSX和Linux下的VMWare中运行,建议读者安装较新的VMWare版本(如Windows版的VMWare8)。

虚拟环境压缩文件位于光盘的根目录,文件名是Ubuntu12.04.vmwarevm.7z,大约3.76GB。解压后大约13GB。建议读者在解压之前确保当前分区至少有15GB的自由空间。在不断读写虚拟环境后可能会使虚拟机文件尺寸增大。读者可以在 Ubuntu Linux 中执行 vmware-toolbox 命令启动VMWare工具来收缩虚拟机磁盘空间,收缩过程如图1所示。

▲图1 收缩虚拟机磁盘空间
▲图2 Ubuntu Linux12.04虚拟环境桌面

虚拟环境已经配置了运行本书例子代码的开发环境,读者只要用 VMWare 打开虚拟环境即可启动Ubuntu Linux。运行本书的例子需要使用root登录。登录后会进入Ubuntu Linux的桌面。在桌面上有Eclipse的链接,双击即可启动Eclipse,如图2所示。本虚拟环境可以使用root登录。如果读者在自己的计算机上新安装了Ubuntu Linux12.04 LTS。用root登录的方法可以参考虚拟环境桌面的“用root登录”文档进行设置。

虚拟环境中包含了本书所有的例子代码,这些代码位于/root/drivers目录中的相应子目录。

每一个源代码目录都提供了build.sh脚本文件。读者只要在Linux终端中进入该目录的相应子目录,执行build.sh脚本文件即可。该脚本文件会指导读者下一步如何做。

大多数程序的 build.sh 脚本文件都会要求读者选择编译平台,这些平台包括 Ubuntu Linux、Android模拟器和S3C6410开发板。如果读者选择Android模拟器,应确保Android模拟器已经启动。如果选择了S3C6410开发板,应确保S3C6410开发板通过USB线连接到PC上。如果同时有多个 Android 设备,脚本程序会显示一个选择菜单,需要选择具体的 Android 设备(运行平台)。选择时要注意选择正确的Android设备,例如,如果在编译程序时选择的平台是Android模拟器,那么运行平台也应该选择Android模拟器。脚本程序会自动将相应的二进制文件上传到Android设备上并安装,如果选择错了,尽管可以上传,但无法安装(会出现格式错误的提示信息)。如果只有一个Android设备,并不会出现设备选择菜单,脚本程序会直接操作这个Android设备。

有一些程序只能在某些Android设备上运行,例如,LED驱动只能在S3C6410开发板上才可以安装和测试,所以这些程序的build.sh脚本文件并不会要求选择编译平台,而会直接使用相应的Linux内核进行编译。

由于光盘空间的限制,虚拟环境并未包含Android源代码。但本书有一些章节的例子(如HAL)需要使用到Android源代码。读者可以按着第4章介绍的方法下载相应版本的Android源代码,或购买S3C6410开发板时随机光盘会包含相应版本的Android源代码。

读者可以直接在虚拟环境中下载Android源代码,也可以将下载后的源代码复制到虚拟环境中,通常将Android源代码放在/working目录中。当然,读者也可以将Android源代码放在其他的目录中,但要注意修改相应的路径。

勘误和支持

由于本人的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。为此,特意提供了一个答疑 E-mail:techcast@126.com。读者可以将书中的错误、建议、技术问题发到该E-mail。非常期待能够得到你们的真挚反馈,编辑联系邮箱为zhangtao@ptpress.com.cn。

致谢

感谢所有在本书写作过程中给予我指导、帮助和鼓励的朋友,尤其是人民邮电出版社的编辑,他们不仅对本书提出了宝贵的写作建议,而且还对本书进行了仔细的审阅。

感谢一直以来信任、鼓励、支持我的家人和朋友。

感谢eoeandroid、移动开发者社区的朋友对我技术上的帮助。

谨以此书献给我最亲爱的家人,以及众多热爱Android的朋友们!

编者

第一篇 Android驱动开发前的准备

Android系统移植与驱动开发概述

搭建Android开发环境

Git使用入门

源代码的下载和编译

搭建S3C6410开发板的测试环境

第1章 Android系统移植与驱动开发概述

毋庸置疑,Android 已经成为当前智能手机操作系统的老大,市场占有率已遥遥领先于 iOS (IPhone 和 IPad)。Android 在几年时间发展如此神速,在很大程度上取决于任何人都可以利用Android的源代码定制完全属于自己的嵌入式系统,而不需要向Google交一分钱。

由于Android原生的代码支持的设备并不多,因此,要想在自己的设备(包括手机、MP4、智能电视、平板电脑、车载系统等)上完美运行 Android,就需要另外开发一些程序,使得 Android可以识别相应设备中的硬件(显示器、蓝牙、音频、Wi-Fi等)。这个为特定设备定制Android的过程被称为“移植”。那么,在移植的过程中开发得最多的就是支持各种硬件设备的Linux驱动程序(Android是基于Linux内核的)。因此,讲移植就必须要讲驱动开发。

本章作为学习Linux驱动的第一道门,将对Android以及Linux驱动做一个总体的介绍,以便使读者对开发Linux驱动有一个感性的认识,并为更好地学习Linux驱动的方法和技巧打下基础。

1.1 Android系统架构

Android 是一个非常优秀的嵌入式操作系统。经过几年的发展和演进,Android 已经形成了非常完善的系统架构,如图1-1所示。

▲图1-1 Android系统架构

从图1-1可以看出,Android的系统架构分为4层。这4层所包含的内容如下。

第1层:Linux内核

由于Android是基于Linux内核的,因此,Android和其他Linux系统(如Ubuntu Linux、Fedora Linux等)的核心部分差异非常小。这一层主要包括Linux的驱动程序以及内存管理、进程管理、电源管理等程序。Android使用Linux 2.6 作为其内核。不过不同版本的Android使用的Linux内核版本有细微的差异,所以不同Android版本的驱动可能并不通用。本书主要讲的就是开发第1层的驱动程序,以及如何在不同Linux版本、硬件平台移植驱动程序。

第2层:C/C++代码库

这一层主要包括使用C/C++编写的代码库(Linux下的.so文件),也包括Dalivk虚拟机的运行时(Runtime)。

第3层:Android SDK API

由于Android SDK API是用Java 语言编写的,因此,这一层也可称为Java API层。实际上,这一层就是用Java编写的各种Library。只不过这些Library是基于Dalvik虚拟机格式的。笔者所著《Android开发权威指南》主要就是介绍了这一层的Android SDK API 的使用方法及技巧。

第4层:应用程序

这一层是所有的 Android 用户(包括程序员和非程序员)都要接触到的。因为这一层相当于Android的UI。所有的Android应用程序(包括拍照、电话、短信、Android的桌面、浏览器以及各种游戏)都属于这一层。而这一层主要依靠第3层中的Android SDK API 来完成各种功能。

1.2 Android系统移植的主要工作

Android移植可分为两部分:应用移植和系统移植。应用移植是指将如图1-1所示第4层的应用程序移植到某一个特定硬件平台上。由于不同硬件平台之间的差异,Android SDK API 也有可能存在差异(有的厂商会修改部分Android SDK API 以适应自身硬件的需要),或者将应用程序从低版本Android移植到高版本的Android上。为了保证应用程序可以在新的硬件平台正常运行,需要对源代码进行一些修改。当然,如果没有或无法获取源代码,只有重新在新的平台上实现了。一般Android应用移植并不涉及驱动和HAL程序库(Android新增加的硬件抽象层,将在后面的章节介绍)的移植,而且Android应用程序移植也不在本书讨论的范围内,因此,本书后面出现的Android移植都是指Android操作系统的移值(包括Linux驱动、HAL程序库的移植)。

Android系统移植是指让Android操作系统在某一个特定硬件平台上运行。使一个操作系统在特定硬件平台上运行的一个首要条件就是该操作系统支持硬件平台的CPU架构。Linux 内核本身已经支持很多常用的CPU架构(ARM、X86、PowerPC等),因此,将Android在不同的CPU架构之间移植并不用做过多的改动(有时仍然需要做一些调整)。要想Android在不同硬件平台上正常运行,只支持CPU架构还不行,必须要让Android可以识别平台上的各种硬件(如声卡、显示器、蓝牙设备等)。这些工作主要也是由Linux内核完成的。其中的主角就是Linux驱动。因此,系统移植除了移植CPU架构外,最重要的就是移植Linux驱动。例如,为硬件平台增加了一个新型的Wi-Fi模块,就需要为这个Wi-Fi模块编写新的驱动程序,或修改原来的驱动程序,已使得Linux内核可以与Wi-Fi模块正常交互。

除了 Linux 驱动需要移植外,在 Android 系统中还增加了一个硬件抽象层(HAL,Hardware Abstraction Layer),为了方便,本书后面的部分都使用HAL表示硬件抽象层。

HAL 位于如图1-1所示的第2层,也是普通的Linux程序库(.so文件),只是Android SDK 通过HAL直接访问Linux驱动。也就是说,Android并不像其他的Linux系统一样由应用程序直接访问驱动,而是中间通过HAL隔了一层。Google这样设计的原因很多,例如,由于Linux内核基于GPL开源协议,而很多驱动厂商不想开放源代码,所以增加了HAL层后,可以将Linux驱动的业务逻辑放在HAL层,这样处理Linux驱动开源技术,也只是一个空架子而已。关于Android支持HAL的原因将在后面的章节详细介绍。

如果为 Android 增加了新的驱动或修改原来的驱动代码,HAL 中的代码就要做相应的调整。因此,Android移植的主要工作如下:

移植Linux驱动;

移植HAL。

移植的工作也可能不多,当然,也可能非常多。如果要移植的Android系统提供了驱动源代码,那就好办多了,直接根据移植的目标平台修改驱动代码就可以了。不过很多时候由于某些原因,无法获得驱动的源代码,或者要实现的驱动程序所对应的硬件是自己特有的,这就需要从头开始编写驱动程序以及相关的配置文件。对于HAL的移植也和Linux驱动差不多。总之,Android移植的基本原则是尽可能找到驱动和HAL的源代码,在源代码的基础上改要比从头开始编写容易得多,实在无法获取源代码,就只有从头开始做起了。不过在了解了编写Linux 驱动和AndroidHAL程序库的步骤和规则以后,看着也没那么复杂。因为驱动和HAL 的代码远没有Android SDK 和Android应用程序的代码量大。

注意

Android移植在很大程度上是Linux内核的移植。Linux内核移植主要就是移植驱动程序。不同 Linux 版本的驱动程序不能通用,需要重新修改源代码,并在新的Linux 内核下重新编译才可以运行在新的 Linux 内核版本下。Android 版本和 Linux版本不同。无论哪个Android版本,其Linux内核版本都是Linux 2.6或Linux 3.0(将来有可能使用更高版本的Linux内核),只是小版本号不同。由于Android开放源代码,所以就算同一个Android版本,Linux的内核也可能不同(有很多自制的ROM会更换不同的Linux内核,以至于和官方同一Android版本的Linux内核不同),例如,笔者曾见过有的Android 2.3使用了Linux 2.6.29,而官方的Android 2.3使用了Linux 2.6.35。在移植Linux驱动时,主要应考虑Linux内核的版本,就算Android 版本不同,只要 Linux 内核版本相同,Linux 驱动就可以互相替换(有时也需要考虑HAL是否和Linux驱动兼容)。

1.3 查看Linux内核版本

目前Linux内核主要维护3个版本:Linux 2.4、Linux 2.6 和Linux 3.x,大多数Linux 系统都使用了这3个版本的内核,其中Linux 2.6 是目前使用最广泛的Linux内核版本,Android 就使用了该内核版本。而 Linux 2.4 由于其内部设计缺陷(主要是进程调度上的缺陷),除了一些遗留Linux系统,已很少有新的Linux系统使用Linux2.4了。Linux 3.x 是最新推出的Linux 内核版本。最新的Android 4.x 采用了这个新的 Linux 3.0.8 内核版本,还有很多新推出的 Linux 系统(如Ubuntu Linux 11.10)都使用了Linux3 .0。读者可在Android 系统中的“设备”>“关于手机”中查看当前Android系统所采用的Linux内核版本,如图1-2所示。

▲图1-2 查看Android的Linux内核版本

如果想查其他Linux系统的内核版本,可使用下面两种方法。

方法1

在Linux终端执行下面的命令。

uname -a

如果当前系统是UbuntuLinux11.10,会在Linux终端输出如图1-3所示的信息。白框内是Linux内核的版本。

▲图1-3 使用uname命令查看Linux内核版本

方法2

在Linux终端执行下面的命令。

cat /proc/version

在Linux终端输出如图1-4所示的信息。白框内是Linux内核的版本。

▲图1-4 查看proc/version文件获取Linux内核版本

注意

/proc不是普通的文件系统,而是系统内核的映像,也就是说,该目录中的文件是存放在系统内存之中的,它以文件系统的方式为访问系统内核数据的操作提供接口。而uname命令就是从/proc/version文件中获取信息的,当然直接查看/proc/version文件的内容(方法2)也可以获取同样的信息。uname命令加上参数“-a”可以获取更多的信息,否则只显示当前的系统名,也就是只会输出“Linux”。

1.4 Linux内核版本号的定义规则

Linux内核版本号由下面几部分组成。

主版本号;

次版本号;

修订版本号;

微调版本号;

为特定的Linux 系统特别调校的描述。

在Linux内核版本2.6.29.7-flykernel-12a中,2是主版本号,6是次版本号,29是修订版本号,7是对2.6.29的微调,称为微调版本号,而flykernel-12a则是该Linux内核专门为flykernel调校。要注意的是,调校描述可以是任意字符串,由开发者自行定义。主版本和次版本号会组成一个 Linux 内核版本的系列,如2.6.0表示2.6系列Linux内核。读者可以到如下网站获取详细的Linux内核版本信息。

http://www.kernel.org

1.5 如何学习Linux驱动开发

由于Linux的内核版本更新较快(稳定版本1至3月更新一次,升级版本1至2周更新一次),每一次内核的变化就意味着Linux驱动的变化(就算不需要修改驱动代码,至少也得在新的Linux内核版本下重新编译),所以Linux内核的不断变化对从事Linux驱动开发的程序员影响比较大。不过这对于学习Linux驱动开发来说影响相对较小。因为不管是哪个版本的Linux内核,开发Linux驱动的方法和步骤基本相同,只要掌握了一个Linux内核版本(建议使用Linux 2.6或Linux 3.x内核版本)的驱动开发,其他Linux内核版本就很容易掌握了。

学习Linux驱动开发只有Linux内核还不行,需要有一个真正的操作系统来搭建Linux驱动的开发环境,并在该系统下测试Linux驱动。开发Linux驱动强烈建议使用Linux系统。目前在个人操作系统领域比较常用的Linux系统有很多,读者可以选择自己熟悉的Linux系统作为自己的实验环境。由于本书主要介绍如何开发和测试Linux驱动,而Google测试Android源代码时使用的就是Ubuntu Linux,因此,强烈建议读者使用Ubuntu Linux 10.04 或以上版本来开发并测试Linux驱动。本书的所有代码都在Ubuntu Linux 11.10下测试通过。为了方便读者学习,在随书光盘中提供了VMWare的虚拟机映像文件(Ubuntu Linux 11.10,内存:2GB,登录用户名:root,登录密码:androidkernel),并且已经配置好了Linux驱动的开发环境,而且包含了本书涉及的所有源代码。读者可以很容易地按照本书给出的方式编译和运行本书的示例。

GNU C也是学习Linux驱动的一个必须掌握的技术。GNU C是对标准C的扩展。是Linux/UNIX下最常用的C 语言编译环境。如果读者比较熟悉标准C,掌握GNUC 并不困难。当然,如果读者还不了解C语言,建议在阅读本书之前先学习一下C语言的相关知识(C语言的相关内容并不属于本书的讲解范围)。除了掌握GNU C外,还需要掌握一些与驱动相关的硬件知识,本书会在介绍特定驱动时介绍这部分知识。

为了测试Linux驱动在Android中的运行效果,最好准备一块开发板。当开发完成驱动程序后,需要在支持 Android 的开发板上测试驱动程序是否能正确地运行。本书建议采用比较流行的基于ARM11的开发板,例如,三星的S3C6410,或在S3C6410的基础上改进的其他开发板。如本书的驱动代码采用了飞凌的OK6410开发板进行测试。

当然,除了掌握学习Linux驱动的必要知识外,剩下的就是不断地练习了,因为实践是最好的老师。

最后总结一下学习Linux驱动要做些什么。

准备一个自己熟悉的Linux操作系统,用于开发和测试Linux驱动,建议使用Ubuntu Linux 10.04及以上版本。

准备一块开发板(建议采用基于ARM11的开发板)。

学习GNU C。

学习相关的硬件知识。

不断地实践。

1.6 Linux设备驱动

随着计算机技术的不断发展,与计算机(也包括手机等计算设备)相关的硬件设备的种类也不断丰富起来。这就需要大量的Linux设备驱动来与这些硬件设备进行交互。为了使读者在学习如何编写Linux驱动之前对Linux驱动有一个初步的认识,本节介绍了设备驱动在整个操作系统中的作用以及设备驱动的分类。

1.6.1 设备驱动的发展和作用

任何一台计算机系统的运行都是由软硬件共同作用的结果,没有硬件的软件是空中楼阁,而没有软件的硬件则是一堆废铁。在计算机软件发展的初期,并没有驱动的概念,在这个时期的软件都是直接访问计算机的硬件。一般会通过计算机上的各种元器件和接口(如网卡上的中断、I/O端口、串口、寄存器等)与要控制的硬件通信。例如,本书曾经使用TC2.0(DOS环境)直接和串口通信来获取外部设备中的数据。

应用程序与硬件直接通信从技术上当然没什么问题,但却未对应用软件程序员的职责做更细的划分,所造成的后果就是应用软件程序员也必须要了解外部设备与计算机之间的通信协议以及一些硬件的知识才能使应用程序与这些设备通信,例如,控制打印机。问题还不止这些,大家试想,现在有一个应用程序要将生成的电子表格输出到打印机。应用程序最开始是为A型号打印机做的,而此时A型号打印机恰好坏了,换了B型号的打印机。由于A型号打印机和B型号打印机的打印指令差别很大,这就造成原来的应用程序无法控制B型号的打印机,为了使应用程序可以正常使用B型号打印机,必须重新修改应用程序的源代码以适应 B 型号打印机的打印指令。通过这个案例很容易知道如果应用程序直接访问硬件,就会造成与硬件耦合度过高的情况。

为了解决上述问题,软件不得不向前发展(几乎所有的新技术和新理论都是为了应对曾经无法解决的问题或使问题解决得更好而出现的,也就是说,由需求决定出现哪些新的技术和理论)。降低软件和硬件之间的耦合度成为当前首要解决的问题。了解面向对象的读者会很容易想到,降低对象与对象之间耦合度最有效的方法是通过接口(Interface)对类进行抽象,也就是说,抽象度越高,耦合度越低。

抽象这个概念同样也可以用在硬件上。只要将同一类型(如打印机)但不同型号的设备抽象成统一的接口就可以很容易解决上述问题。毫无悬念,这个抽象硬件的任务就落在了“驱动”身上。

驱动是直接和硬件交互的一类程序,负责对硬件进行抽象。如前面提到的打印机的例子。如果设计一套抽象的打印机驱动,并提供应用程序可访问的API。那么就算换了其他型号的打印机,只要应用程序通过驱动来访问打印机,就不需要再修改应用程序的源代码。而且开发应用程序的程序员并不需要了解打印机的打印指令。在解决上述接口问题的同时,又产生了一个新的技术领域:驱动程序开发。当然,开发驱动程序的技术人员通常被称为驱动工程师。

1.6.2 设备的分类及特点

计算机系统的硬件主要由 CPU、存储器和外设组成。随着技术的不断提高,芯片的集成度也越来越高,往往在 CPU 内部就集成了存储器和外设适配器。ARM、PowerPC、MIPS 等处理器都集成了UART、USB控制器、SDRAM控制器等,有的处理器还集成了片内RAM和Flash。

驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设),而不是针对CPU核。Linux将存储器和外设分为3大类:

字符设备(Character devices);

块设备(Block devices);

网络设备(Network devices)。

字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标、键盘等。块设备可以用任意顺序进行访问,以块为单位进行操作,如硬盘、软驱等。字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲。但是,字符设备与块设备并没有明显的界限,如 Flash设备符合块设备的特点,但是也可以把它作为一个字符设备来访问。

字符设备和块设备的驱动设计有很大的差异,但对用户而言,它们都使用文件系统(Linux通过文件系统访问驱动)的操作接口open、close、read、write等函数进行访问。

在Linux系统中,网络设备面向数据包的接收和发送而设计,它并不对应于文件系统的节点。Linux内核与网络设备的通信和Linux核与字符设备、块设备的通信方式完全不同。

另外,USB驱动、PCI驱动、LCD驱动等大体可归入上述3类设备,但对于这些复杂的设备, Linux系统还定义了独特的体系结构。

1.7 见识一下什么叫Linux驱动:LED

Linux 驱动这个家伙到现在为止仍然是只见其声,未见其人,不过在本节会向读者展示一下Linux驱动到底是个什么东西。如果读者看到Linux驱动的代码感到头晕,这属于正常现象。因为如果一看就明白的话,那就没有阅读本书的必要了。本节的目的只为向读者展示Linux驱动程序的结构,以及使读者对Linux驱动有一个大致的印象,读者无须理解其中的细节。当读者阅读完本书时,自然会对这些细节部分了如指掌。

下面给出一个简单的 Linux 驱动的核心代码(用 C 语言实现),这个驱动的作用就是控制S3C6410开发板上的4个LED(关于开发板的使用方法将在后面详细介绍)。我们姑且将其称为LED驱动。LED驱动属于字符设备驱动,核心代码如下:

#include <linux/miscdevice.h>

… …

//此处包含了多个头文件

// 定义了设备名,驱动程序会在/dev目录下建立一个leds设备文件,通过访问该设备文件可以访问LED驱动#define DEVICE_NAME "leds"

// 向LED发送数据及从LED读取数据

static long s3c6410_leds_ioctl(struct file *filp, unsigned int cmd,

unsigned long arg)

{

switch (cmd)

{

unsigned tmp;

case 0:

case 1:

if (arg > 4)

{

return -EINVAL;

}

tmp = readl(S3C64XX_GPMDAT);

if (cmd == 0) // 关闭LED

{

tmp &= (~(1 << arg));

}

else  // 打开LED

{

tmp |= (1 << arg);

}

// 向LED设备写数据

writel(tmp, S3C64XX_GPMDAT);

// 输出调试信息

printk(DEVICE_NAME"_lining: %d %d\n", arg, cmd);

return 0;

default:

return -EINVAL;

}

}

// 描述设备文件的操作和相关数据的结构体

static struct file_operations dev_fops =

{ .owner = THIS_MODULE, .unlocked_ioctl = s3c6410_leds_ioctl, };

static struct miscdevice misc =

{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, };

// 驱动的初始化函数

static int _init dev_init(void)

{

int ret;

unsigned tmp;

//gpm0-3 pull up

tmp = readl(S3C64XX_GPMPUD);

tmp &= (~0xFF);

tmp |= 0xaa;

writel(tmp,S3C64XX_GPMPUD);

//gpm0-3 output mode

tmp =readl(S3C64XX_GPMCON);

tmp &= (~0xFFFF);

tmp |= 0x1111;

writel(tmp,S3C64XX_GPMCON);

//gpm0-3 output 0

tmp = _raw_readl(S3C64XX_GPMDAT);

tmp |= 0x10;

writel(tmp,S3C64XX_GPMDAT);

ret = misc_register(&misc);

printk (DEVICE_NAME"\tinitialized\n");

return ret;

}

static void _exit dev_exit(void)

{

misc_deregister(&misc);

}

module_init( dev_init);

module_exit( dev_exit);

// 指定了当前驱动在哪个协议下发布,在这里是GPL协议

MODULE_LICENSE("GPL");

LED 驱动的代码涉及了很多系统的函数和结构体,如 readl、writel、printk、miscdevice 、module_exit 、file_operations、miscdevice 等。读者目前并不需要了解这些函数和结构体的作用和使用方法。只要知道任何的Linux驱动都有一个装载函数(装载驱动时调用)和一个卸载函数(卸载驱动时调用)即可。装载函数和卸载函数分别通过mobule_init和module_exit宏指定。

1.8 小结

学习 Linux 驱动编程一定要了解 Linux 驱动只与 Linux 内核有关,与用户使用的 Linux 系统(Ubuntu Linux、Fedora Linux、Android 等)无关。也就是说,不管是哪个Linux系统,只要使用了同样的Linux内核,驱动就可以通用。唯一可以判断Linux内核是否相同的方法就是Linux内核版本号。在1.4节介绍了Linux内核版本号的定义规则,只有组成内核版本号的五部分完全相同,才能说明两个Linux系统的内核是相同的。从这一点可以看出,学习Android驱动开发,实际上就是学习Linux驱动开发,只是Android增加了一个HAL,这是Android特有的。一般的Android驱动都会有对应的HAL,不过HAL也不是必需的,通过NDK也可以直接访问Linux驱动。但Google建议最好为Linux驱动编写对应的HAL程序库。

第二篇 Android底层开发入门

第一个Linux驱动程序:统计单词个数

LED将为我闪烁:控制发光二级管

让开发板发出声音:蜂鸣器驱动

硬件抽象层:HAL

嵌入式Linux的调试技术

第6章 第一个Linux驱动程序:统计单词个数

从本章开始将进入最令人激动人心的时刻。由于本书的宗旨就是实战。所以本书除了会介绍大量的Android移植和Linux驱动开发的理论知识外,还会提供大量的案例供读者联系。从本章开始会逐渐为您揭开Linux驱动的神秘面纱。当我们可以看清Linux驱动真面目时就会惊奇地发现,哇!原来开发Linux驱动程序是这么简单,甚至比开发Android应用程序还容易掌握。

为了使读者在刚开始学习Linux驱动开发时就充满期待,本章不打算只提供一个Helloworld程序来讲解 Linux驱动开发,而是提供了一个稍微复杂一些(至少 Linux驱动开发初学者是这样认为的)例子来详细介绍开发和测试 Linux驱动程序的完整过程。尤其是测试 Linux驱动,本章会介绍如何在多种平台,使用多种方式测试Linux驱动。看到这,也许很多读者急着想知道本章的例子到底是什么。其实这个例子在本章的标题已经给出了:统计单词个数。当然,统计一篇文章或一段文字中的单词个数的算法是很简单的,但本例的目的并不是讲解如何统计单词个数,而是该算法的实现技术:Linux 驱动。要注意:统计单词个数的功能是封装在 Linux驱动中的。在很多读者的印象中,驱动程序往往是直接和硬件打交道的,那么怎么还可以与普通的Library一样供其他程序调用来实现特定的功能呢(如统计单词个数)?要想知道其中玄机,就请读者详细阅读本章吧!

6.1 Linux驱动到底是个什么东西

对于从未接触过驱动开发的程序员可能会感觉Linux驱动很神秘。感觉开发起来会很复杂。其实,这完全是误解。实际上Linux驱动和普通的Linux API 没有本质的区别。只是使用Linux 驱动的方式与使用LinuxAPI 的方式不同而已。

在学习 Linux 驱动之前我们先来介绍一下 Linux 驱动的工作方式。如果读者以前接触过Windows或其他非UNIX体系的操作系统,最好将它们的工作方式暂时忘掉,因为这些记忆会干扰我们理解Linux底层的一些细节。

Linux 驱动的工作和访问方式是 Linux 的亮点之一,同时受到了业界的广泛好评。Linux 系统将每一个驱动都映射成一个文件。这些文件称为设备文件或驱动文件,都保存在/dev目录中。这种设计理念使得与Linux驱动进行交互就像与普通文件进行交互一样容易。当然,也比访问Linux API更容易。由于大多数Linux驱动都有与其对应的设备文件,因此与Linux驱动交换数据就变成了与设备文件交换数据。例如,向Linux打印机驱动发送一个打印命令,可以直接使用C语言函数open打开设备文件,再使用C语言函数ioctl向该驱动的设备文件发送打印命令。

当然,要编写Linux驱动程序还需要更高级的功能。如向打印机驱动写入数据时,对于打印机驱动来说,需要接收这些被写入的数据,并将它们通过PC的并口、USB等端口发送给打印机。要实现这一过程就需要Linux驱动可以响应应用程序传递过来的数据。这就是Linux驱动的事件,虽然在C语言里没有事件的概念,但却有与事件类似的概念,这就是回调(callback)函数。因此,编写Linux驱动最重要的一步就是编写回调函数,否则与设备文件交互的数据将无法得到处理。图6-1是应用软件、设备文件、驱动程序、硬件之间的关系。

▲图6-1 Linux 驱动的调用关系

6.2 编写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驱动正常工作。

注意

本节给出的 8步并不一定按顺序进行。读者可根据自己的需要选择先完成其中的某些步骤。

6.3 第一个Linux驱动:统计单词个数

源程序目录:<光盘根目录>/sources/word_count

本节将给出我们的第1个Linux驱动的例子。这个驱动程序并没有访问硬件,而是利用设备文件作为介质与应用程序进行交互。应用程序通过向设备文件传递一个由空格分隔的字符串(每一个被空格隔开的子字符串称为一个单词),然后从设备文件读出来的是该字符串包含的单词数。本例的驱动程序使用C语言实现,源代码文件路径如下。

随书光盘:<光盘根目录>/sources/ch06/word_count/word_count.c

虚拟环境:/root/drivers/ch06/word_count/word_count.c

6.3.1 编写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。

6.3.2 编写Linux驱动程序的骨架(初始化和退出驱动)

本节将编写Linux驱动程序的骨架部分,也就是6.2节介绍的第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驱动源代码。

# make -C /usr/src/linux-headers-3.0.0-15-generic M=/root/driver/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。

▲图6-2 编译 Linux驱动程序

使用ls命令列出/root/drivers/ch06/word_count目录中的文件后发现,除了多了几个.o和.ko文件,还多了一些其他的文件,如图6-3所示。这些文件是由编译器自动生成的,一般并不需要理会这些文件的内容。

▲图6-3 word_count 目录中的文件列表

本节编写的 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所示的信息说明读者已成功完成本节的学习,可以继续看下一节了。

▲图6-4 测试 Linux 驱动

dmesg 命令实际上是从/var/log/messages(Ubuntu Linux 10.04)或/var/log/syslog(Ubuntu Linux 11.10)文件中读取的日志信息,因此也可以执行下面的命令获取由Linux驱动输出的日志信息。

# cat /var/log/syslog | grep word_count | tail –n 2

执行上面的命令后会输出更多的信息,如图6-5所示。

▲图6-5 显示由 Linux 驱动输出的日志信息

6.3.3 指定与驱动相关的信息

虽然指定这些信息不是必需的,但一个完整的 Linux 驱动程序都会指定这些与驱动相关的信息。一般需要为Linux驱动程序指定如下信息。

 模块作者:使用MODULE_AUTHOR宏指定。

 模块描述:使用MODULE_DESCRIPTION 宏指定。

 模块别名:使用MODULE_ALIAS 宏指定。

 开源协议:使用MODULE_LICENSE 宏指定。

除了这些信息外, Linux 驱动模块自己还会包含一些信息。读者可以执行下面的命令查看word_count.ko的信息。

# modinfo word_count.ko

执行上面的命令后,会输出如图6-6所示的信息。其中 depends 表示当前驱动模块的依赖, word_count并没有依赖什么,因此该项为空。vermagic表示当前Linux驱动模块在哪个Linux内核版本下编译的。

▲图6-6 基本的 Linux 驱动模块信息

现在使用下面的代码指定上述4种信息。一般会将这些代码放在word_count.c文件的最后。

MODULE_AUTHOR("lining");

MODULE_DESCRIPTION("statistics of word count.");

MODULE_ALIAS("word count module.");

MODULE_LICENSE("GPL");

现在使用上一节的方法重新编译word_count.c 文件。然后再执行modinfo命令,就会显示如图6-7所示的信息。从图6-7可以看出,上面的代码设置的信息都包含在了word_count.ko文件中。

▲图6-7 完整的 Linux 驱动模块信息

1.开源协议

虽然很多个人开发者或小公司并不太考虑开源协议的限制,但对于较大的公司如果违反开源协议,可能会有被起诉的风险。所以对有一定规模和影响力的公司使用开源软件时一定要注意这些软件使用的开源协议。

为了降低发布Linux驱动的难度和安装包尺寸,很多Linux驱动都是开放源代码的。在Linux驱动源代码中使用MODULE_LICENSE宏指定开源协议。例如,word_count驱动使用了GPL协议。那么我们要编写Linux驱动程序应采取什么协议呢?目前开源协议比较多。读者可以到下面的页面查看所有的开源协议。

http://www.opensource.org/licenses/alphabetical

下面将介绍最常用的5种开源协议的基本情况。这5种开源协议以及其他的开源协议的详细情况请参阅Open Source Initiative 组织的相关页面。

2.GPL协议

对于喜欢钻研技术的程序员来说应该很喜欢GPL协议。因为GPL协议强迫使用该开源协议的软件开源。例如,Linux内核就采用了GPL协议。GPL的出发点就是免费/开源。但与其他开源协议(如BSD、Apache Licence)不同的是GPL协议开源的更彻底。不仅要求采用GPL协议的软件开源/免费,还要求其衍生代码开源/免费。例如,A软件采用了GPL协议,B软件使用了A软件,那么B软件也必须免费/开源。而其B软件必须也采用GPL协议。C软件又使用了B软件,C软件也必须开源/免费,当然,C软件也必须采用GPL协议。这就是所谓的“传染性”。这也是目前有很多Linux发行版及其他使用GPL协议的软件开源的原因。

由于GPL协议严格要求使用了GPL协议的软件产品必须使用GPL协议,而且必须开源/免费。对于商业软件或者对代码有保密要求的部门就非常不适合使用 GPL 协议发布软件,或引用基于GPL协议的类库。为了满足商业公司及保密的需要,在GPL协议的基础上又出现了LGPL协议。

3.LGPL协议

LGPL主要是为类库使用设计的开源协议。与GPL要求任何使用/修改/衍生的GPL类库的的软件必须采用 GPL 协议不同。LGPL 允许商业软件通过类库引用(link)方式使用 LGPL 类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。

但是如果修改 LGPL 协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用 LGPL 协议。因此,LGPL 协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。

4.BSD协议

BSD开源协议是一个给予使用者很大自由的协议。基本上使用者可以“为所欲为”,可以自由地使用、修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。但“为所欲为”的前提是当你发布使用了BSD协议的代码,或以BSD协议代码为基础做二次开发自己的产品时,需要满足如下3个条件。

(1)如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。

(2)如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。

(3)不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD 协议鼓励代码共享,但需要尊重源代码作者的著作权。BSD 由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。

5.Apache Licence 2.0协议

Apache Licence 是著名的非盈利开源组织Apache 采用的协议。该协议和BSD 类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似。

(1)需要给代码的用户一份Apache Licence。

(2)如果你修改了代码,需要在被修改的文件中说明。

(3)在延伸的代码中(修改和由源代码衍生的代码中)需要带有原来代码中的协议、商标、专利声明和其他原来作者规定需要包含的说明。

(4)如果再次发布的产品中包含一个 Notice 文件,则在 Notice 文件中需要带有 Apache Licence。你可以在Notice 中增加自己的许可,但不可以表现为Apache Licence。

Apache Licence 也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。

6.MIT协议

MIT是和BSD一样限制宽松的许可协议,作者只想保留版权,而无任何其他限制。也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的。

6.3.4 注册和注销设备文件

本节将为word_count驱动建立一个设备文件,该设备文件的名称是wordcount,位于/dev目录中。设备文件与普通文件不同,不能使用IO函数建立,需要使用misc_register函数建立设备文件,使用misc_deregister函数注销(移除)设备文件。这两个函数的定义如下:

extern int misc_register(struct miscdevice * misc);

extern int misc_deregister(struct miscdevice *misc);

一般需要在初始化Linux驱动时建立设备文件,在卸载Linux驱动时删除设备文件。而且设备文件还需要一个结构体(miscdevice)来描述与其相关的信息。miscdevice 结构体中有一个重要的成员变量fops,用于描述设备文件在各种可触发事件的函数指针。该成员变量的数据类型也是一个结构体file_operations。

本节需要修改word_count.c文件的word_count_init和word_count_exit函数,并定义一些宏和变量。修改部分的代码如下:

// 定义设备文件名

#define DEVICE_NAME "wordcount"

// 描述与设备文件触发的事件对应的回调函数指针

// owner:设备事件回调函数应用于哪些驱动模块,THIS_MODULE表示应用于当前驱动模块

static struct file_operations dev_fops =

{ .owner = THIS_MODULE};

// 描述设备文件的信息

// minor:次设备号 MISC_DYNAMIC_MINOR,:动态生成次设备号 name:设备文件名称

// fops:file_operations结构体变量指针

static struct miscdevice misc =

{.minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops };

// 初始化Linux驱动

static int word_count_init(void)

{

int ret;

// 建立设备文件

ret = misc_register(&misc);

// 输出日志信息

printk("word_count_init_success\n");

return ret;

}

//卸载Linux驱动

static void word_count_exit(void)

{

// 注销(移除)设备文件

misc_deregister(&misc);

// 输出日志信息

printk("word_init_exit_success\n");

}

编写上面代码需要注意如下几点。

 设备文件由主设备号和次设备号描述。而使用misc_register 函数只能设置次设备号。主设备号统一设为10。主设备号为10的设备是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。如果读者实现的驱动的功能并不复杂,可以考虑使用10作为其主设备号,而次设备号可以自己指定,也可以动态生成(需要指定MISC_DYNAMIC_MINOR常量)。因为采用这样的方式可以使用misc_register和misc_deregister函数简化注册和注销设备文件的步骤。在后面的章节会详细介绍如何使用register_chrdev_region和alloc_chrdev_region函数同时指定主设备号和次设备号的方式注册和注销设备文件。

 miscdevice.name 变量的值就是设备文件的名称。在本例中设备文件名称为wordcount。

 虽然 file_operations 结构体中定义了多个回调函数指针变量,但本节并未初始化任何一个回调函数指针变量。只初始化了file_operations.owner变量。如果该变量的值为module结构体,表示 file_operations 可被应用在这些由 module 指定的驱动模块中。如果 owner 变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。

 如果成功注册了设备文件,misc_register函数返回非0的整数,如果注册设备文件失败,返回0。

 可能有的读者注意到了。word_count.c 中的所有函数、变量都声明成了static。这是因为在C语言中用static声明函数、变量等资源,系统会将这些函数和变量单独放在内存的某一个区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留内存,因此这些函数和变量资源会一直在内存中。也就是说多次调用这些资源不用再进行压栈、出栈操作了,有利于提高驱动的运行效率。

现在重新编译word_count.c文件并使用如下的命令安装word_count驱动。

# insmod word_count.ko

如果word_count驱动已经被安装,应先使用下面的命令下载word_count驱动,然后再使用上面的命令安装word_count驱动。

# rmmod word_count

安装完word_count驱动后,使用下面的命令查看/dev目录中的设备。

# ls –a /dev

执行上面的命令后,会输出如图6-8所示的信息,其中多了一个wordcount文件(在白框中)。

如果想查看wordcount设备文件的主设备号和次设备号,可以使用如下的命令。

# ls –l /dev

执行上面的命令会输出如图6-9所示的信息,白框中的第一个数字是主设备号,第二个数字是从设备号。

▲图6-8 /dev目录中的设备文件
▲图6-9 显示 wordcount 设备文件的主从设备号

使用下面的命令可获显示当前系统中有哪些主设备以及主设备号。

# cat /proc/devices

执行上面的命令后会输出如图6-10所示的信息,从中可以找到misc设备以及主设备编号10。

▲图6-10 显示主设备和主设备号

6.3.5 指定回调函数

本节讲的内容十分关键。不管 Linux 驱动程序的功能多么复杂还是多么“酷”,都必须允许用户空间的应用程序与内核空间的驱动程序进行交互才有意义。而最常用的交互方式就是读写设备文件。通过file_operations.read和file_operations.write成员变量可以分别指定读写设备文件要调用的回调函数指针。

在本节将为 word_count.c 添加两个函数:word_count_read 和word_count_write。这两个函数分别处理从设备文件读数据和向设备文件写数据的动作。本节的例子先不考虑word_count要实现的统计单词数的功能,先用word_count_read和word_count_write函数做一个读写设备文件数据的实验,以便让读者了解如何与设备文件交互数据。本节编写的 word_count.c 文件是一个分支,读者可在word_count/read_write目录找到word_count.c文件。可以用该文件覆盖word_count目录下的同名文件测试本节的例子。

本例的功能是向设备文件/dev/wordcount写入数据后,都可以从/dev/wordcount设备文件中读出这些数据(只能读取一次)。下面先看看本例的完整的代码。

#include <linux/module.h>

#include <linux/init.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/miscdevice.h>

#include <asm/uaccess.h>

#define DEVICE_NAME "wordcount"  // 定义设备文件名

static unsigned char mem[10000];  // 保存向设备文件写入的数据

static char read_flag = 'y';   // y:已从设备文件读取数据 n:未从设备文件读取数据

static int written_count = 0;   //向设备文件写入数据的字节数

// 从设备文件读取数据时调用该函数

// file:指向设备文件 buf:保存可读取的数据 count:可读取的字节数 ppos:读取数据的偏移量

static ssize_t word_count_read(struct file *file, char _user *buf, size_t count, loff_t*ppos)

{

// 如果还没有读取设备文件中的数据,可以进行读取

if(read_flag == 'n')

{

// 将内核空间的数据复制到用户空间,buf中的数据就是从设备文件中读出的数据

copy_to_user(buf, (void*) mem, written_count);

// 向日志输出已读取的字节数

printk("read count:%d", (int) written_count);

// 设置数据已读状态

read_flag = 'y';

return written_count;

}

// 已经从设备文件读取数据,不能再次读取数据

else

{

return 0;

}

}

// 向设备文件写入数据时调用该函数

// file:指向设备文件 buf:保存写入的数据 count:写入数据的字节数 ppos:写入数据的偏移量

static ssize_t word_count_write(struct file *file, const char _user *buf, size_t count,loff_t *ppos)

{

// 将用户空间的数据复制到内核空间,mem中的数据就是向设备文件写入的数据

copy_from_user(mem, buf, count);

// 设置数据的未读状态

read_flag = 'n';

// 保存写入数据的字节数

written_count = count;

// 向日志输出已写入的字节数

printk("written count:%d", (int)count);

return count;

}

// 描述与设备文件触发的事件对应的回调函数指针

// 需要设置read和write成员变量,系统才能调用处理读写设备文件动作的函数

static struct file_operations dev_fops =

{ .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write };

// 描述设备文件的信息

static struct miscdevice misc =

{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops };

// 初始化Linux 驱动

static int word_count_init(void)

{

int ret;

// 建立设备文件

ret = misc_register(&misc);

// 输出日志信息

printk("word_count_init_success\n");

return ret;

}

//卸载Linux驱动

static void word_count_exit(void)

{

// 删除设备文件

misc_deregister(&misc);

// 输出日志信息

printk("word_init_exit_success\n");

}

// 注册初始化Linux驱动的函数

module_init( word_count_init);

// 注册卸载Linux驱动的函数

module_exit( word_count_exit);

MODULE_AUTHOR("lining");

MODULE_DESCRIPTION("statistics of word count.");

MODULE_ALIAS("word count module.");

MODULE_LICENSE("GPL");

编写上面代码需要了解如下几点。

 word_count_read 和 word_count_write 函数的参数基本相同,只有第 2 个参数 buf 稍微有一点差异。word_count_read 函数的 buf 参数类型是 char*,而 word_count_write 函数的 buf参数类型是 const char*,这就意味着 word_count_write 函数中的 buf 参数值无法修改。word_count_read 函数中的 buf参数表示从设备文件读出的数据,也就是说,buf中的数据都可能由设备文件读出,至于可以读出多少数据,取决于 word_count_read 函数的返回值。如果word_count_read函数返回n,则可以从buf读出 n个字符。当然,如果 n为0,表示无法读出任何的字符。如果 n 小于 0,表示发生了某种错误(n 为错误代码)。word_count_write 函数中的buf表示由用户空间的应用程序写入的数据。buf参数前有一个“_user”宏,表示buf的内存区域位于用户空间。

 由于内核空间的程序不能直接访问用户空间中的数据,因此,需要在 word_count_read 和word_count_write函数中分别使用copy_to_user和copy_from_user函数将数据从内核空间复制到用户空间或从用户空间复制到内核空间。

 本例只能从设备文件读一次数据。也就是说,写一次数据,读一次数据后,第二次无法再从设备文件读出任何数据,除非再次写入数据。这个功能是通过read_flag变量控制的。当read_flag变量值为n,表示还没有读过设备文件,在word_count_read函数中会正常读取数据。如果read_flag变量值为y,表示已经读过设备文件中的数据,word_count_read函数会直接返回0,应用程序将无法读取任何数据。

 实际上 word_count_read 函数的 count 参数表示的就是从设备文件读取的字节数。但因为使用cat命令测试word_count驱动时直接读取了32768个字节,因此count参数就没什么用了(值总是32768)。所以要在word_count_write函数中将写入的字节数保存,在word_count_read函数中直接使用写入的字节数。也就是说,写入多少个字节,就读出多少个字节。

 所有写入的数据都保存在 mem 数组中。该数组定义为 10000 个字符,因此写入的数据字节数不能超过10000,否则将会溢出。

为了方便读者测试本节的例子,笔者编写了几个Shell脚本文件,允许在Ubuntu Linux、S3C6410开发板和Android模拟器上测试word_count驱动。其中有一个负责调度的脚本文件build.sh。本书所有的例子都会有一个build.sh脚本文件,执行这个脚本文件就会要求用户选择将源代码编译到那个平台,选择菜单如图6-11所示。用户可以输入1、2或3选择编译平台。如果直接按回车键,默认值会选择第1 个编译平台(Ubuntu Linux)。

▲图6-11 编译平台的选择菜单

build.sh脚本文件的代码如下:

source /root/drivers/common.sh

# select_target是一个函数,用于显示如图6-11所示的选择菜单,并接收用户的输入

# 改函数在common.sh文件中定义

select_target

if [ $selected_target == 1 ]; then

source ./build_ubuntu.sh  #执行编译成Ubuntu Linux平台驱动的脚本文件

elif [$selected_target == 2 ]; then

source ./build_s3c6410.sh  #执行编译成 s3c6410平台驱动的脚本文件

elif [$selected_target == 3 ]; then

source ./build_emulator.sh   #执行编译成Android模拟器平台驱动的脚本文件

fi

在 build.sh 脚本文件中涉及了 3 个脚本文件(build_ubuntu.sh、build_s3c6410.sh 和 build_emulator.sh),这3个脚本文件的代码类似,只是选择的Linux内核版本不同。对于S3C6410和Android模拟器平台,编译完后Linux驱动,会自动将编译好的Linux驱动文件(*.so文件)上传到相应平台的/data/local目录,并安装Linux驱动。例如,build_s3c6410.sh脚本文件的代码如下:

source /root/drivers/common.sh

# S3C6410_KERNEL_PATH变量是适用于S3C6410平台的Linux内核源代码的路径,

# 该变量及其他类似变量都在common.sh脚本文件中定义

make -C $S3C6410_KERNEL_PATH M=${PWD}

find_devices

# 如果什么都选择,直接退出

if [ "$selected_device" == "" ]; then

exit

else

# 上传驱动程序(word_count.ko)

adb -s $selected_device push ${PWD}/word_count.ko /data/local

# 判断word_count驱动是否存在

testing=$(adb -s $selected_device shell lsmod | grep "word_count")

if [ "$testing" != "" ]; then

# 删除已经存在的word_count驱动

adb -s $selected_device shell rmmod word_count

fi

# 在S3C6410开发板中安装word_count驱动

adb -s $selected_device shell "insmod /data/local/word_count.ko"

fi

使用上面的脚本文件,需要在read_write目录建立一个Makefile文件,内容如下:

obj-m := word_count.o

现在执行build.sh脚本文件,选择要编译的平台,并执行下面的命令向/dev/word_count设备文件写入数据。

# echo 'hello lining' > /dev/wordcount

然后执行如下的命令从/dev/word_count设备文件读取数据。

# cat /dev/wordcount

如果输出“hello lining”,说明测试成功。

注意

如果在 S3C6410 开发板和 Android 模拟器上测试 word_count 驱动,需要执行shell.sh 脚本文件或 adb shell 命令进入相应平台的终端。其中 shell.sh 脚本在/root/drivers 目录中。这两种方式的区别是如果有多个 Android 设备和 PC 相连时, shell.sh 脚本会出现一个类似于如图6-11所示的选择菜单,用户可以选择进入哪个Android 设备的终端,而adb shell 命令必须要加-s 命令行参数指定Android 设备的ID才可以进入相应Android设备的终端。

6.3.6 实现统计单词数的算法

本节开始编写 word_count 驱动的业务逻辑:统计单词数。本节实现的算法将由空格、制表符(ASCII:9)、回车符(ASCII:13)和换行符(ASCII:10)分隔的字符串算做一个单词,该算法同时考虑了有多个分隔符(空格符、制表符、回车符和换行符)的情况。下面是 word_count 驱动完整的代码。在代码中包含了统计单词数的函数get_word_count。

#include <linux/module.h>

#include <linux/init.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/miscdevice.h>

#include <asm/uaccess.h>

#define DEVICE_NAME "wordcount"  // 定义设备文件名

static unsigned char mem[10000];  //保存向设备文件写入的数据

static int word_count = 0;   // 单词数

#define TRUE -1

#define FALSE 0

// 判断指定字符是否为空格(包括空格符、制表符、回车符和换行符)

static char is_spacewhite(char c)

{

if(c == ' ' || c == 9 || c == 13 || c == 10)

return TRUE;

else

return FALSE;

}

// 统计单词数

static int get_word_count(const char *buf)

{

int n = 1;

int i = 0;

char c = ' ';

char flag = 0; //处理多个空格分隔的情况,0:正常情况,1:已遇到一个空格

if(*buf == '\0')

return 0;

// 第1 个字符是空格,从0 开始计数

if(is_spacewhite(*buf) == TRUE)

n--;

// 扫描字符串中的每一个字符

for (; (c = *(buf + i)) != '\0'; i++)

{

// 只由一个空格分隔单词的情况

if(flag == 1 && is_spacewhite(c) == FALSE)

{

flag = 0;

}

// 由多个空格分隔单词的情况,忽略多余的空格

else if(flag == 1 && is_spacewhite(c) == TRUE)

{

continue;

}

// 当前字符为空格时单词数加1

if(is_spacewhite(c) == TRUE)

{

n++;

flag = 1;

}

}

// 如果字符串以一个或多个空格结尾,不计数(单词数减1)

if(is_spacewhite(*(buf + i - 1)) == TRUE)

n--;

return n;

}

// 从设备文件读取数据时调用的函数

static ssize_t word_count_read(struct file *file, char _user *buf, size_t count, loff_t*ppos)

{

unsigned char temp[4];

// 将单词数(int类型)分解成4 个字节存储在buf中

temp[0] = word_count >> 24;

temp[1] = word_count >> 16;

temp[2] = word_count >> 8;

temp[3] = word_count;

copy_to_user(buf, (void*) temp, 4);

printk("read:word count:%d", (int) count);

return count;

}

// 向设备文件写入数据时调用的函数

static ssize_t word_count_write(struct file *file, const char _user *buf, size_t count,loff_t *ppos)

{

ssize_t written = count;

copy_from_user(mem, buf, count);

mem[count] = '\0';

// 统计单词数

word_count = get_word_count(mem);

printk("write:word count:%d", (int)word_count);

return written;

}

// 描述与设备文件触发的事件对应的回调函数指针

static struct file_operations dev_fops =

{ .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write };

// 描述设备文件的信息

static struct miscdevice misc =

{ .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops };

// 初始化 Linux驱动

static int word_count_init(void)

{

int ret;

// 建立设备文件

ret = misc_register(&misc);

// 输出日志信息

printk("word_count_init_success\n");

return ret;

}

//卸载Linux驱动

static void word_count_exit(void)

{

// 删除设备文件

misc_deregister(&misc);

// 输出日志信息

printk("word_init_exit_success\n");

}

// 注册初始化Linux驱动的函数

module_init( word_count_init);

// 注册卸载Linux驱动的函数

module_exit( word_count_exit);

MODULE_AUTHOR("lining");

MODULE_DESCRIPTION("statistics of word count.");

MODULE_ALIAS("word count module.");

MODULE_LICENSE("GPL");

编写word_count驱动程序需要了解如下几点。

 get_word_count 函数将 mem 数组中第 1 个为“\0”的字符作为字符串的结尾符,因此在word_count_write函数中将mem[count]的值设为“\0”,否则 get_word_count函数无法知道要统计单词数的字符串到哪里结束。由于mem数组的长度为10000,而字符串最后一个字符为“\0”,因 此待统计的字符串最大长度为9999。

 单词数使用 int 类型变量存储。在 word_count_write 函数中统计出了单词数(word_count变量的值),在word_count_read函数中将word_count整型变量值分解成4个字节存储在buf中。因此,在应用程序中需要再将这4个字节组合成int类型的值。

6.3.7 编译、安装、卸载Linux驱动程序

在上一节 word_count 驱动程序已经全部编写完成了,而且多次编译测试该驱动程序。安装和卸载 word_count 驱动也做过多次。word_count 驱动与 read_write 目录中的驱动一样,也有一个build.sh和3个与平台相关的脚本文件。这些脚本文件与6.3.5节的实现类似,这里不再详细介绍。现在执行 build.sh 脚本文件,并选择要编译的平台。然后执行下面两行命令查看日志输出信息和word_count驱动模块(word_count.ko)的信息。

# dmesg |tail -n 1

# modinfo word_count.ko

如果显示如图6-12所示的信息,表明word_count驱动工作完全正常。

▲图6-12 显示 word_count 驱动的日志输出信息和驱动本身的信息

本书的脚本文件都是使用insmod命令安装Linux驱动的,除了该命令外,使用modprobe命令也可以安装 Linux 驱动。insmod 和modprobe的区别是modprobe命令可以检查驱动模块的依赖性。如A模块依赖于B模块(装载A之前必须先装载B)。如果使用insmod命令装载A模块,会出现错误。而使用 modprobe 命令装载 A 模块,B模块会现在装载。在使用 modprobe 命令装载驱动模块之前,需要先使用 depmod 命令检测 Linux驱动模块的依赖关系。

# depmod /root/drivers/ch06/word_count/word_count.ko

depmod命令实际上将Linux驱动模块文件(包括其路径)添加到如下的文件中。

/lib/modules/3.0.0-16-generic/modules.dep

使用depmod命令检测完依赖关系后,就可以调用modprobe命令装载Linux驱动。

# modprobe word_count

使用depmod和modprobe命令需要注意如下几点。

 depmod 命令必须使用Linux驱动模块(.ko 文件)的绝对路径。

 depmod 命令会将内核模块的依赖信息写入当前正在使用的内核的 modules.dep 文件。例如,笔者的 Ubuntu Linux 使用的是 Linux 3.0.0.16,所以应到 3.0.0-16-generic 目录去寻找modules.dep文件。如果读者使用了其他Linux内核,需要到相应的目录去寻找modules.dep文件。

 modprobe 命令只需使用驱动名称即可,不需要跟.ko。

6.4 使用多种方式测试Linux驱动

在上一节已经实现了一个简单的Linux驱动程序,该驱动的功能是统计给定字符串中的单词数,并且在最后已经将该Linux驱动的源代码成功编译成动态Linux驱动模块word_count.ko。下一步就是测试该模块。测试的方法很多,最常用的就是直接在Ubuntu Linux 中测试。当然,这对于本章实现的Linux 驱动是没问题的,但是对于需要直接访问硬件的驱动在Ubuntu Linux 上测试就不太方便。在这种情况下就需要在相应的硬件上进行测试。

对于一个Linux驱动程序,一开始可以在Ubuntu Linux 上做前期开发和测试。对于访问硬件的部分也可以在 Ubuntu Linux 用软件进行模拟。当基本开发完成后,就需要在开发板或工程样机上使用真实的硬件进行测试。当然,最后还需要在最终销售的手机上进行测试。最终测试通过,Linux驱动才能算真正开发完成。在开发Linux驱动的过程中一个重要的步骤就是测试。本节将结合实际的开发流程介绍在不同平台上测试Linux驱动程序。这些测试平台包括Ubuntu Linux、Android 模拟器和S3C6410开发板。

6.4.1 使用Ubuntu Linux测试Linux驱动

本节将介绍如何在 Ubuntu Linux 下测试驱动程序。由于上一节编写的 Linux 驱动程序通过 4个字节从设备文件(/dev/wordcount)返回单词数,所以不能使用cat命令测试驱动程序(cat命令不会将这4个字节还原成int类型的值显示)。但可以使用如下命令从日志中查看单词数。

# sh build.sh

# echo 'I love you.' > /dev/wordcount

# dmesg

执行上面的命令后,如果输出如图6-13所示白框中的信息,说明驱动程序成功统计了单词数。

虽然使用echo和dmesg命令可以测试 Linux驱动程序,但这种方式并不是真正的测试。为了使测试效果更接近真实环境,一般需要编写专门用于测试的程序。本节将为 word_count 驱动编写一个专门的测试程序(test_word_count.c)。test_word_count.c 通过直接操作/dev/wordcount设备文件与word_count驱动进行交互。测试程序的代码如下:

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdlib.h>

▲图6-13 输出单词个数

#include <string.h>

int main(int argc, char *argv[])

{

int testdev;     // 打开设备文件(/dev/wordcount)的句柄

unsigned char buf[4];   // 表示单词数的4个字节

// 打开设备文件

testdev = open("/dev/wordcount", O_RDWR);

// 如果open函数返回-1,表示打开设备文件失败

if (testdev == -1)

{

printf("Cann't open file \n");

return 0;

}

// 如果test_word_count后面跟有命令行参数,程序会将第1个参数值当作待统计的字符串

// 如果没有命令行参数,则只读取设备文件中的值

if (argc > 1)

{

// 向设备文件写入待统计的字符串

write(testdev, argv[1], strlen(argv[1]));

// 输出待统计的字符串

printf("string:%s\n", argv[1]);

}

// 读取设备文件中的单词数(4个字节)

read(testdev, buf, 4);

int n = 0;  // 单词数

// 将4个字节还原成int类型的值

n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8| ((int) buf[3]);

// 分别输出从设备文件获取的4个字节的值

printf("word byte display:%d,%d,%d,%d\n", buf[0], buf[1], buf[2], buf[3]);

// 输出统计出的单词数

printf("word count:%d\n", n);

// 关闭设备文件

close(testdev);

return 0;

}

test_word_count程序可以跟1个命令行参数(多个命令行参数只会使用第1个命令行参数)。如果命令行参数值含有空格,需要使用单引号(')或双引号(")将参数值括起来。可以使用下面的一组命令测试word_count驱动程序。

# gcc test_word_count.c -o test_word_count

# test_word_count

# test_word_count "I love you."

执行上面的命令后,如果输出如图6-14所示的信息(假设word_count以前统计过一个含有4个单词的字符串),表示word_count驱动成功测试。

▲图6-14 在 Ubuntu Linux 下测试 word_count 驱动

6.4.2 在Android模拟器上通过原生(Native)C程序测试Linux驱动

虽说我们开发的是 Linux驱动,但本书主要介绍的是 Android 版的Linux 内核,因此,Linux驱动只在 Ubuntu Linux 上测试成功还不能保证在 Android 设备上一定能正常工作,所以必须在Android设备上进行测试。Android设备有很多种类,如安装了Android的开发板、运行Android系统的手机或平板电脑等。但离我们最近的并不是这些硬件设备,而是Android模拟器。Android模拟器可以模拟绝大多数真实的环境,所以可以利用Android模拟器测试Linux内核。

在Android模拟器上测试Linux驱动首先应该想到的,也是最先应该做的就是将word_count.ko驱动模块安装在模拟器上。可能读者使用过adbshell命令。如果进入Android模拟器的命令提示符为“#”,说明通过命令行方式进入Android模拟器直接就是root权限(命令提示符为“$”,表示非root权限),因此从理论上可以使用insmod命令将word_count.ko驱动模块直接安装在Android模拟器中。现在我们来测试一下,看看是否可以将 word_count.ko 安装在 Android 模拟器上。现在执行 build.sh脚本,并选择“Android模拟器”,脚本会自动将word_count.ko文件上传到Android模拟器的/data/local目录,并进行安装。如果读者选择的是S3C6410开发板,在安装word_count.ko时就会输出如下的错误信息,表示编译Linux驱动的Linux内核版本与当前Android模拟器的版本不相同,无法安装。所以在编译Linux驱动时,必须选择与当前运行的Linux内核版本相同的Linux内核进行编译,否则就无法安装Linux驱动。

insmod: init_module ‘/data/local/word_count.ko’failed(Functionnot implemented)

注意

建议上传文件到Android模拟器或开发板时,将文件放到/data/local目录,系统很多其他的目录,如/system/bin,都是只读的,除非将 word_count.ko 文件打包进system.img,否则无法向这些目录写数据,即使有root权限也不行。

用于 Android 模拟器的 goldfish 内核默认不允许动态装载 Linux 驱动模块,因此需要在编译Linux内核之前执行如下命令配置Linux内核。

# cd ~/kernel/goldfish

# make menuconfig

执行上面的命令后,会出现如图6-15所示的设置界面。按空格键将第二项“Enable loadable module support”选中(前面是[*]),然后按回车键进入子菜单,选中前3 项,如图6-16所示,否则Linux驱动模块仍然无法安装和卸载。当退出设置菜单时保持设置。最后按4.2.3节的方法重新编译 Linux 内核,成功编译内核后,Android 模拟器可以使用新生成的 zImage 内核文件动态装载Linux驱动模块。

▲图6-15 Linux 内核设置主菜单
▲图6-16 内核设置第二级菜单(允许在 Android模拟器中安装和卸载Linux驱动模块)

现在执行build.sh 脚本文件完成对word_count 驱动的编译、上传和安装的工作,然后进入Android模拟器的终端,使用echo和dmesg命令可以测试word_count驱动和查看测试结果,方法与上一节相同。

注意

编译可在Android模拟器上运行的Linux驱动模块要使用goldfish内核,使用其他的内核编译word_count.c,安装时会出现如下错误。

insmod: error inserting 'word_count.ko':-1 Invalid module format

在Android 模拟器上不仅可以使用Linux命令测试驱动,也可以像Ubuntu Linux 一样使用本地C/C++程序进行测试。可能有的读者要问,Android 不是只能运行由Java 编写的 APK程序吗?顶多是在APK程序中嵌入NDK代码。还能直接运行普通的Linux程序吗?答案是肯定的。不过要满足如下两个条件。

 Android 模拟器、开发板或手机需要有root 权限。

 可执行文件需要使用交叉编译器进行编译,以便支持ARM 处理器。

现在使用交叉编译器来编译在上一节编写的test_word_count.c文件。为了使编译步骤尽可能简单,我们使用Android.mk设置编译参数,并使用make命令进行编译。首先在/root/drivers/ch06/word_count目录中建立一个Android.mk文件,并输入如下的内容。

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

# 指定要编译的源代码文件

LOCAL_SRC_FILES:= test_word_count.c

# 指定模块名,也是编译后生成的可执行文件名

LOCAL_MODULE := test_word_count

LOCAL_MODULE_TAGS := optional

include $(BUILD_EXECUTABLE)

Android.mk文件中有如下两个地方需要说明一下。

LOCAL_MODULE_TAGS

表示当前工程(Android.mk文件所在的目录)在什么模式下编译。如果设为optional,表示不考虑模式,也就是说,在任何模式下都会编译。该变量可以设置的值有user、userdebug、eng、optional。其中eng是默认值。

 user:限制用户对Android 系统的访问,适合于发布产品。

 userdebug:类似于 user 模式,但拥有 root 访问权限,并且可以从日志中获取大量的调试信息。

 eng:一般在开发的过程中设置该模式。除了拥有userdebug的全部功能外,还会带有大量的调试工具。

LOCAL_MODULE_TAGS的值与TARGET_BUILD_VARIANT变量有关。TARGET_BUILD_VARIANT变量用于设置当前的编译模式,可设置的值包括user、userdebug和eng。如果想改变编译模式,可以在编译Android源代码之前执行如下命令。

# export TARGET_BUILD_VARIANT = user

或使用lunch命令设置编译模式。

# lunch full-eng

其中 full 表示建立的目标,除了 full 目标(为所有的平台建立)外,还有专门为 x86 建立的full-x86。详细的建立目标执行lunch命令后就会列出。在图4-8已经显示了Android4支持的建立目标的编译模式。读者可以到第4章查看该图。

include $(BUILD_EXECUTABLE)

BUILD_EXECUTABLE 表示建立可执行的文件。可执行文件路径是<Android 源代码目录>/out/target/product/generic/system/bin/test_word_count。如果想编译成动态库(.so)文件,可以使用include $(BUILD_SHARED_LIBRARY)。动态库的路径是<Android 源代码目录>/ out/target/product/generic/system/lib/test_word_count.so。如果想编译成静态库(.a)文件,可以使用 include$(BUILD_STATIC_LIBRARY)。静态库的路径是<Android 源代码目录>/ out/target/product/generic/obj/STATIC_LIBRARIES/test_word_count_intermediates/test_word_count.

为了将 test_word_count.c 文件编译成可在 Android 模拟器上运行的可执行程序,可以将word_count目录复制到<Android源代码目录>的某个子目录,也可以在<Android源代码目录>目录中为 word_count 目录建立一个符号链接。为了方便,我们采用如下命令为 word_count 目录在<Android 源代码目录>/development 目录建立一个符号链接(假设 Android 源代码的目录是/sources/android/android4/development/word_count)。

# ln -s /root/drivers/ch06/word_count /sources/android/android4/ development/ word_count

现在进入/sources/android/android4目录,执行下面的命令初始化编译命令。

# source ./build/envsetup.sh

可以使用下面两种方法编译test_word_count.c。

(1)进入/sources/android/android4/development/word_count目录,并执行如下的命令。

# mm

(2)在/sources/android/android4目录下执行如下的命令。

# mmm development/word_count

成功编译后可以在<Android 源代码目录>/out/target/product/generic/system/bin 目录中找到test_word_count 文件。在随书光盘和模拟环境中已经带了编译好的 test_word_count 程序(包括Emulator 版本和Ubuntu Linux 版本),可执行程序一般不需要考虑Linux 内核的版本,用交叉编译器编译的支持ARM处理器的程序既可以在Android模拟器上运行,也可以在S3C6410开发板或其他有root权限的手机中运行。

Emulator版本的路径

随书光盘:<光盘根目录>/sources/ch06/word_count/emulator/test_word_count

模拟环境:/root/drivers/ch06/word_count/emulator/test_word_count

Ubuntu Linux版本的路径

随书光盘:<光盘根目录>/sources/ch06/word_count/ubuntu/test_word_count

模拟环境:/root/drivers/ch06/word_count/ubuntu/test_word_count

现在执行下面的命令将test_word_count文件上传到Android模拟器。

# adb push ./emulator/test_word_count /data/local

然后进入Android模拟器的终端,并执行下面的命令测试word_count驱动(需要先使用chmod命令设置test_word_count的可执行权限)。

# chmod 777 /data/local/test_word_count

# /data/local/test_word_count

# /data/local/test_word_count 'a bb ccc ddd eee'

执行上面的命令后,如果输出的单词个数是5,表示程序测试成功。

6.4.3 使用Android NDK测试Linux驱动

在 Android 系统中 Linux驱动主要的使用者是 APK 程序。因此,Linux 驱动做完后必须要用APK程序进行测试才能说明Linux驱动可以正常使用。由于上一节在Android虚拟机上使用C语言编写的可执行程序测试了Linux驱动,因此很容易想到可以利用Android NDK来测试Linux驱动,

由于Android NDK也使用C/C++来编写程序,因此可以利用上一节的C语言代码,当然,还得加上一些Android NDK特有的代码。在使用Android NDK测试Linux驱动之前需要做如下两件事。

 由于 Linux 驱动模块不会随 Android 系统启动而装载,因此必须执行 build.sh 脚本文件安装word_count驱动。

 不能使用默认方式启动 Android 模拟器,而要使用我们自己编译的 Linux 内核启动Android模拟器,启动模拟器的命令如下:

# emulator -avd myavd -kernel /root/kernel/goldfish/arch/arm/boot/zImage

为了方便,读者也可以在随书光盘的 Ubuntu Linux 虚拟环境中直接执行如下的命令来启动Android模拟器。其中emulator.sh文件在/root/drivers目录中。

# sh emulator.sh

本节的例子已经包含在随书光盘和虚拟环境中,路径如下。

随书光盘:<光盘根目录>/sources/ch06/word_count/word_count_ndk

虚拟环境:/root/drivers/ch06/word_count/word_count_ndk

word_count_ndk工程的代码部分由WordCountNDKTestMain.java和ndk_test_word_count.c文件组成。工程结构如图6-17所示。

▲图6-17 word_count_ndk 工程的结构

ndk_test_word_count.c文件用于访问word_count驱动。该文件包含两个供Java访问的函数,分别用来读取/dev/wordcount设备文件中的单词数和向/dev/wordcount设备文件写入字符串。下面先看看ndk_test_word_count.c文件的完整代码。

#include <string.h>

#include <jni.h>

#include <fcntl.h>

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

#include <stdlib.h>

// JNI函数:readWordCountFromDev

// 用于从/dev/wordcount设备文件读取单词数

jint Java_mobile_android_word_count_ndk_WordCountNDKTestMain_readWordCountFromDev(JNIEnv* env, jobject thiz)

{

int dev;    // open函数打开/dev/wordcount设备文件后返回的句柄,打开失败返回-1

jint wordcount = 0;  // 单词数

unsigned char buf[4];  // 以4个字节形式存储的单词数

// 以只读方式打开/dev/wordcount设备文件

dev = open("/dev/wordcount", O_RDONLY);

// 从dev/wordcount设备文件中读取单词数

read(dev, buf, 4);

int n = 0;    // 存储单词数的int类型变量

// 将由4个字节表示的单词数转换成int类型的值

n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]);

// 将int类型的单词数转换成jint类型的单词数

wordcount = (jint) n;

// 关闭/dev/wordcount设备文件

close(dev);

// 返回单词数

return wordcount;

}

// 将jstring类型的值转换成char *类型的值

char* jstring_to_pchar(JNIEnv* env, jstring str)

{

char* pstr = NULL;

// 下面的代码会调用Java中的String.getBytes方法获取字符串的字节数

// 获取java.lang.String类

jclass clsstring = (*env)->FindClass(env, "java/lang/String");

// 将字符串“utf-8”转换成jstring类型的值

jstring strencode = (*env)->NewStringUTF(env, "utf-8");

// 获取java.lang.String.getBytes方法

jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");

// 调用String.getBytes方法将str变量的值转换成jbytearray类型的值

jbyteArray byteArray = (jbyteArray)( (*env)->CallObjectMethod(env, str, mid,strencode));

// 获取字节长度

jsize size = (*env)->GetArrayLength(env, byteArray);

// 将jbytearray类型的值转换成jbyte*类型的值

jbyte* pbyte = (*env)->GetByteArrayElements(env, byteArray, JNI_FALSE);

if (size > 0)

{

// 为char*类型变量pstr分配空间

pstr = (char*) malloc(size);

// 将pbyte变量中的值复制到pstr变量中

memcpy(pstr, pbyte, size);

}

// 返回转换后的值

return pstr;

}

// JNI函数:writeStringToDev

// 用于向/dev/wordcount设备文件写入字符串

void Java_mobile_android_word_count_ndk_WordCountNDKTestMain_writeStringToDev(

JNIEnv* env, jobject thiz, jstring str)

{

int dev; // open函数打开/dev/wordcount设备文件后返回的句柄,打开失败返回-1

// 以只写方式打开/dev/wordcount设备文件

dev = open("/dev/wordcount", O_WRONLY);

// 将jstring类型字符串转换成char*类型的值

char* pstr = jstring_to_pchar(env, str);

if (pstr != NULL)

{

// 向/dev/wordcount设备文件写入字符串

write(dev,pstr, strlen(pstr));

}

// 关闭/dev/wordcount设备文件

close(dev);

}

编写上面的代码有一个重点就是jstring_to_pchar函数。该函数可以将jstring类型的数据转换成char*类型的数据。转换的基本思想就是调用Java方法String.getBytes,获取字符串对应的字节数组(jbyteArray)。由于 write 函数需要的是char *类型的数据,因此,还必须将jbyteArray类型的数据转换成char *类型的数据。采用的方法是,先将jbyteArray类型的数据转换成jbyte 类型的数据,然后调用 memcpy 函数将 jbyte 类型的数据复制到使用 malloc 函数分配的 char *指针空间中。在jstring_to_pchar函数中有如下的一行代码。

jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes","(Ljava/lang/ String;)[B"]);

看到getMethodID 方法最后一个参数的值是"(Ljava/lang/String;)[B",可能 Android NDK 初学者会对此感到困惑,以为是写错了。实际上这是JNI(Android NDK 程序实际上就是遵循JNI 规则的程序)对方法参数和返回类型的描述。在JNI 程序中为了方便描述Java数据类型,将简单类型使用了一个大写英文字母表示,如表6-1所示。

表6-1 Java数据类型在JNI中的描述符

除了表6-1所示的Java简单类型外,还有一些数据类型需要在JNI代码中与其对应。表6-2是这些数据类型在JNI中的描述符。

从表6-2所示的数据类型对照关系很容易想到本例中的"(Ljava/lang/String;)[B"是什么意思。jstring_to_pchar函数调用的是如下的getBytes方法的重载形式。

表6-2 复杂类型在JNI中的描述符

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException

在JNI中调用Java方法需要指定方法参数和返回值的数据类型。在JNI中的格式如下:

"(参数类型)返回值类型"

getBytes 方法的参数类型是 String,根据表6-2 的描述,String 类型中 JNI 的描述符是"Ljava/lang/String; "。getBytes 方法的返回值类型是 byte[]。这里就涉及一个数组的表示法。在 JNI中数组使用左中括号([)表示,后面是数组中元素的类型。每一维需要使用一个“[”。byte[]是一维字节数组,所以使用"[B"表示。如果是byte[][][],应使用"[[[B"表示。如果Java方法未返回任何值(返回值类型是void),则用 V 表示。如void mymethod(int value)的参数和返回值类型可表示为"(I)V"。

Android NDK 程序还需要一个Android.mk文件,代码如下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ndk_test_word_count

LOCAL_SRC_FILES := ndk_test_word_count.c

include $(BUILD_SHARED_LIBRARY)

注意

为了方便读者在Eclipse中开发Android应用程序,本节的例子采用了2.4.5节的方法进行配置。详细的配置信息请读者查看随书光盘或虚拟环境中的例子。虚拟环境中的所有配置和目录位置与笔者写作本书时使用的 Ubuntu Linux 的环境完全相同,读者可直接运行程序。但在随书光盘中的例子需要将相关的路径修改成读者自己机器上的路径。当然,如果恰巧读者机器的环境与笔者完全相同,就不需要做任何修改了。

在编写Java代码调用JNI函数之前,先看一下本例的界面,如图6-18所示。

读者需要先在PC上运行build.sh脚本文件安装word_count驱动。然后单击“从/dev/wordcount读取单词数”按钮,会在按钮下方输出当前/dev/wordcount 设备文件中统计出的单词数。读者也可以在输入框中输入一个由空格分隔的字符串,然后单击“向/dev/wordcount写入字符串”按钮,再单击“从/dev/wordcount读取单词数”按钮,就会统计出字符串中包含的单词数,效果如图6-19所示。

下面看一下本例中Java部分(WordCountNDKTestMain.java)的完整代码。

▲图6-18 测试 word_count 驱动的主界面
▲图6-19 统计字符串中的单词数

package mobile.android.word.count.ndk;

import android.app.Activity;

import android.os.Bundle;

import android.view.View;

import android.widget.EditText;

import android.widget.TextView;

import android.widget.Toast;

public class WordCountNDKTestMain extends Activity

{

private TextView tvWordCount;

private EditText etString;

@Override

public void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

tvWordCount = (TextView) findViewById(R.id.textview_wordcount);

etString = (EditText) findViewById(R.id.edittext_string);

}

// “从/dev/wordcount读取单词数”按钮的执行代码

public void onClick_ReadWordCountFromDev(View view)

{

// 显示单词数

tvWordCount.setText("单词数:" + String.valueOf(readWordCountFromDev()));

}

//“向/dev/wordcount写入字符串”按钮的执行代码

public void onClick_WriteStringToDev(View view)

{

// 向/dev/wordcount设备文件写入字符串

writeStringToDev(etString.getText().toString());

Toast.makeText(this, "已向/dev/wordcount写入字符串", Toast.LENGTH_LONG).show();

}

// native方法

public native int readWordCountFromDev();

public native void writeStringToDev(String str);

static

{

System.loadLibrary("ndk_test_word_count");

}

}

WordCountNDKTestMain.java中的代码只是简单地调用了JNI函数来操作/dev/wordcount文件。其他的代码都是常规的Android应用级别的代码。如果读者对这部分不熟悉,可以参阅笔者所著的《Android开发权威指南》。

6.4.4 使用Java代码直接操作设备文件来测试Linux驱动

如果Android拥有root权限,完全可以直接使用Java代码操作/dev/wordcount设备文件(没有root权限,Linux驱动模块是无法安装的)。本节将介绍如何使用Java代码来测试Linux驱动(测试程序不使用一行C/C++代码)。本节示例的路径如下。

随书光盘:<光盘根目录>/sources/ch06/word_count/word_count_java

虚拟环境:/root/drivers/ch06/word_count/word_count_java

word_count_java工程中只有一个源代码文件WordCountJavaTestMain.java。该文件的内容如下:

package mobile.android.word.count.java;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import android.app.Activity;

import android.os.Bundle;

import android.view.View;

import android.widget.EditText;

import android.widget.TextView;

import android.widget.Toast;

public class WordCountJavaTestMain extends Activity

{

private TextView tvWordCount;

private EditText etString;

@Override

public void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

tvWordCount = (TextView) findViewById(R.id.textview_wordcount);

etString = (EditText) findViewById(R.id.edittext_string);

}

// “从/dev/wordcount读取单词数”按钮的执行代码

public void onClick_ReadWordCountFromDev(View view)

{

// 显示单词数

tvWordCount.setText("单词数:" + String.valueOf(readWordCountFromDev()));

}

//“向/dev/wordcount写入字符串”按钮的执行代码

public void onClick_WriteStringToDev(View view)

{

// 向/dev/wordcount设备文件写入字符串

writeStringToDev(etString.getText().toString());

Toast.makeText(this, "已向/dev/wordcount写入字符串", Toast.LENGTH_LONG).show();

}

// 下面是用Java实现的操作/dev/wordcount设备文件的代码

// 读取/dev/wordcount设备文件中的单词数

private int readWordCountFromDev()

{

int n = 0;

byte[] buffer = new byte[4];

try

{

// 打开/dev/wordcount设备文件

FileInputStream fis = new FileInputStream("/dev/wordcount");

// 从设备文件中读取4个字节

fis.read(buffer);

// 将4个字节转换成int类型的值

n = ((int) buffer[0]) << 24 | ((int) buffer[1]) << 16| ((int) buffer[2]) << 8 | ((int) buffer[3]);

fis.close();

}

catch (Exception e)

{

}

return n;

}

// 向/dev/wordcount设备文件中写入字符串

private void writeStringToDev(String str)

{

try

{

// 打开/dev/wordcount设备文件

FileOutputStream fos = new FileOutputStream("/dev/wordcount");

// 写入字符串

fos.write(str.getBytes("iso-8859-1"));

fos.close();

}

catch (Exception e)

{

}

}

}

本例的运行效果和使用方法与上一节的例子类似。读者可以运行随书光盘或虚拟环境中的例子与上一节的例子进行比较。

6.4.5 使用S3C6410开发板测试Linux驱动

前面几节使用了不同方法来测试word_count驱动,但归根结底都是在PC上进行测试。那么本节将换一种平台来测试word_count驱动。当然,如果读者有Android手机的相应Linux内核源代码,也可以使用本节的方法在手机上测试word_count驱动。

6.4.2节、6.4.3节和6.4.4节中的例子都可以在S3C6410开发板上运行(有的需要重新编译,有的可以直接运行)。下面就挨个介绍如何使其在S3C6410开发板上运行。

首先应打开S3C6410开发板的电源开关,然后使用USB数据线连接S3C6410开发板和PC。最后执行build.sh脚本文件将word_count驱动安装在S3C6410开发板上。

1.在S3C6410开发板上使用可执行程序测试Linux驱动

由于在S3C6410 开发板运行的是Android 2.3.4,因此,需要在Android 2.3.4 下使用6.4.2 节的方法重新编译test_word_count.c文件。然后将编译好的test_word_count程序上传到开发板。测试的方法与Android模拟器相同。

注意

本书的主题之一就是介绍如何将 Android 移植到不同的硬件上。那么使用test_word_count 在不同硬件平台上运行实际上也是一种移植,只不过这种移植并不是移植操作系统,而是移植应用程序,所有可称为应用程序移植。最简单的应用程序移植就是将应用程序源代码编译成可在不同目标平台运行的二进制文件。当然,如果恰巧这些平台中都包含应用程序所使用的 API,那么直接在不同平台编译即可(有时需要使用交叉编译器)。但不幸的是,在很多时候,并不是所有的 API 在各个平台都有。有的API可能名字变化了,但有的API在某些平台根本就没实现。面对这样的情况,一般需要先移植这些 API,然后再移植应用程序。应用程序移植在Android系统中也会经常发生,如果某些特殊的Android系统(基于ARM芯片)需要一些用C语言实现的Library或可执行程序,但Android平台并没有这些功能,而其他平台(如Ubuntu Linux)有这样的程序,完全可以修改并重新编译成ARM 平台的目标文件放到Android系统中。

2.在S3C6410开发板上使用Android NDK测试Linux驱动

在Eclipse 中重新编译6.4.3节编写的Android NDK 程序就可以在S3C6410 开发板上运行,测试方法与6.4.3节使用的方法相同,测试效果如图6-20所示。

▲图6-20 在 S3C6410开发板上使用 Android NDK测试 word_count 驱动

3.在S3C6410开发板上使用Java代码测试Linux驱动

在6.4.4节编写的测试word_count驱动的Android程序可以使用同样的方法在S3C6410开发板上运行。测试效果与图6-20类似。

6.4.6 将驱动编译进Linux内核进行测试

前面几节都是将Linux驱动编译成模块,然后动态装载进行测试。动态装载驱动模块不会随着Android系统的启动而自动装载,因此Android系统每次启动都必须使用insmod或modprobe命令装载Linux驱动模块。

对于嵌入式系统(包括嵌入式Android、嵌入式Linux等)一般都采用将Linux驱动编译进内核的方式。这样做虽然没有动态装载灵活,但Linux驱动会随着Android的启动而自动装载。一般在开发过程中为了测试和调试方便,会将Linux驱动以模块形式装载到Linux内核中。当Linux驱动通过最终测试后,会将Linux驱动编译进Linux内核再进行测试。

本节将介绍如何将word_count驱动编译进Linux内核,并分别在Android模拟器和S3C6410开发板上测试word_count驱动。

Linux内核源代码被设计成可装卸式结构。也就是说只需要修改配置文件,就可以使某个Linux驱动编译成模块(.ko文件),或编译进Linux内核,当然,也可以将该Linux驱动从Linux内核去除。核心的配置文件如下。

 .config:该文件位于 Linux 内核源代码的顶层目录,为隐藏文件。该文件用于配置 Linux内核中的模块。在.config文件中可以对Linux驱动进行三方面的配置:编译成驱动模块(.ko文件)、编译进内核和从Linux内核去除。可以手工修改.config 文件,也可以使用make menuconfig命令用菜单方式来设置.config文件。

 Kconfig:每一个想要连接进Linux 内核的模块目录都有该文件。该文件主要用于定义makemenuconfig命令显示的菜单(包括菜单项名称、帮助信息、选项类型、模块依赖等信息),除此之外,Kconfig文件还可以导入位于其他目录的Kconfig文件。make命令通过Kconfig文件的递归引用,可以找到Linux内核中的所有Kconfig文件,从而建立一个完整的配置菜单。

 Makefile:一般与Kconfig文件同时出现。每有一个Kconfig文件,就必要有一个Makefile文件。该文件用于指定如何编译Makefile文件所在目录的源代码。

现在还使用word_count驱动的例子来详细说明如何将一个Linux驱动加入Linux内核源代码树中。由于word_count驱动属于字符驱动,所以可以使用如下的步骤将word_count驱动加入Linux内核源代码树。

第1步:将word_count.c文件放入Linux内核源代码

将word_count.c文件放到<Linux内核目录>/drivers/char目录中。

第2步:修改Kconfig文件

打开/root/kernel/goldfish/drivers/char/Kconfig文件,找到endmenu,并在endmenu前面添加如下代码。

config WORD_COUNT

bool "word_count driver"

help

This is a word count driver. It can get a word count from /dev/wordcount

其中,config后面的字符串将作为Shell变量名的后半部分,前半部分是CONFIG_。也就是说,每一个具体的模块都会对应一个 Shell 变量来保存该模块的 3 个编译行为(生成.ko 文件、编译进Linux内核或从Linux内核中去除)。word_count驱动模块的变量是CONFIG_WORD_COUNT。该变量的值会保存在.config文件中。

bool表示word_count驱动只能进行两项设置(被编译进内核与从Linux内核中去除),后面会介绍如何设置菜单项的三项设置。bool后面的字符串就是菜单项的文本。help用于设置菜单项的帮助信息。

第3步:修改Makefile文件

打开/root/kernel/goldfish/drivers/char/Makefile文件。该文件大多都是如图6-21所示的内容,随便找个位置插入如下内容。

▲图6-21 在 Makefile 文件中添加 word_count 驱动模块

obj-$(CONFIG_WORD_COUNT) += word_count.o

通过第2步的设置产生了一个CONFIG_WORD_COUNT变量,而在第3步中obj-后使用了该变量,而不是使用固定的值(y或m)。make命令在编译Linux内核时会将该变量替换成相应的值。

第4步:设置.config文件

.config文件可以通过手工配置,也可以通过make menuconfig命令在菜单中配置。在这里我们采用菜单配置的方法。现在进入 Linux 内核顶层目录(/root/kernel/goldfish)。然后执行 make menuconfig 命令显示配置菜单,并进入“Device Drivers”>“Character devices”子菜单,找到“word_count_driver”菜单项,按空格键将“word_count_driver”菜单项前设置成星号(*),如图6-22所示。然后退出配置界面并保存所做的修改。

按“h”键可以显示word_count驱动的帮助信息,如图6-23所示。

▲图6-22 配置 word_count 驱动模块
▲图6-23 word_count 驱动的帮助信息

在配置完.config文件后,读者可以打开.config文件,并找到CONFIG_WORD_COUNT,会发现该变量的值已被设成“y”。

第5步:编译Linux内核

进入/root/kernel/goldfish目录,执行下面的命令编译Linux内核。

# make

如果读者以前编译过当前的Linux内核,并不需要担心编译的时间过长,因为make足够智能,它只会编译最新修改的模块及其依赖的模块。

当成功编译Linux内核后,读者可以到/root/kernel/goldfish/arch/arm/boot目录找到zImage文件,并使用Android模拟器运行这个内核。读者会发现,在/dev目录中有一个wordcount设备文件,而我们并没有运行build.sh脚本文件安装word_count驱动。这是因为Android模拟器在装载zImage内核文件时已自动装载了word_count驱动。不过在使用前面的例子测试word_count驱动时仍然需要执行下面的命令设置/dev/wordcount设备文件的访问权限。

# adb shell chmod 777 /dev/wordcount

如果读者不想将word_count.c复制到/root/kernel/goldfish/drivers/char目录,可以使用下面的命令在/root/kernel/goldfish/drivers/char目录建立一个符号链接。

# ln -s /root/drivers/ch06/word_count /root/kernel/ goldfish/drivers/ char/word_count

将 word_count 目录加入 Linux 内核源代码树的步骤如下(在进行下面的步骤之前需要将上面步骤所做的设置注释掉)。

第1步:建立新的Kconfig文件

在word_count目录中建立一个Kconfig文件,并输入如下内容:

config WORD_COUNT

tristate "word_count driver"

default y

help

This is a word count driver. It can get a word count from /dev/wordcount

其中tristate表示三态类型(编译进内核、编译成模块,从Linux内核移除)。如果使用tristate代替bool,菜单项前面就变成尖括号。按“y”键,尖括号中显示星号(*),表示编译进内核。按“M”键,尖括号中显示 M,表示编译成模块。按“N”键,尖括号在符号消失,表示 word_count驱动被忽略。如果不断按“空格”键,这3种状态会循环切换。

default用来设置默认值。如果使用tristate,default可以设置y、m和n三个值,分别对应编译进内核、编译成模块和从Linux内核中移除。当模块第一次设置时会处于default设置的默认状态。

注意

如果使用tristate,必须按照6.4.2节的方法打开“Enable loadable module support”选项,否则无法将驱动设为编译成模块状态(M状态),菜单项前面仍然是一对中括号。

第2步:修改Makefile文件

word_count目录中的Makefile文件目前的内容如下:

obj-m := word_count.o

在Makefile文件中已经将编译类型设为Linux驱动模块(obj-m表示编译成.ko文件)。但现在要将word_count驱动加入Linux内核源代码树中,因此需要使用CONFIG_WORD_COUNT变量来代替m,所以Makefile文件的内容需要按如下内容修改。

obj-$(CONFIG_WORD_COUNT) := word_count.o

修改Makefile文件后,如果还想使用前面几节的脚本文件测试word_count驱动,需要将.config文件中 CONFIG_WORD_COUNT 变量值设为 m,如果.config 文件中没有该变量,就添加一个CONFIG_WORD_COUNT 变量。当然,也可以使用make menuconfig 命令设置。

为了可以单独编译word_count驱动,也可以和Linux内核一同编译,我们可以采用如下形式重新编写Makefile文件。当CONFIG_WORD_COUNT变量未定义时,说明没有与Linux内核一同编译。

# 与Linux内核一同编译

ifdef CONFIG_WORD_COUNT

obj-$(CONFIG_WORD_COUNT) := word_count.o

else

# 单独编译

obj-m := word_count.o

endif

第3步:修改上层目录的Kconfig文件

为了能找到 word_count 目录中的 Kconfig 文件,需要在 drivers/char/Kconfig 文件中引用word_count 目录中的 Kconfig 文件。现在打开/root/kernel/goldfish/drivers/char/Kconfig 文件,在“endmenu”之前添加如下一行代码。

source "drivers/char/word_count/Kconfig"

第4步:修改上层目录的Makefile文件

在drivers/char/Makefile文件中添加如下一行,以便使 make命令可以找到word_count目录中的Makefile文件。

obj-$(CONFIG_WORD_COUNT) += word_count/

接下来的工作就和前面介绍的五步中的第4步和第5步一样了。在进入如图6-24所示的设置界面时,可以按“M”键将word_count驱动模块编译成.ko文件。

▲图6-24 设置 word_count 驱动模块的编译类型

注意

当修改Linux内核设置后重新编译内核,以前使用该Linux内核编译的Linux驱动模块可能由于格式错误无法安装,因此,在重新编译Linux内核后,需要重新编译Linux驱动模块。

如果想将word_count驱动模块编译进其他内核也可采用与上面类似的做法。

6.5 使用Eclipse开发和测试Linux驱动程序

在前面几节开发的word_count驱动和测试程序大多都需要在Linux终端进行编译(Android应用程序除外)和运行,而且也无法跟踪到Linux内核函数、变量、宏的内部(除非自己到Linux内核源代码中就寻找这些源代码文件),这并不利于深入了解 Linux内核技术。在本节将为读者展示如何在Eclipse中开发Linux驱动程序,并且可以像跟踪Java代码一样直接跟踪到Linux内核源代码。除此之外,Eclipse也可以开发基于C/C++的测试程序。本节的例子需要在Eclipse中安装CDT插件,还没安装CDT的读者请按照2.4.2节介绍在方法安装CDT。

6.5.1 在Eclipse中开发Linux驱动程序

在Ubuntu Linux中通常使用vi或gedit来编辑C/C++源代码文件。在Linux的其他发行版vi文本编辑器一般是存在的,当然,也可以使用任何自己喜欢的文本编辑器来编辑 C/C++源代码文件。虽然很多文本编辑器都可以根据文件扩展名使源代码的关键字变色(如显示的是.c文件,会根据C语言的规则以不同颜色显示源代码中的不同部分),但除此之外并没有什么特殊的功能辅助编码工作。为了获得更多的辅助编码功能,本节将介绍如何利用Eclipse自身的功能开发Linux驱动,并自动进行安装。

本节示例的路径如下。

随书光盘:<光盘根目录>/sources/ch06/word_count/word_count_eclipse

虚拟环境:~/drivers/ch06/word_count/word_count_eclipse

使用Eclipse开发Linux驱动程序的步骤如下。

第1步:建立C工程

在Eclipse 中单击“File”>“New”>“Other”菜单项,打开“New”对话框,选择“C Project”项(选“C++ Project”项也可),如图6-25所示。

然后单击“Next”按钮,并按照如图6-26所示进行设置,最后单击“Finish”按钮建立一个空的C工程。

▲图6-25 选择“C Project”项
▲图6-26 选择 C 工程存放的路径

第2步:建立C源代码文件链接

单击word_count_eclipse 工程右键菜单中的“New”>“Soruce Folder”菜单项目,打开“New Soruce Folder”对话框,在“Folder name”文本框中输入“src”,如图6-27所示。然后单击“Finish”按钮建立一个保存Linux驱动源代码文件的目录。

▲图6-27 建立保存 Linux驱动源代码文件的目录

注意

建立src目录并不是必需的,但将源代码文件放到一个指定的目录是一个好习惯。

接下来就是将word_count.c文件放到src目录中。当然,最简单的方法是直接将位于word_count目录的 word_count.c 文件复制到 src 目录中,但这样就有两个 word_count.c 文件了,还需要维护word_count.c文件的一致性。为了方便,只需要在src目录建立一个word_count/word_count.c文件的链接即可。建立文件链接仍然需要导入文件。首先单击src目录,在右键菜单中单击“Import”菜单项,打开“Import”对话框,选择“File System”项,如图6-28所示。

单击“Next”按钮进入下一个设置界面,并按着图6-29所示选择word_count.c文件,并单击“Advanced”按钮,选择“Create links in workspace”复选框。最后单 击“Finish”按钮建立word_count.c文件中src目录中的链接。

▲图6-28 选择“File System”项
▲图6-29 建立 word_count.c文件中 src目录中的链接

建立完word_count.c文件的链接后,我们会发现虽然在Eclipse工程中查看,src目录中有一个word_count.c文件,但从文件浏览器查看,src目录中什么也没有。

第3步:设置include路径

这一步解决了如下两个问题。

 可以跟踪进Linux内核源代码。

 输入函数、宏等内容时显示代码提示。

为了实现这两个功能,就需要让Eclipse找到Linux内核源代码中的相关资源。C/C++中的资源都是在头文件(.h文件)中定义的,因此,需要为word_count_eclipse工程指定头文件的路径。Linux内核源代码中有很多的文件路径,并不一定指定所有的路径,只需要指定当前工程中C/C++源代码文件引用的头文件所在的路径即可。

单击word_count_eclipse工程右键菜单的“Properties”菜单项(或选中word_count_eclipse,按Alt+Enter 键)打开工程属性对话框。选中“C/C++ General”>“Paths and Symbols”项,选中右侧“Includes”页的“GNU C”项,单击右侧的“Add”按钮添加如下两个路径,如图6-30所示。

/root/kernel/goldfish/include

/root/kernel/goldfish/arch/arm/include

▲图6-30 设置 include 路径

现在单击“OK”按钮关闭“Properties”对话框。这时word_count.c文件的错误一一消失了。在Eclipse中打开word_count.c文件,按住Ctrl键,用鼠标左键单击某一个Linux内核函数(如copy_to_user),或直接在某个函数、宏上按 F3 键,会直接跟踪到 Linux 内核函数的定义部分,如图6-31所示。

▲图6-31 跟踪 Linux 内核函数

当按下内容助手(Content Assist)快捷键(可通过单击“Help”>“Key Assist”菜单项查看当前的内容助手快捷键),会显示如图6-32所示的内容助手列表。里面列出了所有Eclipse能找到的函数、宏、变量等资源。

▲图6-32 内容辅助列表

第4步:编译Linux驱动

Linux 驱动不能在Eclipse 中直接运行。但可以编译和安装。可以利用build.sh脚本文件来编译和安装 Linux 驱动。读者可以按照2.4.5节介绍的方法为工程添加一个Program,并且去掉系统原先的两个建立项前的复选框。其中Argument直接填写脚本文件的路径即可(Location和Working Directory的设置与2.4.5节的设置相同)。本例只设置“Main”页。其他页不需要设置。设置完 Program 后。编译word_count_eclipse工程,就会在Console视图中显示编译和安装信息,如图6-33所示。

▲图6-33 在 Eclipse 中编译和安装 Linux 驱动

6.5.2 在Eclipse中测试Linux驱动

安装了 CDT 插件后,Eclipse 不仅可以开发 Java 程序,也可以开发普通的 C/C++程序。使用Eclipse 开发 C/C++程序是一个好主意。因为可以带来很多好处,如高亮显示代码、格式化代码、跟踪代码、Content Assist 列表、调试代码等。本节将介绍如何在Eclipse 中建立C/C++项目,并测试Linux驱动。

首先需要像上一节方法一样(只是将 CROSS GCC 改成 Linux GCC)建立一个 C 工程(word_count_eclipse_test),并且建立一个存储源代码的src目录。word_count_eclipse_test工程的路径如下。

随书光盘:<光盘根目录>/sources/ch06/word_count/word_count_eclipse_test

虚拟环境:~/drivers/ch06/word_count/word_count_eclipse_test

然后按如下步骤测试Linux驱动。

第1步:导入test_word_count.c文件

按照上一节第2步的方法建立test_word_count.c文件的连接。

第2步:设置include路径

按照上一节第3步的方法设置include路径。本例只需设置/usr/include即可。

第3步:建立Target

单击word_count_eclipse_test工程右键菜单的“MakeTargets”>“Create”菜单项,打开“Create Make Target”对话框,在“Target name”文本框中输入“word_count_eclipse_test”,然后单击“OK”按钮关闭对话框。

第4步:Build工程

单击 word_count_eclipse_test 工程右键菜单的“Make Targets”>“Build”菜单项,打开“Make Targets”对话框,如图6-34所示,选中在第3步建立的“word_count_eclipse_test”,然后单击“Build”按钮,会在word_count_eclipse_test工程中生成一些配置文件,如图6-35所示。

▲图6-34 “Make Targets”对话框
▲图6-35 word_count_eclipse_test工程的结构

第5步:运行测试程序

如果是第一次运行word_count_eclipse_test,需要单击工程右键菜单的“Run As”>“Local C/C++Application”菜单项。如果以前运行过word_count_eclipse_test,可在Eclipse工具栏运行或调试按钮下拉菜单中选择“word_count_eclipse_test”运行程序。运行程序后会将测试结果输出到 Console视图中,如图6-36所示。

▲图6-36 输出测试结果

6.6 小结

统计单词数的word_count驱动是本书实现的第一个完整的Linux驱动。虽然word_count驱动的功能并不复杂,但足以让以前从未接触过Linux驱动的开发人员了解Linux驱动的完整开发过程。本章不仅仅是介绍一个示例,还介绍了如何使用不同的方法测试Linux驱动。这些方法主要以平台来区分。例如,可以直接使用Ubuntu Linux 测试Linux 驱动。有很多驱动只要再利用相应的Linux内核编译一遍,就可以直接安装在Android、Linux等嵌入式系统中。当然,也可以直接在Android模拟器、开发板甚至手机上测试Linux驱动。为了更快地开发Linux驱动,也可以考虑采用6.5节介绍的Eclipse集成开发环境。

相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程