前言
中断顾名思义,就是在CPU执行的时候被某个中断信号打断,保护现场后跳转到预先设定好的中断处理代码中执行,执行完毕后返回并恢复现场,继续执行被打断前的任务。
一、中断相关概念
1 .中断号:每个中断都有一个中断号,内核中使用一个int表示中断号,在Linux中,可以使用
cat /proc/interrupts
来查看当前系统的中断。
2.申请中断:默认使能中断,不需要enable_irq
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
irq: 要申请中断的中断号。
handler: 中断处理函数,当中断发生以后就会执行此中断处理函数。
flags: 中断标志,可以在内核源码目录 include/linux/interrupt.h 文件中查看所有的中断标志,
这里我们介绍几个常用的中断标志,可以通过“|”实现组合
IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必
须指定此标志。如果使用共享中断的话, request_irq
函数的 dev 参数就是唯一区分他们的标志。
IRQF_ONESHOT 单次中断,中断执行一次就结束。
IRQF_TRIGGER_NONE 无触发。
IRQF_TRIGGER_RISING 上升沿触发。
IRQF_TRIGGER_FALLING 下降沿触发。
IRQF_TRIGGER_HIGH 高电平触发。
IRQF_TRIGGER_LOW 低电平触发。
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将
dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。
3、 释放函数
void free_irq(unsigned int irq, void *dev)
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放
掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。
irq: 要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中
断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
4、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,
需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
中断处理函数的返回值为 irqreturn_t 类型, irqreturn_t 类型定义如下所示:
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED);
5、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq) 要等中断执行完才返回
void disable_irq_nosync(unsigned int irq) 立即返回
全局中断:
local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对, local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。
local_irq_restore 用于恢复中断,将中断到 flags 状态。
二、上半部与下半部
也称顶半部和底半部,使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部。
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
主要目的就是实现中断的快进快出,
对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行
没有明确哪些属于上半部,哪些属于下半部,可以参考:
1、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
2、如果要处理的任务对时间敏感,可以放到上半部。
3、如果要处理的任务与硬件有关,可以放到上半部
4、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部在中断处理函数解决,下半部一般使用linux内核提供的处理机制。
1.软中断
Linux 内核使用结构体 softirq_action 表示软中断
struct softirq_action
{
void (*action)(struct softirq_action *);
};
内核中定义了一个有十个软中断的全局数组在kernel/softirq.c,
NR_SOFTIRQS 是枚举类型,定义在include/linux/interrupt.h 中,=10
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
对于所有cpu都可以访问,软中断服务函数是相同的。
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
注册软中断处理函数:
void open_softirq(int nr, void (*action)(struct softirq_action *))
函数参数和返回值含义如下:
nr:要开启的软中断,在上面枚举中选择一个。
action:软中断对应的处理函数。
返回值: 没有返回值。
注册好后通过raise_softirq触发
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
nr:要触发的软中断,在上面枚举中选择一个。
返回值: 没有返回值。
软中断必须在编译的时候静态注册,linux内核使用softirq_init 函数初始化软中断(kernel/softirq.c)
会默认打开HI_SOFTIRQ和TASKLET_SOFTIRQ
2.tasklet
利用软中断来实现的另外一种下半部机制,在两者之间建议选择tasklet
Linux 内核使用结构体 tasklet_struct 来表示 taskled,该结构体定义在include/linux/interrupt.h 文件中
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
要使用,必须先定义一个tasklet,初始化:
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
t:要初始化的 tasklet
func: tasklet 的处理函数。
data: 要传递给 func 函数的参数
返回值: 没有返回值。
也可以用宏:
DECLARE_TASKLET(name, func, data)
在上半部,也就是中断处理函数调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值: 没有返回值。
示例:
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
3.工作队列
另外一种下半部执行的方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行。
如果要推后的工作可以睡眠,就选择工作队列
Linux 内核使用 work_struct 结构体表示一个工作,
该结构体定义在内核源码目录include/linux/workqueue.h 头文件中
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作处理函数 */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
工作队列用workqueue_struct 结构体表示,该结构体定义在内核源码目录 kernel/workqueue.c 文件中
内核使用工作者线程(worker thread)来处理工作队列中的各个工作
用worker结构体表示,该结构体定义在内核源码目录 kernel/workqueue_internal.h
实际开发中,我们定义工作work_struct即可,每个工作者有线程自己处理工作队列的所有工作
使用 INIT_WORK 宏来初始化工作, INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作, _func 是工作对应的处理函数
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct), f 表示工作对应的处理函数
和tasklet一样要调度才能运行
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work: 要调度的工作。
返回值: 0 成功,其他值 失败
示例:
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
三、设备树中断信息节点
对于zynq mpsoc来说,gic节点就是中断控制器节点,内容如下:
gic: interrupt-controller@f9010000 {
compatible = "arm,gic-400"; //可找到GIC中断控制器驱动文件
#interrupt-cells = <3>; 表示中断控制器下cell大小,即几个cell,每个都是32位大小
reg = <0x0 0xf9010000 0x10000>, 指定gic相关寄存器基地址和大小
<0x0 0xf9020000 0x20000>,
<0x0 0xf9040000 0x20000>,
<0x0 0xf9060000 0x20000>;
interrupt-controller; 表示当前节点是中断控制器节点
interrupt-parent = <&gic>;
interrupts = <1 9 0xf04>; 3个cell:中断类型(0spi共享外设中断 1ppi私有外设中断),中断号,标志(触发类型)
};
对于gpio来说,gpio节点也可以作为中断控制器,示例:
test-node {
compatible = "test-node";
interrupt-parent = <&gpio>; 设置中断控制器,选择gpio作为中断控制器
interrupts = <40 2>; 中断信息,40引脚,2位下降沿触发
};
简单总结一下与中断有关的设备树属性信息:
1、 #interrupt-cells,指定中断源的信息 cells(参数)个数。
2、 interrupt-controller,表示当前节点为中断控制器。
3、 interrupts,指定中断号,触发方式等。
4、 interrupt-parent,指定父中断,也就是中断控制器。
从设备树中获取中断号:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
dev: 设备节点。
index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
获取设备树中指定的中断类型
int irq_get_trigger_type(unsigned int irq_num)
会解析 key 节点中的 interrupt-parent 和 interrupts 属性然后得到一个中断号
使用gpio的话:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio: 要获取的 GPIO 编号。
返回值: GPIO 对应的中断号
设备树设置中断属性:
interrupt-parent = <&gpio>; GPIO中断控制器为GPIO
interrupt = <40 IRQ_TYPE_EDGE_BOTH>;
触发方式定义:include/dt-bindings/interrupt-controller/irq.h
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
四、中断实验
假设设备树中有一节点描述了按键控制,结构如下:
key{
compatible = "test_key";
status = "okay";
key-gpio = <&gpio 40 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio>;
interrupts = <40 IRQ_TYPE_EDGE_BOTH>;
};
驱动程序中,我们采用中断的方式,当检测到按键按下时,进行消抖处理,并通过read将状态送到上层应用中。可以采用在中断中唤醒定时器的方式达到消抖的目的,驱动代码如下:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define KEY_MAJOR 201
#define KEY_MINOR 0
#define KEY_NAME "led_init"
enum key_status{
KEY_UP,
KEY_DOWN,
KEY_KEEP,
};
// extern u64 __jiffy_data jiffies_64;
struct key_dev {
struct class * fclass;
struct device * fdevice;
struct cdev * cdev;
dev_t dev_num;
struct device_node * nd;
int major;
int minor;
int gpio;
int irq_num;
spinlock_t lock;
struct timer_list timer;
int period;
};
struct key_dev key = {0};
int status = KEY_KEEP;
int key_open (struct inode *a, struct file *b)
{
unsigned long flags;
spin_lock_irqsave(&key.lock,flags);
b->private_data = &key;
spin_unlock_irqrestore(&key.lock,flags);
return 0;
}
ssize_t key_read (struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
unsigned long flags;
int ret = 0;
spin_lock_irqsave(&key.lock,flags);
ret = copy_to_user(buf,&status,sizeof(status));
if(!ret)
printk("key_read copy_to_user failed !\n");
status = KEY_KEEP;
spin_unlock_irqrestore(&key.lock,flags);
return 0;
}
struct file_operations fop =
{
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
};
void time_function(struct timer_list *timer)
{
static int flag = 1;
unsigned long flags;
int current_val = 0;
spin_lock_irqsave(&key.lock,flags);
current_val = gpio_get_value(key.gpio);
if(current_val == 0 && flag == 1)
status = KEY_DOWN;
else if(current_val == 1 && flag == 0)
status = KEY_UP;
else
status = KEY_KEEP;
flag = current_val;
spin_unlock_irqrestore(&key.lock,flags);
}
irqreturn_t key_interrupt(int irq, void *arg)
{
mod_timer(&key.timer,jiffies_64 + msecs_to_jiffies(30));//中断,消抖30ms
return IRQ_HANDLED;
}
int __init key_test_init(void)
{
const char *str = {0};
int ret = 0;
unsigned int flag = 0;
key.nd = of_find_node_by_name(NULL,"key");
if(key.nd == NULL)
{
printk("key of_find_node_by_name = NULL !\n");
return -1;
}
ret = of_property_read_string(key.nd,"compatible",&str);
if(ret < 0)
printk("key read status failed !\n");
else
printk("key read status success , str = %s\n",str);
if(!strcmp(str,"test_key"))
printk("key compatible ok !\n");
else
{
printk("key compatible wrong !\n");
return -1;
}
ret = of_property_read_string(key.nd,"status",&str);
if(ret < 0)
printk("key read status failed !\n");
else
printk("key read status success , str = %s\n",str);
if(!strcmp(str,"okay"))
printk("key status on !\n");
else
{
printk("key status off !\n");
return -1;
}
key.gpio = of_get_named_gpio(key.nd,"key-gpio",0);
if(!gpio_is_valid(key.gpio))
{
printk("key gpio unvalid, failed ~\n");
return -1;
}
key.irq_num = irq_of_parse_and_map(key.nd,0);
gpio_request(key.gpio,"key-GPIO");
gpio_direction_input(key.gpio);
flag = irq_get_trigger_type(key.irq_num);
if(flag == IRQF_TRIGGER_NONE)
flag = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
ret = request_irq(key.irq_num,
key_interrupt,
flag,
"key-interrpt",
NULL);
if(ret)
printk("key request irq failed !\n");
else
printk("key request irq success !\n");
key.cdev = cdev_alloc();
key.dev_num = MKDEV(KEY_MAJOR,KEY_MINOR);
register_chrdev_region(key.dev_num, 1, KEY_NAME);
cdev_init(key.cdev,&fop);
cdev_add(key.cdev,key.dev_num,1);
key.fclass = class_create(THIS_MODULE, "key_class");
key.fdevice = device_create(key.fclass, NULL, key.dev_num, NULL, "key_device");
key.major = KEY_MAJOR;
key.minor = KEY_MINOR;
timer_setup(&key.timer,time_function,0);
spin_lock_init(&key.lock);
printk("key init success, jiffies_64 = %llu,ms = %d ~~~\n",jiffies_64,jiffies_to_msecs(jiffies_64));
return 0;
}
void __exit key_test_exit(void)
{
device_destroy(key.fclass,key.dev_num);
class_destroy(key.fclass);
cdev_del(key.cdev);
unregister_chrdev(KEY_MAJOR, KEY_NAME);
gpio_free(key.gpio);
return;
}
module_init(key_test_init);
module_exit(key_test_exit);
MODULE_LICENSE("GPL");
总结
以上就是今天要讲的内容,本文简单介绍了Linux下中断的使用,而Linux内核提供了完善的中断框架,大量能使我们快速便捷地处理中断的函数和方法,使用非常方便。