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

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<linux/init.h>  // 定义了module_init等函数
#include<linux/module.h> // 最基本的头文件,其中定义了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<linux/init.h> // 定义了module_init
#include<linux/module.h> // 最基本的头文件,其中定义了MODULE_LICENSE等宏
#include<linux/fs.h>  // 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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

// 要调用的设备文件名
#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("<<<<<<test file name:%s>>>>>\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 file name:/dev/xxx>>>>>
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函数源码解析

  • 5
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Android是一种广泛使用的移动操作系统,其开发涉及到多个层次,其中底层驱动开发是其中重要的一环。底层驱动开发是指为Android设备上的硬件提供驱动程序的开发过程。驱动程序是操作系统与硬件之间的桥梁,它们负责管理和控制硬件设备的工作。 在Android底层驱动开发的过程中,首先需要了解设备的硬件结构和规格,以及相应的硬件接口和协议。根据硬件设备的不同,可能包括显示器、触摸屏、摄像头、声卡等各种设备。开发者需要研究硬件设备的工作原理和特性,以便编写相应的驱动程序。 其次,开发者需要熟悉Android底层的相关开发工具和API,如HAL(硬件抽象层)、Binder IPC(进程间通信)等,以便与操作系统和应用程序进行交互。底层驱动开发还需要理解Linux内核的相关知识,因为Android是基于Linux内核开发的。 在实际的驱动开发过程中,开发者需要根据硬件设备的特性和要求,编写对应的设备驱动程序。这些驱动程序通常使用C或C++语言编写,并需要遵循一定的编程规范和命名规则。开发者需要确保驱动程序的正确性和稳定性,以及与操作系统的兼容性。 总之,Android底层驱动开发是一项复杂且技术要求较高的工作。它需要开发者具备扎实的硬件和软件知识,以及良好的编程能力。只有通过深入研究和实践,才能开发出高质量的驱动程序,为Android设备的稳定运行做出贡献。 ### 回答2: Android 底层驱动开发是指在 Android 操作系统中编写和调试设备驱动程序的过程。设备驱动程序是连接硬件和操作系统之间的桥梁,能使操作系统识别和与硬件进行通信。对于 Android 应用程序开发人员来说,了解底层驱动开发至关重要,因为它可以帮助他们更好地理解系统的工作原理,并在需要时进行定制和优化。 编写 Android 底层驱动程序需要掌握 C/C++ 编程语言、Linux 内核知识和硬件架构相关概念。在开发过程中,需要了解设备的硬件接口和规范,包括寄存器配置、中断处理、数据传输等。通过编写驱动程序,开发人员可以为设备提供访问系统资源的接口,并处理各种硬件事件和数据传输。 开发 Android 底层驱动程序的主要步骤包括以下几个方面: 1. 硬件初始化:配置设备的寄存器、中断等硬件资源,并注册到操作系统中。这包括初始化 I/O 端口、设备中断控制器、时钟和 DMA 控制器等。 2. 中断处理:在中断发生时,驱动程序需要对中断进行处理。处理中断的程序需要根据中断的类型和来源来执行相应的操作。例如,处理数据接收中断、数据发送中断等。 3. 数据传输:驱动程序需要实现对设备的数据传输功能。这包括读取和写入设备的数据。数据传输可以使用 DMA、中断或轮询方式进行。 4. 设备控制:驱动程序需要实现对设备的控制功能。这包括设置设备的工作模式、参数调整和状态查询等。通过提供合适的接口,应用程序可以通过系统调用来与设备进行交互和控制。 5. 调试和优化:在驱动程序开发的过程中,需要使用调试工具进行问题排查和性能优化。这包括使用 printk() 输出调试信息、使用 gdb 进行调试和性能剖析等。 总之,Android 底层驱动开发涉及到多个技术领域和专业知识,需要开发人员具备扎实的编程基础和理解操作系统和硬件的能力。掌握底层驱动开发可以帮助开发人员更好地理解系统的工作原理,并在需求变化时进行相应的定制和优化。 ### 回答3: Android是目前最流行的移动操作系统之一,其特点是开放源代码、可定制性强以及适应性广泛。在Android系统中,底层驱动开发是非常重要的一部分,它涉及到硬件和操作系统之间的交互。 Android底层驱动开发PDF主要介绍了Android系统中的底层驱动开发相关知识和技术。这些知识和技术包括硬件抽象层(HAL)、内核驱动开发、设备树(Device Tree)、引导加载程序(Bootloader)等。 首先,硬件抽象层(HAL)是Android系统的一个重要组成部分,用于提供硬件与操作系统之间的接口。通过HAL,Android系统可以与各种硬件设备进行通信和交互。PDF中会介绍如何编写和调试HAL,以及如何实现硬件的适配和兼容性。 其次,内核驱动开发Android底层驱动开发的核心之一。PDF中将详细介绍如何编写设备驱动程序,如独立设备驱动和平台设备驱动。此外,还会涉及到设备树(Device Tree)的使用,设备树用于描述硬件设备的信息和属性。 另外,引导加载程序(Bootloader)也是Android底层驱动开发的重要环节之一。PDF中会介绍如何编写和调试引导加载程序,以及如何通过引导加载程序来引导Android系统。 总之,Android底层驱动开发PDF会全面介绍Android系统中的底层驱动开发相关知识和技术,包括HAL、内核驱动开发、设备树、引导加载程序等。对于想要深入了解Android系统底层开发开发者来说,这本PDF是一本非常有价值的参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值