Android深度探索(卷1):HAL与驱动开发
上QQ阅读APP看书,第一时间看更新

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

第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驱动和Android HAL程序库的步骤和规则以后,看着也没那么复杂。因为驱动和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系统使用Linux 2.4了。Linux 3.x是最新推出的Linux内核版本。最新的Android 4.x采用了这个新的 Linux 3.0.8 内核版本,还有很多新推出的 Linux 系统(如Ubuntu Linux 11.10)都使用了Linux 3.0。读者可在Android系统中的“设备”>“关于手机”中查看当前Android系统所采用的Linux内核版本,如图1-2所示。

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

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

方法1

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

uname -a

如果当前系统是Ubuntu Linux 11.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,掌握GNU C并不困难。当然,如果读者还不了解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程序库。