1、Linux内核中断处理简介
1.1、Linux中断
(1)先知道你要使用的中断对应的中断号。
(2)再申请request_irq,此函数会激活中断。
(3)如果不用中断了,那就释放掉,使用free_irq。
(4)中断处理函数irqreturn_t (*irq_handler_t) (int, void *)。
(5)使能和禁止中断。
local_irq_save(flags);//用于禁止中断,并且将中断状态保存在flags中。
local_irq_restore(flags);//用于恢复中断,将中断到flags状态。
1.2、上半部与下半部
**上半部:**上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
**下半部:**如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了。Linux 内核提供了多种下半部机制。
软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS] 10个
要使用软中断,要先注册,使用函数open_softir。注册以后使用raise_softirq触发。
软中断我们不要去用!!
tasklet
也需要用到上半部,只是上半部的中断处理函数重点是调用tasklet_schedule。
①、定义一个tasklet函数。
②、初始化、重点是设置对应的处理函数
2、如何在设备树中添加中断节点信息
2.1、设备树中断信息节点
Linux 内核通过读取设备树中的中断属性信息来配置中断。
设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。
打开 imx6ull.dtsi 文件,其中的 intc 节点就是I.MX6ULL 的中断控制器节点。
1 intc: interrupt-controller@00a01000 {
2 compatible = "arm,cortex-a7-gic";
3 #interrupt-cells = <3>;
4 interrupt-controller;
5 reg = <0x00a01000 0x1000>,
6 <0x00a02000 0x100>;
7 };
对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示:
1 gpio5: gpio@020ac000 {
2 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
3 reg = <0x020ac000 0x4000>;
4 interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
6 gpio-controller;
7 #gpio-cells = <2>;
8 interrupt-controller;
9 #interrupt-cells = <2>;
10 };
打开 imx6ull-alientek-emmc.dts 文件,找到如下所示内容:
//fxls8471 设备节点
1 fxls8471@1e {
2 compatible = "fsl,fxls8471";
3 reg = <0x1e>;
4 position = <0>;
5 interrupt-parent = <&gpio5>;
6 interrupts = <0 8>;
7 };
fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片,fxls8471 有一个中断引脚链接到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00。
第 5 行,interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
第 6 行,interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。
我们在使用时,只是添加第5、6行的内容。
3、编写驱动
3.1、修改设备树
将按键作为一个中断输入。在设备树中添加按键节点。打开imx6ull-alientek-emmc.dts,找到key这么一个节点。这个节点只是添加了key作为一个普通IO,现在要把中断相关的节点添加进去。
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。
设置 interrupts 属性,也就是设置中断源,第一个cells的18表示GPIO1组的18号IO。IRQ_TYPE_EDGE_BOTH 定义在文件 include/linux/irq.h 中。
make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /home/yang/linux/tftpboot/ -f
启动开发板:
/ #
/ # cd proc/device.tree/
/sys/firmware/devicetree/base # ls
/sys/firmware/devicetree/base # cd key/
3.2、编写驱动(使用上半部)
创建工程:
3.2.1、编写驱动框架
搭建一个字符设备驱动框架。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define IMX6UIRQ_CNT 1 /* 设备号个数 */
#define IMX6UIRQ_NAME "imx6uirq" /* 名字 */
/* key设备结构体 */
struct imx6uirq_dev{
dev_t devid; /* 设备号 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
};
struct imx6uirq_dev imx6uirq; /* irq设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
filp->private_data = &imx6uirq; /* 设置私有数据 */
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
return ret;
}
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.write = imx6uirq_write,
.read = imx6uirq_read,
.release = imx6uirq_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init imx6uirq_init(void)
{
int ret = 0;
/* 注册字符设备驱动 */
/* 1、创建设备号 */
imx6uirq.major = 0;
if (imx6uirq.major) { /* 定义了设备号 */
imx6uirq.devid = MKDEV(imx6uirq.major, 0);
ret = register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME); /* 申请设备号 */
imx6uirq.major = MAJOR(imx6uirq.devid); /* 获取分配号的主设备号 */
imx6uirq.minor = MINOR(imx6uirq.devid); /* 获取分配号的次设备号 */
}
if(ret < 0) {
goto fail_devid;
}
printk("imx6uirq major=%d,minor=%d\r\n",imx6uirq.major, imx6uirq.minor);
/* 2、初始化cdev */
imx6uirq.cdev.owner = THIS_MODULE;
cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
/* 3、添加一个cdev */
ret = cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
if(ret)
goto fail_cdevadd;
/* 4、创建类 */
imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.class)) {
ret = PTR_ERR(imx6uirq.class);
goto fail_class;
}
/* 5、创建设备 */
imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.device)) {
ret = PTR_ERR(imx6uirq.device);
goto fail_device;
}
return 0;
fail_device:
class_destory(imx6uirq.class);
fail_class:
cdev_del(&imx6uirq.cdev);
fail_cdevadd:
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
fail_devid:
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit imx6uirq_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&imx6uirq.cdev);/* 删除cdev */
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT); /* 注销设备号 */
device_destroy(imx6uirq.class, imx6uirq.devid);
class_destroy(imx6uirq.class);
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yang");
3.2.2、编写中断驱动
编写一个按键初始化函数。在这个函数中
(1)按键初始化:
获取节点;
获取GPOIO对应的编号;新建一个新的结构体来描述按键的一些信息,这个按键信息就不能放在imx6uirq设备结构体中了。
#define KEY_NUM 1
/* key结构体 */
struct irq_keydesc{
int gpio; //io编号
int irqnum; //中断号
unsihned char value; //键值
char name[10]; //名字
//中断处理函数
};
在imx6uirq_dev设备结构体中添加按键信息
struct imx6uirq_dev{
...
struct irq_keydesc irqkey[KEY_NUM];
};
(2)按键中断初始化:
static int keyio_init(void)
{
int ret = 0;
int i = 0;
/* 按键初始化 */
imx6uirq.nd = of_find_node_by_path("/key");
if(imx6uirq.nd == NULL){
ret = -EINVAL;
goto fail_nd;
/* 按键中断初始化 */
return 0;
fail_nd:
return ret;
}
接下来获取按键编号;如果有很多IO,就循环获取:
初始化IO,也是循环,使用gpio_request;
/* 1、按键初始化 */
...
for(i=0; i<KEY_NUM; i++){
imx6irq.irqkey[i].gpio = of_get_named_gpio(imx6uirq.nd, "key.gpios", i);
}
/* 2、按键中断初始化 */
在按键初始化函数中,可以传递参数struct imx6uirq_dev *dev,则函数修改为如下:
static int keyio_init(struct imx6uirq_dev *dev)
{
int ret = 0;
int i = 0;
/* 按键初始化 */
dev->nd = of_find_node_by_path("/key");
if(dev->nd == NULL){
ret = -EINVAL;
goto fail_nd;
}
for(i=0; i<KEY_NUM; i++){
dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key.gpios", i);
}
for(i=0; i<KEY_NUM; i++){
memset(dev->irqkey[i].name, 0, sizeof(imx6uirq.irqkey[i].name));
sprintf(dev->irqkey[i].name, "KEY%d", i);
gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name);
gpio_direction_input(dev->irqkey[i].gpio);
}
/* 2、按键中断初始化 */
return 0;
fail_nd:
return ret;
}
编译一下:可以在入口函数中,使用按键初始化函数,添加头文件
#include <linux/string.h>
加载到开发板上:
make
sudo cp imx6uirq.ko /home/yang/linux/nfs/rootfs/lib/modules/4.1.15/ -f
接下来,获取各个gpio对应的中断号
for(i=0; i<KEY_NUM; i++){
memset(dev->irqkey[i].name, 0, sizeof(imx6uirq.irqkey[i].name));
sprintf(dev->irqkey[i].name, "KEY%d", i);
gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name);
gpio_direction_input(dev->irqkey[i].gpio);
dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);//获取中断号
#if 0
dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i);//通用的获取中断号
#endif
}
/* 2、按键中断初始化 */
for(i = 0; i<KEY_NUM; i++){
ret = request_irq(dev->irqkey[i].irqnum, );//参数:中断号,中断处理函数
参考Linux内核中使用中断处理函数的用法:
/* 按键中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
return IRQ_HANDLED;
}
那么:先将中断处理函数添加到结构体中,然后再做个初始化处理。
/* key结构体 */
irqreturn_t (*handler) (int, void*) /* 中断处理函数 */
还需要定义按键值
#define KEY0VALUE 0x01
#define INVAKEY 0xFF
dev->irqkey[0].handler = key0_handler;
dev->irqkey[0].value = KEY0VALUE;
/* 2、按键中断初始化 */
for(i = 0; i<KEY_NUM; i++){
ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[i].handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, dev->irqkey[i].name, &imx6uirq);//参数:中断号,中断处理函数,触发方式,中断名字,传递个中断函数的参数
if(ret){
printk("irq %d request failed!\r\n", dev->irqkey[i].irqnum);
goto fail_irq;
}
}
return 0;
fail_irq://到此时,request需要释放
for(i=0;i<KEY_NUM;i++){
gpio_free(dev->irqkey[i].gpio, );
}
fail_nd:
return ret;
在驱动入口函数中,初始化IO
/* 初始化IO */
ret = keyio_init(&imx6uirq);
if(ret<0){
goto fail_keyinit;
}
return 0;
fail_keyinit:
在驱动出口函数释放IO,要先释放中断
int i = 0;
/* 1、释放中断 */
for(i=0; i<KEY_NUM; i++){
free_irq(imx6uirq.irqkey[i].irqnum, &imx6uirq);
}
/* 2、释放IO */
for(i=0; i<KEY_NUM; i++){
gpio_free(imx6uirq.irqkey[i].gpio);
}
编译一下:
添加头文件:
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/ide.h>//会引用interrupt.h
在按键中断函数中做处理:
/* 按键中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
int value = 0;
struct imx6uirq_dev *dev = dev_id;
value = gpio_get_value(dev->irqkey[0].gpio);
if(value == 0){ /* 按下 */
printk("KEY0 Push!\r\n");
}else if(value == 1){ /* 释放 */
printk("KEY0 Release!\r\n");
}
return IRQ_HANDLED;
}
编译并拷贝到开发板:
make
sudo cp imx6uirq.ko /home/yang/linux/nfs/rootfs/lib/modules/4.1.15/ -f
/ # cd lib/modules/4.1.15/
/lib/modules/4.1.15/ # ls
/lib/modules/4.1.15/ # depmod
/lib/modules/4.1.15/ # modprobe imx6uirq.ko
/lib/modules/4.1.15/ # ls /proc/interrupts
/lib/modules/4.1.15/ # cat /proc/interrupts
KEY0 Push!//按下按键
KEY0 Release!//松开按键
3.2.3、按键消抖
通过定时器来消抖。
在imx6uirq_dev设备结构体中添加定时器相关的东西。
struct imx6uirq_dev{
...
struct timer_list timer;
};
定时器需要初始化的,紧接着在按键初始化完了之后进行定时器初始化。刚开始的定时器的周期值不能设定,我们按下按键后才能让定时器工作,后面通过mod_timer来启动定时器。
还需要添加定时器的处理函数
/* 定时器的处理函数 */
static void timer_func(unsigned long arg){
}
/* 2、按键中断初始化 */
/* 3、定时器初始化 */
init_timer(&imx6uirq.timer);
imx6uirq.timer.function = timer_func;
在退出驱动的时候,需要删除定时器。
/* 1、释放中断 */
/* 2、释放IO */
/* 3、删除定时器 */
del_timer(&imx6uirq.timer);
del_timer_sync(&imx6uirq.timer);
按键按下去,进入按键中断处理函数,开启定时器,做一个简单的延时,比如10ms,10ms以后,定时器函数处理会执行,如果定时器函数执行的话,代表消抖已经过去啦,所以key0_handler()中,只需要触发定时器10ms即可。
static irqreturn_t key0_handler(int irq, void *dev_id)
{
int value = 0;
struct imx6uirq_dev *dev = dev_id;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));/* 10ms定时 */
return IRQ_HANDLED;
}
10ms定时结束以后,定时器处理函数timer_func就会执行。进入这个函数之后,我们再去读取按键值,
/* 定时器的处理函数 */
static void timer_func(unsigned long arg){
int value = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev*)arg;
value = gpio_get_value(dev->irqkey[0].gpio);
if(value == 0){ /* 按下 */
printk("KEY0 Push!\r\n");
}else if(value == 1){ /* 释放 */
printk("KEY0 Release!\r\n");
}
}
编译,验证,发现有一点点问题,修改10ms–>20ms,
在定时器处理函数中添加一句打印消息:
printk("timer_func\r\n");
value......
编译验证,没有问题。
3.2.4、完善驱动
接下来要做:向应用层上报按键值。
在imx6uirq_dev设备结构体中添加2个变量。
第一个:按键值用atomic_t原子变量,
第二个:需要一个变量来标记按键值有没有被按下。
atomic_t keyvalue;
atomic_t releaekey;
将这两个原子变量在驱动入口函数中初始化,
/* 初始化IO */
/* 初始化原子变量 无效按键*/
atomic_set(&imx6uirq.keyvalue, INVAKEY);
atomic_set(&imx6uirq.relasekey, 0);
要在定时器函数处理里面读取按键值value,按下以后要进行处理,每个按键都i有对应的一个值,将按键的值赋给keyvalue。释放的话,给按键值最高位置1。或上一个0x80。
/* 定时器的处理函数 */
static void timer_func(unsigned long arg)
{
...
if(value == 0){ /* 按下 */
atomic_set(&dev->keyvalue, dev->irqkey[0].value);
}else if(value == 1){ /* 释放 */
atomic_set(&dev->keyvalue, 0x80 | (dev->irqkey[0].value));
atomic_set(&dev->rleasekey, 1);/* 完整的一个按键过程 */
}
}
还需要写一个read函数,因为应用程序需要读,在read函数中,先把私有数据提取出来。先定义2个变量,一个用来保存按键值,另一个保存有没有被释放。出现有效按键就要上报,如果是0x80,则说明释放掉了,释放掉以后把最高位置1,释放掉,然后把keyvalue拷贝给内核。判断release后需要清0。
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keyvalue;
unsigned char releasekey;
struct imx6uirq_dev *dev = filp->private_data;
keyvalue = atomic_read(&dev->keyvalue);
releasekey = atomic_read(&dev->releasekey);
if(releasekey) { /* 有效按键 */
if(keyvalue & 0x80){
keyvalue &= -0x80;
ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
} else {
goto data_error;
}
atomic_set(&dev->releasekey, 0);//按下标志清零
}
return ret;
data_error:
return -EINVAL;
}
3.3、编写应用APP
将上个实验的timerAPP拷贝过来。
cd /linux/IMX6ULL/Linux_Driver/13_timer/
ls
cp timerAPP.c ../14_imx6uirq/
ls
mv timerAPP.c imx6uirqAPP.c
在imx6uirqAPP.c中:
/* 循环读取 */
while(1){
ret = read(fd, &data, sizeof(data));
if(ret < 0){
}else{
if(data)
printf("key value = %#x\r\n", data);
}
}
编译
arm-linux-gnueabihf-gcc imx6uirqAPP.c -o imx6uirqAPP
sudo cp imx6uirq.ko imx6uirqAPP /home/yang/linux/nfs/tootfs/lib/modules/4.1.15/ -f
加载驱动执行APP
lsmod
rmmod imx6uirq
modprobe imx6uirq.ko
./imx6uirqAPP /dev/imx6uirq
一直打印0x01,说明驱动有问题,应该按下一次,打印一次,在驱动里面,只处理了releasekey有效的按键,因此需要添加无效的时候。应该goto error。
if(releasekey){ /* 有效按键 */
......
} else {
goto data_error;
}
验证:按下释放才会传递按键值0x01。
3.2、修改驱动(使用下半部)
复制一份驱动文件,直接在上面修改,将复制的文件重新命名为:tasklet.c,将Makefile文件改为tasklet.o
3.2.1、tasklet
在key设备结构体里面添加(一个按键一个中断):
struct tasklet_struct tasklet;
在init初始化函数中添加tasklet_init();
/* 2、按键中断初始化*/
for(i=0 ....)
{
ret=...
if(ret...)
...
tasklet_init(&dev->tasklet, );//第二个参数就是tasklet所对应的处理函数
}
编写tasklet所对应的处理函数,位置放到按键中断处理函数下面:
/* tasklet */
static void key_tasklet(unsigned long data)
{
}
也需要将其初始化,
//dev->irqkey[0].tasklet = key_tasklet;
/* 2、按键中断初始化*/
for(i=0 ....)
{
ret=...
if(ret...)
...
tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);//第二个参数就是tasklet所对应的处理函数.不管是哪几个io,使用的都是一个key_tasklet,
}
在按键中断处理函数中只需要调度一下tasklet_schedule();
/* 按键中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
//int value = 0;
struct imx6uirq_dev *dev = dev_id;
#if 0
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));/* 10ms定时 */
#endif
tasklet_schedule(&dev->irqkey[0].tasklet);//写死了是0
return IRQ_HANDLED;
}
调度完了之后,key_tasklet()里面执行:
/* tasklet */
static void key_tasklet(unsigned long data)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev)data;
printk("key_task!\r\n");
dev->timer.data = data;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));/* 20ms定时 */
}
3.2.2、工作队列
cp tasklet.c work.c
与tasklet用法类似,也可以参考Linux源码里面的用法进行使用。