详细内容参考《I.MX6U 嵌入式 Linux 驱动开发指南 V1.6 》第五十一章,本篇文章仅说明重点内容。
Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。
1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。
2、request_irq 函数
request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
3、free_irq 函数
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。
void free_irq(unsigned int irq,
void *dev)
4、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下所示:
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
5、中断使能与禁止函数
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
void disable_irq_nosync(unsigned int irq)//disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
local_irq_enable()//使能当前处理器中断系统
local_irq_disable()//禁止当前处理器中断系统
local_irq_save(flags)//local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中
local_irq_restore(flags)//local_irq_restore 用于恢复中断,将中断恢复到 flags 状态
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
1、软中断
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,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
2、tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data)
也 可 以 使 用 宏 DECLARE_TASKLET 来 一 次 性 完 成 tasklet 的 定 义 和 初 始 化 DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
关于 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);
......
}
3、工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 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; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */
struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* I: rescue worker */
int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */
struct workqueue_attrs *unbound_attrs; /* WQ: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* WQ: only for unbound wqs */
char name[WQ_NAME_LEN]; /* I: workqueue name */
/*
* Destruction of workqueue_struct is sched-RCU protected to allow
* walking the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;
/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* FR: unbound pwqs indexed by node */
};
Linux 内核使用**工作者线程(worker thread)**来处理工作队列中的各个工作,Linux 内核使用worker 结构体表示工作者线程,worker 结构体内容如下:
struct worker {
/* on idle list while idle, on busy hash table while busy */
union {
struct list_head entry; /* L: while idle */
struct hlist_node hentry; /* L: while busy */
};
struct work_struct *current_work; /* L: work being processed */
work_func_t current_func; /* L: current_work's fn */
struct pool_workqueue *current_pwq; /* L: current_work's pwq */
bool desc_valid; /* ->desc is valid */
struct list_head scheduled; /* L: scheduled works */
/* 64 bytes boundary on 64bit, 32 on 32bit */
struct task_struct *task; /* I: worker task */
struct worker_pool *pool; /* I: the associated pool */
/* L: for rescuers */
struct list_head node; /* A: anchored at pool->workers */
/* A: runs through worker->node */
unsigned long last_active; /* L: last active timestamp */
unsigned int flags; /* X: flags */
int id; /* I: worker id */
/*
* Opaque string set with work_set_desc(). Printed out with task
* dump for debugging - WARN, BUG, panic or sysrq.
*/
char desc[WORKER_DESC_LEN];
/* used only by rescuers to point to the target workqueue */
struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */
};
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 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 一样,工作也是需要调度才能运行的,工作的调度函数为 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);
......
}
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件,其中的 intc 节点就是I.MX6ULL 的中断控制器节点,节点内容如下所示:
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示:
gpio5: gpio@020ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,
int index)
函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。
我们驱动 I.MX6U-ALPHA 开发板上的 KEY0 按键,不过我们采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。
我们使用到了按键 KEY0,按键 KEY0 使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:
key{
compatible = "alientek,key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; //此处gpio1_io18作为输入,电平可随便输入,可为高,也可为低
status= "okay";
interrupt-parent = <&gpio1>;//设置 interrupt-parent 属性值为“gpio1”因为 KEY0 所使用的 GPIO 为 GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;//设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18 号 IO。IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效。
};
使用“make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb 文件启动 Linux 系统。
新建工程,在 imx6uirq.c 里面输入如下内容:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define IMX6UIRQ_CNT 1 /* 设备号个数 */
#define IMX6UIRQ_NAME "imx6uirq" /* 名字 */
#define KEY_NUM 1 /* KEY0 按键值 */
#define KEY0VALUE 0x01 /* 按键数量 */
#define INVAKEY 0xFF /* 无效的按键值 */
/* 中断 IO 描述结构体 */
struct irq_keydesc
{
int gpio; /*io 编号*/
int irqnum; /*中断号*/
unsigned char value; /*键值*/
char name[10]; /*名字*/
irqreturn_t (*handler)(int, void *); /*中断处理函数*/
};
/*imx6uirq 设备结构体*/
struct imx6uirq_dev
{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
struct irq_keydesc irqkey[KEY_NUM]; /* 按键描述数组 */
struct timer_list timer; /* 定义一个定时器*/
atomic_t keyvalue; /* 有效的按键键值 */
atomic_t releasekey; /* 标记是否完成一次完成的按键*/
};
struct imx6uirq_dev imx6uirq; /*irq设备 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
filp->private_data = &imx6uirq;
return 0;
}
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keyvalue;
unsigned char releasekey;
struct imx6uirq_dev *dev = filp->private_data;
keyvalue = atomic_read(&dev->keyvalue);
releasekey = atomic_read(&dev->releasekey);
if (releasekey) /*有效按键*/
{
if (keyvalue & 0x80) /*释放*/
{
keyvalue &= ~0x80;
ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
}
else
{
goto data_error;
}
atomic_set(&dev->releasekey, 0); /*按下标志清0*/
}
else
{
goto data_error;
}
return 0;
data_error:
return -EINVAL;
}
/*操作集*/
static const struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
};
/*按键中断处理函数*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct imx6uirq_dev *dev = dev_id;
dev->timer.data = (volatile unsigned long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /*20ms定时*/
return IRQ_HANDLED;
}
/*定时器处理函数*/
static void timer_func(unsigned long arg)
{
int value = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
value = gpio_get_value(dev->irqkey[0].gpio);
if (value == 0) /*按下*/
{
atomic_set(&dev->keyvalue, dev->irqkey[0].value);
}
else if (value == 1) /*释放*/
{
atomic_set(&dev->keyvalue, 0x80 | (dev->irqkey[0].value));
atomic_set(&dev->releasekey, 1); /*完整的按键过程*/
}
}
/*按键初始化*/
static int keyio_init(struct imx6uirq_dev *dev)
{
int ret = 0;
int i = 0;
/*1,按键初始化*/
dev->nd = of_find_node_by_path("/key");
if (dev->nd == NULL)
{
ret = -EINVAL;
goto fail_nd;
}
/* 提取 GPIO */
for (i = 0; i < KEY_NUM; i++)
{
dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i);
}
/* 初始化 key 所使用的 IO,并且设置成中断模式 */
for (i = 0; i < KEY_NUM; i++)
{
memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));
sprintf(dev->irqkey[i].name, "KEY%d", i);
gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name);
gpio_direction_input(dev->irqkey[i].gpio);
dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); /*获取中断号*/
#if 0
dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd,i);
#endif
}
/* 申请中断 */
dev->irqkey[0].handler = key0_handler;
dev->irqkey[0].value = KEY0VALUE;
/*2,按键中断初始化*/
for (i = 0; i < KEY_NUM; i++)
{
ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[0].handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
dev->irqkey[i].name, &imx6uirq);
if (ret)
{
printk("irq %d failed!\r\n", dev->irqkey[i].irqnum);
goto fail_irq;
}
}
/*初始化定时器*/
init_timer(&imx6uirq.timer);
imx6uirq.timer.function = timer_func;
return 0;
fail_irq:
for (i = 0; i < KEY_NUM; i++)
{
gpio_free(dev->irqkey[i].gpio);
}
fail_nd:
return ret;
}
/*驱动入口函数*/
static int __init imx6uirq_init(void)
{
int ret = 0;
/*1,注册字符设备驱动*/
imx6uirq.major = 0;
if (imx6uirq.major)
{ /*给定主设备号*/
imx6uirq.devid = MKDEV(imx6uirq.major, 0);
ret = register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
}
else
{
ret = alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
imx6uirq.major = MAJOR(imx6uirq.devid);
imx6uirq.minor = MINOR(imx6uirq.devid);
}
if (ret < 0)
{
goto fail_devid;
}
printk("imx6uirq major =%d, minor =%d \r\n", imx6uirq.major, imx6uirq.minor);
/*2,初始化cdev*/
imx6uirq.cdev.owner = THIS_MODULE;
cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
/*3,添加cdev*/
ret = cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
if (ret)
{
goto fail_cdevadd;
}
/*4,创建类*/
imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.class))
{
ret = PTR_ERR(imx6uirq.class);
goto fail_class;
}
/*5,创建设备*/
imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.device))
{
ret = PTR_ERR(imx6uirq.device);
goto fail_device;
}
/*初始化IO*/
ret = keyio_init(&imx6uirq);
if (ret < 0)
{
goto fail_keyinit;
}
/*初始化原子变量*/
atomic_set(&imx6uirq.keyvalue, INVAKEY);
atomic_set(&imx6uirq.releasekey, 0);
return 0;
fail_keyinit:
fail_device:
class_destroy(imx6uirq.class);
fail_class:
cdev_del(&imx6uirq.cdev);
fail_cdevadd:
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
fail_devid:
return ret;
}
/*驱动出口函数*/
static void __exit imx6uirq_exit(void)
{
int i = 0;
/*1、释放中断*/
for (i = 0; i < KEY_NUM; i++)
{
free_irq(imx6uirq.irqkey[i].irqnum, &imx6uirq);
}
/*2、释放IO*/
for (i = 0; i < KEY_NUM; i++)
{
gpio_free(imx6uirq.irqkey[i].gpio);
}
/*3、删除定时器*/
del_timer(&imx6uirq.timer);
/*注销字符设备驱动*/
cdev_del(&imx6uirq.cdev);
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
device_destroy(imx6uirq.class, imx6uirq.devid);
class_destroy(imx6uirq.class);
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("supersmart");
新建名为 imx6uirqApp.c 的文件,然后输入如下所示内容:
#include
#include
#include
#include
#include
#include
#include
#include
/*
*argc:应用程序参数个数
* argv[]:具体的参数内容,字符串形式
* ./imx6uirqAPP <0:1> 0 关灯,1 开灯
* ./imx6uirqAPP /dev/imx6uirq
*/
int main(int argc, char *argv[])
{
int fd, ret;
char *filename;
unsigned char data;
if (argc != 2)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("file %s open failed!\r\n", filename);
return -1;
}
/*循环读取*/
while (1)
{
ret = read(fd, &data, sizeof(data));
if (ret < 0)
{
}
else
{
if(data)
{
printf("key value = %#x \r\n",data);
}
}
}
close(fd);
return 0;
}
1、编译驱动程序
2、编译测试 APP
加载驱动
驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册上
可以看出 imx6uirq.c 驱动文件里面的 KEY0 中断已经存在了,触发方式为跳边沿(Edge),中断号为 47。