Linux下的中断实验


前言

中断顾名思义,就是在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内核提供了完善的中断框架,大量能使我们快速便捷地处理中断的函数和方法,使用非常方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值