参考:https://blog.csdn.net/fengyuwuzu0519/article/details/71046343
字符设备驱动程序之中断方式的按键驱动_编写代码
使用中断方式,那么肯定有一个中断的初始化注册,就是告诉内核,我按下按键的时候会触发一个中断,同时一定有一个中断处理函数来处理中断发生时应该做什么。
linux内核中 如何告诉内核我按下按键了给我触发中断并实现中断处理函数呢。
注册中断(open驱动程序时调用):int request_irq(unsigned int irq, irq_handler_t handler,unsigned long irqflags, const char *devname, void *dev_id)
request_irq()函数参数解析:
※※※重要!!void *:void即“无类型”,void *则为“无类型指针”,可以指向任何数据类型。所以后面的代码传进来结构体。
1、从原理图可知IRQ中断号irq:(IRQ_EINT0……)
2、向系统注册的中断处理函数,中断发生时,系统调用这个函数,dev_id参数被传递给它,中断处理函数handler的格式:
3、触发方式:irqflags:type(IRQT_BOTHEDGE双边沿触发:上升沿和下降沿都可以触发中断)。
4、devname:中断名称,可以使用cat /proc/interrupts 查看此名称
5、dev_id:用法很简单。在free_irq卸载时,通过irq与dev_id结合在一起,来确定卸载哪一个irqaction结构。
释放中断(卸载驱动程序时,解除按键中断):
void free_irq(unsigned int irq, void *dev_id)
参数:irq中断号。dev_id用法很简单,在free_irq卸载时,通过irq与dev_id结合在一起,来确定卸载哪一个irqaction结构。
驱动程序:third_drv.c
/*一、驱动框架:1.先定义file_operations结构体,其中有对设备的打开,读和写的操作函数。2.分别定义相关的操作函数3.定义好对设备的操作函数的结构体(file_operations)后,将其注册到内核的file_operations结构数组中。此设置的主设备号为此结构在数组中的下标。4.定义出口函数:卸载注册到内核中的设备相关资源5.修饰 入口 和 出口函数6.给系统提供更多的内核消息,在sys目录下提供设备的相关信息。应用程序udev可以据此自动创建设备节点,创建一个class设备类,在此类下创建设备
*/#include <linux/module.h> //内涵头文件,含有一些内核常用函数的原形定义。
#include <linux/kernel.h> //最基本的文件,支持动态添加和卸载模块。Hello World驱动要这一个文件就可以。
#include <linux/fs.h> //包含了文件操作相关的struct的定义,例如struct file_operations
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h> //包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义
#include <asm/irq.h>
#include <asm/io.h> //包含了ioremap、ioread等内核访问IO内存等函数的定义
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>static struct class *thirddrv_class; //一个类
static struct class_device *thirddrv_class_dev; //一个类里面再建立一个设备volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;static irqreturn_t button_irq(int irq, void *dev_id)
{printk("irq = %d\n",irq);return IRQ_HANDLED;
}static int third_drv_open(struct inode *inode, struct file *file)
{/* 配置GPF0,2为输入引脚 *//* 配置GPF3,11为输入引脚 */request_irq(IRQ_EINT0, button_irq, IRQT_BOTHEDGE, "S2", 1); //配置为中断引脚request_irq(IRQ_EINT2, button_irq, IRQT_BOTHEDGE, "S3", 1);request_irq(IRQ_EINT11, button_irq, IRQT_BOTHEDGE, "S4", 1);request_irq(IRQ_EINT19, button_irq, IRQT_BOTHEDGE, "S5", 1);return 0;
}ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{/* 返回4个引脚的电平 */unsigned char key_vals[4];int regval;//如果传进来的size不等于我们返回的4个字节,返回一个错误值if (size != sizeof(key_vals))return -EINVAL;/* 读GPF0,2为输入引脚 */regval = *gpfdat;key_vals[0]=(regval & (1<<0)) ? 1 : 0;key_vals[1]=(regval & (1<<2)) ? 1 : 0;/* 读GPG3,11为输入引脚 */regval = *gpgdat;key_vals[2]=(regval & (1<<3)) ? 1 : 0;key_vals[3]=(regval & (1<<11)) ? 1 : 0;copy_to_user(buf, key_vals, sizeof(key_vals));return sizeof(key_vals);
}int third_drv_close(struct inode *inode, struct file *file)
{free_irq(IRQ_EINT0, 1);free_irq(IRQ_EINT2, 1);free_irq(IRQ_EINT11, 1);free_irq(IRQ_EINT19, 1);return 0;
}static struct file_operations third_drv_fops = {.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */.open = third_drv_open, .read = third_drv_read,.release = third_drv_close,
};int major;static int third_drv_init(void)
{major = register_chrdev(0, "third_drv", &third_drv_fops);//创建一个类thirddrv_class = class_create(THIS_MODULE, "firstdrv");//在这个类下面再创建一个设备//mdev是udev的一个简化版本//mdev应用程序,就会被内核调用,会根据类和类下面的设备这些信息thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons");/* /dev/buttons *///建立地址映射:物理地址->虚拟地址gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)gpfdat = gpfcon + 1; //加1,实际加4个字节gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16); //指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)gpgdat = gpgcon + 1; //加1,实际加4个字节return 0;
}static void third_drv_exit(void)
{unregister_chrdev(major, "third_drv");class_device_unregister(thirddrv_class_dev);class_destroy(thirddrv_class);iounmap(gpfcon);iounmap(gpgcon);return 0;
}module_init(third_drv_init);
module_exit(third_drv_exit);MODULE_LICENSE("GPL");
验证驱动中断:(下面步骤,不写应用程序来验证驱动程序)
使用命令:exec 5</dev/buttons,打开/dev/buttons这个设备,定位到文件描述符fd5,挂载到5下,会调用open)
cat /proc/interrupts (产生的中断的信息)
使用ps命令查看,当前进程是-sh(shell),PID是772。
使用命令,ls -l /proc/772/fd,文件描述符fd5指向/dev/buttons,以后通过文件描述符5来访问buttons设备。
(exec是用来执行一个进程的)
(linux中,所有设备都是文件,/dev/buttons也是文件,有文件描述符exec 5</dev/button 将 /dev/button文件关联到文件描述符5,以后对5的操作,就是对设备文件的操作)
(在Shell里执行exec 5</dev/button,因此5是Shell新打开的文件描述符,Shell的进程ID是772)
(在proc文件系统里772的fd目录,表示772进程打开的文件描述符)
(ps查看当前进程(可以查看进程状态s:休眠))
使用命令,exec 5<&-,关闭文件描述符fd5,释放中断。(会调用release)
测试:(按下按键)
IRQ_EINT0:16=16+0,IRQ_EINT2:18=16+2,IRQ_EINT11:55=16+39,IRQ_EINT19(复位键):63=16+47。
因为是双边沿触发,所以每次按下按键,松开按键,打印两次。
优化上面的程序(读出按键值)
1、内核有一个系统函数s3c2410_gpio_getpin(引脚PIN):读出引脚的值。
2、定义了一个结构体pin_desc:
这个结构体在request_irq函数里传进去。
3、在read函数中,如果没有按键动作发生,休眠(让出CPU,不返回);如果有按键动作发生,直接返回。
休眠:wait_event_interruptible(button_waitq, ev_press)
(把进程挂在button_waitq队列里面)
如果ev_press=0,让应用程序休眠,不返回,程序停止在此处。当被唤醒时,从此处继续执行。
定义上面两个参数:
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中断时间标志,中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press=0;
4、当中断发生,执行中断处理函数,此时唤醒队列中次应用的进程,继续执行,返回结果。
唤醒:(去button_waitq队列,把挂在这个队列的进程唤醒)
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程,去button_wq队列的进程唤醒 */
完整的驱动代码:third_drv.c
/*一、驱动框架:1.先定义file_operations结构体,其中有对设备的打开,读和写的操作函数。2.分别定义相关的操作函数3.定义好对设备的操作函数的结构体(file_operations)后,将其注册到内核的file_operations结构数组中。此设置的主设备号为此结构在数组中的下标。4.定义出口函数:卸载注册到内核中的设备相关资源5.修饰 入口 和 出口函数6.给系统提供更多的内核消息,在sys目录下提供设备的相关信息。应用程序udev可以据此自动创建设备节点,创建一个class设备类,在此类下创建设备
*/#include <linux/module.h> //内涵头文件,含有一些内核常用函数的原形定义。
#include <linux/kernel.h> //最基本的文件,支持动态添加和卸载模块。Hello World驱动要这一个文件就可以。
#include <linux/fs.h> //包含了文件操作相关的struct的定义,例如struct file_operations
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h> //包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义
#include <asm/irq.h>
#include <asm/io.h> //包含了ioremap、ioread等内核访问IO内存等函数的定义
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>static struct class *thirddrv_class; //一个类
static struct class_device *thirddrv_class_dev; //一个类里面再建立一个设备volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;/* 下面两个是定义休眠函数的参数 */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);/* 中断时间标志,中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press=0;/* 引脚描述的结构体 */
struct pin_desc{unsigned int pin;unsigned int key_val;
};/* 键值:按下时,0x01,0x02,0x03,0x04 */
/* 键值:松开时,0x81,0x82,0x83,0x84 */static unsigned char keyval; //键值/* 在request_irq函数中把结构体传进去 */
struct pin_desc pins_desc[4] = { //键值先赋初始值0x01,0x02,0x03,0x04{S3C2410_GPF0, 0x01}, //pin=S3C2410_GPF0, key_val(按键值)=0x01{S3C2410_GPF2, 0x02}, //pin=S3C2410_GPF2, key_val(按键值)=0x02{S3C2410_GPG3, 0x03}, //pin=S3C2410_GPF3, key_val(按键值)=0x03{S3C2410_GPG11, 0x04}, //pin=S3C2410_GPF11, key_val(按键值)=0x04
};/** 确定按键值*/
static irqreturn_t button_irq(int irq, void *dev_id) //中断处理函数
{/* irq = IRQ_EINT0 …… *//* dev_id = 结构体struct pins_desc */struct pin_desc * pindesc = (struct pin_desc *)dev_id;unsigned int pinval;/* 读取引脚PIN值 */pinval = s3c2410_gpio_getpin(pindesc->pin);/* 确定按键值,按下管脚低电平,松开管脚高电平 */if(pinval){/* 松开 */ keyval = 0x80 | pindesc->key_val; //规定的:0x8X}else{/* 按下 */keyval = pindesc->key_val; //0x0X}/* 唤醒 */ev_press = 1; /* 表示中断发生了 */wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程,去button_wq队列,把挂在队列下的进程唤醒 */ return IRQ_RETVAL(IRQ_HANDLED);
}static int third_drv_open(struct inode *inode, struct file *file)
{/* 配置GPF0,2为输入引脚 *//* 配置GPF3,11为输入引脚 *//* request_irq函数的第五个参数是void *,为无类型指针,可以指向任何数据类型 */request_irq(IRQ_EINT0, button_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);request_irq(IRQ_EINT2, button_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);request_irq(IRQ_EINT11, button_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);request_irq(IRQ_EINT19, button_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);return 0;
}ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{if (size != 1)return -EINVAL;/* 如果没有按键动作,休眠,休眠:让出CPU *//* 休眠时,把进程挂在button_wq 队列里 *//* 如果休眠后被唤醒,就会从这里继续往下执行 *//* 一开始没有按键按下,ev_press = 0 */wait_event_interruptible(button_waitq, ev_press);//ev_press=0,休眠,让我们的测试程序休眠;ev_press!=0,直接往下运行/* 如果有按键动作,返回键值 */copy_to_user(buf, &keyval, 1); //把键值 拷回去ev_press = 0; //清零,如果不清零,下次再读,立马往下执行,返回原来的值return 1;
}int third_drv_close(struct inode *inode, struct file *file)
{free_irq(IRQ_EINT0, &pins_desc[0]);free_irq(IRQ_EINT2, &pins_desc[1]);free_irq(IRQ_EINT11, &pins_desc[2]);free_irq(IRQ_EINT19, &pins_desc[3]);return 0;
}static struct file_operations third_drv_fops = {.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */.open = third_drv_open, .read = third_drv_read,.release = third_drv_close,
};int major;static int third_drv_init(void)
{major = register_chrdev(0, "third_drv", &third_drv_fops);//创建一个类thirddrv_class = class_create(THIS_MODULE, "firstdrv");//在这个类下面再创建一个设备//mdev是udev的一个简化版本//mdev应用程序,就会被内核调用,会根据类和类下面的设备这些信息thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons");/* /dev/buttons *///建立地址映射:物理地址->虚拟地址gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)gpfdat = gpfcon + 1; //加1,实际加4个字节gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16); //指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)gpgdat = gpgcon + 1; //加1,实际加4个字节return 0;
}static void third_drv_exit(void)
{unregister_chrdev(major, "third_drv");class_device_unregister(thirddrv_class_dev);class_destroy(thirddrv_class);iounmap(gpfcon);iounmap(gpgcon);return 0;
}module_init(third_drv_init);
module_exit(third_drv_exit);MODULE_LICENSE("GPL");
测试程序:thirddrvtest.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>/* thirddrvtest*/
int main(int argc, char **argv)
{int fd;unsigned char key_val;int cnt = 0;fd = open("/dev/buttons", O_RDWR);if (fd < 0){printf("can't open!\n");}while (1){//用查询方式读按键坏处:占用CPU大//根本不知道按键什么时候按下,不可预料,只能不断地读,知道它返回read(fd, &key_val, 1);printf("key_val = 0x%x\n", key_val);}return 0;
}
Makefile文件
KERN_DIR = /work/system/linux-2.6.22.6all:make -C $(KERN_DIR) M=`pwd` modules clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderobj-m += third_drv.o
最后进行测试:
insmod third_drv.ko
./thirddrvtest & (在后台执行)
然后按下开发板的四个按键,再松开。。
怎么卸载模块呢?
因为模块正在被使用
所以,先找到在后台运行的程序thirddrvtest,查看它的进程号为845,
用命令kill -9 845杀死进程,
再卸载模块。
书上或者光盘的参考代码:s3c24xx_button.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>#define DEVICE_NAME "buttons" /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define BUTTON_MAJOR 232 /* 主设备号 */struct button_irq_desc {int irq;unsigned long flags;char *name;
};/* 用来指定按键所用的外部中断引脚及中断触发方式, 名字 */
static struct button_irq_desc button_irqs [] = {{IRQ_EINT19, IRQF_TRIGGER_FALLING, "KEY1"}, /* K1 */{IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */{IRQ_EINT2, IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */{IRQ_EINT0, IRQF_TRIGGER_FALLING, "KEY4"}, /* K4 */
};/* 按键被按下的次数(准确地说,是发生中断的次数) */
static volatile int press_cnt [] = {0, 0, 0, 0};/* 等待队列: * 当没有按键被按下时,如果有进程调用s3c24xx_buttons_read函数,* 它将休眠*/
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);/* 中断事件标志, 中断服务程序将它置1,s3c24xx_buttons_read将它清0 */
static volatile int ev_press = 0;static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{volatile int *press_cnt = (volatile int *)dev_id;*press_cnt = *press_cnt + 1; /* 按键计数加1 */ev_press = 1; /* 表示中断发生了 */wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */return IRQ_RETVAL(IRQ_HANDLED);
}/* 应用程序对设备文件/dev/buttons执行open(...)时,* 就会调用s3c24xx_buttons_open函数*/
static int s3c24xx_buttons_open(struct inode *inode, struct file *file)
{int i;int err;for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {// 注册中断处理函数err = request_irq(button_irqs[i].irq, buttons_interrupt, button_irqs[i].flags, button_irqs[i].name, (void *)&press_cnt[i]);if (err)break;}if (err) {// 释放已经注册的中断i--;for (; i >= 0; i--)free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);return -EBUSY;}return 0;
}/* 应用程序对设备文件/dev/buttons执行close(...)时,* 就会调用s3c24xx_buttons_close函数*/
static int s3c24xx_buttons_close(struct inode *inode, struct file *file)
{int i;for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {// 释放已经注册的中断free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);}return 0;
}/* 应用程序对设备文件/dev/buttons执行read(...)时,* 就会调用s3c24xx_buttons_read函数*/
static int s3c24xx_buttons_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{unsigned long err;/* 如果ev_press等于0,休眠 */wait_event_interruptible(button_waitq, ev_press);/* 执行到这里时,ev_press等于1,将它清0 */ev_press = 0;/* 将按键状态复制给用户,并清0 */err = copy_to_user(buff, (const void *)press_cnt, min(sizeof(press_cnt), count));memset((void *)press_cnt, 0, sizeof(press_cnt));return err ? -EFAULT : 0;
}/* 这个结构是字符设备驱动程序的核心* 当应用程序操作设备文件时所调用的open、read、write等函数,* 最终会调用这个结构中的对应函数*/
static struct file_operations s3c24xx_buttons_fops = {.owner = THIS_MODULE, /* 这是一个宏,指向编译模块时自动创建的__this_module变量 */.open = s3c24xx_buttons_open,.release = s3c24xx_buttons_close, .read = s3c24xx_buttons_read,
};/** 执行“insmod s3c24xx_buttons.ko”命令时就会调用这个函数*/
static int __init s3c24xx_buttons_init(void)
{int ret;/* 注册字符设备驱动程序* 参数为主设备号、设备名字、file_operations结构;* 这样,主设备号就和具体的file_operations结构联系起来了,* 操作主设备为BUTTON_MAJOR的设备文件时,就会调用s3c24xx_buttons_fops中的相关成员函数* BUTTON_MAJOR可以设为0,表示由内核自动分配主设备号*/ret = register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &s3c24xx_buttons_fops);if (ret < 0) {printk(DEVICE_NAME " can't register major number\n");return ret;}printk(DEVICE_NAME " initialized\n");return 0;
}/** 执行”rmmod s3c24xx_buttons.ko”命令时就会调用这个函数 */
static void __exit s3c24xx_buttons_exit(void)
{/* 卸载驱动程序 */unregister_chrdev(BUTTON_MAJOR, DEVICE_NAME);
}/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_buttons_init);
module_exit(s3c24xx_buttons_exit);/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("http://www.100ask.net"); // 驱动程序的作者
MODULE_DESCRIPTION("S3C2410/S3C2440 BUTTON Driver"); // 一些描述信息
MODULE_LICENSE("GPL"); // 遵循的协议