A53字符设备驱动学习的第一天

第一天学习字符设备驱动.


 |         应用程序         |          **原理:应用程序中首先运行我们写的程序,然后调用相关的**
 ----------------------------         **系统API函数,进入LINUX内核中更新相应的设备驱动程序**
 |         Linux内核        |		    **(要注意的是:设备驱动程序本身在LINUX内核中,内核不变)**
 ----------------------------		    **而后根据设备驱动程序来更新嵌入式硬件的参数,从而软件控制**
 |       设备驱动程序       |		**硬件(分析的大概)**
 ----------------------------
 |       嵌入式硬件         |
 ----------------------------

*********************************采用分层处理的思想******************************************

2. 设备驱动开发课程重点学习内容
{
Linux字符设备驱动框架
并发与竞态,IO控制,轮询和异步操作
中断处理和延迟机制
内核地址空间和内存的使用
设备驱动模型和虚拟文件系统,平台设备驱动
常用硬件接口驱动和总线设备驱动,例如ADC驱动,I2C设备驱动,SPI设备驱动
}

参考书籍:
{
宋宝华 Linux驱动开发详解
Linux内核的设计与实现
}

一、嵌入式Linux设备驱动概念

{
1. 设备驱动的地位和作用
{
设备驱动是内核的一部分,用来控制硬件的程序。
在带有操作系统的嵌入式系统中,设备驱动程序在系统中的位置

----------------------------
|         应用程序         |
---------------------------- 
|         Linux内核        |
----------------------------
|       设备驱动程序       |
----------------------------
|       嵌入式硬件         |
----------------------------

驱动的作用

: 就是管理对应的硬件,为用户提供操作硬件的方法(接口)

}


## 二、 内核模块

{	  //大多数的设备驱动都是以内核模块的形式来实现的。

1. 内核模块的概念
{

> 	内核模块是具有一定功能的,可以被单独编译的一段内核代码, 它可以在需要的时候动态 	加载到内核,从而动态的增加内核的功能。
> 在不需要的时候,可以动态的从内核卸载,从 	而节约内核资源,不管是加载还是卸载,都不需要重新启动整个系统。

}

    2. 内核模块框架
    {     //最简单的内核模块
#include   //包含内核模块使用的头文件 在 include 目录下
	#include 

	int  hello_init(void)  //编写模块初始化函数
	{
	printk ("Hello world!\n");
	return 0;
	}

	void hello_exit(void)  //编写模块退出函数
	{
	printk ("Goodbye world\n"); 
	}

	module_init(hello_init);  //指定模块初始化函数
	module_exit(hello_exit);  //指定模块退出函数

	MODULE_LICENSE ("GPL");  //license 声明
}
//测试文件:hellotest.c 即应用程序
	{	
#include 
#include 
#include 
#include 
#include 
#include 
unsigned char buf[100]={0};
/*

./hellotest w  "写的内容"
./hellotest r
./hellotest ledon
./hellotest ledoff

 
*/
#define  HELLO_DEVICE  		 'A'
#define  CMD_LED_ON  		_IO(HELLO_DEVICE,0)  //打开LED灯命令
#define  CMD_LED_OFF 		_IO(HELLO_DEVICE,1)  //关闭LED灯命令


int main(int argc, char *argv[])
{
	int rw_len = 0;
	int fd_hello = open("/dev/hello",O_RDWR);


	if(fd_hello < 0)
	{
		perror("open file error\n");
	}

	if(strcmp(argv[1],"r") == 0)
	{
		rw_len = read(fd_hello,buf,50);
		printf("read len:%d buf:%s\n",rw_len, buf);

	}
	else if(strcmp(argv[1],"w") == 0)
	{
		memset(buf,0,100);
		strcpy(buf,argv[2]);
		rw_len = write(fd_hello,buf,strlen(buf));
		
	}
	else if(strcmp(argv[1],"ledon") == 0)
	{
		ioctl(fd_hello, CMD_LED_ON,0);
	}
	else if(strcmp(argv[1],"ledoff") == 0)
	{
		ioctl(fd_hello, CMD_LED_OFF,0);
	}


	close(fd_hello);

	return 0;
}



		
}
	

3. 内核模块的编译
{ //通过调用linux总目录下的Makefile来实现
	模块的Makefile 内容如下:
	KERNELDIR := /home/wins/kernel-3.4.39  //指定内核路径在开发板
	//KERNELDIR := /usr/src/linux-headers-4.4.0-141-generic  //指定内核路径在内核
	obj-m:=hello.o   //指定要编译的模块
	//PWD := $(shell pwd)  //将当前路径赋值给 PWD
	MDIR:= $(shell pwd)        

	all:
		$(MAKE) -C $(KDIR) M=$(MDIR) modules  //内核编译
		cp -f ./hello.ko /nfsroot/rootfs/home/  
		//gcc hellotest.c -o test
		arm-none-linux-gnueabi-gcc hellotest.c -o test
		cp -f ./test /nfsroot/rootfs/home/

	clean:
		rm -rf  *.o  .*  *.ko *.ko.cmd  test *.mod.o.cmd *.o.cmd *.order *.symvers *.mod.c  *.mod.o Module*  modules* hellotest\
		/nfsroot/rootfs/home/* ------------*/


}

4. 内核模块的加载和卸载
{
	在开发板命令行执行如下命令
	// 加载模块
	insmod  xxx.ko  
	--->触发执行模块加载函数(xxx_init)
	//卸载模块
	rmmod xxx.ko
	--->触发执行卸载函数(xxx_exit)

	//查看当前系统已经加载的模块
	lsmod 

	注意:  
	在开发板上调用rmmod出错,出现找不到 /lib/modules 目录的错误,执行如下命令创建目录即可。
	#mkdir /lib/modules/3.4.39-farsight

}

5. 将多个源文件编译成一个内核模块
{ //假设 mul.ko 模块由 hello.c 和 bar.c文件组成
	hello.c 文件内容如下:
	{
	#include 
	#include 

	extern void bar(void); //外部函数声明
	int  hello_init(void)
	{
	printk ("Hello world!\n");
	bar(); 
	return 0;
}

	void hello_exit(void)
	{
	printk ("Goodbye world\n");

	}

	module_init(hello_init);
	module_exit(hello_exit);

	MODULE_LICENSE ("GPL");
	}

bar.c 文件内容如下:
{
	#include 

	void  bar(void)
	{
	printk("bar is ok\n");
	} 
}

多源文件模块Makefile 如下:
{
	KERNELDIR := /home/wins/kernel-3.4.39
	obj-m:=mul.o             #指定模块的名称
	mul-objs = hello.o bar.o #指定模块mul 依赖 bar.o hello.o 目标文件

	PWD := $(shell pwd)

	all:
		$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
		cp -f ./mul.ko /nfsroot/rootfs/home/

	clean:
		rm -rf  *.o  *.ko  *.mod.c   Module*  modules*
}


}
  1. 内核模块参数
    {

// 内核模块参数允许用户在加载模块时通过命令行给模块传递参数 内核模块参数的定义
module_param(参数名,参数类型,参数权限);
module_param_array(数组参数名,数组元素类型,NULL,参数权限)

参数类型可以为 int short long charp(字符串指针) 等。

参数权限 { 权限在include/linux/stat.h中有定义 权限位一般定义为 S_IRUGO ,表示所有用户可读 }

加载模块后,如果使用了模块参数,会存在在/sys/module/模块名/paramters/模块参数名文件
这些文件的权限和定义模块参数时给的权限一致,这些文件中保存了模块参数的值 如果我们修改这些文件值,对应模块参数的值就会发生变化。

加载模块时,通过命令行给模块传递参数 #insmod mul.ko band=115200

如果是传递给数组,则这么写 #insmod mul.ko port=1,2,3,4 //port 为整型数组参数

}

  1. 内核模块依赖关系
    {

//如果A模需要调用B模块的全局变量或函数时,我们就说A模块依赖B模块 内核符号表,就是在内核中可供外部引用的函数和变量的符号表。
内核的符号表文件System.map

导出模块符号 EXPORT_SUMBOL //导出的内容能够被所有的模块使用 EXPORT_SYMBOL_GPL
//导出的内容只能被遵循GPL协议的模块使用 例如 { #include #include

//包含导出内核符号表宏定义 #include

void show_exportmod(void) { printk(“I am export module info\n”); }

EXPORT_SYMBOL(show_exportmod); //导出show_exportmod函数

MODULE_LICENSE (“GPL”); }

另外一个模块要使用其他模块导出的函数或变量时, 通过extern 关键字声明即可

}

  • 内核模块编程注意事项:
    • {
    • a.不能使用C库和C标准头文件
    • b.必须使用GNU C规范
    • c.不能处理浮点运算
    • d.注意同步和并发的问题,可移植性的问题
    • f.打印时使用printk, printk 无法输出浮点数
    • }

}


## 三、系统调用

{
1. 系统调用原理
{
	a.	应用程序调用open
	b.	进程会调用C库中open实现
	c.	open实现会将open对应的系统调用号保存在寄存器中
	d.	open实现调用swi(软中断),进入软中断异常
	e.	系统跳转到异常向量表中软中断处理的代码(vector_swi)
	f.	该函数根据系统调用号,在内核预先定义好的系统调用表中找到open对应内核实现(sys_open)
	系统调用表:arch/arm/kernel/calls.S
	g.	执行该函数,执行完毕后,原路返回用户空间
}

2. 手动为内核加上一个系统调用
{
a.	在内核代码arch/arm/kernel/sys_arm.c添加一个系统调用的内核实现sys_lzembed
{
	//data1 data2 为用户空间传递给内核空间的参数
	asmlinkage int sys_lzembed(int data1, int data2, struct pt_regs *regs)
	{
	printk("Guohui Cao test System Call\n");
	printk("para0:%d  para1:%d\n",data1, data2);
	return 0;
}
}

b.	在内核代码arch/arm/include/asm/unistd.h文件中添加一个新的系统调用号
	#define __NR_lzembed		(__NR_SYSCALL_BASE+378) //对应的系统调用号为 378

c.	在内核代码arch/arm/kernel/calls.S中的系统调用表中添加一项
	CALL(sys_lzembed)

d.	重新编译内核,在用户空间调用syscall函数调用新添加的系统调用
{ 	// 用户空间通过syscall 调用内核空间的系统调用实现函数
	int main(int argc, char *argv[])
{
	//378 为系统调用号, 66, 88 为用户空间传递给内核空间的参数 
	int res = syscall(378,66,88);
	printf("res = %d\n",res);
	return 0;
}
}

}


}

## 四、字符设备驱动

{

**1. 设备驱动开发基础**

{
**Linux设备分类:**
	**字符设备** 
	以字节为单位访问设备,按字节流访问,只有顺序访问能力,
	不能随机读取字符设备存储器中的数据, 常见字符设备有:按键 串口 LCD屏 触摸屏...

	**块设备**
	按数据块访问,具有随机访问块设备存储器中数据的能力
	常见块设备有: SD卡,emmc卡,U盘,Flash,硬盘等。

	**网络设备**
	网卡,网络接口。
	内核中通常是通过TCP/IP协议栈来访问。

	设备驱动在内核中的位置

	----------------------------------
	|				应用程序			|
	----------------------------------
	|			系统调用接口		|
	----------------------------------
	| 			虚拟文件系统 VFS|
	----------------------------------
|	设备驱动|文件系统|网络子系统|
	----------------------------------
	| 				嵌入式硬件		|
	----------------------------------
}

**# 2. 字符设备驱动基本概念**

{
**设备文件:**
{	 	   	
	在Linux系统中,一个设备文件就代表一个具体的硬件设备,Linux向管理文件一样来管理硬件设备,
	Linux通过访问设备文件来访问硬件设备。

	设备文件都在/dev目录下

	Linux如何访问设备文件?

	和访问普通文件完全一样,通过调用普通文件操作函数来访问设备文件。
	open read write ioctl .....close...


	

> 如何通过设备文件来找到设备对应的设备驱动呢? 	<设备号>

}


**设备号:**
{
	

设备号分为主设备号和次设备号,一个占32位,其中主设备号占高12位,次设备号占低20位

	设备号格式: 
	-------------------------------------
	|   主设备号   |       次设备号     |
	-------------------------------------
	12bit             20bit

	主设备号: 用来找到对应驱动
	次设备号: 用来区分使用同一个驱动的不同硬件

	设备号的操作:

	在Linux内核中,设备号的数据类型是dev_t

内核提供了一些操作宏来操作设备号

	MAJOR  ------------ 通过设备号获取主设备号
	MINOR  ------------ 通过设备号获取次设备号
	MKDEV  ------------ 通过主设备号和次设备号生成设备号

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)

	#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
	#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
	#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

  

}

如何获取设备号?
{

设备号属于资源,在内核中如果想要使用设备号必须向内核申请

(1)静态申请
{

a.查看 /proc/devices 文件,找到一个未被使用的主设备号
500

b.根据设备个数分配次设备号,如果只有一个设备就用一个次设备号,次设备号一般从0开始
	dev_t dev = MKDEV(major,minor);

c.调用register_chrdev_region向内核申请

}

(2)动态申请
{

动态申请就是由内核自动分配一个设备号
	alloc_chrdev_region

}
}
}
}

五、字符设备驱动关键数据结构

{


1. struct cdev  //字符设备驱动的结构,内核中用来表示一个字符设备驱动
{ //struct cdev 结构体定义
	struct cdev
	{
	struct kobject kobj;  //内嵌内核对象
	struct module *owner; //字符设备所在内核模块对象的指针
	const struct file_operations *ops; //设备支持的操作函数集合
	struct list_head list;  //内核链表,用来将所有注册到内核的cdev结构体变量连接成链表
	dev_t dev;               //设备号
	unsigned int count;  //属于同一主设备号的次设备号的个数
};


如何往内核中添加一个cdev
{


 //往内核添加一个cdev结构体

	构造一个cdev

	初始化cdev ----- cdev_init
	void cdev_init(struct cdev *cdev, const struct file_operations *fops)

	添加cdev到内核 ----- cdev_add
	int cdev_add(struct cdev *p, dev_t dev, unsigned count)

	删除内核中的cdev  ------ cdev_del
	void cdev_del(struct cdev *p)


}		
}

**2. struct file_operations //文件操作函数结构体**
{	
	

//struct file_operations文件操作函数结构体定义
	struct file_operations
	{
	struct module *owner;
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	int (*open) (struct inode *, struct file *);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	..... //其他文件操作函数


}

在字符设备驱动中有这样的一个操作函数集合,用户应用使用系统调用访问设备文件时
最终调用的就是cdev中对应的操作函数集合

实际编写字符设备驱动程序,就是编写这些操作函数,并用这些操作函数初始化struct file_operations
结构体。
}

**3. struct inode //描述一个文件的物理结构**
{
	文件(普通文件或设备文件)一旦创建,内核中就会创建一个对应的inode结构,
	文件销毁,对应inode就删除.
	主要数据成员:

	dev_t   i_rdev;		//设备号
	struct cdev  *i_cdev;		//指向一个cdev
}

**4. struct file //描述文件的打开属性**
{
	open打开文件成功后创建
	close关闭文件后销毁

	主要数据成员:
	//描述文件的打开属性,包括只读,只写,阻塞或非阻塞等。
	unsigned int 	f_flags;      

}

}

六、应用程序访问字符设备驱动流程

{

	1. 应用程序调用 open
	--->内核 sys_open
	--->驱动的 xxx_open 函数

	2. 应用程序调用 read
	--->内核 sys_read
	--->驱动的 xxx_read 函数

	3. 应用程序调用 write
	--->内核 sys_write
	--->驱动的 xxx_write 函数

	4. 应用程序调用 ioctl
	--->内核 sys_ioctl
	--->驱动的 xxx_ioctl 函数

	5. 应用程序调用 close
	--->内核 sys_close
	--->驱动的 xxx_close 函数
	......
	其他文件操作函数类似	      

}

七、内核空间和用户空间的数据传输

{

	Linux中内核空间和用户空间相互独立,不能直接互相访问
	如果要进行数据传输,需要借助以下函数
   copy_to_user  // 内核--->用户
	copy_from_user  // 用户--->内核
  这两个函数有可能导致睡眠,不能在中断服务处理程序中使用

}

八、ioctl函数的使用

{

	//ioctl函数是设备驱动开发中一个常用的函数
	//主要用来给设备发送控制命令

	ioctl函数原型:
	int ioctl(int fd, ind cmd, unsigned long arg)

	fd: 要操作的设备的设备文件描述符

	cmd: 发送给设备的控制命令
	{ //如何形成 cmd

	ioctl函数中的第二个cmd参数用来区分各个不同的控制操作

	cmd分为4个区域
	-----------------------------------------
	| 方向  | 数据尺寸 | 设备类型 | 序列号 |
	----------------------------------------
	| 2bit  |  14bit   |  8bit    |  8bit  | 
	-----------------------------------------


	可以使用_IO类型的宏来生成cmd

	#define _IO(type,nr)  _IOC(_IOC_NONE,(type),(nr),0)
	#define _IOC_NONE 0U

	#define _IOC(dir,type,nr,size) \  
	(((dir)  << _IOC_DIRSHIFT) | \      //30
	((type) << _IOC_TYPESHIFT) | \    //8
	((nr)   << _IOC_NRSHIFT) | \      //0
	((size) << _IOC_SIZESHIFT))      //16
	}

	arg: 传送给命令的参数

}

九、字符设备驱动开发步骤

{

	//编写设备驱动程序
	1. 编写设备文件操作函数集合 	 
	xxx_open, 		xxx_close,		xxx_read,		xxx_write, 	xxx_ioctl

	2. 定义并初始化内核表示设备驱动的结构体 cdev 结构体,cdev_init

	3. 添加 cdev结构体到内核, cdev_add

	//测试设备驱动

	1. 编写设备驱动测试应用程序

	2. 创建设备文件节点 

	#mknod   /dev/led0  		 c 		    108		     0
	----------------------------------------------------
	设备文件名  字符设备  主设备号   次设备号

}

十、Linux设备驱动中的并发控制

{

**1. 并发与竟态**
{
	并发(concurrency)指的是多个执行单元同时、并行被执行。
	因并发的执行单元对共享资源(硬件资源和软件上的全局、静态变量)的访问
	而导致的竞争状态,称为竟态(race conditions)。

内核中产生竟态的场景
{
	SMP(对称多处理器)之间
	单个CPU中进程和进程之间
	中断和进程之间
	中断与中断之间
}

}

**2.解决竞态的方法**
{
	当有执行单元在访问共享资源,应该禁止其他的执行单元来访问共享资源
	对于访问共享资源的代码区称之为临界区,临界区应该使用互斥机制进行保护.

	内核中互斥机制:
	中断屏蔽
	原子操作
	自旋锁
	信号量   
	互斥量   
}

**3.中断屏蔽**

{  	
	中断屏蔽可以解决中断和进程之间的竟态
	同时因为内核进程的调度也 这样内核抢占进程之间的竟态也避免了。

	local_irq_disable()  //屏蔽中断

	......临界区代码

	local_irq_enable()  //恢复中断


	中断屏蔽可以解决除SMP以外所有竞态,但是由于CPU很多操作依赖于中断,
	如果屏蔽的时间过长或者过于频繁,将导致系统响应变慢,慎用
	屏蔽时间要尽量短。
}

**4.原子操作**

{  //原子操作指的是在执行过程中不会被打断的操作,原子操作分为位原子操作和整型原子操作
	//原子变量可以解决所有情况下的竞态
	位原子操作函数
	{
	void set_bit(int nr, volatile unsigned long *addr); //将地址addr的第nr位设置1
	void clear_bit(int nr, volatile unsigned long *addr);//将地址addr的第nr位清0
	void change_bit(int nr, volatile unsigned long *addr);//将地址addr的第nr位翻转
	int test_bit(int nr, volatile unsigned long *addr);//返回地址addr的第nr位的值

}

整数原子操作
{
a. 原子变量的定义和初始化
{
	数据类型:atomic_t //定义在 arch/arm/include/asm/atomic.h
	atomic_t v = ATOMIC_INIT(1);//初始化为1

	或者:
	atomic_t v;
	atomic_set(&v,1);
}

b.内核中提供的操作整型原子变量的函数或宏定义
{
	atomic_set(v,data)   //设置原子变量 v 的初值为 data
	atomic_read(v)  //读取原子变量 v的值

	void atomic_add(int i, atomic_t *v) //原子变量 v加 i
	void atomic_sub(int i, atomic_t *v) //原子变量 v减 i
	atomic_inc(v)  //原子变量v加1 
	atomic_dec(v)  //原子变量v减1 

	//原子变量自增,自减操作后,测试原子变量是否为 0,为0返回 true,否则返回 false
	atomic_inc_and_test(v)
	atomic_dec_and_test(v)
}

c. 原子变量应用举例
{
	//使用原子变量实现设备只能被一个进程打开
	static atomic_t atomic = ATOMIC_INIT(1); //定义原子变量并初始化为 1

	static int open(struct inode *inode, struct file *filp)
	{
	...
	if(!atomic_dec_and_test(&atomic )) //原子变量减1后并判断是否为0,为0返回真
	{  //原子变量减1后,不为0,表示该设备已经被其他进程打开
	atomic_inc(&atomic);
	return -EBUSY;
}

	//该设备没有被其他进程打开,打开设备成功
	...
	return 0;
}

static int release(struct inode *inode, struct file *filp)
{
	atomic_inc(&atomic); //进程关闭该设备后,原子变量加 1
	return 0;
}
}
}

}

**5.自旋锁**

{
	自旋锁是内核中一种典型的对临界资源进行互斥访问的手段.
	如果一个内核任务尝试获取一个已经被占用的自旋锁,那么该任务进入忙等待,原地自旋,
	重复去尝试获取,直到锁重新可用,所以自旋锁可以实现对临界区代码的互斥访问。
	可以解决除中断以外的所有竞态情况。

	自旋锁的使用:

	包含头文件 linux/spinlock.h

	1)定义初始化自旋锁
	spinlock_t lock;
	spin_lock_init(&lock);

	2)获取自旋锁
	spin_lock(&lock);//获取不到,原地自旋
	spin_trylock(&lock);//不管是否获取,立即返回,获取到返回真,获取不到返回假

	3)执行临界区代码
	临界区速度快,不能调用引起睡眠的函数

	4)使用完释放自旋锁
	spin_unlock(&lock)

	//使用自旋锁保护字符设备驱动中的临界代码

}

**6. 信号量**

{
	信号量也是一种保护临界资源的互斥机制,本质上是一种睡眠锁。当任务获取不到信号量时,
	将导致该任务睡眠。这个时候处理器可以去完成其他工作。
	直到这个信号量可用,任务会被唤醒,唤醒的任务就获取到了信号量

如何使用信号量
{
(1)分配初始化

	添加头文件linux/semaphore.h
	struct semaphore sema;
	sema_init(&sema,val);

(2)获取信号量

	down(&sema);//如果进程无法获取信号量,进入不可中断的睡眠状态

	//如果进程无法获取信号量,进入可中断的睡眠状态,在睡眠时可以接收外来的信号
	down_interruptible(&sema);

	//该函数需要判断返回值,如果返回0表示获取到了信号量,返回非0表示收到了信号
	down_trylock(&sema);//不进入睡眠,直接返回,0表示获取到了信号量,非0表示没有

	//如果进程无法获取信号量,进入不可中断的睡眠状态,但是睡眠有时间限制
	down_timeount(&sema,long jiffies);


(3)访问共享资源(执行临界区代码)

(4)释放信号量
up(&sema);

}
}

**7. 互斥锁**
{
	使用方法及场合和信号量一致,只是在任意时刻只允许一个进程进入临界区

	struct mutex my_mutex;  //定义互斥量
	mutex_init(&my_mutex);  //初始化互斥量		
	void mutex_lock(struct mutex *lock); //上锁		
	void mutex_unlock(struct mutex *lock); //解锁

}


// 三种互斥机制的比较和总结
	原子变量: 极少使用   
	自旋锁: 开销低,临界代码短,不会引起系统睡眠,中断程序可使用,适用于短期锁定。
	信号量: 开销比较大,等待信号量可能导致进程睡眠,适用于长期加锁。
	互斥锁: 和信号量类似

}


//阻塞,非阻塞,轮询,异步通知

十一、设备的阻塞与非阻塞访问

{

	应用程序访问设备的两种方式: 阻塞和非阻塞
	阻塞访问
	系统调用read/write,如果程序所需的条件不满足,则应用程序阻塞(进程进入挂起状态),
	进程在阻塞这段时间应用程序不消耗CPU时间。
    非阻塞访问
	系统调用read/write,如果程序所需的条件不满足,应用程序不阻塞,立即返回错误码

}

十二、 等待队列,设备阻塞访问的实现方式

{

	1. 等待队列的概念:
	等待队列是内核的基本功能单位,以队列作为基础数据结构,以任务调度相结合,
	能够实现设备驱动的阻塞访问,异步通知等操作。

	重要数据结构: 
	等待队列头 数据类型 ------ wait_queue_head_t
	等待队列节点 数据类型 ---- wait_queue_t


	2. 等待队列的基本操作
	{
	(1)定义并初始化等待队列头
	wait_queue_head_t my_queue;     //定义等待队列头
	init_waitqueue_head(&my_queue); //初始化等待队列头

	或者使用宏定义 DECLARE_WAIT_QUEUE_HEAD:
	DECLARE_WAIT_QUEUE_HEAD(my_queue);//定义并初始化等待队列头

	(2)等待事件
	//在条件不成立的情况下,将当前进程阻塞,等待条件为真,睡眠的进程不可被信号打断
	wait_event(queue,condition);

	//跟wait_event一样,增加了超时功能
	wait_event_timeout(queue,condition,timeount);

	//跟wait_event一样,和wait_event不一样的地方在,通过
	// wait_event_interruptible阻塞的进程能被信号打断
	wait_event_interruptible(queue,condition);

	//跟wait_event_interruptible 相比,增加了超时机制
	wait_event_interruptible_timeout(queue,condition,timeount);                

	(3)唤醒等待队列
	//唤醒等待队列上的所有进程
	wake_up(&queue);  //与wait_event配对使用
	wake_up_interruptible(&queue) //与wait_event_interruptible配对使用配对使用配对使用配对使用
	}

	3. 使用等待队列实现设备的阻塞访问
	{   //使用等待队列实现读阻塞访问

	a. 定义并初始化等待队列头
	DECLARE_WAIT_QUEUE_HEAD(my_queue);

	b. 在xxx_read函数中判断是否阻塞访问,是否有数据可读,没有就阻塞
	{ //阻塞进程

	int data_ready_flag = 0; //1表示有数据可读,0 表示没有数据可读

	int xxx_read(struct file *file,.....)
	{
	...
	if(file->f_flags & O_NONBLOCK)
	{
	//非阻塞访问
	printk("no data to read ,noblock!\n");
	return -1;
	}
	else
	{
	//阻塞访问,让进程等待 条件 为真
	wait_event(my_queue, data_ready_flag);
	}
}
}

c. 在 xxx_write函数中唤醒等待队列上的进程
{ //唤醒进程

int xxx_write(struct file *file,.....)
{
...
data_ready_flag = 1;
wake_up(&my_queue);  //唤醒进程
}
}

}

}

十三、I/O多路复用(轮询操作)

{

1. I/O多路复用概念: 应用程序在访问设备之前,先调用poll或select函数询问一下该设备是否有数据
可读或可写,然后根据询问的结果来决定是否访问该设备。

应用程序通过IO多路复用技术,可以同时轮询多个IO设备。



2. I/O多路复用应用程序的实现
{   //应用程序调用poll函数轮询

poll函数用法
{
#include 
struct pollfd 结构的指针,用于描述需要对哪些文件描述符的哪种类型的操作进行监控。

struct pollfd  
{  
int fd; 	/* 需要监听的文件描述符 */ 
short events; /* 需要监听的事件 */  
short revents; /* 返回的已发生的事件 */  
}  

常用的events有:
POLLIN :表示有数据可读
POLLRDNORM:普通数据可读

POLLOUT: 表示数据可写
POLLWRNORM:普通数据可写

POLLERR: 发送错误


int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能:等待一组文件描述符中准备好文件描述符执行输入输出
参数:fds:  struct pollfd 结构体指针,在fds参数中指定要监视的文件描述符集
nfds:监控的文件描述符的个数
timeout:超时时间设置[0:立即返回不阻塞  >0:等待指定毫秒数 其他:永远等待
返回值:成功:>0       超时:=0       出错:-errno
}

应用层IO多路复用调用举例
{
//保存着我们要监控的描述符的集合
//需要监听多个文件描述符时,定义相应的数组元素即可
struct pollfd fds[1]; 

fd = open(...);

fds[0].fd = fd; //要监控的描述符
fds[0].events = POLLIN;
fds[0].revents = 0; //输出参数

iret = poll(fds, 1, -1); //监控描述符的状态

//如果poll函数返回,表示有数据可读

if(fds[0].revents & POLLIN)
{
//有数据可读
rlen = read(fd,rbuff,1024);
printf("read len: %d  buf:%s\n", rlen,rbuff);

}

}



}


3. I/O多路复用驱动层实现
{
在应用空间调用select poll 系列函数是最终调用驱动中f_ops中的poll函数

在该函数中可以调用poll_wait向驱动poll_table中添加一个等待队列

poll函数应该返回设备当前状态(POLLIN...)

wait_queue_head_t   xxx_pool_que; //定义等待队列

unsigned int xxx_poll(struct file *file, struct poll_table_struct *p)
{

int mask = 0;

poll_wait(file,&xxx_pool_que ,p);

if(xxx_flag == BUF_EMPTY)
{
mask |= POLLOUT;  //可写
}

if(xxx_flag == BUF_FULL)
{
mask |= POLLIN; //可读
}

return mask;

}

//在条件满足的位置调用 wake_up函数唤醒 xxx_pool_que 队列中的进程。


}


}

}

十四、异步通知

{

1. 异步通知的概念: 一旦设备就绪,设备驱动则主动通知应用程序,这样设备驱动就不需要
查询设备的状态。
异步通知通过信号来实现,当设备就绪时,设备驱动程序给相应的应用程序发送信号。

2. 应用程序异步通知的实现
{
//struct sigaction 结构体,记录了信号的处理方式
struct sigaction {
__sighandler_t sa_sigaction ; //指定信号的处理函数

//一些重要的标志位,比较重要的是 SA_SIGINFO,当设定了该标志位时,
//表示信号附带的参数可以传递到信号处理函数中
unsigned long sa_flags; 
void (*sa_restorer)(void); //不再使用
sigset_t sa_mask;   /*信号集,指定在信号处理过程中,哪些信号被屏蔽 */
  		};


//编写信号处理函数
void xxx_sig_handler(int signum, siginfo_t *siginfo, void *act)
{		
printf("enter led_sig_handle\n");
if(signum == SIGIO)
{

if(siginfo->si_band & POLLIN) //在ubuntu16.04版本上无法识别该标志
{
//表示有数据可读,读这个设备


}
if(siginfo->si_band & POLLOUT)
{
//表示该设备可写

}

}
}

//安装信号处理函数
{
//初始化SIGIO信号的处理结构体,并设置SIGIO信号的处理
struct sigaction act;
struct sigaction oldact;

memset(&act,0, sizeof(struct sigaction));
memset(&oldact,0, sizeof(struct sigaction));

act.sa_flags = SA_SIGINFO; //设置信号可以传递参数到信号处理函数中
act.sa_sigaction = xxx_sig_handler;

sigaction(SIGIO, &act,&oldact); // 安装信号处理函数

}

//设置文件的拥有者为当前进程,目的是使驱动程序根据打开的文件file结构,
//能找到对应的进程,从而向该进程发送信号
fcntl(fd, F_SETOWN, getpid())
fcntl(fd, F_SETSIG,SIGIO); //设置标志输入输出的信号

int flag = 0; //设置文件的FASYNC标志,启动异步通知机制
flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|FASYNC)

-D_GNU_SOURCE

}

3. 设备驱动程序中异步通知的实现
{
(1) 定义异步通知链表头
struct fasync_struct  *fasync_xxx;

(2) 实现异步通知fasync接口函数,在该函数中调用fasync_helper构造struct fasync_struct
并加入到链表中
int xxx_fasync(int fd, struct file *file , int on)
{

return fasync_helper(fd, file,on, &fasync_xxx);

}

(3) 在资源可用时,调用kill_fasync 发送信号
kill_fasync(&fasync_xxx,SIGIO,POLL_IN);
}

}

//中断和时间管理

十五、中断的基本概念

{

 1 什么是中断?
	  所谓中断,是指CPU在执行程序过程中,出现了某些突发事件急待处理,CPU必须
	  暂停执行当前程序,转去处理突发事件,处理完毕后,CPU又返回原程序被中断的
	  位置并继续执行。
	  
	 2 中断的作用
	  {
	  	  a. 提高CPU和外设之间通信的效率,当外设准备好时,通过中断机制主动通知CPU,
	  	     CPU不再需要不停的去查询设备的状态;
	  	  
	  	  b. 提高外部事件处理的实时性
	  }
	  
	  
	 3 中断在硬件上的连接处理方式

			计算机外设 ----> 中断控制器 -----> CPU

			外设产生电信号,发送给中断控制器,中断控制器能够检测和处理电信号,
			决定是否发送给CPU,如果发送给了CPU,CPU就可以响应这个电信号,进行后续的中断处理。

	 4 CPU中断处理流程
		
		    ---》中断产生,中断有优先级,高优先级可以打断低优先级的中断
		    ---》中断异常向量表
		    ---》保护现场
		    ---》处理中断(执行中断处理程序)
		    ---》恢复现场
		    ---》跳转返回程序被打断位置,继续执行
		   
	  嵌入式Linux系统中的中断处理,在设备驱动程序中实现。

}

十六、设备驱动程序中使用中断

{

	中断在内核中是一种资源,使用前需要申请,关于向量表和中断控制器的初始化和配置工作内核已经完成
		内核中使用struct irq_desc表示描述一个中断。
			
		1 设备驱动程序使用中断的步骤:
		{
				(1)  编写中断服务处理程序
				(2)  向内核申请中断
				(3)  允许中断
				(4)  中断使用完成后,释放中断
		}
		
		2 内核中断相关操作函数
		{
					a. 内核申请中断函数 request_irq
					{
						 	 int request_irq(unsigned int irq, irq_handler_t handler, 
					 									 	unsigned long flags, const char *name, void *dev)   

								参数:
								        irq:要申请的中断号
								        handler:向系统注册的中断处理函数,中断发生后调用该函数,同时将中断号和dev传递给他
								        flags:中断标志位,一般设置为IRQF_DISABLED,表示在中断处理时屏蔽所有中断
								        	                          IRQF_SHARED ,则表示多个设备共享中断
								        	                          IRQF_TRIGGER_RISING 上升沿触发中断
								        	                          IRQF_TRIGGER_FALLING 下降沿触发中断

								        	
								        name:中断名称 在/proc/interrupts中可看到该名称
								        dev:传递给中断处理函数的参数,一般用于在共享中断时传递设备信息

								 成功返回0,失败返回负数
					}
					
					b. 释放中断 free_irq
					{
				    	void free_irq(unsigned int irq, void *dev_id)
				    
				    	第一个参数是要释放的中断号
				    	第二个参数必须和中断申请函数的最后一个参数相同
   
				  }
				  
				  c. 中断禁止、使能相关函数
				  {
				  	  禁止和使能所有中断
						  	  local_irq_disable()   //禁止本CPU内所有中断
						  	  local_irq_enable()  //使能本CPU所有中断

							禁止和使能一个中断
					        disable_irq(unsigned int irq) //禁止中断
					        enable_irq(unsigned int irq) //使能中断
					        
					        irq 为需要禁止和使能的中断号 
				  }

		}
		
		3  按键中断
		{ 
			  //linux-3.4.39 中断号定义文件 s5p6818_irq.h
			  //查看电路原理图   KEY3--->PB8        触发方式都设置为下降沿触发
			  //								 KEY4--->PB16   
			  
			   获取KEY对应的中断号
			   {
			   	     获取中断号2种方法:
			   	     
			   	     (1) 通过 gpio_to_irq 获取 GPIO管脚的中断号.
								  在Linux内核中,对于有中断功能的GPIO管脚,可以通过gpio_to_irq获得对应GPIO管脚的中断号.
									内核中gpio相关函数://实现在 drivers/gpio/gpiolib.c //对GPIO口进行统一管理
									{

											    int 	gpio_request(unsigned gpio, const char *label)   //向内核申请一个GPIO口资源
											    int 	gpio_direction_input(unsigned gpio); //设置GPIO口为输入模式
											    int 	gpio_direction_output(unsigned gpio, int value) //设置GPIO口为输入模式
											    int 	gpio_get_value(unsigned gpio) //获取GPIO口状态
											    int 	gpio_set_value(unsigned gpio,int value) // 设置 GPIO口状态
											    void 	gpio_free(unsigned gpio) // 释放GPIO口
											    int 	gpio_to_irq(unsigned gpio) //GPIO口转换成相应的中断号
													....
													
													参数gpio ,表示相应的GPIO口号,需要自己定义,定义方法如下:
														
													//例如 定义PC14端口 如下
													#define	CFG_IO_FS6818_BEEP				(PAD_GPIO_C + 14)
									}
									
						   (2) 在s5p6818_irq.h 文件中定义了 S5P6818 CPU所有的中断号
				 }
					
				配置内核,去掉原有的按键驱动
				{ //如果不做这一步,会发生中断冲突

						Device Drivers  --->     
						    Input device support  --->

						        [ ]  Keyboards  --->(不选)

						重新编译内核
				}
				
				编写中断服务处理程序,并注册中断
				{
				       //中断服务处理程序
							irqreturn_t key_handler(int irq, void *data)
							{
								printk("key interrupt ,handler irq:%d \n ",irq);
								
								return IRQ_HANDLED;
							}
							
							iret=request_irq(key3_irq_no, key_handler,IRQ_TYPE_EDGE_FALLING,"key3",NULL);
								
				}	    		   		    
		}

}

十七、中断上半部分好中断下半部分

{

 1 中断上半部分和中断下半部分的概念
		{
				理想情况下我们希望中断的处理程序越快越好,但是在某些场合无法满足这个要求,比如网卡接收数据,
				如果网卡长时间处于中断,占用CPU资源,影响系统的并发能力和相应能力

				为了解决这个问题,Linux内核中引入中断顶半部和底半部的机制,其实就是将中断处理程序分为两部分:
					
				上半部分: 就是中断处理程序,完成一些比较紧急,需要立即处理的事情,比如说将网卡数据拷贝到内存
       						 这个过程不可中断,其他事情就可以在中断下半部分中完成,在上半部分登记下半部分

				下半部分:做一些不紧急,相对耗时的工作,比如将数据包交给协议层处理的过程。
									中断下半部分可以被别的中断打断
		}
		
		2 中断下半部分的实现
		{
				 中断下半部分主要有3种实现方式:tasklet,工作队列 和软中断
				 	
				 	a. tasklet机制实现中断下半部分
				 	{
				 					//tasklet 结构体定义
			 		    		struct tasklet_struct
									{
											struct tasklet_struct *next;
											unsigned long state;
											atomic_t count;
											void (*func)(unsigned long);
											unsigned long data;
									};
									
									(1) 定义并初始化 tasklet 结构体变量
									{
											  struct tasklet_struct mytasklet;
							       		tasklet_init(&mytasklet,tasklet_func,data);

												//tasklet_func: 中断下半部分对应的处理函数
												//data: 传递给 tasklet_func 函数的参数
													
							       		或者
							       		
							        	DECLARE_TASKLET(mytasklet,tasklet_func,data);
							    }
							        
							    (2) 在中断处理函数(上半部分)中调度tasklet

								        tasklet_schedule(&mytasklet);

								  (3) 系统会在合适的时候,调用tasklet处理函数

								  注:tasklet还是工作在中断上下文,不允许睡眠

				 	}
				 	
				 	b. 工作队列机制实现中断下半部分
				 	{
				 				//工作队列结构体
					 		  struct work_struct {
										atomic_long_t data;
										struct list_head entry;
										work_func_t func; //工作队列处理函数
										#ifdef CONFIG_LOCKDEP
											struct lockdep_map lockdep_map;
										#endif
								};
								
								typedef void (*work_func_t)(struct work_struct *work);

								(1) 定义并初始化 work_struct 工作队列
								    struct work_struct mywork;
								    INIT_WORK(&mywork,work_func);
								    
								(2) 在中断处理函数(上半部)调度工作队列
   									 schedule_work(&mywork);

								(3) 调度完成后,内核会在适当的时候执行中断的上半部分					
				 	}
				 		 	
		}

}

十八、内核定时器与时间服务

{

 1. 内核系统定时器
		 {
		 	    内核中有一个系统定时器(硬件定时器),可以通过软件设置他的工作频率,周期性产生时钟中断。
		 	    称为滴答时钟.
		 	    
		 	    内核系统时钟中断就必然有对应的中断处理函数,中断处理函数中需要完成如下任务:
					{
				        更新系统运行时间
				        更新实际时间
				        检查进程的时间片信息
				        执行超时的定时器(软件定时器)
				        执行一些统计信息
				       	 ......
			    }
			    
			    系统时钟相关变量
			    {
					HZ:	记录了时钟定时器的频率,也就是代表一秒钟产生多少个时钟中断;
					在ARM中,HZ=100

					tick: 1/HZ,发生一次时钟中断的时间间隔, 1tick = 10ms

					jiffies:内核汇总一个32位全局变量,内核一般用它表示时间,它记录了开机以来发生了多少次时钟中断  
					持续时间
					:16个月

					jiffies+2*HZ   //2s后的时间

			    }
			    
			    内核提供的时间相关函数
			    {
			    		  //内核表示时间的结构体
			    		   struct timespec {
								     __kernel_time_t tv_sec;   /* seconds */
								     long  tv_nsec;  /* nanoseconds */
								 };

								 struct timeval {
								     __kernel_time_t  tv_sec;  /* seconds */
								     __kernel_suseconds_t tv_usec; /* microseconds */
								 };
  	
			    					//时间比较函数
			    		   time_after(a,b)  			//时间a在b之后,返回true
						     time_before(a,b) 			//时间a在b之前,返回true
						     time_after_eq(a,b)	 	//时间a在b之后或者相等,返回true
						     time_before_eq(a,b)	 	//时间a在b之前或者相等,返回true
						     time_in_range(a,b,c)  //时间a在b和c之间,返回true

						  		//时间转换函数
						   	 jiffies_to_msecs    // jiffies-->ms
    						 jiffies_to_usecs    // jiffies-->us
   							 msecs_to_jiffies
   							 usecs_to_jiffies

    						 timespec_to_jiffies
    						 jiffies_to_timespec

   						   timeval_to_jiffies
   						   jiffies_to_timeval
			    }
			    
			    
			    内核延时函数
			    {
			    	 //这些延时函数都是忙等待延时,不会导致睡眠,白白消耗CPU时间
			    	  void ndelay(unsigned long x)   //纳秒级延时
			    	  udelay(n)  //微秒级延时
			    	  mdelay(n)  //毫秒级延时
			    	  
			    	  void msleep(unsigned int msecs) //会引起睡眠
			    	  
			    }
		 }
		 
		 2. 内核定时器的使用(软件定时器的使用)
		 {
		 		  //软件定时器结构体
			 		struct timer_list 
			 		{
					     /*
					      * All fields that change during normal runtime grouped to the
					      * same cacheline
					      */
					     struct list_head entry;
					     unsigned long expires;//超时时jiffies值
					     struct tvec_base *base;

					     void (*function)(unsigned long);//超时处理函数
					     unsigned long data;//传递给超时函数参数

					     int slack;
					};


			    (1) 分配定义定时器

			        struct timer_list mytimer;

			    (2) 初始化定时器

							
			        init_timer(&mytimer);//我们关心的三个字段需要另外指定
			        
			        mytimer.expires = .....//指定超时时间
			        mytimer.function = ....//指定超时处理函数
			        mytimer.data = ........//指定传递给超时处理函数的参数


			    (3) 向内核添加启动定时器
			        add_timer(&mytimer);   //后续和定时器有关的操作由内核完成

			    		一旦超时时间到,自动调用超时处理函数

			    (4)  如果要修改定时器
			         mod_timer(&mytimer,jiffies+xxx);//设置超时时间为xxx之后

			    (5)  删除定时器
			         del_timer(&mytimer);	   
		 }

}

// 内核内存的使用 getconf PAGESIZE 查看内存页面的大小

十九、内核空间内存分配函数

{

1. kmalloc:用户申请指定长度的连续物理内存空间,分配过程中可能导致系统睡眠 
{
 void *kmalloc(size_t size, gfp_t flags);
	 
 参数flags: 
		较常用的 flags(分配内存的方法)

		GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
		GFP_KERNEL —— 正常分配内存;
		GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)				 
 size:
		要分配内存的大小,单位字节
		注意: 申请内存不能超过128KB。
	
 对应的内存释放函数为:
	void kfree(const void *objp);
 
}

2. kzalloc 函数
{
kzalloc() 函数与 kmalloc() 非常相似,参数及返回值是一样的,
可以说是前者是后者的一个变种,因为 kzalloc() 实际上只是额外附加了 __GFP_ZERO 标志。
所以它除了申请内核内存外,还会对申请到的内存内容清零。
}

3. vmalloc函数
{
void *vmalloc(unsigned long size);

存的大小
			  参数: size, 指定分配内
	vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存
	在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对
	申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。

	对应的内存释放函数为:

	void vfree(const void *addr);
	注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。 
}

4. __get_free_pages/ __get_free_page 函数 //在内核中分配指定的页面
{

	unsigned long  __get_free_pages(gfp_t gfp_mask, unsigned int order)
		
		gfp_mask(分配内存的方法):
				   GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会
												被(高优先级进程或中断)打断;
					 GFP_KERNEL —— 正常分配内存;
					 GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)
					
		order: 表示分配  2^order 次方页
			
		返回值为对应的内核内存虚拟地址
	
	//对应的内存释放函数为 free_pages
	 void free_pages(unsigned long addr, unsigned int order)
	
	
		//分配一页
	 unsigned long  __get_free_page(gfp_t gfp_mask);
	 
	 //对应的内存释放函数
	 void free_page(unsigned long addr);
	

}

5. IO内存访问
{
(1) IO内存的概念: IO内存通常指的是存在与计算机外设里面,用来控制计算机外设工作的寄存器,在ARM体系
					结构中,这些用来控制计算机外设工作的寄存器,称为IO内存。
					ARM体系结构将IO内存和计算机普通内存进行统一编址,所以访问IO内存和访问普通内存一样。

(2) IO内存映射
对于IO空间的地址(寄存器地址),不能直接访问其物理地址,需要进行映射到虚拟地址空间之后才能访问		
物理地址映射到虚拟地址映射函数:ioremap

	void *ioremap(unsigned long offset, unsigned long size)

		offset: 要映射的IO物理地址
		size:映射的大小
		映射成功,返回映射的虚拟地址

				完成映射后,访问映射的虚拟地址就相当于访问原物理IO地址

		解除映射:iounmap
						void iounmap(void *addr);


(3) 访问映射的IO内存的函数

		readb(c) //读8位,c是要读的地址
		readw(c) //读16位,c是要读的地址
		readl(c) //读32位,c是要读的地址
	
		//和上面等价,新接口
	  ioread8(c) 
	  ioread16(c) 
	  ioread32(c) 

		writeb(v,c)	//写8位 ,v是要写的数据, c是地址
		writew(v,c)	 //写16位 ,v是要写的数据, c是地址
		writel(v,c)	//写32位 ,v是要写的数据, c是地址

		// 和上面等价,新接口
		iowrite8(v,c)	
		iowrite16(v,c)	
		iowrite32(v,c)	

}

}

二十、自动创建设备文件

{

内核中设备的添加,删除或修改都会向应用程序发送热差拔事件,应用程序可以通过捕获这些
	事件来自动完成某些操作,例如: 自动加载驱动,自动创建设备文件节点等。

	1. 应用层自动创建设备节点
	{
	使用mdev自动创建设备节点的2个时机。

	a. 执行 #mdev -s 命令,  #mdev -s通常在根文件系统挂载完成后运行一次,它递归扫描 /sys/block 目录
	和 /sys/class目录下的文件,根据文件的内容来自动创建设备文件。

	b. 当内核发生了热插拔事件后,mdev会自动被调用,这通过查看 /etc/init.d/rcS 可以看到

	echo /sbin/mdev > /proc/sys/kernel/hotplug

	内核中一种发送热插拔事件调用应用程序的方式,就是执行 /proc/sys/kernel/hotplug 文件中的程序。

}

2. 驱动程序支持自动创建设备文件节点
{
	mdev创建自动创建设备节点依赖 /sys/class 目录下的文件,在驱动程序中,我们只要调用
	相应的函数在 /sys/class 目录下创建相应的文件即可。


	创建类的API函数
	 class_create(owner, name) 
		owner 所属模块对象指针,THIS_MODULE 
		name: 类名;
				
	 device_create
	 
	  函数原型: 
			 struct device *device_create(struct class *class, struct device *parent,
			 dev_t devt, void *drvdata, const char *fmt, ...)
			 
		  参数说明:   
				class:  由 class_create 返回的类的结构体;
				parent: 父类,一般写成NULL即可
				devt: 设备号
				drvdata: 添加到device中的数据,一般设为NULL即可;
				fmt: 对应创建设备节点的设备名称
			
	 代码举例:
		 xxx_class = class_create(THIS_MODULE, "xxx_class");		
		 
		 //对应的创建设备文件节点名为 gmem
		 dev_tmp =  device_create(xxx_class, NULL,devno , NULL, "gmem");	
								 
}

}

二十一、Linux设备驱动模型

{

1. 设备驱动模型基础
{

	sys文件系统
	Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,
	并向用户模式程序提供详细的内核数据结构信息。
	sys文件系统详细列出了所有设备,驱动和硬件相关的信息, 通过sys 文件系统,用户程序可以
	访问内核中设备硬件的详细信息。

	sys文件系统挂载
	mount -t sysfs sysfs /sys

	//sys文件是根据什么依据,来创建其内容呢?他的信息来源是什么呢
	sys 文件系统信息来源
{
	kobject是Linux设备模型的基本结构,类似于C++中的基类。
	在实际应用中会将他嵌入到更大的对象中用来描述设备模型,比如device, bus, driver

		所有的这些对象都使用了kobject,通过kobject联系到一起,形成一个树状结构,
		这个树状结构就和/sys目录相对应
		
		每个在内核中注册的kobject对象都会在/sys下有一个目录与之对应。
		
		
		kset   kobject的集合。
		kobject通过kset组织成层次化的结构,kset是具有相同类型的kobject的集合。
		
		kobject 和 kset 是组成Linux设备驱动模型的基本数据结构。

}

}

2. Linux总线设备驱动模型意义
{

	Linux设备驱动软,硬件分离设计思想

	当硬件连接方式(所使用的硬件资源)发生改变后,对应的驱动也需要进行修改,但是我们只需要修改
	其中和硬件有关的部分,软件实现部分基本不变;
	我们之前写的驱动是一个整体,一旦硬件进行任何改动,我们需要修改整个驱动。

	这种驱动的移植性很差,为了提高驱动的可移植性和可维护性,我们可以将驱动进行分解成多个部分来实现。

	总线设备驱动模型就是一种将驱动分解成多个部分实现的机制。		 
}

3. Linux 总线设备驱动模型
{  	
	Linux总线设备驱动模型将驱动分为两个部分:和硬件相关部分(struct device) 和
	硬件无关的部分(struct device_driver)
	内核中定义了一条总线(struct bus_type)来管理device和driver,实现他们匹配和管理.

	设备,设备驱动和总线是构成Linux总线设备驱动模型最重要的3个数据结构。


	设备、设备驱动、总线的关系

	(1) 设备、设备驱动都挂接在总线上,总线上有一## 标题个设备链表和设备驱动链表,分别用来挂接设备
		和设备驱动。同时总线上还有一个match函数,用来匹配设备和设备驱动。
		
		struct bus_type
		{
			 ......
			 int (*match)(struct device *dev, struct device_driver *drv);//匹配函数 
			 struct klist klist_devices; //设备链表
			 struct klist klist_drivers; //设备驱动链表
			 ...... 
		}

	(2) 设备结构体包含了总线成员(表示设备所挂接的总线) 和 设备驱动成员(设备所使用的驱动)
	 struct device  保存了设备驱动的硬件相关信息。
			struct device 
			{
					...
					struct bus_type	*bus; //表示该设备所挂接的总线
					struct device_driver *driver; //表示该设备对应的驱动
					...

			  }
		  
	(3) 设备结构体包含了总线成员(表示设备所挂接的总线)和设备链表(使用该驱动的设备链表)
		 struct device_driver 实现了设备驱动的与硬件无关的操作
		   struct device_driver 
		   {
			  ......
				struct bus_type		*bus; //表示该设备驱动所挂接的总线
				struct klist klist_devices; //该链表挂接使用该设备驱动的设备链表
			  ......
		   }

	但是Linux并不直接使用上述的结构来实现具体的设备驱动模型,而是使用它们嵌入到某种具体驱动模型中去,
	从而实现软件和硬件的分离。设备,设备驱动和总线相当于父类,具体的设备,具体的设备驱动和
	具体的总线相当于子类。

}

4. Platform 设备和设备驱动
{
	platform 总线: 在总线设备驱动模型中,设备必须挂接在总线上,这对于I2C设备,SPI设备和USB设备
	这类挂接在具体的总线上的设备没有问题。

	但是对于嵌入式设备而言,很多集成的外设,比如GPIO口,ADC等并没有挂接在具体的总线上,为了解决
	这类问题,Linux发明了虚拟总线,称为Platform总线,挂接Platform总线上的设备称为平台设备(platform_device),
	挂接在Platform总线上的设备驱动称为平台设备驱动(platform_driver);

	//platform_bus 平台总线结构体定义
	struct bus_type platform_bus_type = 
	{
			.name		= "platform",
			.dev_attrs	= platform_dev_attrs,
			.match		= platform_match,
			.uevent		= platform_uevent,
			.pm		= &platform_dev_pm_ops,
	};


//平台设备结构体定义,用来表示平台设备
struct platform_device 
{
		const char	* name;  //用于设备和设备驱动匹配
		int		id;		//当用于同名设备的匹配,不用给 -1
		struct device	dev;  //内嵌struct device结构体
		u32		num_resources;  //资源数组 长度(资源个数)
		struct resource	* resource; // 设备所使用的资源

		const struct platform_device_id	*id_entry;
			......
	
};

//平台设备驱动结构体定义,用来表示平台设备驱动
struct platform_driver 
{
		int (*probe)(struct platform_device *); //匹配成功后调用的函数
		int (*remove)(struct platform_device *);
		void (*shutdown)(struct platform_device *);
		int (*suspend)(struct platform_device *, pm_message_t state);
		int (*resume)(struct platform_device *);
		struct device_driver driver;  //内嵌struct driver结构体,name字段用于设备和设备驱动匹配
		const struct platform_device_id *id_table;
};


	当往platform 总线注册设备时(往总线的设备链表添加节点),内核就会遍历platform_driver链表,
	取出其中的每一个节点和添加的硬件节点比较匹配,如果匹配成功就调用platform_driver 节点的probe函数,
	并且将硬件信息(platform_device)传递给该函数,  probe函数应该完成什么工作,有程序员决定。

	当往platform 总线注册设备驱动时(往总线的设备驱动链表添加节点),内核就会遍历platform_device链表,
	取出其中的每一个节点和添加的设备驱动节点比较匹配,如果匹配成功就调用platform_driver 节点的probe函数,
	并且将硬件信息(platform_device)传递给该函数,probe函数应该完成什么工作,有程序员决定。

	对于platform模型,我们只需要关注platform_device和platform_driver

}

5. 如何往内核中添加platform_device和platform_driver
{

	(1) 定义并初始化struct resourse结构

		struct resourse xxx_res[] = 
		{

			[0] = {
				.start = 起始IO地址/中断号
				.end = 结束IO地址/中断号
				.flags = 资源类型 IORESOURSE_MEM/IORESOURCE_IRQ

			},
			.....
		}
		
		//在probe函数调用platform_get_resource 获取资源信息
		struct resource *platform_get_resource(struct platform_device *dev,
									 unsigned int type, unsigned int num)
		
		参数: dev 需要获取的平台设备 platform_device 结构体指针;
				type 资源类型
				num 资源编号,不同类型的资源都从0开始编号
				
				
				
	(2) 定义并初始化 platform_device结构

		struct platform_device xxx_dev = 
		{

			.name = 名字,   //用于匹配
			.id = -1,   //用于区分同名资源,不用-1
			.num_resourses = ARRAY_SIZE(xxx_res), //资源个数
			.resourse = xxx_res,
					...
		}	
		
		调用platform_device_register向内核添加 platform_device  
		
		
					
	(3) 定义并初始化 platform_driver结构

	struct platform_driver xxx_drv = 
	{
			.driver = {
				.name = 名字,  //用于设备和设备驱动匹配匹配
				.owner = THIS_MODULE,
			},
			.probe = xxx_probe,
			.remove = xxx_remove,
			......

		};
		
		调用platform_driver_register向内核注册
	}
	}

练习:
//写一个纯粹的platform驱动框架, 不进行任何硬件操作
// ARM接口驱动 GPIO口驱动 按键中断
// PWM 驱动 看门狗定时器驱动 ADC驱动
// I2C驱动 SPI驱动
//块设备驱动 ,网络设备驱动

你可能感兴趣的:(C语言设备驱动,C语言,设备驱动)