中断在嵌入式方面用得很多,本篇文章具体介绍一下中断。
中断的概念
CPU执行过程中,出现了突变事件,而暂停正在进行的工作,转去处理突变事件,处理完以后CPU返回源程序被中断的位置并继续执行。
为什么要有中断
先说说轮询,由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。很明显,轮询的方式很实在,但是效率很低,因为CPU的处理速度远远大于外设的处理速度,就拿串口传送数据来说,串口的处理速度很慢,每次发完数据,CPU都要等待串口继续发送数据,为了保证不丢失串口发来的数据,CPU必须在那里死读串口,在这里,CPU就浪费了大量的时间,下面是一个没有采用中断的按键驱动程序
#include <linux/init.h> //module_init
#include <linux/module.h> //printk
#include <linux/fs.h> //file_operations
#include <linux/cdev.h> //cdev
#include <linux/device.h> //class
#include <asm/gpio.h> //S3C2440_GPF(0)
#include <plat/gpio-cfg.h> //gpio_request..
#include <linux/uaccess.h>
static int major;
static struct cdev btn_cdev; //分配cdev
static struct class *cls; //设备类
static ssize_t btn_read(struct file *file,
char __user *buf,
size_t count,
loff_t *ppos)
{
unsigned int pinstate;
unsigned char keyval;
//1.获取GPIO的状态
pinstate = gpio_get_value(S3C2440_GPF(0));
if (pinstate == 0) //按下
keyval = 0x50;
else if (pinstate == 1) //松开
keyval = 0x51;
//2.设置一个按键值,上报按键值即可
copy_to_user(buf, &keyval, 1);
return count;
}
static struct file_operations btn_fops = {
.owner = THIS_MODULE,
.read = btn_read
}; //分配驱动硬件操作集合
static int btn_init(void)
{
dev_t dev_id;
//1.申请设备号
alloc_chrdev_region(&dev_id, 0, 1, "mybuttons");
major = MAJOR(dev_id);
//2.初始化注册cdev
cdev_init(&btn_cdev, &btn_fops);
cdev_add(&btn_cdev, dev_id, 1);
//3.创建设备节点
cls = class_create(THIS_MODULE, "mybuttons");
device_create(cls, NULL, dev_id, NULL, "buttons"); //dev/buttons
//4.申请GPIO资源
gpio_request(S3C2440_GPF(0), "KEY_UP");
//5.配置输入口
gpio_direction_input(S3C2440_GPF(0));
return 0;
}
static void btn_exit(void)
{
//1.获取设备号
dev_t dev_id = MKDEV(major, 0);
//2.释放GPIO资源
gpio_free(S3C2440_GPF(0));
//3.删除设备节点
device_destroy(cls, dev_id);
class_destroy(cls);
//4.删除cdev
cdev_del(&btn_cdev);
//5.释放设备号
unregister_chrdev_region(&btn_cdev, dev_id);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
测试程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main(int argc, char *argv[])
{
int fd;
unsigned char keyval;
fd = open("/dev/buttons", O_RDWR);
if (fd < 0) {
printf("open failed.\n");
return -1;
}
while (1) {
read(fd, &keyval, 1);
printf("keyval = %#x\n", keyval);
}
close(fd);
return 0;
}
因为我们要不断的去监听有没有按键按下,所以我们的程序写的是一个死循环,而死循环的CPU利用率,可想而知,100%。那么造成严重的CPU资源浪费。
而中断就是有按键被按下的时候,我们CPU采取处理这个事件,其他的时间我们就来做其他的事情
中断的执行过程
这里以按键为例,外设按键先接的中断控制器,中断控制器集成在了CPU内部,通过一根管家与CPU相连,用于管理中断的优先级以及负责发送信号和使能/关闭中断等等操作,我们的按键一按下,中断控制器发送一个电信号给CPU,然后CPU跳转到异常向量表路口去了,到这里为止是硬件操作,而异常向量表是内核根据CPU定义好的
下面给个图来说一下我们的中断硬件过程
第一个按键被触发,CPU跳转到异常向量表,打断正在处理的程序,先保存现场,然后处理中断服务程序,当处理时,按键二来了,优先级高于按键一,又跳转到异常向量表,在处理二之前,又要保存好中断一的现场,再去处理中断二,按键三来了,再判断优先级,跳到异常向量表,然后打断二的过程,再保存现场,跳到异常向量表,处理中断三,然后原路返回,恢复现场二,再返回,恢复现场一。
而异常向量表、保存现场、响应服务程序、恢复现场等操作内核已经帮我们做好了,软件部分的操作就由我们通过中断服务程序来写。
中断编程
在驱动程序中只需调用request_irq向内核申请外设对应的中断和注册这个中断对应的中断处理函数即可。一旦完成申请和注册,每当中断发生以后,内核就会执行这个中断对应的处理函数。
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long irqflags,
const char *name,
void *dev_id)
函数功能:向内核申请外设对应的中断号,然后注册这个中断号对应的处理函数。
- irq:要申请的中断号。内核给每一个中断线都分配一个唯一的号,这个号就是中断号,对于内核来说,它也是一种宝贵的资源,使用之前先向内核去申请这个中断号。IRQ_EINT(0),这个在我们的开发板原理图上可见。
- handler:要注册的中断处理函数。每当中断发生以后,内核就会执行此函数。原型:
irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
返回值的数据类型:irqreturn_t,本质上是一个枚举,返回值有两个:IRQ_NONE(中断不处理),IRQ_HANDLED(中断处理完毕) irq:申请的中断号
dev_id:给中断处理函数传递的参数信息
irqflags:中断标志
IRQF_SHARED: 表示多个设备共享中断,如果没有此项,默认的中断不共享。
IRQF_SAMPLE_RANDOM: 用于随机数种子的随机采样,对于外部中断,需指定中断的触发方式,对于内部中断,无需指定,一般给0即可。
IRQF_TRIGGER_RISING: 上升沿触变中断
IRQF_TRIGGER_FALLING: 下降沿触变中断
IRQF_TRIGGER_HIGH: 高电平触变中断
IRQF_TRIGGER_LOW: 低电平触变中断
如果想支持多个标志,只需做宏的位或运算即可。- name:中断名称,通过cat /proc/interrupts来查看注册的中断名称
- dev_id:注册中断处理函数时,给处理函数传递的参数信息。
中断处理函数的要求:
1. 中断处理函数的执行速度越快越好
2. 中断不属于任何进程,内核为每一个中断分配的栈为1页
3. 中断不能和用户空间进行数据的交换,如果要进行数据的交互,要配合系统调用来实现
4. 中断处理函数中不能调用引起阻塞的函数
如果中断不再使用,我们要释放中断号资源和卸载中断处理函数
void free_irq(unsigned int irq, void *dev_id);
irq:申请的中断号
dev_id:给中断处理函数传递的参数,这个参数一定要和注册中断时传递参数要一致
中断处理函数:由于中断时随机发送,中断处理函数不属于任何进程,执行速度要快和简洁,中断栈为一页,不能向用户空间发送数据或者接收数据,不能调用可能引起阻塞的函数:copy_to_user/copy_from_user/kmalloc
中断的底半部机制
中断处理函数的核心就是要处理的速度越快越好,但是在某些场合,这个要求是没法得到满足–中断处理函数会长时间占用CPU的资源。由于中断不属于任何进程,也就不会参与进程之间的调度,一旦中断处理函数长时间占有CPU资源,会影响系统的并发能力,也会造成其他中断无法获取CPU资源,会影响系统的响应能力。所以为了提高系统的并发和响应能力,内核将中断处理函数进行划分,分为底半部和顶半部
顶半部:就是中断处理函数,一旦发生中断,首先执行顶半部,这里面做一些紧急,相对耗时较少的事情,然后登记底半部的内容,CPU在适当的时候会再去处理底半部的内容,一旦顶半部执行完毕然后返回,那么CPU的资源就得到释放,其他的任务就可以获取CPU的资源了。
比如网卡将数据包从网卡的硬件缓冲区里读到内存的缓冲区,这个过程不可中断,而将数据包提交给协议层的过程是不紧急的
底半部:完成一些比较耗时的,不紧急的事情。这个过程可以被其他中断所打断,中断的机制有三种:tasklet、工作队列、软中断
tasklet
struct tasklet_struct{
func; //底半部的处理函数
data; //给处理函数传递的参数
};
用法:
- 分配tasklet
struct tasklet_struct tasklet;
- 初始化
tasklet_init(&tasklet, 处理函数,传递参数);
或者:
DECLARE_TASKLET(tasklet, 处理函数,传递参数);
- 在顶半部中断处理函数中调用tasklet_schedule(&tasklet)进行登记,(CPU会在适当的时候去执行底半部的处理函数)
- 注意事项:tasklet工作在中断上下文,要符合中断处理函数的一些编程要求(越快越好,不属于任何进程,不和用户空间进行数据的交互(配合系统调用来实现),不能调用可能引起阻塞的函数)。
范例:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
static int mydata = 0x5555;
//tasklet的底半部处理函数,data存放的是mydata的地址
static void btn_tasklet_func(unsigned long data)
{
printk("%s: data = %#x\n",
__func__, *(int *)data);
}
//分配初始化tasklet
static DECLARE_TASKLET(btn_tasklet,
btn_tasklet_func,
(unsigned long)&mydata);
//中断处理函数
static irqreturn_t button_isr(int irq, void *dev_id)
{
//1.在顶半部中断处理函数中登记底半部,
//CPU在执行完顶半部以后,在适当的时间处理底半部
tasklet_schedule(&btn_tasklet);
printk("%s\n", __func__);
return IRQ_HANDLED; //处理成功
}
static int btn_init(void)
{
int ret;
//1.申请和注册中断相关信息
ret = request_irq(IRQ_EINT(0),
button_isr,
IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
"KEY_UP", &mydata);
if (ret < 0) {
printk("request irq failed.\n");
return -EBUSY; //中断资源忙
}
printk("requeset irq successfully!\n");
return 0;
}
static void btn_exit(void)
{
//1.卸载中断相关信息
free_irq(IRQ_EINT(0), &mydata);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
工作队列
由于tasklet的处理函数不能调用可能引起休眠的函数,但是某些场合需要休眠,此时就不能使用tasklet,此时就能用到工作队列了
struct work_struct{
work_func_t func; //工作的处理函数
}
或者:
struct delayed_work{
struct work_struct work; //只关注处理函数
struct timer_list timer;
}
使用:
- 分配工作或者延时工作
struct work_struct work;
struct delayed_work dwork;
- 初始化工作或者延时工作
INIT_WORK(&work, 工作处理函数);
INIT_DELAYED_WORK(&dwork, 工作处理函数);
- 在顶半部的处理函数中,调用:
schedule_work(&work); //CPU空闲时执行工作的处理函数
或者schedule_delayed_work(&dwork, 5*HZ); //5s以后执行延时工作的处理函数
调用这两个函数,内核会将咱们的工作和延时工作交给内核已经创建好的(默认)工作队列和内核线程,内核线程会遍历这个工作队列,取出每一个节点(工作或者延时工作),然后执行工作或者延时工作的处理函数,工作队列工作在进程上下文,因此会参与进程的调度和延时。
如果使用schedule_delayed_work登记延时工作,如果超时时间没有到期,又重新登记,那么前一次的登记就会变得无效,后一次登记才有效。
这里的延时工作队列在后面的文章会提到我们的延时问题。下面是一个工作队列的例程:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
//定义按键硬件数据结构
struct btn_resource{
int irq;
char *name;
};
//初始化按键信息
static struct btn_resource btn_info[] = {
[0] = {
.irq = IRQ_EINT(0),
.name = "KEY_UP"
}
};
//分配工作和延时工作
static struct work_struct work;
static struct delayed_work dwork;
//工作的处理函数
//work指针指向分配的工作work
static void btn_work_func(struct work_struct *work)
{
printk("%s\n", __func__);
}
//延时工作的处理函数
static void btn_dwork_func(struct work_struct *work)
{
printk("%s\n", __func__);
}
//中断处理函数--顶半部
static irqreturn_t button_isr(int irq, void *dev_id)
{
//1.登记工作,CPU在适当的时候去执行工作处理函数
//schedule_work(&work);
//2.登记延时工作
schedule_delayed_work(&dwork, 5*HZ); //5s以后内核执行延时工作处理函数
printk("%s\n", __func__);
return IRQ_HANDLED;
}
static int btn_init(void)
{
int i;
//1.注册中断
for (i = 0; i < ARRAY_SIZE(btn_info); i++)
request_irq(btn_info[i].irq,
button_isr,
IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
btn_info[i].name,
&btn_info[i]);
//2.初始化工作和延时工作
INIT_WORK(&work, btn_work_func);
INIT_DELAYED_WORK(&dwork, btn_dwork_func);
return 0;
}
static void btn_exit(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(btn_info); i++)
free_irq(btn_info[i].irq,&btn_info[i]);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
注意:由于调用以上两个函数,都会将工作和延时工作交给内核默认的线程和工作队列去处理,无形会增大默认内核线程的负载。为了提供效率,可用创建自己的工作队列和内核线程去处理自己的工作或者延时工作。
创建自己的工作队列和额内核线程:
struct workqueue_struct *my_wq = create_workqueue("tangyanjun"); //创建自己的工作队列my_wq和自己的内核线程tangyanjun,可在PS中查看,当然内核也会帮我们完成自己内核线程和工作队列的绑定
接下来只需要将自己的工作和延时工作交给自己的工作队列处理和登记即可:
queue_work(自己的工作队列, 自己的工作);
或者
queue_delayed_work(自己的工作队列,自己的延时工作,延时时间);
例程:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
//定义按键硬件数据结构
struct btn_resource{
int irq;
char *name;
};
//初始化按键信息
static struct btn_resource btn_info[] = {
[0] = {
.irq = IRQ_EINT(0),
.name = "KEY_UP"
}
};
//分配工作和延时工作
static struct work_struct work;
static struct delayed_work dwork;
//分配自己的工作队列指针
static struct workqueue_struct *my_wq;
//工作的处理函数
//work指针指向分配的工作work
static void btn_work_func(struct work_struct *work)
{
printk("%s\n", __func__);
}
//延时工作的处理函数
static void btn_dwork_func(struct work_struct *work)
{
printk("%s\n", __func__);
}
//中断处理函数--顶半部
static irqreturn_t button_isr(int irq, void *dev_id)
{
//1.登记工作,CPU在适当的时候去执行工作处理函数
//schedule_work(&work);
//2.登记延时工作
//schedule_delayed_work(&dwork, 5*HZ); //5s以后内核执行延时工作处理函数
//3.登记工作:将自己的工作交给自己的工作队列
queue_work(my_wq, &work);
//4.登记延时工作:将自己的延时工作交给自己的工作队列
queue_delayed_work(my_wq, &dwork, 3*HZ);
printk("%s\n", __func__);
return IRQ_HANDLED;
}
static int btn_init(void)
{
int i;
//1.注册中断
for (i = 0; i < ARRAY_SIZE(btn_info); i++)
request_irq(btn_info[i].irq,
button_isr,
IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
btn_info[i].name,
&btn_info[i]);
//2.初始化工作和延时工作
INIT_WORK(&work, btn_work_func);
INIT_DELAYED_WORK(&dwork, btn_dwork_func);
//3.创建自己的工作队列和内核线程
//自己的工作队列是my_wq,内核线程是tarena
my_wq = create_workqueue("tangyanjun");
return 0;
}
static void btn_exit(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(btn_info); i++)
free_irq(btn_info[i].irq,&btn_info[i]);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
最后还剩下个软中断,软中断在实际运用中较少,这里也没怎么去了解,给出两点设计的注意事项:
1、软中断的实现必须修改内核代码,静态编译,不能模块加载
2、软中断对应的处理函数必须具备可重入性。