11_linux内核中断

一、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;
}

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值