一、linux中断简介
1、中断API函数
1)request_irq函数
在 linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断, request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。 request_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,中断已经被申请。
2)free_irq函数
free_irq函数释放中断。如果中断不是共享的,那么 free_irq 删除中断处理函数并且禁止中断。函数原型如下:
void free_irq(unsigned int irq, void *dev)
irq:要释放的中断号。
dev:如果中断设置为共享 (IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
3)中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
参数1:中断号。
参数2:指向 void的指针,也就通用指针,需要与 request_irq 函数的 dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。
返回值:返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下:
enum irqreturn {
RQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
4)中断使能与禁止函数
① void enable_irq(unsigned int irq)
使能指定中断,irq 是要使能的中断号。
② void disable_irq(unsigned int irq)
禁止指定中断,irq 是要禁止的中断号。此函数要等当前执行的中断处理函数执行完才返回,因此使用该函数要保证不会有新中断产生,确保所有中断处理程序全部退出。
③ void disable_irq_nosync(unsigned int irq)
禁止指定中断,irq 是要禁止的中断号。此函数调用以后立即返回,不会等待当前中断处理函数执行完毕。
④ local_irq_enable()
使能当前处理器中断系统。
⑤ local_irq_disable()
禁止当前处理器中断系统。
⑥ local_irq_save(flags)
禁止中断,并将中断状态保存在 flags 中。
⑦ local_irq_restore(flags)
恢复中断,将中断恢复到 flags 状态中。
5)获取中断号函数
① irq_of_parse_and_map函数
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
dev:设备节点。
Index:索引号。
② gpio_to_irq函数
如果使用 GPIO,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
gpio:gpio 编号。
返回值:GPIO 对应的中断号。
2、上半部与下半部
如果中断处理函数执行的时间较长,要将处理过程分为上半部和下半部。
上半部:中断处理函数中处理过程较快的,不会占用很长时间的放在上半部完成。
下半部:中断处理过程时间较长的,将比较耗时的代码提取出来,放在下半部完成。
上下半部安排参考:
1)如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
2)如果要处理的任务对时间敏感,可以放到上半部。
3)如果要处理的任务与硬件有关,可以放到上半部。
4)除了上述三点以外的其他任务,优先考虑放到下半部。
1)上半部处理机制
编写中断处理函数即可。
2)下半部处理机制
① 软中断
linux 内核使用结构体 softirq_action 表示软中断,softirq_action 结构体定义在 include/linux/interrupt.h 中:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10个软中断,如下所示:
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
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, /* RCU软中断 */
NR_SOFTIRQS
};
一共有十个软中断,softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的CPU都能访问。
要使用软中断,要先使用 open_softirq 函数注册对应的软中断处理函数,函数定义如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,即以上枚举类型的其中一类。
action:软中断对应的处理函数。
注册好软中断后,需要使用 raise_softirq 函数,函数原型如下:
void raise_softirq(unsigned int nr)
nr:要触发的软中断,即以上枚举类型的其中一类。
注意:软中断必须在编译的时候静态注册。
② tasklet
驱动开发中 tasklet 比软中断常用,linux 内核定义了结构体:
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,必须先定义一个 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,宏定义如下:
DECLARE_TASKLET(name, func, data)
name:要定义的 tasklet 的名字。
func:tasklet 的处理函数。
data:要传输给 func 函数的参数。
在上半部中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet。
tasklet 使用参考框架:
/* 定义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);
......
}
③ 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
linux 使用 work_struct 结构体表示一个工作,定义如下:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下:
struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};
linux 内核使用 worker thred 来处理工作队列中的各个工作,worker 结构体表示 worker thred,结构体定义如下:
struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};
可以看出,每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际驱动开发中,只需定义 work_struct 即可。
首先定义一个 work_struct 结构体变量,然后使用 INIT_WORK 宏来初始化工作,宏定义如下:
#define INIT_WORK(_work, _func)
_work:要初始化的工作。
_func:工作对应的处理函数。
也可以使用 DECLARE_WORK 来定义并初始化工作,宏定义如下:
#define DECLARE_WORK(n, f)
n:要定义的工作。
f:工作对应的处理函数。
工作也是需要调度才能正常运行的,调度函数为 schedule_work,函数原型如下:
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);
......
}
二、中断实验
按键中断后启动定时器,在定时中断中再次读取按键状态,以此实现消抖。
1、添加设备树节点
1)添加设备节点
key_input {
compatible = "key_test";
status = "okay";
pinctrl-names = "default";
pinctrl = <&keyinput>;
gpio_key = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupt = <18 IRQ_TYPE_EDGE_BOTH>;
};
第 7 行,设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为 GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。
第 8 行,设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18 号 IO。 IRQ_TYPE_EDGE_BOTH 定义在文件 include/linux/irq.h 中。
2)添加pinctrl节点
在 iomuxc 节点下的子节点 imx6ul-evk 中添加 pinctrl 节点:
keyinput: keygrp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xf080
>;
};
2、添加设备结构体
/* 设备结构体 */
struct irq_dev {
dev_t devid; //设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *key_nd; //按键节点
struct timer_list timer; //定时器
struct spinlock lock; //自旋锁
int key_gpio; //按键gpio编号
int irq_num; //中断号
atomic_t key_state;
};
struct irq_dev irq;
3、编写加载和卸载注册函数
加载和卸载注册函数如下:
module_init(Irq_init);
module_exit(Irq_exit);
入口函数:
/* 入口函数 */
static int __init Irq_init(void)
{
int ret = 0;
spin_lock_init(&irq.lock); //初始化自旋锁
atomic_set(&irq.key_state, KEY_LOOSE);
/* 设备号处理 */
irq.major = 0;
if(irq.major){ //设置了主设备号
irq.devid = MKDEV(irq.major, 0);
ret = register_chrdev_region(irq.devid, DEVICE_CNT, DEVICE_NAME);
if(ret < 0){ //注册失败
printk("fail to register devid\r\n");
return -EINVAL;
}
}else{ //没有设置主设备号
ret = alloc_chrdev_region(&irq.devid, 0, DEVICE_CNT, DEVICE_NAME);
if(ret < 0){ //申请设备号失败
printk("fail to alloc devid\r\n");
return -EINVAL;
}
irq.major = MAJOR(irq.devid);
irq.minor = MINOR(irq.devid);
printk("major = %d\r\nminor = %d\r\n",irq.major, irq.minor);
}
/* 注册字符设备 */
irq.cdev.owner = THIS_MODULE;
cdev_init(&irq.cdev, &irq_fops);
ret = cdev_add(&irq.cdev, irq.devid, DEVICE_CNT);
if(ret < 0){ //注册失败
ret = -EINVAL;
printk("fail to add cdev\r\n");
goto fail_cdev;
}
/* 自动创建设备 */
irq.class = NULL;
irq.device = NULL;
irq.class = class_create(THIS_MODULE, DEVICE_NAME);
if(irq.class == NULL){ //创建类失败
ret = -EINVAL;
printk("fail to create class\r\n");
goto fail_create_class;
}
irq.device = device_create(irq.class, NULL, irq.devid, NULL, DEVICE_NAME);
if(irq.device == NULL){ //创建设备失败
ret = -EINVAL;
printk("fail to create device\r\n");
goto fail_create_device;
}
printk("device init\r\n\r\n");
timer_init(); //初始化定时器
ret = key_init(); //初始化按键中断
return 0;
fail_cdev:
unregister_chrdev_region(irq.devid, DEVICE_CNT);
fail_create_class:
cdev_del(&irq.cdev);
unregister_chrdev_region(irq.devid, DEVICE_CNT);
fail_create_device:
class_destroy(irq.class);
cdev_del(&irq.cdev);
unregister_chrdev_region(irq.devid, DEVICE_CNT);
return ret;
}
出口函数:
如果激活了定时器,在卸载模块时,一定要先删除定时器,否则将无法卸载模块。
/* 出口函数 */
static void __exit timer_exit(void)
{
gpio_free(irq.key_gpio); //注销gpio
del_timer_sync(&irq.timer); //删除定时器
free_irq(irq.irq_num, &irq); //注销中断
device_destroy(irq.class, irq.devid); //删除设备
class_destroy(irq.class); //删除类
cdev_del(&irq.cdev); //删除字符设备
unregister_chrdev_region(irq.devid, DEVICE_CNT); //注销设备号
}
4、编写按键中断初始化函数
/* key gpio初始化函数 */
static int key_init(void)
{
int ret = 0;
/* 获取设备树节点 */
irq.key_nd = of_find_node_by_name(NULL, "key_input");
if(irq.key_nd == NULL){
printk("fail to find node\r\n");
return -EINVAL;
}
/* 获取gpio编号 */
irq.key_gpio = of_get_named_gpio(irq.key_nd, "gpio_key", 0);
if(irq.key_gpio < 0){
printk("fail to get gpio num\r\n");
return -EINVAL;
}
/* 设置gpio属性 */
gpio_request(irq.key_gpio, "key");
gpio_direction_input(irq.key_gpio);
/* 获取中断号 */
irq.irq_num = gpio_to_irq(irq.key_gpio);
/* 申请中断 */
ret = request_irq(irq.irq_num, key_handler, IRQF_TRIGGER_FALLING, "key_irq", &irq);
return 0;
}
5、编写中断回调函数
/* 中断回调函数 */
static irqreturn_t key_handler(int irq_num, void *dev_id)
{
struct irq_dev *dev = (struct irq_dev *)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
6、编写定时器初始化函数
/* 定时器初始化函数 */
void timer_init(void)
{
unsigned long flags;
init_timer(&irq.timer); //初始化定时器
irq.timer.function = timer_function; //注册定时回调函数
irq.timer.data = (unsigned long)&irq; //设置回调函数参数
}
7、编写定时回调函数
linux 内核的定时器启动后只会运行一次,如果要连续定时,需要在回调函数中重新启动定时器。
/* 定时回调函数 */
void timer_function(unsigned long arg)
{
struct irq_dev *dev = (struct irq_dev *)arg;
if(gpio_get_value(dev->key_gpio) == 0) //按键真的按下
{
atomic_set(&irq.key_state, KEY_PRESS);
}
}
8、编写设备的具体操作函数
/* open函数 */
static int Irq_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &irq; //设置数据私有化
timer_init(); //初始化定时器
ret = key_init(); //初始化按键中断
return ret;
}
/* read函数 */
static ssize_t Irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int key_state,ret;
struct irq_dev *dev = filp->private_data;
key_state = atomic_read(&dev->key_state);
if(key_state == KEY_PRESS){
ret = copy_to_user(buf, &key_state, cnt);
}
atomic_set(&dev->key_state, KEY_LOOSE);
return 0;
}
/* 操作函数集合 */
static const struct file_operations irq_fops = {
.owner = THIS_MODULE,
.open = Irq_open,
.read = Irq_read,
};
4、添加头文件
参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数或库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
man 命令数字含义:1:标准命令 2:系统调用 3:库函数
添加以下头文件:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_platform.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/atomic.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#define DEVICE_CNT 1
#define DEVICE_NAME "irq"
5、添加 License 和作者信息
驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");
6、编写测试应用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include "linux/ioctl.h"
#define TIMER_OPEN 0X0F
#define TIMER_CLOSE 0X1F
#define TIMER_SET 0XFF
int main(int argc, char *argv[])
{
int fd = 0;
int ret = 0;
u_int32_t cmd = 0;
u_int32_t arg = 0;
char *filename;
int key_state;
if(argc != 2){
printf("missing parameter!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("open file %s failed\r\n", filename);
return -1;
}
while(1){
read(fd, &key_state, sizeof(key_state));
if(key_state == 0)
printf("key press\r\n");
key_state = 1;
}
ret = close(fd);
if(ret < 0) //返回值小于0关闭文件失败
{
printf("close file %s failed\r\n", filename);
return -1;
}
return 0;
}