linux 中的中断处理

谈谈对中断的理解

1.裸板中断处理过程

中断属于异常的一种
它是计算机中处理异步事件的重要机制

1.1 中断的触发
	中断源级
		配置中断的触发方式  上升沿 下降沿  高 低触发
		中断使能 (监测到中断信号之后,能不能报上去)
		
	中断控制器级
		配置中断的优先级
		中断使能
		配置以irq frq 形式上报
		配置报给哪个核
		
	ARM core 
		中断的使能 I=0

1.2 中断的处理过程
	中断异常产生硬件自动做4件事 
		1) 备份CPSR 
		2) 修改CPSR
				MODE
				T 
				F
				I
		3) 保存返回地址到LR
		4) PC = vector_base + 0x18
			跳转到异常向量表执行
			...
			ldr pc, =irq_handler
			irq_handler:
				现场保护
					bl c_irq_handler
				恢复现场
			c_irq_handler()
			{
				区分哪个硬件触发的IRQ异常
					调用该硬件的中断处理函数 hardware_isr ();
				清除中断pending
			}

2.linux中中断的处理过程

	linux中中断的处理过程和裸板中中断的处理过程相同
	linux将能帮你写好的中断处理代码都写好了
	写好的这部分代码称作 "linux中断子系统"

	特定硬件的中断触发方式 需要自行配置
	特定硬件中断处理函数需要自行编程实现 hardware_isr ();
	
	//通过这个API可以将需要做的配置,需要实现的中断处理函数"揉"到内核中去
	//中断注册函数
	request_irq(unsigned int irq, irq_handler_t handler, 
	unsigned long flags, const char *name, void *dev)
	
		irq, 中断号
		PAD_GPIO_A +28 --->//这个是GPIO的编号而不是中断号
		gpio_to_irq (PAD_GPIO_A +28);
		返回该管脚的中断编号
		irq_to_gpio (IRQ_GPIO_A_START + 28);
		arch/arm/mach-s5p6818/include/machs5p6818_irq.h 
		这里面有各个设备的中断号 Physical interrupt number
		IRQ_GPIO_A_START + 28
		
		handler, 要注册的中断处理函数 (hardware_isr)
			irq_handler_t  是typedef irqreturn_t (*irq_handler_t)(int, void *); 的别名
			typedef enum irqreturn irqreturn_t; 
			enum irqreturn {
  				IRQ_NONE    = (0 << 0),
				IRQ_HANDLED   = (1 << 0),
		    	IRQ_WAKE_THREAD   = (1 << 1),	
			};
			flags, 常用的取值
				IRQF_TRIGGER_FALLING  //下降沿触发
				IRQF_TRIGGER_RISING //上升沿触发
				IRQF_TRIGGER_HIGH //高电平触发
				IRQF_TRIGGER_LOW //低电平触发
			name, 名称
			dev, 当内核调用handler函数时 传递的第二个参数值

		 	函数功能:
		 		1) 约定什么情况下(上升沿...)下产生IRQ中断
		 		2) 约定IRQ号中断产生了 内核应该调用什么handler
		 		3) 约定内核调用handler 传递的第二个参数值
		 			handler (xxx, dev);
		 			
	free_irq (unsigned int irq, void *dev_id)
		函数功能: 注销中断服务程序
				取消request_irq中关于irq中断的约定

		特别注意: 注册中断服务程序时给的最后一个参数
				也必须是注销时给的最后一个参数
				
	电路原理图
	  K1 捕获它按下的动作 由硬件监测GPIOA28管角上的下降沿

	下面展示一些 `内联代码片`。
	注意:模块中的 xxx_init函数返回非0 内核认为模块注册失败
	lsmod时 看不到该模块
	中断注册失败的原因:
		内核中自带了按键驱动程序
		该驱动程序中已经注册了该中断号对应的中断服务程序
	如果要完成实验 需要将内核中的该驱动程序裁剪
	cd driver/kernel
	make menuconfig
	Device Drivers --->
		Input device support --->
		[*] Keyboards --->
			<> SLsiAP push Keypad support 
			
	make uImage
	cp arch/arm/boot/uImage /tftpboot
	tftp 48000000 uImage
	mmc write 48000000 800 3000
	insmod btn_drv.ko
	cat /proc/interrupts
	找到"up"的中断
	vi btn_drv.c
#include<linux/init.h>
#include<linux/module.h>
#include <linux/interrupt.h>
#include <mach/platform.h>

MODULE_LICENSE("GPL");
typedef struct btn_desc 
{
	int irq;//中断号
	char *name;
}btn_desc_t;
btn_desc_t buttons[] = 
{
	{IRQ_GPIO_A_START+28,"UP"},
	{IRQ_GPIO_B_START+30,"DOWN"},
	{IRQ_GPIO_B_START+31,"LEFT"},
	{IRQ_GPIO_B_START+9,"RIGHT"}
};
irqreturn_t btn_isr(int irq, void * dev)
{
	//获取到哪个按键按下了
	btn_desc_t *pdata = (btn_desc_t *) dev;
	printk("%s key is pressed!\n", pdata->name);
	return IRQ_HANDLED;
}
int __init btn_drv_init (void)
{
	int ret = 0;
	int i = 0;
    //注册按键的中断服务程序  
    printk("irq num = %d\n", IRQ_GPIO_A_START + 28);
    for (; i <ARRAY_SIZE(buttons); i++){
    	ret = request_irq(buttons[i].irq, btn_isr
    				IRQF_TRIGGER_FALLING,
    				buttons[i].name,
    				&(buttons[i])
    				);
    	if (ret)
    	{
    		printk("request_irq failed!\n");
    		i--;
    		while (i>=0) {
    			free_irq (buttons[i].irq, &(buttons[i]));
    			i--;
    		}
    		//返回非0值,内核认为模块注册失败
    		return -EAGAIN;
    	}
    }
    return 0;
}
void __exit btn_drv_exit(void)
{
	int i = 0;
	for (; i < ARRAY_SIZE (buttons); i++)
	{
		free_irq(buttons[i].irq, &(buttons[i]));
	}
}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
	挑挑问题所在,指出一下中断处理函数的问题:
	int buttons_isr (int num) //类似于irq_handler
	{
		float f = 100.4;
		f = f - 1.2;
		printf ("f=%f\n",f);
		return (int)f;
	}
	问题:
	1)中断处理函数中不应该有返回值
	2)不应该有参数
	3)在许多的处理器、编译器中,浮点一般都是不可重入的,需要让额外的寄存器入栈
		有些处理器、编译器中不允许在ISR中做浮点运算。此外ISR应该是高效而快速的。
	4)printf耗时,也不具有可重入性
		判断一个函数是否具有可重入性:
			看函数中是否使用了全局变量
			使用了全局变量的函数,不具有可重入性。
	16道经典嵌入式笔试题
	中断处理函数的特点
		1)要求执行的速度越快越好
		2) linux的中断处理函数中不能调用引起阻塞或者睡眠的函数
			recv/sleep 
		3)linux中中断处理过程使用独立的栈空间
			通常中断服务程序对应的栈空间较小
			一般物理内存页(4KB)
			中断处理函数中不能使用大数组
		4) 中断处理函数中不能做用户空间和内核空间的数据交互
			不能调用copy_to_user copy_from_user
					get_user / put_user (可能会引起阻塞)
			
	linux中希望中断处理函数执行速度尽量快
	但是有些硬件对应的中断服务程序执行起来就是慢
	针对该问题,内核提出了中断顶半部和中断低半部的机制
	说白了,就是为了解决中断处理程序不能很快结束的问题
	顶半部(top half):
		做紧急的工作
		往往就是做一些特殊功能寄存器的读写操作
		也包括清除中断标志位
		登记底半部
	底半部(bootom half):
		做不紧急的耗时的工作

3.登记底半部的方式

3.1 软中断
	直接修改内核源码
	不能以.ko文件的方式实现
	实现起来不方便
	
3.2 tasklet
	他是基于软中断的方式实现的
	核心数据结构
	interrupt.h
	struct tasklet_struct
	{
		struct tasklet_struct *next;
		unsigned long state;
		atomic_t count;
		void (*func) (unsigned long);//保存底半部函数的地址
		unsigned long data;//当内核调用func函数时,传递的参数值
	}
	使用步骤:
		1)定义tasklet类型的变量
			struct tasklet_struct btn_tasklet;
		2)初始化tasklet变量
			void tasklet_init (struct tasklet_struct *t,
					void (*fun)(unsigned long), unsigned long data);
			DECLARE_TASKLET(name, func, data)
			等价与步骤1, 2
		3) 使用该变量登记底半部
			voi tasklet_schedule(struct tasklet_struct *t);
	vi btn_driver.c
#include<linux/init.h>
#include<linux/module.h>
#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");
typedef struct btn_desc 
{
	int irq;//中断号
	char *name;
}btn_desc_t;
btn_desc_t buttons[] = 
{
	{IRQ_GPIO_A_START+28,"UP"},
	{IRQ_GPIO_B_START+30,"DOWN"},
	{IRQ_GPIO_B_START+31,"LEFT"},
	{IRQ_GPIO_B_START+9,"RIGHT"}
};
//1.定义一个tasklet变量
struct tasklet_struct btn_tasklet;

irqreturn_t btn_isr(int irq, void * dev)
{
	//获取到哪个按键按下了
	btn_desc_t *pdata = (btn_desc_t *) dev;
	printk("%s key is pressed!\n", pdata->name);
	//3.登记底半部
	printk ("register bottom half!n");
	tasklet_scheule(&btn_tasklet);
	printk("exit form top half\n");
	return IRQ_HANDLED;
}
int mydata = 100;
void btn_tasklet_func(unsigned long data){
	//tasklet登记的底半部函数中不能调用引起阻塞或者睡眠的函数
	int *pdata = (int *)data;
	//msleep(1);//会发生吐核的现象 Oops信息,将内核栈的信息输出出来
	printk("do bottom half work:data = %d...\n",*pdata);
	(*pdata)++;
}
int __init btn_drv_init (void)
{
	int ret = 0;
	int i = 0;
    //注册按键的中断服务程序  
    printk("irq num = %d\n", IRQ_GPIO_A_START + 28);
    for (; i <ARRAY_SIZE(buttons); i++){
    	ret = request_irq(buttons[i].irq, btn_isr
    				IRQF_TRIGGER_FALLING,
    				buttons[i].name,
    				&(buttons[i])
    				);
    	if (ret)
    	{
    		printk("request_irq failed!\n");
    		i--;
    		while (i>=0) {
    			free_irq (buttons[i].irq, &(buttons[i]));
    			i--;
    		}
    		//返回非0值,内核认为模块注册失败
    		return -EAGAIN;
    	}
    }
    tasklet_init (&btn_tasklet,btn_tasklet_func, (unsigned long)&mydata);
    return 0;
}
void __exit btn_drv_exit(void{
	int i = 0;
	for (; i < ARRAY_SIZE (buttons); i++)
	{
		free_irq(buttons[i].irq, &(buttons[i]));
	}module_init(btn_drv_init);
module_exit(btn_drv_exit);
		注意:tasklet基于软中断机制实现的
			tasklet.fun工作与中断上下文(context),其中不能调用引起阻塞操作的函数。
			中断上下文指的是:当异常产生之后,硬件自动做4件事,跳转到异常向量表之后,
			直到返回被打断的位置,这个过程
			当执行一个中断处理程序时,内核处于中断上下文。
	
		如果底半部函数中必须调用引起阻塞或者睡眠的函数,这时可以考虑使用工作队列机制
		登记底半部
		
	3.3 工作队列
		核心数据结构:
			workqueue.h:
			struct work_struct{
				work_func_t func;
				......
			};
		使用步骤:
			1)定义work变量
				struct work struct btn_work;
			2)初始化work变量
				INIT_WORK(&btn_work,func);
				DECLARE_WORK(n,f)//完成以上两步
			3)使用work变量登记底半部
				schedule_work (&btn_work);
			4) 卸载模块时 注意对登记未执行work的处理
				flush_work (struct work_struct *work);
				cancel_work_sync (struct work_struct *work);
		vi btn_drv.c
#include<linux/init.h>
#include<linux/module.h>
#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");
typedef struct btn_desc 
{
	int irq;//中断号
	char *name;
}btn_desc_t;
btn_desc_t buttons[] = 
{
	{IRQ_GPIO_A_START+28,"UP"},
	{IRQ_GPIO_B_START+30,"DOWN"},
	{IRQ_GPIO_B_START+31,"LEFT"},
	{IRQ_GPIO_B_START+9,"RIGHT"}
};
//1.定义一个tasklet变量
//struct tasklet_struct btn_tasklet;
//定义一个work_struct变量
struct work_struct btn_work;

irqreturn_t btn_isr(int irq, void * dev)
{
	//获取到哪个按键按下了
	btn_desc_t *pdata = (btn_desc_t *) dev;
	printk("%s key is pressed!\n", pdata->name);
	//3.登记底半部
	printk ("register bottom half!n");
	//tasklet_scheule(&btn_tasklet);
	schedule_work(&btn_work);
	printk("exit form top half\n");
	return IRQ_HANDLED;
}
int mydata = 100;
void btn_tasklet_func(unsigned long data){
	//tasklet登记的底半部函数中不能调用引起阻塞或者睡眠的函数
	int *pdata = (int *)data;
	//msleep(1);//会发生吐核的现象 Oops信息,将内核栈的信息输出出来
	printk("do bottom half work:data = %d...\n",pdata);
	(*pdata)++;
}
void btn_work_func(struct work_struct *work)
{
	msleep(1);//不吐核
	printk("do bottom half work:data...\n");
}
int __init btn_drv_init (void)
{
	int ret = 0;
	int i = 0;
    //注册按键的中断服务程序  
    printk("irq num = %d\n", IRQ_GPIO_A_START + 28);
    for (; i <ARRAY_SIZE(buttons); i++){
    	ret = request_irq(buttons[i].irq, btn_isr
    				IRQF_TRIGGER_FALLING,
    				buttons[i].name,
    				&(buttons[i])
    				);
    	if (ret)
    	{
    		printk("request_irq failed!\n");
    		i--;
    		while (i>=0) {
    			free_irq (buttons[i].irq, &(buttons[i]));
    			i--;
    		}
    		//返回非0值,内核认为模块注册失败
    		return -EAGAIN;
    	}
    }
    //2 初始化tasklet变量
    //tasklet_init (&btn_tasklet,btn_tasklet_func, (unsigned long)&mydata);
    //2初始化work变量
    INIT_WORK(&btn_work,btn_work_func);
    
    return 0;
}
void __exit btn_drv_exit(void{
	int i = 0;
	for (; i < ARRAY_SIZE (buttons); i++)
	{
		free_irq(buttons[i].irq, &(buttons[i]));
	}module_init(btn_drv_init);
module_exit(btn_drv_exit);
	work.func函数工作于进程上下文
	其中对调用无限制
	原因是:内核中维护了system_wq,每调用一次schedule_work(&btn_work);
		就会在system_wq维护的队列中加入一个新的节点,内核中专门创建了一个
		内核线程去扫描队列,挨个执行。
	与tasklet的比较:
		1)可以调用引起阻塞睡眠的函数
		2)  tasklet登记的底半部函数可以更快的被执行到
	希望底半部函数在登记完之后 延时一段时间再执行
	这种情况可以考虑delayed_work
	struct delayed_work {
		struct work_struct work;
		struct timer_list timer;
	};
	使用步骤:
	1)定义delayed_work变量
		struct delayed_work btn_dwork;
	2) 初始化
		INIT_DELAY_WORK (&btn_dwork, func);
	3)登记底半部
		schedule_delayed_work (&btn_dwork, unsigned long delay);
	4)当卸载模块时, 如果底半部函数登记了 未执行
		对应的处理策略:
		a) flush_delayed_work(struct elayed_work *dwork);
		//阻塞等待指定的dwork执行完毕才会返回
		b) cancel_delayed_work (struct delayed_work *work);
		//取消底半部函数的执行
	注意:
		1)连续两次登记底半部,只有第一次有效。
		2)登记底半部之后,没等底半部执行,
		就将模块卸载,则内核崩溃。
			由于模块的卸载 指定函数对应的内存空间释放
			延时时间到 跳转执行的位置存入内容不可预知
	vi btn_drv.c
#include<linux/init.h>
#include<linux/module.h>
#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");
typedef struct btn_desc 
{
	int irq;//中断号
	char *name;
}btn_desc_t;
btn_desc_t buttons[] = 
{
	{IRQ_GPIO_A_START+28,"UP"},
	{IRQ_GPIO_B_START+30,"DOWN"},
	{IRQ_GPIO_B_START+31,"LEFT"},
	{IRQ_GPIO_B_START+9,"RIGHT"}
};

//定义一个delayed_work变量
struct delayed_work btn_dwork;

irqreturn_t btn_isr(int irq, void * dev)
{
	//获取到哪个按键按下了
	btn_desc_t *pdata = (btn_desc_t *) dev;
	printk("%s key is pressed!\n", pdata->name);
	//3.登记底半部
	printk ("register bottom half!n");
	schedule_delayed_work(&btn_dwork, 5*HZ);
	printk("exit form top half\n");
	return IRQ_HANDLED;
}
void btn_dwork_func (struct work_struct *work)
{
	printk("do bottom half work...\n");
}
int __init btn_drv_init (void)
{
	int ret = 0;
	int i = 0;
    //注册按键的中断服务程序  
    printk("irq num = %d\n", IRQ_GPIO_A_START + 28);
    for (; i <ARRAY_SIZE(buttons); i++){
    	ret = request_irq(buttons[i].irq, btn_isr
    				IRQF_TRIGGER_FALLING,
    				buttons[i].name,
    				&(buttons[i])
    				);
    	if (ret)
    	{
    		printk("request_irq failed!\n");
    		i--;
    		while (i>=0) {
    			free_irq (buttons[i].irq, &(buttons[i]));
    			i--;
    		}
    		//返回非0值,内核认为模块注册失败
    		return -EAGAIN;
    	}
    }
    //2初始化delayed_work
    INIT_DELAYED_WORK(&btn_dwork,btn_work_func);
    return 0;
}
void __exit btn_drv_exit(void{
	int i = 0;
	//flush_delay_work(&btn_dwork);
	//cancel_delayed_work(&btn_dwork);
	for (; i < ARRAY_SIZE (buttons); i++)
	{
		free_irq(buttons[i].irq, &(buttons[i]));
	}module_init(btn_drv_init);
module_exit(btn_drv_exit);
	什么是中断?
		异常的一种
		异步事件的处理机制
	中断的触发方式:
		s5p6818+按键为例
	中断异常产生后软硬件完成的工作
	中断处理函数的特点
	Linux中可以将中断处理过程切分为两个半部
	登记底半部有几种方式 各自的特点
	
	5*HZ --->延时5秒之后,执行底半部函数在内核中是怎样实现的?

4.内核定时器

	系统时钟中断:
		周期性的,不间断的产生
		MACHINE_START(){
			...
			.timer = &nxp_cpu_sys_timer;
		}
		struct sys_timer nxp_cpu_sys_timer = {
			.init = timer_initialize;
		}
		timer_event_init ()
		{
			setup_irq(info->irqno, &timer_event_irqcation);
		}
	系统时钟中断处理函数:
		更新墙上时间
		检查当前进程的时间片是否耗尽(线程)
		jiffies++
		...
		
	HZ: 常数
		它决定了1S产生系统时钟中断的次数
		通过printk,知道HZ的具体值 当前系统中 HZ=1000
	tick
	    tick = 1/HZ 它记录了两次系统时钟中断之间的间隔
	jiffies: 变量 unsigned int 
	    它记录了自系统开机以来 经历的系统时钟中断的次数
	    系统上电时,jiffies = 0
	    假设HZ=1000
	            jiffies = 10000 
	            问开机了多长时间?
	            10s
	     1s : 1000
	     1min : 60 * 1000
	     1h: 60*60*1000
	     1day : 24 * 60 * 60 * 1000
	     大约49天,jiffies就溢出了
	 
	 内核定时器
	     软件上实现的定时器
	 核心数据结构
	     struct timer_list {
	         //超时时间
	         unsigned long expires;
	         //定时时间到了之后要执行的动作
	         function;
	         //当调用function函数时,要传递的参数
	         data;
	         ......
	     };
	     
	 操作timer_list的API
	   使用步骤:
	     1) 定义一个timer_list变量
	       struct timer_list btn_timer;
	     2) 初始化 timer_list变量
	       init_timer ( &btn_timer);
	       注:一个#的作用是将变量名称转换为字符串
	        btn_timer.expires =
	        btn_timer.function =
	        btn_timer.data =
	     3) 启动定时器
	        add_timer (&btn_timer);
	     4)停止定时器
	         del_timer (&btn_timer);
	     	 mod_timer (...);
	     	 a) 修改一个已经在计时过程中的定时器 (active);
	     	 b) 启动一个定时器(deactive)并指定超时时间
	vi led_drv.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <mach/platform.h>

MODULE_LICENSE ("GPL");
#define LED1 (PAD_GPIO_C+12)
struct timer_list led_timer;
void led_func(unsigned long data)
{
        //改变对应管脚的电平状态
        gpio_set_value (LED1, data);
        led_timer.data = !data;
        //从当前时间开始计时 1S后时间到
        mod_timer(&led_timer, jiffies + 1*HZ);
}
int __init led_drv_init(void)
{
        //申请GPIO管脚
        gpio_request(LED1,"LED1");
        //设置为输出模式, 默认输出高电平
        gpio_direction_output(LED1,1);
        /*初始化timer*/
        init_timer(&led_timer);
        led_timer.function = led_func;
        led_timer.expires = jiffies + 1*HZ;
        //管脚的电平要设置成的状态
        led_timer.data = 0;
        //启动定时器
        add_timer(&led_timer);
        return 0;
}
void __exit led_drv_exit(void)
{
        //停止定时器
        del_timer (&led_timer);
        //释放gpio管脚
        gpio_free(LED1);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
	内核态的延时
		linux/delay.h
		睡眠延时
			msleep()
		忙等待延时 //相当于是空操作
			mdelay
			ndelay

内核的竞态与并发

	实例:
		PC上的串口是独占式访问设备
		该方式的实现应该是在串口的驱动程序中实现的
	
	要解决的问题:
		希望按键设备只允许同时有一个进程进行访问

在这里插入图片描述

	vi  btn_drv.c	     	
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

MODULE_LICENSE("GPL");

struct cdev btn_cdev;
dev_t dev;
struct class *btn_cls = NULL;

//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
	printk("enter %s\n",__func__);
	return 0;
}

int btn_release (struct inode *inode, struct file *filp)
{
	printk("enter %s\n",__func__);
	return 0;
}
struct file_operations btn_fops = 
{
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,
};

int __init btn_drv_init (void)
{
	//1.申请设备号
	alloc_chrdev_region (&dev, 0, 1, "btn");

	//2.初始化cdev
	cdev_init (&btn_cdev, &btn_fops);
	//3. 注册cdev
	cdev_add (&btn_cdev, dev, 1);
	//4. 自动生成设备文件
	btn_cls = class_create (THIS_MODULE, "mybuttons");

	device_create(btn_cls, NULL, dev, NULL, "buttons");

	return 0;
}
void __exit btn_drv_exit (void)
{
	//销毁设备文件
	device_destroy (btn_cls, dev);
	class_destroy (btn_cls);
	//注销cdev
	cdev_del (&btn_cdev);
	//注销设备号
	unregister_chrdev_region (dev, 1);

}

module_init(btn_drv_init);
module_exit(btn_drv_exit);

	vi test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main (void)
{
	int fd = open("/dev/buttons", O_RDONLY);
	if (fd < 0)
	{
		perror("open failed!\n");
		return -1;
	}
	printf("open successed!\n");
	sleep (10);
	close(fd);
	return 0;
}

	利用全局变量来解决竞态问题:
		这种方法看似解决了问题,但是从汇编的角度看,是存在bug的
		而bug存在的真正原因是两个进程都可以访问cnt这个共享资源

在这里插入图片描述

	vi btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

MODULE_LICENSE("GPL");

struct cdev btn_cdev;
dev_t dev;
struct class *btn_cls = NULL;

int cnt = 1;

//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
	if (--cnt == 0)
	{
		cnt++;
		//这个逻辑可以保证只有一个进程访问
		//但是从汇编的角度看,是有bug的
		return -EAGAIN;
	}
	return 0;
}

int btn_release (struct inode *inode, struct file *filp)
{
	cnt++;
	return 0;
}
struct file_operations btn_fops = 
{
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,
};
int __init btn_drv_init (void)
{
	//1.申请设备号
	alloc_chrdev_region (&dev, 0, 1, "btn");

	//2.初始化cdev
	cdev_init (&btn_cdev, &btn_fops);
	//3. 注册cdev
	cdev_add (&btn_cdev, dev, 1);
	//4. 自动生成设备文件
	btn_cls = class_create (THIS_MODULE, "mybuttons");

	device_create(btn_cls, NULL, dev, NULL, "buttons");

	return 0;
}

void __exit btn_drv_exit (void)
{
	//销毁设备文件
	device_destroy (btn_cls, dev);
	class_destroy (btn_cls);
	//注销cdev
	cdev_del (&btn_cdev);
	//注销设备号
	unregister_chrdev_region (dev, 1);

}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
	竞态:竞争的状态
		竞争共享资源
		
	共享资源:
		硬件, uart lcd 声卡
		文件, 文件锁
		共享内存
		内核中的全局变量
		
	临界区:
		访问共享资源的代码段
		
	产生竞态的原因:
		1) SMP (对称多处理器)
		2) 多任务之间的抢占
		3) 任务和中断之间的抢占
		4) 中断和中断之间的抢占(中断优先级)
			linux中允许中断嵌套

在这里插入图片描述

	解决竞态的策略	
		1) 中断屏蔽
		2) 原子操作
		3) 自旋锁
		4) 信号量

    1.中断屏蔽:
    	可以解决2,3,4三种情况
    	local_irq_disable();//关闭中断
    	critical section code //执行临界区代码
    	local_irq_enable();//打开中断
    	
    	注意: 关中断时间要特别短,一旦过长内核直接崩溃
    		因为linux内核中很多重要机制都是靠中断实现的
    		写驱动程序时,不建议使用该方式
    	注意存在的危险:
    		......
    		...//关中断
    		xxxx_func()
    		{
    			local_irq_disable();//local_irq_save();
    			访问共享资源
    			local_irq_enable();//local_irq_restore();
    		}
    		...//该部分代码执行可能有问题
    		.....//开中断
    		以上情况可以使用local_irq_save()//保存原来中断的状态
    			local_irq_restore();//恢复原来的中断状态
    			
    2. 原子操作
    	整个操作过程不可打断 不可分割
    	2.1 位原子操作
    		对一个bit位进行操作时,保证原子性
    		arch/arm/include/asm/bitops.h
			set_bit(nr,addr)//将addr中的nr位设置为1 并保证原子性
			clear_bit(nr,addr)//将addr中的nr位清0  并保证原子性
			change_bit(nr,addr)//将addr中的nr位取反  并保证原子性    
			......
		2.2 整型原子操作				
			typedef struct {
				  int counter;
			} atomic_t;
	      围绕这个数据结构提供了一套API
	      atomic_add(i,v)//原子变量的值增加i
	      atomic_sub(i,v)//原子变量的值减少i
	      atomic_set(v,i)//原子变量的值设置为i
	      atomic_read(v)//返回原子变量的值
	      #define atomic_inc(v)     atomic_add(1, (v))//原子变量的值增加1
	      #define atomic_dec(v)     atomic_sub(1, (v))//原子变量的值减1
	      atomic_dec_and_test(atomic_t * v);
	      //原子变量v自减 测试结果是否为0 如果为0返回Ture
//定义原子变量
atomic_t btn_tv;


//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
        //if (--cnt != 0)
        if (!atomic_dec_and_test(&btn_tv))
        {
                //cnt++;
                //这个逻辑可以保证只有一个进程访问
                //但是从汇编的角度看,是有bug的
                atomic_inc(&btn_tv);
                return -EAGAIN;
        }
        return 0;
}
int btn_release (struct inode *inode, struct file *filp)
{
        //cnt++;
        atomic_inc(&btn_tv);
        return 0;
}
int __init btn_drv_init (void)
{
        //1.申请设备号
        alloc_chrdev_region (&dev, 0, 1, "btn");

        //2.初始化cdev
        cdev_init (&btn_cdev, &btn_fops);
        //3. 注册cdev
        cdev_add (&btn_cdev, dev, 1);
        //4. 自动生成设备文件
        btn_cls = class_create (THIS_MODULE, "mybuttons");

        device_create(btn_cls, NULL, dev, NULL, "buttons");

        //初始化原子变量
        atomic_set (&btn_tv, 1);

        return 0;
}
		insmod btn_drv.ko
		./test
		telnet 192.168.1.6
		./test
		
		2.3 自旋锁
			2.3.1 核心数据结构:
				struct spinlock //里面的任何一个成员变量都不需要关注
			2.3.2 使用方法
				获取自旋锁
				执行临界区代码 访问共享资源
				释放自旋锁
			2.3.3 特点
				自旋锁只能有一个持有单元
				如果试图获取一个已经被其他执行单元持有的自旋锁
				获取自旋锁不成功,原地自旋等待 直到获取成功为止
			2.3.4 具体使用步骤
				1)定义一把自旋锁
					struct spinlock btn_lock;
				2)初始化自旋锁
					spin_lock_init (&btn_lock);
				3)  获取锁
					spin_lock (&btn_lock);//注意获取不成功的话,原地自旋等待
				4) 执行临界区代码 访问共享资源
				5)释放自旋锁
					spin_unlock (&btn_lock);
			2.3.5 注意事项
				1) 获取 释放自旋锁要成对出现
				2) 持有自旋锁的时间要尽量短  
					临界区中不应该出现睡眠函数
					A任务获取自旋锁成功后 内核几乎不做任务调度
					内核希望A任务尽快执行完临界区代码 
					尽快释放自旋锁
					不要产生获取锁不成功 原地自旋的情况
				3)避免死锁的情况
					出现死锁的情况有两种:
						a) 连续获取同一把锁两次
							spin_lock(&btn_lock);
    						spin_lock(&btn_lock);
						b) 	A任务      B任务
								...				...
								获取m锁	获取n锁
								...				...
								获取n锁	获取m锁
								...				...
								释放n锁	释放m锁
								...				...
								释放m锁	释放n锁
						spin_lock改为spin_trylock //获取自旋锁不成功 立即返回错误
						A任务中spin_trylock 获取m锁成功
						再执行spin_trylock 获取n锁,一旦失败放弃m锁,过段时间
						重新获取m锁,获取n锁		
//定义自选锁
struct spinlock btn_lock;

//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
        //获取自选锁
        spin_lock(&btn_lock);
        if (--cnt != 0)
        {
                cnt++;
                spin_unlock (&btn_lock);
                //这个逻辑可以保证只有一个进程访问
                //但是从汇编的角度看,是有bug的
                return -EAGAIN;
        }
        //释放自选锁
        spin_unlock(&btn_lock);
        return 0;
}
int btn_release (struct inode *inode, struct file *filp)
{
        spin_lock (&btn_lock);
        cnt++;
        spin_unlock (&btn_lock);
        return 0;
}
				注意,此时B进程无法访问共享资源,并不是由于获取不到锁导致的,
				如果获取不到,则会原地自旋等待,而不是直接返回。
				那么如何出现原地自旋等待的现象呢?
				加睡眠函数
//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
        //获取自选锁
        spin_lock(&btn_lock);
        //msleep (1000*10);
        mdelay (1000*10);

        if (--cnt != 0)
        {
                cnt++;
                spin_unlock (&btn_lock);
                //这个逻辑可以保证只有一个进程访问
                //但是从汇编的角度看,是有bug的
                return -EAGAIN;
        }
        //释放自选锁
        spin_unlock(&btn_lock);
        return 0;
}

		临界区代码执行起来就是比较耗时,就考虑使用信号量
		来保护对共享资源的访问
		
		2.4	信号量
			2.4.1 核心数据结构	
				它是基于自旋锁机制实现的,它本质上就是一个计数器
				struct semaphore {
					raw_spinlock_t		lock;
					unsigned int		count;
					struct list_head	wait_list;
				};	
			2.4.2 使用方法
				获取信号量
				执行临界区代码 访问共享资源
				释放信号量
			2.4.3 特点
				信号量有多个持有单元
				获取信号量本质上就是count 计数器减1
				最小减到0,已经为0 再去获取信号量(-1)就会失败
				获取信号量失败 睡眠等待
			2.4.4 具体使用步骤
				1) 定义信号变量
					struct semaphore btn_sem;
				2) 完成初始化
					sema_init (&btn_sem,1);
				3) 获取信号量
					//成功直接返回 失败调用者进入睡眠状态
					down (&btn_sem);
					//失败 调用者进入可中断的睡眠状态
					down_intrruptiable (&btn_sem);
					down_killbale
					//失败直接返回错误信息
					down_trylock
					down_timeout 
					临界区代码 访问共享资源
					释放信号量 (+1)
				4) 释放信号量
					up(&btn_sem); (+1)
struct semaphore btn_sem;

//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
        //获取信号量
        down(&btn_sem);
        //mdelay(1000*10);
        msleep(3*1000);
        if (--cnt != 0)
        {
                cnt++;
                //这个逻辑可以保证只有一个进程访问
                //但是从汇编的角度看,是有bug的
                return -EAGAIN;
        }
        //释放信号量
        up(&btn_sem);
        return 0;
}
int btn_release (struct inode *inode, struct file *filp)
{
        down(&btn_sem);
        cnt++;
        up(&btn_sem);
        return 0;
}
 //初始化信号量
 sema_init (&btn_sem,1);

		自旋锁和信号量的区别:
			1) 信号量保护的临界区 对执行时间长短无要求
				自旋锁保护的临界区 要求执行速度尽量快
					执行时间一旦过长 很容易造成另一个进程获取锁不成功
					原地自旋 系统性能下降
			2)获取自旋锁不成功 原地自旋
				 获取信号量不成功 睡眠等待
struct semaphore btn_sem;
//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
        //获取信号量 -1操作
        //down(&btn_sem);
        if(down_interruptible(&btn_sem))//被信号打断
    	{
        	return -EAGAIN;
    	}
        return 0;
}

int btn_release (struct inode *inode, struct file *filp)
{
        //释放信号量 +1
        up(&btn_sem);
        return 0;
}
//初始化信号量
sema_init (&btn_sem,1);

	1.设备的阻塞方式访问
	
	//有数据直接返回
	//无数据睡眠等待 一旦有了数据立即返回
	回顾recv函数:
	recv (sd, buf, len, flags)
	希望按键设备也能实现以上效果:内核中的等待队列
	
	核心数据结构:
	wait_queue_head_t
	使用步骤:
  	1)定义等待队列头变量
    	wait_queue_head_t  btn_wqh;
  	2) 初始化等待队列头变量
    	init_waitqueue_head(&btn_wqh)
    	以上两步可以使用DECLARE_WAIT_QUEUE_HEAD(btn_wqh)替换
  	3)无数据供用户空间读, 需要阻塞睡眠的位置调用
    	 /*
       	condition,条件
        	          该表达式为False 执行睡眠动作
            	      该表达式为True 立即返回
      	*/
     	wait_event(btn_wqh, condition)
     	wait_event_interruptible(wq, condition)
     
  	4) 一旦有数据供用户空间读 唤醒睡眠的进程
    	wake_up(&btn_wqh)
     	wake_up_interruptible(&btn_wqh) 
		    __wait_event_interruptible
			 {
     			//定义并初始化了等待队列 
     			//(链表中的一个节点,节点中记录了当前进程的新为了将来唤醒时使用)
     			DEFINE_WAIT(__wait);
    		   //将当前进程状态设置为TASK_INTERRUPTIBLE(可中断)状态
     		  	prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);	
     		  	//任务调度 从就绪状态的进程集合中选出一个放入cpu中执行
	           schedule();
 		   }

vi test.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>


int main (void)
{
	char val = 0;
	int fd = open("/dev/buttons", O_RDONLY);
	if (fd < 0)
	{
		perror("open failed!\n");
		return -1;
	}

	printf("open successed!\n");

	while (1) 
	{
		read(fd, &val, 1);
		printf("val = %#x\n",val);
	}

	close(fd);
	return 0;
}

vi btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/delay.h>

#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");

typedef struct btn_desc {
    int irq;  //中断号
    char *name;//名称
    char val ;//按键值
}btn_desc_t;


btn_desc_t buttons[]=
{
    {IRQ_GPIO_A_START+28, "up", 0x10},
    {IRQ_GPIO_B_START+30, "down", 0x20},
    {IRQ_GPIO_B_START+31, "left", 0x30},
    {IRQ_GPIO_B_START+9,  "right", 0x40},
};


/*按键缓冲区*/
char key_buf = 0;
/*
 *记录按键缓冲区是否有按键值的
 * =0, 无键值
 * =1, 有键值
 * */
volatile int ev_press = 0;

struct cdev btn_cdev;
dev_t dev;
struct class *btn_cls = NULL;

struct semaphore btn_sem;

/*定义并初始化等待队列头变量*/
DECLARE_WAIT_QUEUE_HEAD(btn_wqh);
irqreturn_t btn_isr(int irq, void *dev) 
{

    /*获取哪个按键触发的中断*/
    btn_desc_t *pdata = dev;
    
    /*保存键值*/
    key_buf = pdata->val;
    ev_press = 1;
    /*唤醒因无数据可读而睡眠的进程*/
    wake_up_interruptible(&btn_wqh);
    return IRQ_HANDLED;
}

//返回非0 用户空间打开设备就是失败的
int btn_open (struct inode *inode, struct file *filp)
{
	//获取信号量 -1操作
	if (down_interruptible(&btn_sem)) 
	{
		return -EAGAIN;
	}

	return 0;
}
int btn_release (struct inode *inode, struct file *filp)
{
	//释放信号量 +1
	up(&btn_sem);
	return 0;
}

ssize_t btn_read(struct file *filp,
                 char __user *buf, size_t len,
                 loff_t *offset)
{
    	int ret = 0;
    	/*
     	*ev_press =0 意味着按键缓冲区无键值 进程睡眠
        	       =1                 有     进程继续执行
     	* */
	
	wait_event_interruptible(btn_wqh, ev_press);
	ret = copy_to_user(buf, &key_buf, len);
	ev_press = 0;

	return len;

}
struct file_operations btn_fops = 
{
	.owner = THIS_MODULE,
	.open = btn_open,
	.release = btn_release,
	.read = btn_read,
};
int __init btn_drv_init (void)
{
	int i;
	int ret;
	//1.申请设备号
	alloc_chrdev_region (&dev, 0, 1, "btn");

	//2.初始化cdev
	cdev_init (&btn_cdev, &btn_fops);
	//3. 注册cdev
	cdev_add (&btn_cdev, dev, 1);
	//4. 自动生成设备文件
	btn_cls = class_create (THIS_MODULE, "mybuttons");
	device_create(btn_cls, NULL, dev, NULL, "buttons");

	for(i=0; i<ARRAY_SIZE(buttons); i++)
	{
		ret = request_irq(buttons[i].irq,
				btn_isr,
				IRQF_TRIGGER_FALLING,
				buttons[i].name, 
				&(buttons[i]));
		if(ret)
		{
			printk("<2>"  "request irq failed!");
			while(i)
			{
				i--;
				free_irq(buttons[i].irq, 
						&(buttons[i]));
			}
			return -EAGAIN;
		}
	}
	//初始化信号量
	sema_init (&btn_sem,1);
	return 0;
}
void __exit btn_drv_exit (void)
{
	//销毁设备文件
	device_destroy (btn_cls, dev);
	class_destroy (btn_cls);
	//注销cdev
	cdev_del (&btn_cdev);
	//注销设备号
	unregister_chrdev_region (dev, 1);

}

module_init(btn_drv_init);
module_exit(btn_drv_exit);
	2、按键抖动
	1) 定时器
		检测每个下降沿的触发的时间,如果一个下降沿的时间超过10ms,
		才认为确实按下了按键。

在这里插入图片描述

	vi btn_drv.c
struct timer_list btn_timer;
irqreturn_t btn_isr(int irq, void *dev)
{
	//传递按键的参数信息
    btn_timer.data = (unsigned long)dev;
    mod_timer(&btn_timer, jiffies+HZ/10);
    return IRQ_HANDLED;
}
void btn_timer_func(unsigned long data)
{
    /*获取哪个按键触发的中断*/
    btn_desc_t *pdata = (btn_desc_t *)data;
    /*保存键值*/
    key_buf = pdata->val;
    ev_press = 1;//按键缓冲区中有键值
    /*唤醒因无数据可读而睡眠的进程*/
    wake_up_interruptible(&btn_wqh);
}

init_timer (&btn_timer);
btn_timer.function = btn_timer_func;
	3. 按下和释放都关注
		问题1:希望按下和释放都监测 触发中断
			datasheet :让gpio控制器检测上升沿下降沿触发,
			在GPIOxDETMOD0,1,EX这三个寄存器中规定了检测的模式
			request_irq (xxx, btn_isr, IRQF_TRIGGER_FALLING...);
			request_irq (xxx, btn_isr, IRQF_TRIGGER_RISING...);
			//以上这种写法不可以,因为是同一个中断号
			request_irq (xxx, btn_isr, IRQF_TRIGGER_FALLING | 
			IRQF_TRIGGER_RISING );
		问题2:
			如何判断该键是按下触发还是释放触发的?
			可以在保存按键值之前, 通过读取管脚的电平状态
			或者是按下触发还是释放触发,gpio库函数
		问题3:
			驱动程序中判断出是哪个按键触发的
			而且可以判断出是按下触发的还是释放触发的。
			这个信息如何反馈用户空间去
			传不同的值到用户空间
#include <linux/init.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>


MODULE_LICENSE("GPL");

struct timer_list btn_timer;

typedef struct btn_desc{
    int gpio; //管脚编号
    int irq;  //中断号
    char *name;//名称
    char val ;//按键值
}btn_desc_t;

btn_desc_t buttons[]=
{
    {PAD_GPIO_A + 28, IRQ_GPIO_A_START +28 ,"up", 0x10},
    {PAD_GPIO_B + 30, IRQ_GPIO_B_START +30,"down", 0x20},
    {PAD_GPIO_B + 31, IRQ_GPIO_B_START +31,"left", 0x30},
    {PAD_GPIO_B + 9,  IRQ_GPIO_B_START +9,"right", 0x40},
};
/*按键缓冲区*/
char key_buf = 0;
/*
 *记录按键缓冲区是否有按键值的
 * =0, 无键值
 * =1, 有键值
 * */
volatile int ev_press = 0;
struct cdev btn_cdev;
struct class *cls = NULL;
dev_t  dev = 0;

struct semaphore btn_sem;

/*定义并初始化等待队列头变量*/
DECLARE_WAIT_QUEUE_HEAD(btn_wqh);

irqreturn_t btn_isr(int irq, void *dev)
{
    /*传递按键描述信息*/
    btn_timer.data = (unsigned long)dev;
    /*启动定时器 从当前时间开始延时10ms*/
    mod_timer(&btn_timer, jiffies+HZ/100);
    return IRQ_HANDLED;
}

void btn_timer_func(unsigned long data)
{
    int status = 0;
    /*获取哪个按键触发的中断*/
    btn_desc_t *pdata = (btn_desc_t *)data;

    //判断是按下触发还是释放触发
    status = gpio_get_value (pdata->gpio);
    
    /*保存键值*/
    key_buf = pdata->val + !!status;//两个逻辑取反保证值只有0,1
    ev_press = 1;

    /*唤醒因无数据可读而睡眠的进程*/
    wake_up_interruptible(&btn_wqh);
}
int btn_open(struct inode *inode, struct file *filp)
{
    /*获取信号量*/
    //down(&btn_sem);
    if(down_interruptible(&btn_sem))//被信号打断
    {
        return -EAGAIN;
    }

    return 0;
}
int btn_release(struct inode *inode, struct file *filp)
{
    up(&btn_sem);
    return 0;
}
ssize_t btn_read(struct file *filp,
                 char __user *buf, size_t len,
                 loff_t *offset)
{
    int ret = 0;
    /*
     *ev_press =0 意味着按键缓冲区无键值 进程睡眠
               =1                 有     进程继续执行
     * */
    wait_event_interruptible(btn_wqh, ev_press);

    /*将按键值返回到用户空间*/
    ret = copy_to_user(buf, &key_buf, len);
    ev_press = 0;
    return len;
}
struct file_operations btn_fops =
{
    .owner = THIS_MODULE,
    .open = btn_open,
    .release = btn_release,
    .read = btn_read,
};
int __init btn_drv_init(void)
{
    int ret = 0;
    int i = 0;
    
    /*申请注册设备号*/
    alloc_chrdev_region(&dev, 0, 1, "buttons");
    /*初始化cdev*/
    cdev_init(&btn_cdev, &btn_fops);
    /*注册cdev*/
    cdev_add(&btn_cdev, dev, 1);
    /*自动生成设备文件*/
    cls = class_create(THIS_MODULE, "buttons");
    device_create(cls, NULL, dev, NULL, "mybuttons");

    gpio_request (buttons[i].gpio, buttons[i].name);

    for(i=0; i<ARRAY_SIZE(buttons); i++)
    {
        ret = request_irq(buttons[i].irq,
                          btn_isr,
                          IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                          buttons[i].name, 
                          &(buttons[i]));
        if(ret)
        {
            printk("<2>"  "request irq failed!");
            while(i)
            {
                i--;
                free_irq(buttons[i].irq, 
                         &(buttons[i]));
            }
            return -EAGAIN;
        }
    }
    sema_init(&btn_sem, 1);
    
    init_timer(&btn_timer);
    btn_timer.function = btn_timer_func;

    return 0;
}
void __exit btn_drv_exit(void)
{
    int i = 0;

    for(; i<ARRAY_SIZE(buttons); i++)
    {
        free_irq(buttons[i].irq, buttons+i);
	gpio_free(buttons[i].gpio);
    }
    /*销毁设备文件*/
    device_destroy(cls, dev);
    class_destroy(cls);
    /*注销cdev*/
    cdev_del(&btn_cdev);
    /*注销设备号*/
    unregister_chrdev_region(dev, 1);
}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
	vi test.c
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
    int fd = 0;
    char val = 0;
    fd = open("/dev/mybuttons", O_RDONLY);

    if(fd < 0)
    {
        perror("open failed");
        return -1;
    }
    printf("open successed!\n");

    while(1)
    {
        read(fd, &val, 1);
        printf("val = %#x\n", val);
    }

    close(fd);
    return 0;
}

	4.设备的非阻塞方式访问
		open()----> O_NONBLOCK
		//默认以阻塞方式访问
		fd = open("/dev/mybuttons", O_RDONLY);
		//以非阻塞方式打开设备 后续读写未就绪时 直接返回错误,不阻塞
		fd = open("/dev/mybuttons", O_RDONLY|O_NONBLOCK);
		struct file //fs.h
		{
			const struct file_operations	*f_op;
			unsigned int 		f_flags;
		}

在这里插入图片描述

	vi  btn_drv.c
#include <linux/init.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/interrupt.h>
#include <mach/platform.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>


MODULE_LICENSE("GPL");

struct timer_list btn_timer;

typedef struct btn_desc{
    int gpio; //管脚编号
    int irq;  //中断号
    char *name;//名称
    char val ;//按键值
}btn_desc_t;

btn_desc_t buttons[]=
{
    {PAD_GPIO_A + 28, IRQ_GPIO_A_START +28 ,"up", 0x10},
    {PAD_GPIO_B + 30, IRQ_GPIO_B_START +30,"down", 0x20},
    {PAD_GPIO_B + 31, IRQ_GPIO_B_START +31,"left", 0x30},
    {PAD_GPIO_B + 9,  IRQ_GPIO_B_START +9,"right", 0x40},
};
/*按键缓冲区*/
char key_buf = 0;
/*
 *记录按键缓冲区是否有按键值的
 * =0, 无键值
 * =1, 有键值
 * */
volatile int ev_press = 0;
struct cdev btn_cdev;
struct class *cls = NULL;
dev_t  dev = 0;

struct semaphore btn_sem;

/*定义并初始化等待队列头变量*/
DECLARE_WAIT_QUEUE_HEAD(btn_wqh);

irqreturn_t btn_isr(int irq, void *dev)
{
    /*传递按键描述信息*/
    btn_timer.data = (unsigned long)dev;
    /*启动定时器 从当前时间开始延时10ms*/
    mod_timer(&btn_timer, jiffies+HZ/100);
    return IRQ_HANDLED;
}

void btn_timer_func(unsigned long data)
{
    int status = 0;
    /*获取哪个按键触发的中断*/
    btn_desc_t *pdata = (btn_desc_t *)data;

    //判断是按下触发还是释放触发
    status = gpio_get_value (pdata->gpio);
    
    /*保存键值*/
    key_buf = pdata->val + !!status;//两个逻辑取反保证值只有0,1
    ev_press = 1;

    /*唤醒因无数据可读而睡眠的进程*/
    wake_up_interruptible(&btn_wqh);
}

int btn_open(struct inode *inode, struct file *filp)
{
    /*获取信号量*/
    //down(&btn_sem);
    if(down_interruptible(&btn_sem))//被信号打断
    {
        return -EAGAIN;
    }

    return 0;
}
int btn_release(struct inode *inode, struct file *filp)
{
    up(&btn_sem);
    return 0;
}
ssize_t btn_read(struct file *filp,
                 char __user *buf, size_t len,
                 loff_t *offset)
{
    int ret = 0;
    //if (ev_press == 0 && 非阻塞方式访问)
    //	    return -EAGAIN;
    //
    //是不是非阻塞的方式访问的,其实上是用户空间的flag把filp->f_flags中
    //的某一位给清0或者置1了
    if (ev_press == 0 && filp->f_flags&O_NONBLOCK)
    {
	    return -EAGAIN;
    }
    /*
     *ev_press =0 意味着按键缓冲区无键值 进程睡眠
               =1                 有     进程继续执行
     * */
    wait_event_interruptible(btn_wqh, ev_press);

    /*将按键值返回到用户空间*/
    ret = copy_to_user(buf, &key_buf, len);
    ev_press = 0;//按键缓冲区中没有值
    return len;
}
struct file_operations btn_fops =
{
    .owner = THIS_MODULE,
    .open = btn_open,
    .release = btn_release,
    .read = btn_read,
};
int __init btn_drv_init(void)
{
    int ret = 0;
    int i = 0;
    
    /*申请注册设备号*/
    alloc_chrdev_region(&dev, 0, 1, "buttons");
    /*初始化cdev*/
    cdev_init(&btn_cdev, &btn_fops);
    /*注册cdev*/
    cdev_add(&btn_cdev, dev, 1);
    /*自动生成设备文件*/
    cls = class_create(THIS_MODULE, "buttons");
    device_create(cls, NULL, dev, NULL, "mybuttons");

    gpio_request (buttons[i].gpio, buttons[i].name);

    for(i=0; i<ARRAY_SIZE(buttons); i++)
    {
        ret = request_irq(buttons[i].irq,
                          btn_isr,
                          IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                          buttons[i].name, 
                          &(buttons[i]));
        if(ret)
        {
            printk("<2>"  "request irq failed!");
            while(i)
            {
                i--;
                free_irq(buttons[i].irq, 
                         &(buttons[i]));
            }
            return -EAGAIN;
        }
    }
    sema_init(&btn_sem, 1);
    
    init_timer(&btn_timer);
    btn_timer.function = btn_timer_func;

    return 0;
}
void __exit btn_drv_exit(void)
{
    int i = 0;

    for(; i<ARRAY_SIZE(buttons); i++)
    {
        free_irq(buttons[i].irq, buttons+i);
	gpio_free(buttons[i].gpio);
    }
    /*销毁设备文件*/
    device_destroy(cls, dev);
    class_destroy(cls);
    /*注销cdev*/
    cdev_del(&btn_cdev);
    /*注销设备号*/
    unregister_chrdev_region(dev, 1);
}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
	vi test.c
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
    int fd = 0;
    char val = 0;
    //以非阻塞的方式打开
    fd = open("/dev/mybuttons", O_RDONLY|O_NONBLOCK);

    if(fd < 0)
    {
        perror("open failed");
        return -1;
    }
    printf("open successed!\n");

    while(1)
    {
        if(read(fd, &val, 1) == -1)
        {
                perror("read failed!\n");
                sleep(1);
                continue;
        }
        printf("val = %#x\n", val);
    }
    close(fd);
    return 0;
}

按键驱动程序:

上述“上升沿下降沿都关注的代码”可以认为时按键驱动程序的第一版
1.第一版存在的问题:
	1.1 按键值标准化的问题:
		up   0x10/0x11
		down 0x20/0x21
		...
	1.2 按键缓冲区的问题:
		之前的版本,按键缓冲区只有一个字节,之所以没有暴露出问题,是因为
		测试程序写得好,因为应用程序只是在不断的读取键值。
		但是时间间隔一旦拉长,会出现丢键值的现象。
		在test.c的while循环中可以加延时,之后可以看到效果。
		本质上是一个循环队列 链式存储
		考虑竞态的问题

在这里插入图片描述
在这里插入图片描述

	1.3 按住不放的问题
		检测到下降沿之后
		保存键值
		开启另外一个新的定时器该定时器每隔一段时间就记录一次按键值
		直到该按键产生了释放动作为止
		ioctl ();//调节按键的灵敏度
		
2. input子系统
	2.1 什么是input子系统
		就是内核中的一部分代码
		其中最核心的文件
		drivers/input/input.c
	2.2 input子系统的作用:
		linux 内核中支持了大量的硬件驱动程序
		发现其中有一部分硬件,只有输入没有输出
		例如,按键,键盘,鼠标, 触摸屏,	游戏摇杆
		这类硬件在实现驱动时
			设备号申请注册
			cdev的初始化注册
			设备文件的创建
			操作函数集合
				read
					使用等待队列 实现阻塞方式访问
				键值缓冲区 (循环等待队列)
				按住不放的问题
		linux内核的设计者将输入设备驱动程序的通用代码
		在内核中实现好(表现为drivers/input目录下的一系列文件)
		当驱动程序在实现输入设备驱动程序的时候,可以复用这部分代码
		驱动工程师只需要在该输入设备特有的硬件相关代码实现
		驱动即可完成。
		其作用:简化输入设备的驱动编程工作
	2.3 input子系统的使用方式
		核心数据结构:
		struct input_dev {
			const char *name;
			//evbit: bitmap of types of events supported by the device
			unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
			//keybit: bitmap of keys/buttons this device has
			unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];				
		};
		使用步骤:
			1) 定义一个struct input_dev类型的变量
				struct input_dev *input_allocate_device(void);
			2) 初始化input_dev 变量
			3) 注册input_dev变量
			4) 硬件操作
					注册中断
					延时去抖
					保存按键值
			5)报告事件
					保存键值
					唤醒睡眠进程
					void input_event(struct input_dev *dev,
							 unsigned int type, unsigned int code, int value)
						dev, 哪个输入设备报告的事件
						type, 报告的事件类型
						code, 当type=EV_KEY时,按键的编码值
						value, 当type = EV_KEY时, 
								= 1 ,按下触发
								= 0,释放触发
								= 2,  按住不放
			6) 注销input_dev变量
				void input_unregister_device(struct input_dev *dev);
			7) 释放input_dev变量
				void input_free_device(struct input_dev *dev);
			input core 模块中支持多种事件:
			#define EV_SYN			0x00 //同步事件
			#define EV_KEY			0x01 //按键事件
			#define EV_REL			0x02 //相对坐标(鼠标)
			。。。。。。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/input.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/interrupt.h>

MODULE_LICENSE("GPL");
struct input_dev * btn_input = NULL;
typedef struct btn_desc
{
	int gpio;
	int irq;
	char *name;
	char code;//按键编码
}btn_desc_t;

btn_desc_t buttons[] = 
{
	{PAD_GPIO_A + 28, IRQ_GPIO_A_START + 28, "up", KEY_UP},
	{PAD_GPIO_B + 30, IRQ_GPIO_B_START + 30, "down", KEY_DOWN},
	{PAD_GPIO_B + 31, IRQ_GPIO_B_START + 31, "left", KEY_LEFT},
	{PAD_GPIO_B + 9,  IRQ_GPIO_B_START + 9, "right", KEY_RIGHT},

};
struct timer_list btn_timer;
irqreturn_t btn_isr(int irq, void *dev)
{
	//传递按键信息
	btn_timer.data = (unsigned long)dev;
	//延时去除抖动
	mod_timer (&btn_timer, jiffies + HZ/100);
	return IRQ_HANDLED;
}
void btn_timer_func(unsigned long data)
{
	int stat = 0;
	//获取哪个按键触发的
	btn_desc_t * pdata = (btn_desc_t *)data;
	//判断是按下触发还是释放触发
	stat = gpio_get_value (pdata->gpio);
	//保存按键值,唤醒睡眠的进程
	//5. 报告事件
	input_event (btn_input, EV_KEY, pdata->code, !stat);
	//加上一个同步事件,代表报告完毕	
	input_event (btn_input, EV_SYN, 0, 0);
}
int __init btn_drv_init(void)
{
	int i = 0;
	int ret = 0;
	//1.申请input_dev变量空间
	btn_input = input_allocate_device ();
	//2.初始化input_dev变量
	//2.1 name赋值
	btn_input->name = "mybuttons";
	//strcpy(btn_input->name, "mybuttons");
	//2.2 evbit赋值 设置硬件将来要产生的事件类型
	set_bit (EV_SYN, btn_input->evbit);
	set_bit (EV_KEY, btn_input->evbit);
	//可以解决按住不放的问题
	set_bit (EV_REP, btn_input->evbit);

	//2.3 设置EV_KEY事件时会报告的按键的编码
	for (; i < ARRAY_SIZE(buttons);i++)
	{
		set_bit (buttons[i].code, btn_input->keybit);
	}
	//3 注册input_dev变量
	ret = input_register_device (btn_input);
	//4 硬件操作
	for (i = 0; i < ARRAY_SIZE(buttons);i++)
	{
		ret = request_irq (buttons[i].irq,
				btn_isr,
				IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
				buttons[i].name,
				&(buttons[i])
				);
	}
	init_timer(&btn_timer);
	btn_timer.function = btn_timer_func;
	return 0;
}
void __exit btn_drv_exit(void)
{
	int i = 0;
	//停止定时器
	del_timer(&btn_timer);
	for (;i < ARRAY_SIZE(buttons);i++)
	{
		free_irq(buttons[i].irq, &(buttons[i]));
	}
	//6. 注销input_dev
	input_unregister_device (btn_input);
	//7. 释放input_dev空间
	input_free_device (btn_input);
}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
	ls /dev/input/event*
	/dev/input/event0  /dev/input/event2  /dev/input/event4
	/dev/input/event1  /dev/input/event3
	insmod btn_drv.ko
	/dev/input/event0  /dev/input/event2  /dev/input/event4
	/dev/input/event1  /dev/input/event3  /dev/input/event5
	evnt5是自动创建出来的
	hexdump
	序号		秒      微秒    type  code   value
	00001c0 5ee7 54a5 3ac6 000a 0001 0067 0001 0000
	00001d0 5ee7 54a5 3aca 000a 0000 0000 0000 0000
	00001e0 5ee7 54a5 2700 000c 0001 0067 0000 0000
	00001f0 5ee7 54a5 2704 000c 0000 0000 0000 0000
	如何确认哪个对应的是触摸屏的设备文件?
	hexdump /dev/input/event*
	cat /proc/bus/input/devices
		通过name = "xxx" 来判断
	vi test.c	
#include <stdio.h>
#include <fcntl.h>
#include <linux/input.h>

struct key 
{
  unsigned long sec;//秒
  unsigned long usec;//微秒
  short  type; //事件类型
  short code;//按键编码
  unsigned int val;
}keyval;

int main(void)
{
  int fd = open("/dev/input/event5",O_RDONLY);
  while (1) 
  {
    read(fd, &keyval, sizeof(keyval));
    if (keyval.type == EV_KEY)
    {   
      printf("type = %d code = %d value = %d \n", 
          keyval.type, keyval.code, keyval.val);
    }   
  }
  close (fd);
  return 0;
}
3. 研究input子系统的代码

在这里插入图片描述
input.c

			input_init(void)
			{	
			//只要主设备号为13,其对应的操作函数集合就是	input_fops
			register_chrdev_region(INPUT_MAJOR, "input",&input_fops);
			
			} 	
			open ("/dev/input/event5",......);				
			---------------------------------------------------
			struct  file xxx_file
			xxx_file.f_op = &input_fops
			sys_open	
			{
				major = 13-----> input_fops
				input_fops.open ()
				{
					//根据次设备号找到该设备匹配的handler
					handler = input_table[iminor(inode) >> 5];
					//取得handler中的操作函数集合evdev_fops
					new_fops = fops_get(handler->fops);
					//xxx_file.f_op = &evdev_fops 
				}
			} 

在这里插入图片描述

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘德华海淀分华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值