linux内核提供了完善的中断框架
1> 什么是中断号?
每个中断都有一个中断号,通过中断号即可区分不同的中断
中断号也叫中断线
在linux中使用int变量来表示中断号
2> 中断申请函数 request_irq()
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);
irq: 要申请的中断对应的 中断号
handler: 中断处理函数
flags: 中断标志 (中断触发条件)
name: 中断的名字
dev: 传递给中断处理函数的参数
return: 0=中断申请成功, 小于0=申请失败
request_irq函数用于申请中断,request_irq函数可能导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码中使用(自旋锁的临界区域)中使用request_irq函数
request_irq函数本身会使能中断,故不需要我们手动去使能中断
3> 释放中断
中断使用完成后就要通过free_irq函数释放掉相应的中断
void free_irq(unsigned int irq, void *dev);
irq: 要释放的中断对应的中断号
dev: 前面申请中断时传递进去的参数
4> 中断处理函数
irqreturn_t (*handler) (int,void *);
第一个参数是中断号
第二个参数是申请中断时传递进来的参数
5> 上下部基本概念
上下部也叫顶半部和底半部,作用为保证中断处理函数的快进快出
上半部: 处理过程比较快,不太会占用很长时间的处理,例如中断处理函数
下半部: 如果中断处理中需要实现的代码比较耗时,则将这些代码提取出来,交给下半部去执行
哪些代码属于上半部,哪些代码属于下半部并没有明确的规定
一般这几种操作会放到上半部
1. 要处理的内容不希望被其他中断打断
2. 要处理的任务对时间敏感
3. 要处理的任务和硬件有关
除了以上3点以外的处理,其他操作优先考虑放到下半部去执行
6> 实现对上半部处理
编写中断处理函数,将上半部操作在中断处理函数中执行即可
7> 实现对下半部处理 — 软中断
linux内核使用结构体softirq_action来表示软中断
struct softirq_action
{
void (action)(struct softirq_action);
}
在linux内核中使用全局数组softirq_vec来实现所有cpu的软中断触发和控制机制
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS是多少?
==>
enum
{
HI_SOFTIRQ=0, //高优先级软中断
TIMER_SOFTIRQ, //定时器软中断
NET_TX_SOFTIRQ, //网络数据发送软中断
NET_RX_SOFTIRQ, //网络数据接收软中断
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, //tasklet软中断
SCHED_SOFTIRQ, //调度软中断
HRTIMER_SOFTIRQ, //高精度定时器软中断
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq RCU软中断*/
NR_SOFTIRQS
};
enum类型,没有指定成员的值,默认。后面成员的值=前面成员的值+1 故NR_SOFTIRQS=10
所以softirq_vec数组中的10个成员刚好就是enum中定义的10个软中断类型
要使用软中断必须先注册对应的软中断处理函数 -- open_softirq()
void open_softirq(int nr, void (*action)(struct softirq_action *));
nr:要开启的软中断的类型 即前面的enum中的一个成员
action: 该软中断对应的软中断处理函数
软中断和中断的区别
中断通常是指硬中断即由硬件外设产生的中断
硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。
硬中断的中断处理函数是当外设触发某个中断条件之后就会自动执行中断处理函数,而软中断需要我们手动去执行调用软中断处理函数
软中断处理函数的触发
void raise_softirq(unsigned int nr);
nr:要触发的软中断,enum中选一个
软中断必须在编译的时候静态注册,linux内核使用softirq_init来初始化软中断
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
==>
显而易见 softirq_init函数会默认注册TASKLET_SOFTIRQ,HI_SOFTIRQ这两种软中断
8> 实现对下半部处理 —tasklet
tasklet是实现下半部的另一种机制,在软中断和tasklet间,最好选择使用tasklet
struct tasklet_struct
{
struct tasklet_struct *next; //下一个tasklet_struct
unsigned long state; //tasklet的状态
atomic_t count; //计数器,记录对该tasklet的引用树
void (*func)(unsigned long); //tasklet执行的函数 == 类似于中断处理函数
unsigned long data; //传递给func函数的参数
};
要使用tasklet必须先定义一个tasklet,然后使用tasklet_init函数初始化tasklet
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
t: 要初始化的tasklet的指针
func: 该tasklet对应的处理函数
data: 传递该func的参数
宏DECLARE_TASKLET == 定义一个tasklet变量和初始化这个tasklet变量
在上半部即中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行 ==>类似于软中断中的rasie_softirq函数
中断上下半部通过tasklet实现的demo
//1. 定义一个全局的tasklet结构体变量
//2. 初始化tasklet结构体变量
//3. 申请中断
//4. 在中断处理函数中通过tasklet_schedule函数来使tasklet在合适的时间运行
//5. 在tasklet处理函数中作为下半部执行操作
struct tasklet_struct tasklet;
void tasklet_func(unsigned long data)
{
/*tasklet具体处理的内容*/
}
irq_handler_t xxx_handler(int irq,void *dev_id)
{
tasklet_schedule(&tasklet);
...
}
//驱动入口函数
static int __init xxxx_init()
{
tasklet_init(&tasklet,tasklet_func,data);
request_irq(中断号,xxx_handler,触发条件,中断的名称,传递给中断函数的参数(通常为该驱动的全局设备信息结构体));
...
}
==> 对于上面这个demo,中断处理函数xxx_handler就是上半部,tasklet处理函数tasklet_func就是下半部, 思路是在上半部中通过tasklet_schedule函数使tasklet_func函数在合适的时间执行
9> 实现对下半部处理 —工作队列
工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行
因为工作队列工作在进程上下文,所以工作队列允许睡眠或重新调度
工作队列的使用和tasklet使用非常类似
struct worl_struct
{
atomic_long_t data;
struct list_head entry;
work_func_t func; //工作队列处理函数
};
工作队列的使用demo:
struct work_struct textwork;
void textwork_func(struct work_struct *work)
{
/*work具体的处理内容*/
}
irq_handler_t xxx_handler(int irq,void *dev_id)
{
work_schedule(&textwork);
... ...
}
static int __init xxx_init()
{
INIT_WORK(&textwork,textwork_func);
request_irq(中断号,xxx_handler,触发条件,中断的名称,传递给中断函数的参数(通常为该驱动的全局设备信息结构体));
... ...
}
10> 按键中断 demo
设备树信息:
wjcirq {
#address-cells = <1>;
#size-cells = <1>;
compatible = “wjc-irq”;
pinctrl-names = “default”;
pinctrl-0 = <&pinctrl_wjc_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
status = “okay”;
};
/*
irq驱动文件
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define irq_ATOMIC_FLAG 1
#define irq_CNT 1
#define irq_DEVID_NAME “irq_devid”
#define irq_CLASS_NAME “irq_class”
#define irq_DEVICE_NAME “irq_device”
#define irq_LABEL_NAME “irq_label”
#define KEY_NUM 1
//定义一个保存按键信息的结构体
struct irq_keymsg {
int gpio; //gpio编号
int irqnum; //中断号
unsigned char value; //按键的键值
char name[10]; //按键的名称
irqreturn_t (*handler) (int,void *); //该按键的中断处理函数
unsigned long flags; //中断触发方式
};
struct irq_dev{
struct cdev cdev;
dev_t devid;
int major;
int minor;
struct class *class;
struct device *device;
struct device_node *node;
struct timer_list timer;
int timeperiod;
atomic_t key_status; //0:按键按下,1=按键释放,2=按键没有发生改变
struct irq_keymsg irqkeymsg[KEY_NUM];
unsigned char curkeynum; //当前操作按键的键值,就是按键信息结构体数组的下标值
};
struct irq_dev irqdev;
static int irq_open(struct inode inode, struct file filp)
{
printk("**** _ %s ******* \n",FUNCTION);
//1. 将该设备结构体保存中filp的私有数据中
filp->private_data = &irqdev;
return 0;
}
static ssize_t irq_read(struct file filp, char __user buf, size_t cnt, loff_t offt)
{
unsigned char keyvalue,ret;
//1. 从filp中取出保存的驱动信息结构体
struct irq_dev dev = (struct irq_dev )filp->private_data;
//printk("* _ %s ******* \n",FUNCTION);
keyvalue = atomic_read(&dev->key_status);
//printk("read函数中读取到的值:%d \n",keyvalue);
//2. 上报数据
if(keyvalue == 2)
{
//没有按键按下,就不要上报数据
return -EINVAL;
}
else if(keyvalue == 0 || keyvalue == 1)
{
//上报数据
ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue));
//还原为2
atomic_set(&(dev->key_status),2);
printk("read函数中读取到的值:%d \n",keyvalue);
}
return 0;
}
static ssize_t irq_write(struct file filp, const char __user buf, size_t cnt, loff_t lofft)
{
printk("*** _ %s ******* \n",FUNCTION);
return 0;
}
/*
命令控制
cmd: 用户层传递过来的命令
arg:用户空间传递给内核空间的参数
/
static long irq_ioctl(struct file filp, unsigned int cmd, unsigned long arg)
{
//1. 从filp中取出保存的驱动信息结构体
//struct irq_dev dev = (struct irq_dev )filp->private_data;
printk("** _ %s ******* \n",FUNCTION);
return 0;
}
static int irq_release(struct inode inode, struct file filp)
{
//1. 从filp的私有数据中取出该设备的信息结构体
//struct irq_dev dev = filp->private_data;
printk("*** _ %s ******* \n",FUNCTION);
return 0;
}
static struct file_operations irq_fops = {
.owner = THIS_MODULE,
.open = irq_open,
.read = irq_read,
.write = irq_write,
.unlocked_ioctl = irq_ioctl,
.release = irq_release,
};
//中断处理函数
//irq:中断号;dev_arg:申请中断是传递进来的参数
//返回值: 一个enum类型的值,直接看着选,一般是RETVAL
static irqreturn_t key_handle(int irq,void dev_arg)
{
//1. 获取参数
struct irq_dev dev = (struct irq_dev )dev_arg;
printk("*** _ %s ******* \n",FUNCTION);
//2. 判断是那个按键被按下了,并将结果记录下来,本来应该是一个按键对应一个中断处理函数的现在公用一个只能笨办法(读取所有能够产生该中断按键的数值)
dev->curkeynum = 0; //这里默认处理 ,因为只有key0存在,即如果发生中断就是key0按下
//3. 启动定时器.将最新的驱动信息结构体作为参数传递给超时函数
dev->timer.data = (volatile long)dev_arg;
mod_timer(&(dev->timer),jiffies + msecs_to_jiffies(10)); //定时10ms
return IRQ_RETVAL(IRQ_HANDLED);
}
//定义定时器的超时函数
static void timer_function(unsigned long arg)
{
unsigned char key_val;
struct irq_keymsg *keymsg;
//1. 通过定时器传递的参数来获取驱动信息结构体(初始化时使用定时器的data成员变量保存了信息结构体)
struct irq_dev *dev = (struct irq_dev *)arg;
//2. 获取定时器消抖之后的值
//2. 每次超时,都重新获读取一下发生中断的电平值有没有发生变化
printk("****** ^_^ %s ******* \n",__FUNCTION__);
keymsg = &dev->irqkeymsg[0];
key_val = gpio_get_value(keymsg->gpio);
printk("超时函数中读取到的电平:%d \n",key_val);
if(key_val == 0) //消抖后按键仍是按下的状态
{
atomic_set(&(dev->key_status),key_val);
}
else if(key_val == 1) //按键释放
{
atomic_set(&(dev->key_status),key_val);
}
}
static int __init wjcirq_init(void)
{
int ret,i;
struct property *property;
const char *status_str;
printk("****** ^_^ %s ******* \n",__FUNCTION__);
//1. 创建一个保存该设备信息的全局设备结构体变量
//2. 初始化原子变量,0=按键按下,1=按键释放,2=按键没有改变状态
atomic_set(&(irqdev.key_status),2);
//3. 动态注册设备号
ret = alloc_chrdev_region(&(irqdev.devid),0,irq_CNT,irq_DEVID_NAME);
irqdev.major = MAJOR(irqdev.devid);
irqdev.minor = MINOR(irqdev.devid);
//4. cdev操作
irqdev.cdev.owner = THIS_MODULE;
cdev_init(&(irqdev.cdev),&irq_fops);
cdev_add(&(irqdev.cdev),irqdev.devid,irq_CNT);
//5. 创建设备类
irqdev.class = class_create(THIS_MODULE,irq_CLASS_NAME);
if(IS_ERR(irqdev.class))
{
return PTR_ERR(irqdev.class);
}
//6. 创建设备文件
irqdev.device = device_create(irqdev.class,NULL,irqdev.devid,NULL,irq_DEVICE_NAME);
if(IS_ERR(irqdev.device))
{
return PTR_ERR(irqdev.device);
}
//7. 硬件初始化
//a. 先通过路径获取设备节点
irqdev.node = of_find_node_by_path("/wjcirq");
if(irqdev.node == NULL)
{
printk("of_find_node_by_path fun error!\n");
return -EINVAL;
}
//b. 获取compatible属性的值
property = of_find_property(irqdev.node,"compatible",NULL);
if(property == NULL)
{
printk("of_find_property fun error!\n");
return -EINVAL;
}
printk("the property compatible = %s \n",(char *)(property->value));
//c. 获取status属性的值
ret = of_property_read_string(irqdev.node,"status",&status_str);
if(ret < 0)
{
printk("of_property_read_string fun error!\n");
return -EINVAL;
}
//d. 对开发版上所有key对应的GPIO的所有操作的集合(通过循环的方式) 更加接近现实情况
for(i=0; i; //设备树中的定义
interrupts:设置中断源的属性,
18: 还中断控制器(interrupt-parent = <&gpio1>;)gpio1下IO18引脚
IRQ_TYPE_EDGE_BOTH: 触发方式: 上升下降沿触发
*/
irqdev.irqkeymsg[i].irqnum = irq_of_parse_and_map(irqdev.node,i);
//5. 完善剩下按键信息结构体中剩下的成员变量的初始化(触发方式,中断处理函数)
irqdev.irqkeymsg[i].flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
irqdev.irqkeymsg[i].handler = key_handle; //所有按键使同一个中断处理函数 这个可以优化
//6. 申请中断
/*
int request_irq(unsigned int irq,irq_handle_t handle,unsigned long flags,const char *name,void *dev);
fun: 申请一个中断,该中断的中断号为irq,中断处理函数为handle,中断触发方式为flags,中断名称为name,传递给该中断的数据为dev、
name: 中断每次,申请成功会在/proc/interrupts中生成一个同名文件
dev: 最好讲驱动信息结构体传递进来
return: 0=申请成功,<0:申请失败,-EBUSY:表示该中断已经被申请了
*/
ret = request_irq(irqdev.irqkeymsg[i].irqnum,
irqdev.irqkeymsg[i].handler,
irqdev.irqkeymsg[i].flags,
irqdev.irqkeymsg[i].name,
&irqdev);
if(ret != 0)
{
printk("request_irq %d error\n",i);
return -EINVAL;
}
}
//e. 初始化定时器对象(这里只是设置定时器超时函数和定时值,并不激活)
init_timer(&(irqdev.timer));
irqdev.timer.function = timer_function; //定义超时处理函数
irqdev.timer.data = (unsigned long)&irqdev; //将该驱动的设备信息结构体作为参数记录在定时器中,这个值会在该定时器发生超时操作调用超时函数时讲该值作为参数传递进去
irqdev.timeperiod = 1000; //默认定时1s
//8. 实现fops
//9. 错误处理 ---这里省略
return 0;
}
static void __exit wjcirq_exit(void)
{
int i;
printk("****** _ %s ******* \n",FUNCTION);
//1. 释放中断
for(i=0; i
/*
void free_irq(unsigned int irq, void *dev);
fun: 释放中断号为int的中断
*/
free_irq(irqdev.irqkeymsg[i].irqnum,&irqdev);
}
//2. 注销设备文件
device_destroy(irqdev.class,irqdev.devid);
//3. 注销设备类
class_destroy(irqdev.class);
//4. 注销cdev
cdev_del(&(irqdev.cdev));
//5. 注销设备号
unregister_chrdev_region(irqdev.devid,irq_CNT);
}
module_init(wjcirq_init);
module_exit(wjcirq_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“wujc”);
/*
KEY中断测试文件
*/
#include
#include
#include
#include
#include
#include
#include
#include “linux/ioctl.h”
int main(int argc,char *argv[])
{
int fd,retvalue,ret;
unsigned char data;
if(argc != 2)
{
printf("uasge input error !\n");
return -1;
}
fd = open(argv[1],O_RDWR);
if(fd < 0)
{
printf("open file %s error !\n",argv[1]);
return -1;
}
while(1)
{
ret = read(fd,&data,sizeof(data));
if(ret >= 0)
{
printf("data = %d \n",data);
switch(data)
{
case 0:
printf("按键按下!\n");
break;
case 1:
printf("按键释放!\n");
break;
default:
break;
}
}
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("close file %s error!\n",argv[1]);
return -1;
}
return 0;
}