【Android底层学习总结】1. 驱动开发基础

0 目录

    • 1 前言
    • 2 驱动开发认识
      • 2.1 驱动
        • 2.1.1 设备驱动程序的主要功能
        • 2.1.2 驱动程序的主要类型
        • 2.1.3 设备文件
        • 2.1.4 sys文件系统:
    • 3 基础编程
      • 3.1 内核模块
        • 3.1.1 设备驱动的编译和加载方式
        • 3.1.2 一个模块被插入时的主要工作
      • 3.2 内核编程
        • 3.2.1 内核模块编程模板
        • 3.3 字符驱动程序模板
    • 4 总结

1 前言

已经有段时间没好好地写博客了,最近在研究安卓底层,所以想写写我对安卓底层的认识和总结。本篇是安卓底层学习总结系列的第一篇,驱动开发基础。

2 驱动开发认识

安卓系统,想必我也不用作太多介绍,这里我要提及的是安卓系统和嵌入式系统十分接近,所编写的驱动程序实际上大多也可以认为是嵌入式驱动程序。并且安卓的内核是Linux,所以写安卓驱动程序实际上和写Linux内核模块差不多,我门这篇主要认识PC中的Linux驱动。

2.1 驱动

所谓驱动,就是内核与外部设备的媒介,下面介绍有关驱动需要知道的知识。

2.1.1 设备驱动程序的主要功能

  • 对设备初始化和释放
  • 内核与硬件的数据交互
  • 应用程序和硬件的数据交互
  • 硬件的错误检测

2.1.2 驱动程序的主要类型

  • 字符设备
    – 使用自己制定的数据大小,通常以字节为单位输入输出
  • 块设备
    – 以块为单位输入输出
    – 对块设备读写时,利用系统内存作缓冲区,当用户进程对设备请求能满足用户的要求就返回请求的数据
  • 网络设备

2.1.3 设备文件

在shell中查看这个目录

ls -l /dev

可以看到所有的设备文件节点,通常为以下格式

crw-r--r--  1 root    root       10,   235 3月  29 08:12 autofs
  • 文件类型
    – 上面格式的第一个字符c代表了这个设备文件的文件类型为字符设备,b就是块设备,网络设备没有设备文件

  • 主设备号
    – 设备类型和主设备号唯一确定设备文件的驱动程序和界面。在上述格式中10, 235的10就是代表了主设备号。

  • 次设备号
    – 说明目标设备是同类设备的第几个,在上述格式中10, 235的235就是代表了次设备号。
    例:
    crw------- 1 root root 10, 59 3月 29 08:12 cpu_dma_latency
    crw------- 1 root root 10, 203 3月 29 08:12 cuse

    上面两个字符设备同属于一种设备,但不是一个设备。

2.1.4 sys文件系统:

统一管理查看内核功能参数和设备模型

/sys/block # 所有块设备
/sys/bus # 按总线类型分层放置的目录结构
/sys/class # 按设备功能放置
/sys/class/mem # mem目录包含各个设备的链接,指向devices各个具体设备
/sys/devices # 分层次放置
/sys/dev # 字符设备和块设备的主次号
/sys/fs # 描述所有文件系统
/sys/kernel # 内核所有可调整参数位置
/sys/module # 所有模块信息
/sys/power # 系统电源选项

3 基础编程

驱动程序通常是以内核模块的方式编写,并且插入到系统内核进行执行,所以我们得先了解什么是内核模块。

3.1 内核模块

Linux是一个单体内核系统,分成5个子系统,整个内核在一个地址空间。Linux提供了模块机制,来为其增加设备;只需编译模块,再插入内核就可以完成设备增加。而内核模块就是可以在系统运行期间动态安装和拆卸的内核功能单元。

3.1.1 设备驱动的编译和加载方式

  • 直接编译进内核,随同Linux启动时加载。
  • 编译成可加载删除模块,insmod加载,rmmod删除

3.1.2 一个模块被插入时的主要工作

  1. 打开要安装的模块(·ko文件),读进用户空间。
  2. 链接其他函数到内核。即把外部函数的地址填入访问指令和数据结构中
  3. 在内核创建module数据结构,申请系统空间
  4. 将完成链接的模块映像装入内核空间,并在内核登记模块相关的数据结构(里面有相关操作的函数指针)

3.2 内核编程

要编写一个内核模块就要先了解一下基本函数。
首先,内核与用户之间数据是不互通的,要互相使用数据得经过系统调用,系统调用中有着一些基本函数,用来完成基本任务。
比如:

- copy_to_user
	主要用于将内核段中的数据拷贝到用户段的内存中去
- copy_from_user
	主要用于将用户段内存中的数据拷贝到内核中

这些函数在用户态是无法使用的,也就是说,在外部写的.c程序库中是不包含这两个函数的。所以编写内核程序是与编写普通c程序是有所区别的。

3.2.1 内核模块编程模板

下面贴出一个简单的helloworld内核程序,我们在具体程序中进行解释。

#include  // 定义了module_init等函数
#include // 最基本的头文件,其中定义了MODULE_LICENSE等宏

// 当插入内核模块时,系统将调用下面的module_init宏,然后通过module_init调用此函数
static int hello_init(void){
	/**
	*printk在函数内部,有代码申请了一块静态缓冲区,当与控制台建立连接时,将缓冲区打印到终端
	*注意:它不支持浮点数,记得打印时+\n,不然的话不会立即打印,打印级别数字越小级别越高
	*KERN_CRIT表示 critical conditions级别的调试级别,级别数字为2
	**/
	printk(KERN_CRIT "HELLO WORLD!!!\n"); // \n用处很大,最好不要省
	return 0;
}
// 与hello_init对应,在移除该内核模块时调用module_exit宏,然后调用此函数
static void hello_exit(void){
	// KERN_WARNING级别数字为4
	printk(KERN_WARNING "bye bye!!\n");
	return;
}
// 下面都是宏,在加载卸载模块时调用
module_init(hello_init);
module_exit(hello_exit);

// 下面的内容是必须的,用于表明该模块的信息,用modinfo *.ko即可查看
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("一个简单的内核模块测试");

接下来编写Makefile文件,具体请自行查看资料

obj-m:=hello_module.o
PWD:=$(shell pwd)
default:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
     
clean:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

下面,我们对其进行测试,在shell中输入以下命令

	make
	sudo insmod hello_module.ko
	dmesg

即可查看信息,其中:
insmod用于插入内核模块。
dmesg用于打印内核日志信息。
sudo dmesg -C可以清空日志信息。
modinfo *.ko查看模块信息。
sudo rmmod hello_module卸载模块。

最后我的内核日志打印信息为:

[ 9806.210068] HELLO WORLD!!!
[10004.819841] perf: interrupt took too long (3137 > 3130), lowering kernel.perf_event_max_sample_rate to 63750
[10097.027480] bye bye!!

至此我们完成了一个简单的内核模块编程模板。

3.3 字符驱动程序模板

上面我们已经简单介绍了内核模块编写,下面我们来正式写一个有基本输入输出和基本测试程序的字符驱动程序模板。

#include // 定义了module_init
#include // 最基本的头文件,其中定义了MODULE_LICENSE等宏
#include  // file_operations结构体所在

static const char *dev_name = "first_cdev"; // 设置设备名,之后可以在/proc/devices中查看该设备
static unsigned int major = 55; // 设置主设备号

/* open函数,用于打开设备文件
* 注:在linux中,一切皆文件,驱动设备文件也不例外,只不过设备文件是一种
* 特殊的文件,而对驱动程序的操作其实也是基于文件操作的。
*/
static int first_cdev_open(struct inode *inode, struct file *file){
	printk("open\n");
	return 0;
}

// 必须关闭设备文件
static int first_cdev_close(struct inode *inode, struct file *file)
{
	printk(KERN_DEBUG "close\n");
	return 0;
}

// 读取设备文件
static ssize_t first_cdev_read(struct file *file, char *buf,
 size_t count, loff_t *offset)
{
	printk(KERN_DEBUG "read :%ld", count);
	if(count >= sizeof(unsigned int)){ // 如果读到了来自内核的数据
		// 复制数据到用户程序进行输出
		if(copy_to_user((void __user *)buf, 
			(void *)(&file->private_data), sizeof(unsigned int))) 
		return -EFAULT;
	}
	return count;
}

// ioctl操作,主要用于对驱动设备进行命令控制
// 被注释的这种方法已经被废弃static int first_cdev_ioctl(struct inode *inode, struct file *file,
/*
注意:在2.6.36以后ioctl函数已经不存在了,用unlocked_ioctl和compat_ioctl两个函数代替。参数去除了原来ioctl中的struct inode参数,返回值也发生了改变。
1、compat_ioctl:支持64bit的driver必须要实现的ioctl,当有32bit的用户程序调用64bit内核的ioctl的时候,这个callback会被调用到。如果没有实现compat_ioctl,那么32位的用户程序在64位的kernel上执行ioctl时会返回错误:Not a typewriter
2、如果是64位的用户程序运行在64位的kernel上,调用的是unlocked_ioctl,如果是32位的APP运行在32位的kernel上,调用的也是unlocked_ioctl
*/
static long first_cdev_ioctl(struct file *file,
 unsigned int cmd, unsigned long arg)
{
	char argk[4]; // 定义一个字符数组,存放一些字符
	argk[0] = 0;
	argk[1] = 1;
	argk[2] = 2;
	argk[3] = 3;
	printk(KERN_DEBUG "ioctl:%x\n", cmd);
	switch(cmd){ // 根据传来的命令指示进行操作
		case 0: // 指令 0
			printk(KERN_DEBUG "ctl NO.0\n");
			// 将用户态程序的数据覆盖本地定义的字符数组,并打印从用户态程序获取的数据
			if(copy_from_user(argk, (void __user *)arg, 4))
				return -EFAULT;
			printk("arg:%x,%x,%x,%x\n", argk[0], argk[1], argk[2], argk[3]);
		break;
		case 1: // 指令 1
			printk(KERN_DEBUG "ctl NO.1\n");
			// 将数据传入用户态应用程序
			if(copy_to_user((void __user *)arg, argk, 4))
				return -EFAULT;
		break;
		default:break; 
	}
	return 0;
}


// write函数,当向内核程序写数据时调用
static ssize_t first_cdev_write(struct file *file,
 const char __user *buf, size_t size, loff_t *ppos){
	printk("write\n");
	return 0;
}



// 在file_operations中注册open和write等函数
static struct file_operations first_cdev_fo = {
	.owner = THIS_MODULE,
	.open = first_cdev_open,
	.release = first_cdev_close,
	.read = first_cdev_read,
   //	.ioctl= first_cdev_ioctl,
	.unlocked_ioctl = first_cdev_ioctl,
	.write = first_cdev_write,
};

// 插入模块时调用
static int first_cdev_init(void){
	// 注册设备,将file_operations结构体放到内核的特定数组中
	// major作为主设备号
	int res;
	// 注册设备
	res = register_chrdev(major, dev_name, &first_cdev_fo);
	if(res < 0){
		printk(KERN_DEBUG "register fail\n");
		return res;
	}
	//if(dev_id < 0){
	//	printk("error\n");
	//}	

	printk(KERN_CRIT "hello character devices!!\n");
	return 0;
}

// 卸载模块时调用
static void first_cdev_exit(void){
	// 注销设备
	unregister_chrdev(major, dev_name);
	printk(KERN_INFO "bye,character devices\n");
	return;
}

module_init(first_cdev_init);
module_exit(first_cdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("第一个字符驱动模块编写");

下面是测试程序

#include 
#include 
#include 
#include 
#include 

// 要调用的设备文件名
#define DEF_FILE_NAME "/dev/xxx" 

int main(int argc, char **argv)
{
	int fd, size;
	char readbuf[8];
	char writebuf[8] = "writebuf";
	char ioarg[4];

	char *dev_file;
	if(1 == argc){ // 从命令行获取还是使用本地定义的设备文件名
		dev_file = DEF_FILE_NAME;
	} else {
		dev_file = argv[1];
	}
		
	printf("<<<<<>>>>\n", dev_file);
	printf("test write:\n");
	fd = open("/dev/xxx", O_RDWR); // 以读写方式打开设备文件
	if(fd < 0){
		printf("can't open device\n");
	}
	size = write(fd, writebuf, sizeof(writebuf)); // 向设备文件写入数据
	close(fd); // 关闭设备文件

	printf("test read:\n");
	fd = open(dev_file, O_RDONLY); // 以只读方式打开设备文件
	size = read(fd, readbuf, sizeof(readbuf)); // 从设备文件读取数据
	// close(fd)
	printf("read size:%d\n",size);
	for(int i=0; i<size; i++){
		printf("readbuf[%d]:%x\n", i, (unsigned char)readbuf[i]);
	}
	close(fd);
	
	printf("test ioctl:\n");
	fd = open(dev_file, O_RDWR); // 以读写方式打开设备文件
	// 设置初始数组数据
	ioarg[0] = 0xf0;
	ioarg[1] = 0xf1;
	ioarg[2] = 0xf2;
	ioarg[3] = 0xf3;
	
	printf("ioctl test 0\n");
	ioctl(fd, 0, ioarg); // 执行0号命令,将数组写入设备文件
	printf("ioctl test 1\n");
	ioctl(fd, 1, ioarg); // 执行1号命令,从设备文件读
	printf("arg:%x, %x, %x, %x\n", ioarg[0], ioarg[1], ioarg[2], ioarg[3]);
	close(fd);

	return 0;
}

Makefile文件

obj-m:=first_cdev.o
PWD:=$(shell pwd)
default:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 
clean:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 

下面进行测试

make
gcc test.c -o test
sudo mknod /dev/xxx c(设备类型) 55(主设备号) 0(次设备号)
sudo insmod *.ko
./test

注:mknod 用来创建设备文件,指定设备文件主设备号为55,在制定前最好用 ls -l /dev 查看是否主设备号重复.
执行完insmod命令就可以用cat /proc/devices查看设备是否加载成功了。
接下来我们运行测试程序,以下是输出内容

<<<<<>>>>
test write:
can't open devive
test read:
read size:8
readbuf[0]:0
readbuf[1]:0
readbuf[2]:0
readbuf[3]:0
readbuf[4]:9d
readbuf[5]:55
readbuf[6]:0
readbuf[7]:0
test ioctl:
ioctl test 0
ioctl test 1
arg:fffffff0, fffffff1, fffffff2, fffffff3

dmesg查看内核日志

[11955.133491] hello character devices!!
[11960.960142] open
[11960.960148] read :8
[11960.960205] close

最后,卸载驱动程序

sudo rmmod first_cdev
sudo rm /dev/xxx

至此,我们完成了一个有输入输出功能的字符设备驱动程序模板。

4 总结

总的来说,编写驱动程序并不难,但驱动程序主要与硬件相关,编写具体的驱动会需要特定硬件的芯片手册,所以以上只是Linux的驱动程序基础,编写驱动程序还需要进一步学习,比如学习系统的启动、设备树、硬件引脚等概念,学完后希望能在安卓开发板子上动手实践,下篇文章,我将对系统启动流程进行总结介绍。

本系列链接传送:
【Android底层学习总结】2. 安卓系统内核的启动
【Android底层学习总结】3. 内核中driver_init函数源码解析

你可能感兴趣的:(Android,#,Android底层学习,linux,Android驱动,内核)