Linux驱动学习

开发板文件系统操作指令

指令 功能 备注
insmod+xxx.ko 向开发板内核添加驱动文件
rmmod+xxx.ko 从开发板内核中移除对应的驱动文件
ls /sys/bus/xxxbus/devices/ 查看xxx总线中的设备文件 需要添加总线和设备文件
ls /sys/bus/xxxbus/drivers/ 查看xxx总线中的驱动文件 需要添加总线和驱动文件
ls /dev/xxx 查看添加的设备文件 就是device_create()第5个参数
ls /proc 查看设备号
./xxx 执行xxx可执行文件 需要添加可执行文件
ls /proc/device-tree/ 查看添加到设备树上的节点 要在设备树文件中添加节点并重新编译,拷贝编译后的设备树文件到tftpdir目录,重启开发板,利用tftp方式下载到开发板中。
ls /proc/interrupts/ 查看申请的中断 就是request_irq()的第4个参数

ubunt重要文件路径

注:该路径为个人路径

路径 描述 备注
/home/ubuntu/code/kernel/linux-3.14 Linux-3.14内核路径 包含众多内核文件
/home/ubuntu/tftpdir tftp服务路径 包含tftp服务上传下载文件
/home/ubuntu/nfsdir/rootfs nfs服务路径 包含nfs服务共享的文件(根文件系统)
/home/ubuntu/ARM_Tools/gcc-4.6.4/bin/arm-none-linux-gnueabi-gcc gcc交叉编译工具路径
/home/ubuntu/code/kernel/linux-3.14/arch/arm/boot/dts 设备树文件

驱动文件框架

/* 头文件 */
#include 

/* 函数入口实现 */
static int __init chr_dev_init(void)
{
	printk("-----%s-----\n",__FUNCTION__);
	return 0;
}

static void __exit chr_dev_init(void)
{
	printk("-----%s-----\n",__FUNCTION__);
}

/* 模块入口声明 */
module_init(chr_dev_init);
module_exit(chr_dev_init);
MODULE_LICENSE("GPL");

Makefile脚本

KERNEL_DIR=/home/ubuntu/code/kernel/linux-3.14
CUR_DIR=$(shell pwd)
MODULE_NAME=module
APP_NAME=led_app
ROOTFS_DIR=/home/ubuntu/nfsdir/rootfs/drv_module

ifeq ($(KERNELRELEASE), )
all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
	/home/ubuntu/ARM_Tools/gcc-4.6.4/bin/arm-none-linux-gnueabi-gcc $(APP_NAME).c -o $(APP_NAME)
clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
install:
	cp *.ko $(ROOTFS_DIR)
	cp $(APP_NAME).o $(ROOTFS_DIR)
else
	obj-m += $(MODULE_NAME).o
	
endif

应用层实现底层硬件的控制原理

首先应用程序想要实现底层硬件的控制,必须要通过内核去调用设备驱动来控制底层硬件,所以说直接控制硬件的是内核中的设备驱动。那么应用程序又是如何让设备驱动去控制硬件的呢?首先需要在内核中添加应用程序想要控制的硬件的设备驱动,让内核知道有这个驱动设备,有了想要的设备驱动,然后让应用程序关联这个设备驱动,实现应用程序 调用文件IO,设备驱动执行对应的接口函数。我们可以在这个接口函数中,编写程序实现对硬件寄存器的控制(地址的控制),最终实现硬件的控制。

这里有几个知识点

问题1、如何在内核中添加应用层想要的设备驱动?

答:通过创建字符设备文件(设备节点)来实现。这个字符设备文件其实就是一个设备信息结构体,

其中存放了我们想要的设备驱动信息,其中包含了,字符设备的信息内容、我们想要创建的设备驱动的设备号、以及给我们想要创建的设备驱动取得名字等信息。然而这些作为该结构体成员的设备驱动的具体参数值(内容),需要我们去创建出来,然后利用一个返回值为该结构体类型的指针函数struct device *device_create(),将这些参数值(内容)传到这个结构体中。这样才算创建完了一个字符设备文件,此时的字符设备文件中相当于被初始化了,将传入的参数值(内容)作为对应成员的值(内容)。

​ 这里又有个问题:

问题1.1怎么创建该结构体(字符设备文件)的具体参数值(内容)呢?

​ 在回答这个问题前,我们先要知道,这个结构体(设备文件)需要我们创建哪些具体的参数值(内容):创建的设备文件的信息内容、父类对象、设备号、私有数据、设备文件名。好,明白了要创建什么,现在就要知道,怎么创建

  1. 参数1的具体值(内容)-------设备文件的信息内容

    通过 class_create()函数创建
    struct class * class_create(owner,name);
    例如:struct class * cls_led = class_create(THIS_MODULE,"led cls");
    
    参数1:
    	owner:拥有者,一般THIS_MODULE
    参数2:
    	name:字符串,描述信息
    
    返回值:
    	struct class * 
    	信息结构体	
    
  2. 参数2的具体值(内容)-------创建父类对象

    这个参数一般为NULL
    
  3. 参数3的具体值(内容)--------设备号

    在创建设备号之前需要明白:
    设备号:32bit = 主设备号(高12bit) + 次设备号(低20bit)
    			主设备号:表示同一类设备
    			次设备号:表示同一类设备中的不同设备
    
    方式1:
    其实以下两步操作可以通过一个封装好的函数来实现  -------MKDEV(主设备号,次设备号),返回值就是设备号
    
    方式2:
    我们只需要设置主设备号,次设备号自动分配
    /* 向内核申请注册设备号 */
    int register_chrdev(	unsigned int major,
    					const char *name,
    					const struct file_operations *fops)
    
    例如:ret = register_chrdev(250,"led",&fops);
    	进一步计算的到设备号:
    	dev_t devt = 250<<20 | 0;
    	
    					
    					
    参数1:
    		unsigned int major:主设备号
    		无符号整数
    参数2:
    		const char *name:描述一个设备驱动信息,自定义,通俗来说就是设备名叫什么
    		字符串首地址
    参数3:
    		const struct file_operations *fops:文件操作对象
    		结构体地址(指针)
    		解释:文件操作对象,就是一个结构体的实例化对象的地址,该结构体中的成员都是函数指针,这个结构体可以实现应用层程序和存储驱动间的函数关联,置于这个关联有什么用,下面会详细讲,这里大概就说一下,就是应用层调用什么文件IO接口,底层设备驱动就执行该结构体实例化的对象中赋值的函数接口。
    		
    返回值:
    		正确返回0,失败返回负数
    
    
  4. 参数4的具体值(内容)-------私有数据

    私有数据,一般填NULL
    
  5. 参数5的具体值(内容)------设备文件名

    一个字符串
    

问题1.2、得到了这个设备节点的成员的具体的值了(设备节点是一个结构体),怎么赋值给这个设备节点(结构体)呢?

当我们得到结构体(设备节点)所需要的成员的具体值(内容)后,就可以利用下面这个函数将值赋值给(设备节点)结构体中对应的成员。

struct device *device_create(	struct class *class,    //参数1
					struct device *parent,            //参数2
					dev_t devt,                       //参数3
					void *drvdata,                     //参数4
					const char *fmt,                    //参数5
					...)----创建设备节点

例如:struct device * led_dev = device_create(cls_led,NULL,devt,NULL,"led%d",3);

问题2、有了字符设备驱动文件,怎么关联应用程序和设备驱动呢?

首先我们要知道,什么是关联,所谓关联就是:在驱动中实现文件io接口功能,当应用程序调用文件io时,驱动程序也调用对应的文件io接口函数

在内核中设计好的结构体 struct file_operations 每一个成员变量都代表绑定一个系统调用(文件io)函数,只要对结构体中的成员赋值,就代表值绑定上一个文件io函数
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);int (*show_fdinfo)(struct seq_file *m, struct file *f);};//函数指针的集合,
​			每个函数指针赋值为函数地址,就代表当应用程序调用对应的文件io函数时,驱动就执行函数指针赋值的对应函数
例如:
//驱动需要对应执行的IO函数实现,注意,这些IO函数是根据应用程序的需要进行关联和编写的,如果应用程序中没有用到相关的IO函数,就不需要关联和编写
int led_open (struct inode * inode, struct file * file)
{
	printk("led_open\n");
	return 0;
}

int led_close (struct inode * inode, struct file * file)
{
	printk("led_close\n");
	return 0;
}

ssize_t led_read (struct file * file, char __user * data, size_t size, loff_t * ops)
{
	printk("led_read");

	copy_to_user(data,"nihao",6);          //内核向应用程序中输出6字节大小的数据

	return 0;
}
ssize_t led_write (struct file * file, const char __user * data, size_t size, loff_t * ops)
{
	int num = 0;
	copy_from_user(&num,data,size);     //内核从应用程序中接受size大小的字节数据到num中

	printk("num = %d\n",num);
	if(num == 1)
	{	
		*gpx1dat |= 1;
	}
	else
	{
		
		*gpx1dat &= ~1;
	}
	return 0;
}

/* 驱动和应用程序间实现关联 */
const struct file_operations fops = {
	.open = led_open,//当应用程序调用open时,驱动则执行结构体成员open对应的赋值函数
	.release = led_close,
	.read = led_read,
	.write = led_write,
};
	
//注意:这个结构体对象fops的地址,就是创建设备号时register_chrdev()的第三个参数,表示文件操作对象的地址。

这里是一个应用程序

#include 
#include 
#include 
#include 
#include 

int main()
{
	int a = 10;
	char buf[] = "hello world";
	int fd = open("/dev/led3",O_RDWR);

	printf("%s\n",buf);
	sleep(2);

	read(fd,buf,10);        //应用程序从内核驱动中读取10字节大小的内容到buf中
	printf("%s\n",buf);

	while(1)              //实现led3每隔1s闪烁一次
	{
	a = 1;
	write(fd,&a,4);       //应用程序从a中向内核写入4字节大小的数据
	sleep(1);
	a = 0;
	write(fd,&a,4);
	sleep(1);
	}
	close(fd);


	return 0;
}

中断方式实现驱动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgdD28hO-1666346371077)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221016025522916.png)]

面试题:说一下中断

中断简单来讲就是当产生中断信号并通知内核,让CPU停下当前的工作去完成中断处理,完成之后返回并继续执行之前的工作的过程。

从底层中断原理来讲来说,中断根据不同的异常工作模式,分为软中断SWI、快中断FIQ,普通中断IRQ。按照触发方式可分为内部中断和外部中断。按照是否带有操作系统,分为裸机中断和驱动中断。由于操作系统的内核已经添加了中断驱动,gpio控制器驱动、GIC中断控制器驱动,所以我们不需要像裸机开发那样,对gpio控制器引脚、GIC中断寄存器的众多寄存器进行配置,只需要将对应的引脚信息告诉内核,让内核知道我们需要实现哪个引脚的中断功能就可以了。所以相比较而言,裸机中断需要完成的任务更加底层,需要直接控制寄存器。这里说一下裸机开发中的中断控制原理,首先就是当中断源产生中断信号以后,判断是外部中断还是内部中断,如果是外部中断,那么需要通过gpio引脚控制器将中断信号传入到GIC中断控制器中,内部中断则不需要通过GPIO控制器就可以将中断信号传给GIC中断控制器。在GIC控制器中需要完成外部/内部中断使能、中断分发、分发使能、cpu接口选择,接口使能,中断优先级设置,这一系列配置完成之后,根据不同中断类型输入到内核处理器中完成处理,具体的处理过程就是,中断异常处理机制,要完成"四大步,三小步"操作。第一大步是将当前ARM内核工作模式cpsr寄存器的值保存到相应异常模式下的spsr寄存器中;第二大步是将PC寄存器的值保存到相应异常模式下的lr寄存器中。前两步主要完成中断异常现场保护。第三大步,就是将当前工作模式下的cpsr寄存器的值设置为相应的异常中断模式;第四大步就是将PC寄存器的值设置为相应异常中断处理函数的入口地址,也就是异常向量表中对应异常的入口地址。之后在第四大步中分为三小步操作,第一步就是将当前程序中寄存器的值入栈保存;第二步才是跳转到用户用C语言编写的中断异常处理函数的地址,执行具体的中断处理;第三步是当中断处理完毕之后需要恢复中断前的现场,所以需要将栈中的数据出栈,其中就包括了lr寄存器的值出栈赋值给PC寄存器,然后继续执行之前的程序,这样就完成了一次中断异常处理。当然有些简单的项目不涉及到异常向量表,而是使用的中断向量表,不过二者都是类似的,唯一的不同就是使用异常向量表可以处理更多的一些异常,中断就是异常的一种而已。

首先,外设触发中断,然后发送中断信号到驱动设备中,驱动设备检测到中断信号,得到对应的中断号,接着利用得到的中断号向CPU申请中断,CPU接受中断申请后就会调用对应的中断处理函数,实现中断处理,这样就完成了一次由外部硬件触发的中断。
​ 这里有几个知识点;

设备驱动是如何得到对应的中断号的?

​ 答:不同于裸机开发,我们需要手动设置中断控制器,给对应中断配置设置中断号,开启外部中断,总中断使能,以及中断到来时需要编写异常向量表,实现对应中断的入口和跳转到对应的中断处理函数。我们在有操作系统的情况下,更多是将内核的寄存器配置都配置好了,例如这里的中断寄存器GIC。此外设备树中也有许多的硬件设备描述信息,其中就包含了每个可实现中断的硬件设备的中断号。像这样**在设备树中添加硬件设备信息描述(其中分为两部分添加,一个是在.dtsi文件中添加对应控制器的中断控制信息描述,第二部分就是在.dts中添加具体哪一个硬件的中断信息描述,包括了描述信息、父类、在父类中对应第几个中断号、触发方式),在驱动中通过接口函数1找到产生中断的硬件设备在设备树上的节点,利用节点再调用接口函数2实现中断号的获取。**实现接口函数是

具体在设备树中添加硬件设备信息流程,如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9WyBrj2-1666346371078)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017095705686.png)]

设备树文件也有头文件,而且也是一级一级嵌套的,例如

interrupt-parent = <&gpx1>  
其中gpx1就存在于exynos4412.dtsi头文件中

注:

X: GIC_SPI(共享中断)或者GIC_PPI(私有中断)

Y: 物理中断号

Z:触发方式

1 = low-to-high edge triggered  上升沿触发
2 = high-to-low edge triggered  下降沿触发
4 = active high level-sensitive  高电平触发
8 = active low level-senstive  低电平触发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZhot7rk-1666346371080)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221016184837470.png)]

struct device_node *of_find_node_by_path(const char *path);-------接口函数1,找到对应的设备节点

	参数1:硬件设备节点路径,这里的路径是设备树中的节点位置,(因为设备树中有一个根节点,所有添加的设备信息都在该根节点中)
		当内核启动后,扫描设备树,将其中的每个节点都转换为一个device_node结构体,节点中的信息都存放在这个结构体中。这样的话在访问的时候就不需要在去扫描设备树了,只需要访问对应的结构体变量就可完成信息的获取。
	返回值;硬件设备节点的结构体地址

unsigned int irq_of_parse_and_map(struct device_node *dev,int index);------接口函数2,获取中断号
	
	参数1:硬件设备节点结构体地址
	参数2:在当前具体硬件设备的中断列表中的第几个中断编号
	返回值;硬件设备的中断号

例如
    //a、获取中断号
	//获取设备树中的要使用的硬件节点
	struct device_node * node = of_find_node_by_path("/key3_node"); //这个key3_node就是我们自己在设备树中添加的节点名字
	//获取节点中的中断号
	int irqno = irq_of_parse_and_map(node,0);

内核又是如何知道驱动设备要申请的中断号是对应哪一个硬件呢?

答:当驱动获得了对应硬件设备的中断号以后,利用这个中断号向CPU申请中断,由于在内核的设备树中已经添加了对应硬件设备的描述信息(中断号),所以当驱动调用带有中断号的接口函数request_irq()就可以让内核知道这是哪一个具体的硬件设备

/* 申请中断 */
request_irq(unsigned int irq, 
            irq_handler_t handler,
            unsigned long flags,
	    	const char *name, 
            void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

		参数1:
			unsigned int irq:申请中断的中断号

		参数2:irq_handler_t handler
		typedef irqreturn_t (*irq_handler_t)(int, void *);  //typedef类型替换,用irq_handler_t表示一个函数指针
			irqreturn_t (*)(int, void *)    <----   irq_handler_t
			irq_handler_t handler:是一个函数指针,进行注册中断,当产生中断时需要自动调用的中断处理函数
			函数原型:irqreturn_t 函数名 (int irqno, void * dev)
			当该函数同时绑定了多个中断设备,那么就可以通过中断号irqno来判断到底是哪一个中断设备触发的中断,避免了每一个中断			  设备都需要写一个中断处理函数。
			
			
		参数3:
			unsigned long flags:中断处理的触发方式
			
			#define IRQF_TRIGGER_NONE	0x00000000  ----不触发
			#define IRQF_TRIGGER_RISING	0x00000001  -----上升沿触发
			#define IRQF_TRIGGER_FALLING	0x00000002   -----下降沿触发
			#define IRQF_TRIGGER_HIGH	0x00000004   -----高电平触发
			#define IRQF_TRIGGER_LOW	0x00000008   ------低电平触发

		参数4:
			const char *name:字符串首地址,中断的描述信息,就是添加的中断的名字,利用下面这条指令在开发版中参看:
				/proc/inruppter  开发板终端查看中断名字
		参数5:
			void *dev:(作为参数2这个函数的参数)
			这个函数的使用方法:当我们创建的设备机构体实例化对象是一个局部的变量,那么就需要将这个结构体实例化对象的地址传入			到第二个参数,方便中断处理函数使用该结构体中的变量和函数。

		返回值:
			成功返回0,失败返回非0
例如:
request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);

为什么要释放中断

释放中断:
		void free_irq(unsigned int irq,void * dev_id)
		参数1:
			unsigned int irq
			中断号
		参数2:
			void * dev_id:与申请中断第五个参数保持一致

驱动中要实现把硬件产生的数据传递给用户(应用程序)

两部分:驱动中获取到硬件的数据、驱动传递给用户(应用)

a、驱动中获取到硬件的数据
	通过中断获取到数据(中断处理函数中)
	具体硬件对应着一个数据值
		key2------'a'
		key3------'q'

	从硬件的寄存器中获取数据值
		key3--------按下和抬起-------1/0
		读取key3的gpio寄存器---数据寄存器

b、驱动传递给用户(应用)
	驱动在文件io接口实现中,完成传递给应用
	
	驱动在xxxx_read函数中将数据传递给用户
		ret = copy_to_user(buf,&data,1);

用户如何拿到驱动中的数据----编写应用程序

编写应用程序,利用IO接口函数实现数据获取。

虚拟内存地址映射

文件阻塞IO模型

​ 阻塞io模型------休眠等待
​ 阻塞:当进程读取外部资源(数据),但是外部资源没有准备好,进程就进行休眠等待资源可用

具体过程:该过程主要用到CPU进程调度机制(上下文切换,时间片轮转),首先,外部硬件产生中断后,CPU执行中断进程,对中断任务进行处理,此时中断进程处于运行态,当中断处理函数执行完毕后,继续检测是否有对应中断触发。如果有的话,继续执行中断任务;如果外部没有中断触发时,中断进程就进入中止态,具体的操作包括中断进程改变进程状态,让出调度(不占用cpu资源),进入到等待队列中。此后,如果外部中断触发了,那么中断进程被唤醒,从中止态切换到就绪态,占用CPU,然后执行中断任务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqgmOOwL-1666346371081)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221013020242939.png)]

在应用中:
	read
	wirte
	accept
	scanf
	默认都是阻塞的

驱动中实现阻塞:
	创建等待队列头:
		wait_queue_head_t head; //在自定义结构体中添加,定义一个等待队列头变量head
		init_waitqueue_head(&head);   //在模块初始化函数中添加,表示创建一个等待队列头

	1、在需要等待的位置(没有数据),就阻塞等待
		在驱动IO函数中添加,用于判断是否阻塞读写数据
		wait_event_interruptible(wq,condition)-----根据参数是否进行阻塞等待,完成阻塞等待,
		
		参数1:
			wq:等待队列头,把当前进程加入到哪个等待队列中

		参数2:
			condition:是否执行阻塞等待的条件

			condition:真---不进行阻塞
			condition:假---进行阻塞

	2、中断处理函数中进行阻塞唤醒
		wake_up_interruptible(&head);

注:如何让驱动知道打开的文件到底是阻塞打开还是非阻塞打开-----------利用file->f_flags参数查看

f_flags这个变量是一个32位的数,利用特定的位上的值来代表文件的一个属性,例如第几位表示读权限,第几位表示写权限,
这里的O_NONBLOCK就是利用f_flags的某一位来表示,如果应用程序文件打开方式是阻塞等待那么这一位上的值就是0,否者就是1,表示非阻塞等待
利用condition这个变量的值,来判断是否有中断产生,当有中断产生时,该值被设置为1,当没有中断产生时,该值就是0

理解:
	这里无非就是有4种情况,
		1. 阻塞没有中断产生------key_read()阻塞等待中断产生并读取数据,返回给应用程序,返回0
		2. 阻塞有中断产生------key_read()直接读取中断产生的数据,返回给应用程序,返回0
		3. 非阻塞没有中断-----key_read()直接返回-1,
		4. 非阻塞有中断-----key_read()直接读取中断产生的数据,并返回给应用程序,返回0
	在这四种情况中,只有第三种会立即返回,所以可以简写逻辑代码为以下形式:
	表示的含义就是:判断应用程序是否是阻塞方式打开设备驱动,如果为非阻塞方式,而且此时还没有中断触发,产生数据,就会直接返回-1,表示没有读取到数据
	如果为非阻塞,此时有中断触发产生数据,那么直接读取数据并返回1,表示读取到了数据,注意这里就也会执行wait_event_interruptable()函数,因为此时有数据产生(中断触发了),所以conditon变量是1,对于
	wait_event_interruptable()函数内部实现原理而言,conditon变量为1,需要取反判断,如果为取反结果为0,那么就不进入等待队列中,直接读取数据,如果取反结果为1(中断没有触发),那么满足进入等待队列的条件,就需要
	进入到等待队列中等待中断产生,然后唤被醒。
	
总之就是说:
	当conditon=1(中断触发了)时,无论是阻塞还是非阻塞都直接读取数据并返回1,conditon=0(中断没触发),阻塞进入到等待队列中,等待中断产生数据,非阻塞直接返回-1

异步收发数据

为什么要使用异步通知:
大多数情况下,我们更希望是当中断产生后应用程序才去调用IO函数,实现数据的收发。而不是应用程序一直循环调用IO函数,判 断驱动是否有中断产生。因为我们希望应用程序除了接受中断数据以外,还需要执行其他代码,完成其他的功能。

	那么怎么实现异步通知呢?
	两个方面的设置:一是应用程序中需要提前设置信号处理,当信号到达时执行对应操作。二是驱动程序中需要建立与应用程序进程之		间的关联,以及中断触发时发送信号给应用程序

	应用程序设置;
	1. 设置SIGIO信号的处理操作-----信号处理函数      
	2. 设置SIGIO属主进程
	3. 设置为异步信号模式(fasync)
	驱动程序设置:
	1. 与进程进行关联
	2. 发送SIGIO信号

具体的执行过程:
提前在应用程序中设置好信号处理(3点),在驱动程序中设置与进程进行关联(2点),然后当中断产生后在中断处理函数中发送 信号通知具体的一个应用进程,然后具体的应用程序接收到这个信号,执行信号处理函数,调用文件IO收发数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJVyQ8fr-1666346371082)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017193706249.png)]

应用程序配置

涉及的函数;
	应用程序:
		signal(int signum,sighandler_t handler);
			参数1:驱动向应用程序发送的信号类型,除了SIGKILL和SIGSTOP信号
			参数2:描述与信号关联的动作,它可以去以下三种值:
				SIG_IGN:表示忽略该信号
				SIG_DFL:这个符号表示恢复对信号的系统默认处理,不写此处理函数默认也是执行系统默认操作
				sighandler_t类型的指针函数,即用户自定义信号处理函数。
				
		fcntl(fd,F_SETOWN,getpid());
			描述;设置当前进程为SIGIO信号的属主进程
			参数1:open()打开的驱动设备文件返回的文件描述符
			参数2:属主参数
			参数3:进程号,这里一般用getpid()函数来获取当前进程的进程号
			
		int flags = fcntl(fd,F_GETFL);    
		flags |= FASYNC;//添加异步属性,这一步操作的时候就会调用底层的异步信号通知结构体初始化函数fasync_helper()
		fcntl(fd,F_SETFL,flags);
		描述;将io模式设置为异步模式
		参数1:open()打开的驱动设备文件返回的文件描述符
		参数2:文件操作参数,F_GETFL获取文件属性,F_SETFL设置文件属性
		参数3:文件属性参数,FASYNC就是用32bit数据中的某几位表示异步属性
		
	

驱动程序配置

驱动程序:
		1. 需要和进程进行关联--记录信号该发给谁
		fasync_helper(fd,file,no,&(key.fasync))
		添加实现,一个fasync的接口
		struct fsdync_struct *fasyns;
		fasync_struct结构体用来存放对应设备文件的信息(例如fd,filp)并交给内核来管理
	
		例如:
			/* 驱动中配置异步通知信号 */
			细节:要将该函数写在close函数上方,因为在close函数中会用到该函数用来对添加的异步消息节点销毁
			int key_fasync (int fd , struct file * filp, int on)
			{
				printk("key fasync \n");
				//记录信号发送给哪个谁,同时下面这个函数的作用还是初始化key.fasync这个结构体(或者说就是在内核中将异步通知信号链表中添加新的节点),将fd、filp、on参数信息给到结构体,on为0表示将当前异步通知信号从内核中将异步通知信号链表删除,on为1表示添加一个新的节点
				return fasync_helper(fd,filp,on,&(key.fasync));   //fd为应用程序打开的文件获得文件描述符,
			}

			const struct file_operations fops = {
				.open = key_open,
				.release = key_release,
				.read = key_read,
                 .fasync = key_fasync,
			};
			
			
2. 在某个特定的时间去发送信号,在有数据的时候
			kill_fasync()
			例如:
			//中断触发,发送信号给到应用程序,让应用程序执行对应操作,例如读写数据等操作
			kill_fasync(&(key.fasync),SIGIO,POLLIN);
			表示向异步信号结构体中发送SIGIO信号,POLLIN表示一个事件

模块卸载

按照后创建的先卸载原则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SREwrG9I-1666346371083)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221016221954859.png)]

释放映射内存

 iounmap(映射的虚拟地址)

释放中断

释放中断:
		void free_irq(unsigned int irq,void * dev_id)
		参数1:
			unsigned int irq
			中断号
		参数2:
			void * dev_id:与申请中断第五个参数保持一致

注销设备节点

device_destroy(设备类,设备号) 

注销设备类

class_destroy(设备类)

注销设备号

unregister_chrdev(主设备号,设备名字)

注销堆区开辟的空间

kfree(创建的设备结构体地址)

用kzalloc()开辟的堆区空间,该函数不同于malloc(),他可以在创建的时候清空创建的空间

IO多路复用

在应用程序上的应用,其实也是需要在驱动程序中关联。该函数的原型在file_opration这个函数中有声明,只需要在我们自己的驱动程序中像read、open等文件IO操作一样,定义就行了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnoO6Az7-1666346371083)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017175855826.png)]

/* 该函数的作用就是用来监听应用程序有没有事件产生 */
unsigned int (*poll) (struct file *filp, struct poll_table_struct *pts)
{
    //返回一个mask值
    unsigned int mask;
    //调用poll_wait,将当前等待队列注册系统中-----初始化监听队列节点
    poll_wait(filp,&key_dev->wqhead,pts);
    //1. 当没有数据到时返回一个0
    if(!key_dev->key_state)
        mask =0;
    //2.有数据返回一个POLLIN,这个POLLIN就是我们要监听的事件
    if(key_dev->key_state)
        mask |=POLLIN;
    
    return mask;
    
}

/*  
	参数1:struct file *filp
		应用程序打开的设备文件结构体地址(指针)
	参数2:struct poll_table_struct *pts
		监听对列表
*/

/* 
	什么时候调用该函数?实现监听的原理是什么?
	应用层调用poll()函数,内核底层就会调用do_sys_poll()函数,do_sys_poll()函数中会调用do_poll()函数,do_poll()函数中会调用do_pollfd(),这个函数就是在循环遍历监听队列池,就是在应用层编写的pfd结构体数组。第一次进入监听的时候由于没有事件,do_pollfd()返回值为0,在代用do_pollfd()的函数中,会判断,如果返回值为0,计数count不增加,任然为0,这个计数count的目的就是为了用来判断do_pollfd()函数是否退出的。如果计数count为1,则退出do_pollfd()函数,否则为0不退出do_pollfd()函数,程序就阻塞在该函数中,进程存放到等待队列中,应用程序表现为程序停在了poll函数位置。当pfd结构体数组中的事件产生,就会唤醒等待队列中的进程,退出do_pollfd()函数,再次执行刚刚进来的操作,这一次有事件产生了,
	当我们应用程序系统调用open一个驱动文件时,驱动程序也会调用open函数,此时就会产生一个file结构体,该结构体中定义了一个f_op的结构体,f_op结构体中有一个poll()函数,这个poll()函数就是我们在驱动程序中定义的poll()函数。所以当我们在应用程序中系统调用poll函数时,就会通过这种方式调用驱动中的poll函数,同时poll函数的返回值mask就是事件数,如果有事件产生,那么返回值mask被赋值为POLLIN,然后将maks赋值给pollfd()结构体的revents变量,通过事件数是否为0来判断计算count是否自加1,从而来决定是否要退出do_pollfd()函数,count被返回到do_poll()函数,表示返回事件数。,do_poll()函数又返回该值到do_sys_poll()函数,do_sys_poll()函数有返回该值到我们在驱动文件中定义的poll函数,最终返回到应用程序中,进行main函数的判断
	参考:blog.csdn.net/u0128301148/article/details/80465789
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SnuR5q9M-1666346371084)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017180341304.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgKUbJ1f-1666346371085)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017180608425.png)]

中断的下半部

中断特性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H8jj6yqh-1666346371086)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017205340401.png)]

注意:

  1. 中断函数尽可能短,保证中断快出
  2. 中断程序里面极少用delay()和sleep()函数
  3. 中断程序没有参数
  4. 中断不可以被同级和低级中断打断
  5. 中断要立即处理

中断的两部分

中断上半部

中断的上半部主要是要马上处理的代码,刻不容缓。例如数据接收

中断下半部

数据处理和解析等不是特别着急处理的代码,封装成单独的函数,这个函数就是中断的下半部。然后采用一种新的内核调度机制,当系统不忙的时候就去执行中断下半部分函数。

实现中断下半部的三种方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTc4zt4k-1666346371087)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017211035774.png)]

软件中断softirq

softirq:软件中断是一种软件成面上的中断,它的优先级比普通应用程序优先级高,比硬件中断要低,可以被硬件中断打断

小任务tasklet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RJvYPghn-1666346371087)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017214005202.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NfoBvkJK-1666346371088)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017213712824.png)]

初始化小任务,就是将后面两个参数保存到小任务结构体中的过程,如下图;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0HuAs7j-1666346371089)(C:\Users\hejunqi\AppData\Roaming\Typora\typora-user-images\image-20221017214312199.png)]

/* 创建小任务结构体 */
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
	void (*func)(unsigned long);   //中断下半部的实现逻辑
	unsigned long data;			  //传递给到fun函数  
};

/* 创建小任务变量 */
struct tasklet_struct mystasklet;

/* 初始化小任务 */
tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
    参数;
    	t:struct tasklet_struct结构体的地址;
		func:启动下半部分后调函数;
		date:回调函数参数结构体变量地址
         

中断下半部分具体函数实现

该函数并不会在中断中的下半步启动后立即执行,而是等到cpu不忙的时候再去执行

/* 启动下中断半部分后的回调函数 */
static void irq_next_half(unsigned long date)
{
	printk("irq_next_half run\n");
	//该值为1,表示中断触发
	condition = 1;        
	//将等待队列中的进程唤醒
	wake_up_interruptible(&(key.head));
	//异步发送信号给到应用程序,让应用程序执行对应操作,例如读写数据等操作
	kill_fasync(&(key.fasync),SIGIO,POLLIN);
}

在驱动中断函数启动下半步

//启动中断下半部分
tasklet_shedule (&mystasklet);

workqueue工作队列

通过创建线程来处理中断下半部分,线程的开销更大,但是限制最小,可以有delay()、sleep()等函数

练习

中断是否和其他进程一样,都是利用进程调度机制(上下文切换,时间片轮转),还是说当中断产生后cpu当前只执行中断处理,一直执行到中断处理完毕才会执行其他的进程?

练习1

驱动文件


#include 
#include 
#include 
#include 
#include 
#include 

struct pwm_drv
{
	unsigned int major;
	dev_t devno;
	struct class * pwm_cls;
	struct device * pwm_dev;
	unsigned int * pwmtcntb0;
	unsigned int * pwmtcmpb0;
	unsigned int * pwmtcfg0;
	unsigned int * pwmtcfg1;
	unsigned int * pwmtcon;
	unsigned int * gpd0con;
	
};

struct pwm_drv beep;

ssize_t beep_write (struct file * file, const char __user * buf, size_t size, loff_t * ops)
{

}
int beep_open (struct inode * inode, struct file * file)
{

}
int beep_close (struct inode * inode, struct file * file)
{

}


const struct file_operations fops = {

	.open = beep_open,
	.release = beep_close,
	.write = beep_write,
	
};


static int __init beep_init(void)
{
	int ret;
	beep.major = 240;
	beep.devno = beep.major << 20 | 0;
	ret = register_chrdev(beep.major,"beep pwm",&fops);
	if(ret < 0)
	{
		printk("register chrdev error\n");
		goto err_1;
	}

	beep.pwm_cls = class_create(THIS_MODULE,"beep cls");
	if(IS_ERR(beep.pwm_cls))
	{
		printk("class create error\n");
		goto err_2;
	}
	
	beep.pwm_dev = device_create(beep.pwm_cls,NULL,beep.devno,NULL,"beep");
	if(IS_ERR(beep.pwm_dev))
	{
		printk("device create error\n");
		goto err_3;
	}

    /* 寄存器虚拟内存地址映射与赋值 */
	beep.gpd0con = ioremap(0x114000a0,4);
	*(beep.gpd0con) = *(beep.gpd0con) & ~(0xf) | 0x2;
	
	beep.pwmtcntb0 = ioremap(0x139d000c,4);
	*(beep.pwmtcntb0) = 2000;
	beep.pwmtcmpb0 = ioremap(0x139d0010,4);
	*(beep.pwmtcmpb0) = 1000;
	beep.pwmtcfg0 = ioremap(0x139d0000,4);
	*(beep.pwmtcfg0) = *(beep.pwmtcfg0) & ~0xff | 249;
	beep.pwmtcfg1 = ioremap(0x139d0004,4);
	*(beep.pwmtcfg1) &= ~0xf;
	beep.pwmtcon = ioremap(0x139d0008,4);

	*(beep.pwmtcon) |= 1<<3;
	*(beep.pwmtcon) |= 1<<2;
	*(beep.pwmtcon) |= 1<<1;
	*(beep.pwmtcon) &= ~(1<<1);

	*(beep.pwmtcon) |= 1;
	//*(beep.pwmtcon) &= ~1;
	
	return 0;
	
err_3:
	class_destroy(beep.pwm_cls);
	
err_2:
	unregister_chrdev(beep.major,"beep pwm");

err_1:
	return -1;	
}

static void __exit beep_exit(void)
{
}

module_init(beep_init);
module_exit(beep_exit);

MODULE_LICENSE("GPL");

练习2

驱动文件

/* 
	功能描述:
	异步通知实现的可阻塞可非阻塞IO按键中断
	关键字:异步通知、阻塞/非阻塞IO模型、中断
*/
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 


struct key_node
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	dev_t devno;
	wait_queue_head_t head;
	struct fasync_struct * fasync;
};

//获取硬件数据存储的变量
char data;
int condition = 0;

struct key_node key;

ssize_t key_read (struct file * file, char __user * buf, size_t size, loff_t * ops)
{
/*
	if(file->f_flags & O_NONBLOCK == 0)        
	{
		wait_event_interruptible(key.head,condition);
	}
	else
	{
		if(condition == 0)//O_NONBLOCK
	{
		return -1;
	}
	}


	注解;
		f_flags这个变量是一个32位的数,利用特定的位上的值来代表文件的一个属性,例如第几位表示读权限,第几位表示写权限,
		这里的O_NONBLOCK就是利用f_flags的某一位来表示,如果应用程序文件打开方式是阻塞等待那么这一位上的值就是0,否者就是1,表示非阻塞等待
		利用condition这个变量的值,来判断是否有中断产生,当有中断产生时,该值被设置为1,当没有中断产生时,该值就是0

	理解:
		这里无非就是有4种情况,
			1. 阻塞没有中断产生------key_read()阻塞等待中断产生并读取数据,返回给应用程序,返回0
			2. 阻塞有中断产生------key_read()直接读取中断产生的数据,返回给应用程序,返回0
			3. 非阻塞没有中断-----key_read()直接返回-1,
			4. 非阻塞有中断-----key_read()直接读取中断产生的数据,并返回给应用程序,返回0
		在这四种情况中,只有第三种会立即返回,所以可以简写逻辑代码为以下形式:
		表示的含义就是:判断应用程序是否是阻塞方式打开设备驱动,如果为非阻塞方式,而且此时还没有中断触发,产生数据,就会直接返回-1,表示没有读取到数据
		如果为非阻塞,此时有中断触发产生数据,那么直接读取数据并返回1,表示读取到了数据,注意这里就也会执行wait_event_interruptable()函数,因为此时有数据产生(中断触发了),所以conditon变量是1,对于
		wait_event_interruptable()函数内部实现原理而言,conditon变量为1,需要取反判断,如果为取反结果为0,那么就不进入等待队列中,直接读取数据,如果取反结果为1(中断没有触发),那么满足进入等待队列的条件,就需要
		进入到等待队列中等待中断产生,然后唤被醒。
		
	总之就是说:
		当conditon=1(中断触发了)时,无论是阻塞还是非阻塞都直接读取数据并返回1,conditon=0(中断没触发),阻塞进入到等待队列中,等待中断产生数据,非阻塞直接返回-1

*/
	//设置非阻塞
	if(((file->f_flags & O_NONBLOCK) != 0) && (condition == 0))
	//不管是阻塞还是非阻塞,只要有数据都要读取数据继续执行
	{
		return -1;
	}
	//阻塞等待
	wait_event_interruptible(key.head,condition);	

	//拷贝数据
	int ret;
	printk("key read\n");
	ret = copy_to_user(buf,&data,1);

	//下一次能够阻塞
	condition = 0;
	//把存储数据的缓冲区清空
	data = '\0';

	return 1;
}

int key_open (struct inode * inode, struct file * file)
{
	printk("key open\n");
	return 0;
}
int key_release (struct inode * inode , struct file * file)
{
	printk("key close\n");
	return 0;
}


//实现中断处理函数
irqreturn_t key_irq_handler(int irqno, void * dev)
{
	//获取到数据
	data = 'q';

	condition = 1;          //该值为1,表示中断触发
	
	printk("irqno is %d\n",irqno);

	printk("input char '%c'\n",data);//可以认为是一个字符‘q’,也可以从数据寄存器中获取真实的值

	//唤醒,把进程从等待队列中拿出来继续执行
	wake_up_interruptible(&(key.head));


	//中断触发,发送信号给到应用程序,让应用程序执行对应操作,例如读写数据等操作
	kill_fasync(&(key.fasync),SIGIO,POLLIN);
	
	return IRQ_HANDLED;


}
/* 驱动中配置异步通知信号 */
int key_fasync (int fd , struct file * file, int no)
{
	printk("key fasync \n");

	//记录信号发送给哪个应用
	return fasync_helper(fd,file,no,&(key.fasync));   //fd为应用程序打开的文件获得文件描述符,
}


const struct file_operations fops = {

	.open = key_open,
	.release = key_release,
	.read = key_read,
	.fasync = key_fasync,

};

static int __init key_drv_init(void)
{
	//1、申请设备号

	key.major = 230;
	register_chrdev(key.major,"key drv",&fops);

	//2、创建设备节点
	key.cls = class_create(THIS_MODULE,"cls");
	key.devno = MKDEV(key.major,0);
	key.dev = device_create(key.cls,NULL,key.devno,NULL,"key3");

	//4、硬件初始化
	//中断初始化
	//a、获取中断号
	//获取设备树中的要使用的硬件节点
	struct device_node * node = of_find_node_by_path("/key3_node");
	//获取节点中的中断号
	int irqno = irq_of_parse_and_map(node,0);

	//创建等待队列头
	init_waitqueue_head(&(key.head));    //默认是将当前进程放在等待队列中的,当中断触发就会执行下面的中断申请,然后自动调用中断处理函数,在终端处理函数中唤醒中断进程
	//b、申请中断
	request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);

	//irqreturn_t (*handler)(int, void *)函数指着
	//handler = key_irq_handler

	
	return 0;
}

static void __exit key_exit(void)
{

}

module_init(key_drv_init);
module_exit(key_exit);

MODULE_LICENSE("GPL");




应用测试文件

#include 
#include 
#include 
#include 
#include 
#include 

/*
	为什么要使用异步通知:
		大多数情况下,我们更希望是当中断产生后应用程序才去调用IO函数,实现数据的收发。而不是应用程序一直循环调用IO函数,判断		驱动是否有中断产生。因为我们希望应用程序除了接受中断数据以外,还需要执行
		其他代码,完成其他的功能。
	那么怎么实现异步通知呢?
		两个方面的设置:一是应用程序中需要提前设置信号处理,当信号到达时执行对应操作。二是驱动程序中需要建立与应用程序进程之		  间的关联,以及中断触发时发送信号给应用程序

		应用程序设置;
		1. 设置SIGIO信号的处理操作-----信号处理函数
		2. 设置SIGIO属主进程
		3. 设置为异步信号模式(fasync)
		驱动程序设置:
		1. 与进程进行关联
		2. 发送SIGIO信号

	具体的执行过程;
		提前在应用程序中设置好信号处理,在驱动程序中设置与进程进行关联,然后当中断产生后在终端处理函数中发送信号通知具体的一		  个应用进程,然后具体的应用程序接收到这个信号,执行信号处理函数,调用
		文件IO收发数据


*/

int fd;

/* 捕获信号并执行信号处理 */
void catch_signal(int signo)
{
	char num;
	if( read(fd,&num,1) < 0)
	{
		printf("no data;");
		return ;
	}

	printf("data is %c\n",num);
}


int main()
{
	//	int fd = open("/dev/key3",O_RDONLY | O_NONBLOCK);非阻塞
	fd = open("/dev/key3",O_RDONLY);//阻塞

	if(fd < 0)
	{
		perror("open error");
		return -1;
	}

	//1、设置信号处理方式
    //SIGIO这是一个驱动向应用程序发送的信号,应用程序通过检测接收这个信号,控制	  catch_signal()函数的调用
	signal(SIGIO,catch_signal);   
 
	//2、设置当前进程为SIGIO信号的属主进程
	fcntl(fd,F_SETOWN,getpid());

	//3、将io模式设置为异步模式
	int flags = fcntl(fd,F_GETFL);
	flags |= FASYNC;//添加异步属性
	fcntl(fd,F_SETFL,flags);

	//完成其他功能操作
	while(1)
	{
		printf("hello world\n");
		sleep(1);
	}
	close(fd);
	/*
	sleep(1);
	char data = 'a';
	int num = 10;
	while(1)
	{	
		num = 10;
		data = 'a';
		num = read(fd,&data,1);
		printf("%d %c\n",num,data);
			sleep(1);
	}
	*/
	return 0;
}

练习

驱动设备文件

/* 
	描述:
	完成了按键key3设备文件的添加、映射GPX1DAT寄存器的虚拟内存地址、绑定了按键key3中断,创建了等待队列,由于编写了存在了异步发送功能,所以等待队列实现阻塞等待的意义就失去了,等待队列真正用到的地方是,当应用程序需要不断地循环读取数据时,没有数据就需要阻塞等待,让读取进程进入到等待队列中,减少对cpu资源的浪费。考虑到进程不可能只用来是循环读取数据,所以有了异步信号发送,可以实现驱动端发送信号给应用程序,然应用程序执行信号处理函数去执行读取操作(其实就是一个异步中断)。
*/

#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 



#define	GPX1CON_KEY3	0x11000c20
#define IOREMAP_SIZE 	4
#define KEY3 3
#define Hight 1
#define Low 0

struct key3_device {
	unsigned int major;
	struct class* cls;
	dev_t devno;
	struct device* key_dev;
	unsigned int* gpx1con;
	unsigned int* gpx1dat;
	struct device_node* dev_node;
	wait_queue_head_t head;
	unsigned int irqno;
	//struct key_event* key_infomation;
	struct fasync_struct* fasync;


};
struct key_event {
	unsigned int Which_key;
	unsigned int Leveling;

};
struct key_event key_infomation;
struct key3_device key3_my;
int condition = 0;

/* IO函数实现 */
int key_open(struct inode* inode, struct file* file)
{
	printk("chrdev --(/dev/key3_dev_node) open successfully\n");
	return 0;
}

ssize_t key_read(struct file* file, char __user* buf, size_t size, loff_t* ops)
{
	//设置非阻塞,非阻塞且中断没触发,直接返回-1
	if (((file->f_flags & O_NONBLOCK) != 0) && (condition == 0))
	{
		return -1;
	}
	//不管是阻塞还是非阻塞,只要中断触发有数据都要读取数据继续执行
	wait_event_interruptible(key3_my.head, condition);
	//拷贝数据
	int ret;
	//读取按键的电平给到应用程序
	ret = copy_to_user(buf,&key_infomation,sizeof(key_infomation));

	//恢复0,保证下一次能够阻塞
	condition = 0;

	return 1;

}
ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*);

/* 驱动中配置异步通知信号 */
int key_fasync (int fd , struct file * filp, int on)
{
	printk("key3 fasync \n");
	 
	//记录信号发送给哪个应用
	return fasync_helper(fd,filp,on,&(key3_my.fasync));   //fd为应用程序打开的文件获得文件描述符,
}



const struct file_operations fops = {
	.open = key_open,
	.read = key_read,
    .fasync = key_fasync,
};

irqreturn_t key_irq_handler(int irqno, void* dev)
{
	printk("key_irq run\n");
	//该值为1,表示中断触发
	condition = 1;

	//将等待队列中的进程唤醒
	wake_up_interruptible(&(key3_my.head));

	/* 通过按键的数据寄存器的值来判断按键是按下还是抬起 */
	if ((*key3_my.gpx1dat & (0x1 << 2)) == 0)
	{
		printk("dev:key3 dowm\n");
		key_infomation.Which_key = KEY3;
		key_infomation.Leveling = Low;

	}
	else
	{
		printk("dev:key3 up\n");
		key_infomation.Which_key = KEY3;
		key_infomation.Leveling = Hight;
	}

	//中断触发,发送信号给到应用程序,让应用程序执行对应操作,例如读写数据等操作
	kill_fasync(&(key3_my.fasync), SIGIO, POLLIN);

	return IRQ_HANDLED;

}


static int __init key_int_my_init(void)
{
	key3_my.major = 255;
	register_chrdev(key3_my.major, "key3_my_devno", &fops);

	key3_my.cls = class_create(THIS_MODULE, "key3 cls");
	key3_my.devno = MKDEV(key3_my.major, 0);
	key3_my.key_dev = device_create(key3_my.cls, NULL, key3_my.devno, NULL, "key3_dev_node");


	/* 初始化硬件 */
	key3_my.gpx1con = ioremap(GPX1CON_KEY3, IOREMAP_SIZE);
	*(key3_my.gpx1con) = ( * (key3_my.gpx1con) & ~(0xf << 8)) | 0xf;
	key3_my.gpx1dat = ioremap(GPX1CON_KEY3 + 4, IOREMAP_SIZE);

	/* 中断初始化 */
	//通过设备节点获取中断号
	key3_my.dev_node = of_find_node_by_path("/key3_node");
	/*key3_node是下载到开发板上的设备树的根节点上的设备节点
	返回值是设备节点的信息结构体地址*/

	key3_my.irqno = irq_of_parse_and_map(key3_my.dev_node, 0);
	/* 返回值是中断号,参数1是设备节点信息结构体的地址,参数2是
	按键3在该设备节点中的中断编号为第零个中断号*/

	//创建等待队列头
	init_waitqueue_head(&(key3_my.head));
	//默认是将当前进程放在等待队列中的,当中断触发就会执行下面的中断申请,然后自动调用中断处理函数,在终端处理函数中唤醒中断进程

	//绑定中断
	request_irq(key3_my.irqno, key_irq_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "key3_irq", NULL);
	return 0;
}

static void __exit key_int_my_exit(void)
{

}


module_init(key_int_my_init);
module_exit(key_int_my_exit);
MODULE_LICENSE("GPL");


应用程序

/*
	描述:
		完成了信号的设置,用来接受异步信号。
	现象:(我这里只设置了一个按键key3,所以只会显示的是key3的信息,可以自己添加其他按键,然后按键值Which_key 判断是哪个按			键按下,以及查看Leveling 查看当前按键的电平状态)
		当按键key3按下,就会在终端打印出是哪一个按键按下、此时的按键数据寄存器的电平状态为低电平
		当按键key3抬起,就会在终端打印出是哪一个按键抬起、此时的按键数据寄存器的电平状态为高电平
*/


#include 
#include 
#include 
#include 
#include 
#include 

#define KEY3 3
#define Hight 1
#define Low 0

int fd;

struct key_event {
	unsigned int Which_key;
	unsigned int Leveling;

};
struct key_event key_infomation;

/* 捕获信号并执行信号处理 */
void catch_signal(int signo)
{
	int num;
	if (read(fd, &key_infomation, sizeof(key_infomation)) < 0)
	{
		printf("no data;");
		return;
	}

	if (key_infomation.Which_key == KEY3)
	{
		if (key_infomation.Leveling == Hight)
		{
			printf("app:key3 up\n");
			printf("app:key3 leveling is %d\n", key_infomation.Leveling);
		}
		else
		{
			printf("app:key3 down\n");
			printf("app:key3 leveling is %d\n", key_infomation.Leveling);
		}
	}
}

int main()
{
	fd = open("/dev/key3_dev_node", O_RDONLY | O_NONBLOCK);

	//1、设置信号处理方式
	//SIGIO这是一个驱动向应用程序发送的信号,应用程序通过检测接收这个信号,控制	  catch_signal()函数的调用
	signal(SIGIO, catch_signal);

	//2、设置当前进程为SIGIO信号的属主进程
	fcntl(fd, F_SETOWN, getpid());

	//3、将io模式设置为异步模式
	int flags = fcntl(fd, F_GETFL);
	flags |= FASYNC;//添加异步属性
	fcntl(fd, F_SETFL, flags);
	//进程执行其他任务---1s打印一次hello world
	while (1)
	{
		printf("hello world\n");
		sleep(1);
	}

}

Makef

KERNEL_DIR=/home/ubuntu/code/kernel/linux-3.14
CUR_DIR=$(shell pwd)
MODULE_NAME=key_int
APP_NAME=key_int_app
ROOTFS_DIR=/home/ubuntu/nfsdir/rootfs/drv_module

ifeq ($(KERNELRELEASE), )
all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
	/home/ubuntu/ARM_Tools/gcc-4.6.4/bin/arm-none-linux-gnueabi-gcc $(APP_NAME).c -o $(APP_NAME)
clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean

install:
	cp *.ko $(ROOTFS_DIR)
	cp $(APP_NAME) $(ROOTFS_DIR)
else
	obj-m += $(MODULE_NAME).o
	
endif

uct key_event {
unsigned int Which_key;
unsigned int Leveling;

};
struct key_event key_infomation;

/* 捕获信号并执行信号处理 */
void catch_signal(int signo)
{
int num;
if (read(fd, &key_infomation, sizeof(key_infomation)) < 0)
{
printf(“no data;”);
return;
}

if (key_infomation.Which_key == KEY3)
{
	if (key_infomation.Leveling == Hight)
	{
		printf("app:key3 up\n");
		printf("app:key3 leveling is %d\n", key_infomation.Leveling);
	}
	else
	{
		printf("app:key3 down\n");
		printf("app:key3 leveling is %d\n", key_infomation.Leveling);
	}
}

}

int main()
{
fd = open(“/dev/key3_dev_node”, O_RDONLY | O_NONBLOCK);

//1、设置信号处理方式
//SIGIO这是一个驱动向应用程序发送的信号,应用程序通过检测接收这个信号,控制	  catch_signal()函数的调用
signal(SIGIO, catch_signal);

//2、设置当前进程为SIGIO信号的属主进程
fcntl(fd, F_SETOWN, getpid());

//3、将io模式设置为异步模式
int flags = fcntl(fd, F_GETFL);
flags |= FASYNC;//添加异步属性
fcntl(fd, F_SETFL, flags);
//进程执行其他任务---1s打印一次hello world
while (1)
{
	printf("hello world\n");
	sleep(1);
}

}


#### Makef

KERNEL_DIR=/home/ubuntu/code/kernel/linux-3.14
CUR_DIR=$(shell pwd)
MODULE_NAME=key_int
APP_NAME=key_int_app
ROOTFS_DIR=/home/ubuntu/nfsdir/rootfs/drv_module

ifeq ($(KERNELRELEASE), )
all:
make -C ( K E R N E L D I R ) M = (KERNEL_DIR) M= (KERNELDIR)M=(CUR_DIR) modules
/home/ubuntu/ARM_Tools/gcc-4.6.4/bin/arm-none-linux-gnueabi-gcc $(APP_NAME).c -o $(APP_NAME)
clean:
make -C ( K E R N E L D I R ) M = (KERNEL_DIR) M= (KERNELDIR)M=(CUR_DIR) clean
make -C ( K E R N E L D I R ) M = (KERNEL_DIR) M= (KERNELDIR)M=(CUR_DIR) clean

install:
cp *.ko $(ROOTFS_DIR)
cp $(APP_NAME) $(ROOTFS_DIR)
else
obj-m += $(MODULE_NAME).o

endif


你可能感兴趣的:(linux,学习,ubuntu)