1、-在 Ubuntu 上为 Android 系统编写 Linux 内核驱动程序一、引言在智能手机时代,每个品牌的手机都有自己的个性特点。正是依靠这种与众不同的个性来吸引用户,营造品牌凝聚力和用户忠城度,典型的代表非 iphone 莫属了。据统计,截止 2011 年 5 月,AppStore 的应用软件数量达 381062 个,位居第一,而 Android Market 的应用软件数量达 294738,紧随 AppStore 后面,并有望在 8 月份越过 AppStore。随着Android 系统逐步扩大市场占有率,终端设备的多样性亟需更多的移动开发人员的参与。据业内统计,Android 研发人才
2、缺口至少 30 万。目前,对 Android 人才需求一类是偏向硬件驱动的 Android 人才需求,一类是偏向软件应用的 Android 人才需求。总的来说,对有志于从事 Android 硬件驱动的开发工程师来说,现在是一个大展拳脚的机会。那么,就让我们一起来看看如何为 Android 系统编写内核驱动程序吧。这里,我们不会为真实的硬件设备编写内核驱动程序。为了方便描述为 Android 系统编写内核驱动程序的过程,我们使用一个虚拟的硬件设备,这个设备只有一个 4 字节的寄存器,它可读可写。想起我们第一次学习程序语言时,都喜欢用“Hello, World”作为例子,这里,我们就把这个虚拟的设
3、备命名为“hello” ,而这个内核驱动程序也命名为 hello 驱动程序。其实,Android 内核驱动程序和一般 Linux 内核驱动程序的编写方法是一样的,都是以 Linux 模块的形式实现的,具体可参考前面 Android 学习启动篇一文中提到的 Linux Device Drivers 一书。不过,这里我们还是从 Android 系统的角度来描述 Android 内核驱动程序的编写和编译过程。参照这两篇文章在 Ubuntu 上下载、编译和安装 Android 最新源代码和在 Ubuntu 上下载、编译和安装 Android 最新内核源代码(Linux Kernel)准备好 Andro
4、id 内核驱动程序开发环境。二. 进入到 kernel/common/drivers 目录,新建 hello 目录::/Android$ cd kernel/common/:/Android/kernel/common/drivers$ mkdir hello三. 在 hello 目录中增加 hello.h 文件:#ifndef _HELLO_ANDROID_H_ #define _HELLO_ANDROID_H_ #include #include #define HELLO_DEVICE_NODE_NAME “hello“ #define HELLO_DEVICE_FILE_NAME “h
5、ello“ #define HELLO_DEVICE_PROC_NAME “hello“ #define HELLO_DEVICE_CLASS_NAME “hello“ struct hello_android_dev int val; struct semaphore sem; struct cdev dev; ; #endif 这个头文件定义了一些字符串常量宏,在后面我们要用到。此外,还定义了一个字符设备结构体 hello_Android_dev,这个就是我们虚拟的硬件设备了,val 成员变量就代表设备里面的寄存器,它的类型为 int,sem 成员变量是一个信号量,是用同步访问寄存器 va
6、l 的,dev 成员变量是一个内嵌的字符设备,这个 Linux 驱动程序自定义字符设备结构体的标准方法。四.在 hello 目录中增加 hello.c 文件,这是驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。这里,提供了三种访问设备寄存器的方法,一是通过 proc 文件系统来访问,二是通过传统的设备文件的方法来访问,三是通过 devfs 文件系统来访问。下面分段描述该驱动程序的实现。首先是包含必要的头文件和定义三种访问设备的方法:#include #include #include #include #include #include #include #
7、include “hello.h“ /*主设备和从设备号变量*/ static int hello_major = 0; static int hello_minor = 0; /*设备类别和设备变量*/ static struct class* hello_class = NULL; static struct hello_android_dev* hello_dev = NULL; /*传统的设备文件操作方法*/ static int hello_open(struct inode* inode, struct file* filp); static int hello_release(s
8、truct inode* inode, struct file* filp); static ssize_t hello_read(struct file* filp, char _user *buf, size_t count, loff_t* f_pos); static ssize_t hello_write(struct file* filp, const char _user *buf, size_t count, loff_t* f_pos); /*设备文件操作方法表*/ static struct file_operations hello_fops = .owner = THI
9、S_MODULE, .open = hello_open, .release = hello_release, .read = hello_read, .write = hello_write, ; /*定义设备属性*/ static DEVICE_ATTR(val, S_IRUGO | S_IWUSR, hello_val_show, hello_val_store); /*访问设置属性方法*/ static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf); static
10、 ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count); 定义传统的设备文件访问方法,主要是定义 hello_open、hello_release、hello_read 和hello_write 这四个打开、释放、读和写设备文件的方法:/*打开设备方法*/ static int hello_open(struct inode* inode, struct file* filp) struct hello_android_dev* dev;
11、 /*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/ dev = container_of(inode-i_cdev, struct hello_android_dev, dev); filp-private_data = dev; return 0; /*设备文件释放时调用,空实现*/ static int hello_release(struct inode* inode, struct file* filp) return 0; /*读取设备的寄存器 val 的值 */ static ssize_t hello_read(struct file* filp, c
12、har _user *buf, size_t count, loff_t* f_pos) ssize_t err = 0; struct hello_android_dev* dev = filp-private_data; /*同步访问*/ if(down_interruptible( if(count val) goto out; /*将寄存器 val 的值拷贝到用户提供的缓冲区 */ if(copy_to_user(buf, goto out; err = sizeof(dev-val); out: up( return err; /*写设备的寄存器值 val*/ static ssiz
13、e_t hello_write(struct file* filp, const char _user *buf, size_t count, loff_t* f_pos) struct hello_android_dev* dev = filp-private_data; ssize_t err = 0; /*同步访问*/ if(down_interruptible( if(count != sizeof(dev-val) goto out; /*将用户提供的缓冲区的值写到设备寄存器去*/ if(copy_from_user( goto out; err = sizeof(dev-val);
14、 out: up( return err; 定义通过 devfs 文件系统访问方法,这里把设备的寄存器 val 看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现 hello_val_show 和 hello_val_store 两个方法,同时定义了两个内部使用的访问 val 值的方法_hello_get_val 和_hello_set_val :/*读取寄存器 val 的值到缓冲区 buf 中,内部使用*/ static ssize_t _hello_get_val(struct hello_Android_dev* dev, char* buf) int val = 0;
15、/*同步访问*/ if(down_interruptible( val = dev-val; up( return snprintf(buf, PAGE_SIZE, “%d/n“, val); /*把缓冲区 buf 的值写到设备寄存器 val 中去,内部使用 */ static ssize_t _hello_set_val(struct hello_android_dev* dev, const char* buf, size_t count) int val = 0; /*将字符串转换成数字*/ val = simple_strtol(buf, NULL, 10); /*同步访问*/ if(
16、down_interruptible( dev-val = val; up( return count; /*读取设备属性 val*/ static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf) struct hello_android_dev* hdev = (struct hello_android_dev*)dev_get_drvdata(dev); return _hello_get_val(hdev, buf); /*写设备属性 val*/ static ssi
17、ze_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count) struct hello_android_dev* hdev = (struct hello_android_dev*)dev_get_drvdata(dev); return _hello_set_val(hdev, buf, count); 定义通过 proc 文件系统访问方法 ,主要实现了 hello_proc_read 和 hello_proc_write 两个方法,同时定义了在 p
18、roc 文件系统创建和删除文件的方法 hello_create_proc 和hello_remove_proc:/*读取设备寄存器 val 的值,保存在 page 缓冲区中*/ static ssize_t hello_proc_read(char* page, char* start, off_t off, int count, int* eof, void* data) if(off 0) *eof = 1; return 0; return _hello_get_val(hello_dev, page); /*把缓冲区的值 buff 保存到设备寄存器 val 中去*/ static ss
19、ize_t hello_proc_write(struct file* filp, const char _user *buff, unsigned long len, void* data) int err = 0; char* page = NULL; if(len PAGE_SIZE) printk(KERN_ALERT“The buff is too large: %lu./n“, len); return -EFAULT; page = (char*)_get_free_page(GFP_KERNEL); if(!page) printk(KERN_ALERT“Failed to a
20、lloc page./n“); return -ENOMEM; /*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/ if(copy_from_user(page, buff, len) printk(KERN_ALERT“Failed to copy buff from user./n“); err = -EFAULT; goto out; err = _hello_set_val(hello_dev, page, len); out: free_page(unsigned long)page); return err; /*创建/proc/hello 文件*/ static void hel
21、lo_create_proc(void) struct proc_dir_entry* entry; entry = create_proc_entry(HELLO_DEVICE_PROC_NAME, 0, NULL); if(entry) entry-owner = THIS_MODULE; entry-read_proc = hello_proc_read; entry-write_proc = hello_proc_write; /*删除/proc/hello 文件*/ static void hello_remove_proc(void) remove_proc_entry(HELLO
22、_DEVICE_PROC_NAME, NULL); 最后,定义模块加载和卸载方法,这里只要是执行设备注册和初始化操作:/*初始化设备*/ static int _hello_setup_dev(struct hello_Android_dev* dev) int err; dev_t devno = MKDEV(hello_major, hello_minor); memset(dev, 0, sizeof(struct hello_android_dev); cdev_init( dev-dev.owner = THIS_MODULE; dev-dev.ops = /*注册字符设备*/ er
23、r = cdev_add( if(err) return err; /*初始化信号量和寄存器 val 的值*/ init_MUTEX( dev-val = 0; return 0; /*模块加载方法*/ static int _init hello_init(void) int err = -1; dev_t dev = 0; struct device* temp = NULL; printk(KERN_ALERT“Initializing hello device./n“); /*动态分配主设备和从设备号*/ err = alloc_chrdev_region( if(err dev);
24、cleanup: kfree(hello_dev); unregister: unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1); fail: return err; /*模块卸载方法*/ static void _exit hello_exit(void) dev_t devno = MKDEV(hello_major, hello_minor); printk(KERN_ALERT“Destroy hello device./n“); /*删除/proc/hello 文件*/ hello_remove_proc(); /
25、*销毁设备类别和设备*/ if(hello_class) device_destroy(hello_class, MKDEV(hello_major, hello_minor); class_destroy(hello_class); /*删除字符设备和释放设备内存*/ if(hello_dev) cdev_del( kfree(hello_dev); /*释放设备号*/ unregister_chrdev_region(devno, 1); MODULE_LICENSE(“GPL“); MODULE_DESCRIPTION(“First Android Driver“); module_in
26、it(hello_init); module_exit(hello_exit); 五.在 hello 目录中新增 Kconfig 和 Makefile 两个文件其中 Kconfig 是在编译前执行配置命令 make menuconfig 时用到的,而 Makefile 是执行编译命令 make 是用到的:Kconfig 文件的内容config HELLOtristate “First Android Driver“default nhelpThis is the first android driver.Makefile 文件的内容obj-$(CONFIG_HELLO) += hello.o在
27、 Kconfig 文件中,tristate 表示编译选项 HELLO 支持在编译内核时,hello 模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行 make menuconfig 命令来配置编译选项,使得 hello 可以以模块或者内建的方法进行编译。在 Makefile 文件中,根据选项 HELLO 的值,执行不同的编译方法。六. Make menuconfig修改 arch/arm/Kconfig 和 drivers/kconfig 两个文件,在 menu “Device Drivers“和 endmenu 之间添加一行:source “driv
28、ers/hello/Kconfig“这样,执行 make menuconfig 时,就可以配置 hello 模块的编译选项了。. 七. 修改 drivers/Makefile 文件,添加一行:obj-$(CONFIG_HELLO) += hello/八. 配置编译选项::/Android/kernel/common$ make menuconfig找到“Device Drivers“ = “First Android Drivers“选项,设置为 y。注意,如果内核不支持动态加载模块,这里不能选择 m,虽然我们在 Kconfig 文件中配置了 HELLO 选项为 tristate。要支持动态加
29、载模块选项,必须要在配置菜单中选择 Enable loadable module support 选项;在支持动态卸载模块选项,必须要在 Enable loadable module support 菜单项中,选择 Module unloading 选项。九. 编译::/Android/kernel/common$ make编译成功后,就可以在 hello 目录下看到 hello.o 文件了,这时候编译出来的 zImage已经包含了 hello 驱动。十. 验证 hello 驱动参照在 Ubuntu 上下载、编译和安装 Android 最新内核源代码( Linux Kernel)一文所示,运行
30、新编译的内核文件,验证 hello 驱动程序是否已经正常安装::/Android$ emulator -kernel ./kernel/common/arch/arm/boot/zImage &:/Android$ adb shell进入到 dev 目录,可以看到 hello 设备文件:rootandroid:/ # cd devrootandroid:/dev # ls进入到 proc 目录,可以看到 hello 文件:rootandroid:/ # cd procrootandroid:/proc # ls访问 hello 文件的值:rootandroid:/proc # cat hell
31、o0rootandroid:/proc # echo 5 hellorootandroid:/proc # cat hello5进入到 sys/class 目录,可以看到 hello 目录:rootandroid:/ # cd sys/classrootandroid:/sys/class # ls进入到 hello 目录,可以看到 hello 目录:rootandroid:/sys/class # cd hellorootandroid:/sys/class/hello # ls进入到下一层 hello 目录,可以看到 val 文件:rootandroid:/sys/class/hello
32、# cd hellorootandroid:/sys/class/hello/hello # ls访问属性文件 val 的值:rootandroid:/sys/class/hello/hello # cat val5rootandroid:/sys/class/hello/hello # echo 0 valrootandroid:/sys/class/hello/hello # cat val0至此,我们的 hello 内核驱动程序就完成了,并且验证一切正常。这里我们采用的是系统提供的方法和驱动程序进行交互,也就是通过 proc 文件系统和 devfs 文件系统的方法,下一篇文章中,我们将通过自己编译的 C 语言程序来访问/dev/hello 文件来和 hello 驱动程序交互,敬请期待。