中断是操作系统最常见的事件之一,无论是系统层的“软中断”还是CPU底层的“硬中断”都是编程时常用的。中断的作用之一是充分利用CPU资源,正常情况下,CPU执行用户任务,当外设触发中断产生时,CPU停止当前任务,转而去处理中断信息。处理完中断再返回任务处继续执行。
对于硬中断,顾名思义,由硬件产生,CPU定时器、各类总线、GPIO以及外设键盘、磁盘、鼠标等。对于嵌入式来说,触摸屏、传感器等都可以产生中断信号。硬中断处理要实时和高效率,一般由驱动层处理。
对于软中断,是由操作系统实现,不会直接中断CPU。操作系统在任务管理、调度过程会有软中断过程。对于驱动层面,软中断往往结合硬中断一起使用。
1. linux中断
1.1 中断上半部和下半部
中断的基本原则是“快速执行完并退出”,但一些设备中往往需要处理大量的耗时事务。不同于裸机编程,操作系统是多个进程和多个线程执行,宏观上达到并行运行的状态,外设中断则会打断内核中任务调度和运行,及屏蔽其外设的中断响应,中断函数耗时过长会使得系统实时性和并发性降低。为了提高系统的实时性和并发性,linux内核将中断处理程序分为上半部(top half)和下半部(bottom half)。上半部分任务比较少,处理一些寄存器操作、时间敏感任务,以及“登记中断”通知内核及时处理下半部的任务。下半部分,则负责处理中断任务中的大部分任务,特别是耗时任务必需放在下半部。
以触摸屏外设为例:
中断上半部:有触摸信号时,产生一个中断通知CPU,驱动负责将中断信息登记到内核,并通知内核处理, 然后退出中断。
中断下半部:内核获取中断信息,读取触摸屏数据返回给系统使用。
中断上下部区别:
- 上半部由外设中断触发,下半部由上半部触发。
- 上半部不会被打断,下半部可以被其他中断打断。
- 上半部分处理时间敏感任务,主要任务、耗时任务放在下半部。
1.2 中断设计
一个完整的中断程序由上半部和下半部分共同构成,在编写设备驱动程序前,就需考虑好上半部和下半部的分配。很多时候上半部与下半部并没有严格的区分界限,主要由程序员根据实际设计,如某些外设中断可以没有下半部。关于上下半部的划分原则,就是主要事务、耗时事务划分在下半部处理。
可以参考以下原则:
- 与硬件相关的操作,如寄存器访问,必须放在上半部。
- 对时间敏感、要求实时性的任务放在上半部。
- 该任务不能被其他中断或者进程打断的放在上半部。
- 实时性要求不高的任务、耗时任务放在下半部。
1.3 中断上半部实现
上半部中断一般包括几个步骤
- 硬件相关中断配置
- 中断回调函数
- 中断号申请
- 中断注册
1.3.1 中断回调函数
该部分为真正的中断上半部,处理时间敏感任务和中断状态清除,同时触发内核调度下半部。 中断回调函数类型,是一个函数指针,位于“kernel/include/linux/interrupt.h”中。
typedef irqreturn_t (*irq_handler_t)(int, void *);
- 参数1,中断号。
- 参数2,通用void指针,一般指向设备数据结构体,引用前通过强制转换获取设备私有信息。
- 返回值,irqreturn_t 枚举类型,位于“kernel/include/linux/irqreturn.h”。
/*** enum irqreturn* @IRQ_NONE interrupt was not from this device or was not handled* @IRQ_HANDLED interrupt was handled by this device* @IRQ_WAKE_THREAD handler requests to wake the handler thread*/
enum irqreturn {IRQ_NONE = (0 << 0), /* 收到的中断信号与注册中断源信号不一致 */IRQ_HANDLED = (1 << 0), /* 接收到正确中断信号,并且作相应处理 */IRQ_WAKE_THREAD = (1 << 1),
};typedef enum irqreturn irqreturn_t;
注
中断函数是不存在返回值的,该回调函数返回值,表示系统响应中断信号的处理状态。
1.3.2 中断号
系统中断源是多元的,linux内核会给每个中断源分配一个唯一的中断号,用以区分不同设备的中断。终端号为一个int类型的整数。引入设备树后,中断号一般在会设备树中设备节点描述了,驱动程序通过指定函数获取。
- 从设备树获取中断号
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
参数 | 含义 |
---|---|
引用 | #include <linux/of_irq.h> |
node | 设备节点 |
index | 索引序号,获取指定序号中断信息(如果有多个),只有一个中断信息填0 |
返回 | 成功返回中断号,失败返回负数 |
- 对于gpio,可以通过gpio序号转为中断号
int gpio_to_irq(unsigned gpio) /* 把gpio序号转换为中断号 */
1.3.3 中断注册
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
request_ir用于中断注册,同时函数内部会使能中断,不需手动再去使能。
参数 | 含义 |
---|---|
引用 | #include <linux/interrupt.h> |
irq | 中断序号 |
handler | 中断处理回调函数,1.3.1节定义 |
flags | 中断类型,详细见下面描述 |
name | 中断名称,可以在"/proc/interrupts"文件查看 |
dev | 数据结构体,一般是设备数据结构,传递给回调函数第二个形参;设备共享中断线时(IRQF_SHARED),可以用来区分不同设备 |
返回 | 成功返回0,失败返回负数,中断已存放返回-EBUSY |
关于中断类型(flags),“interrupt.h”有相关定义
#define IRQF_TRIGGER_NONE 0x00000000 /* 无触发中断 */
#define IRQF_TRIGGER_RISING 0x00000001 /* 上升沿触发 */
#define IRQF_TRIGGER_FALLING 0x00000002 /* 下降沿触发 */
#define IRQF_TRIGGER_HIGH 0x00000004 /* 高电平触发 */
#define IRQF_TRIGGER_LOW 0x00000008 /* 电平触发 */#define IRQF_SHARED 0x00000080 /* 多个设备共享中断 */
#define IRQF_PROBE_SHARED 0x00000100
#define __IRQF_TIMER 0x00000200
#define IRQF_PERCPU 0x00000400
#define IRQF_NOBALANCING 0x00000800
#define IRQF_IRQPOLL 0x00001000
#define IRQF_ONESHOT 0x00002000
#define IRQF_NO_SUSPEND 0x00004000
#define IRQF_FORCE_RESUME 0x00008000
#define IRQF_NO_THREAD 0x00010000
#define IRQF_EARLY_RESUME 0x00020000
#define IRQF_COND_SUSPEND 0x00040000
1.3.4 中断使能和失能
void disable_irq_nosync(unsigned int irq); /* 失能中断,立即返回 */
bool disable_hardirq(unsigned int irq);
void disable_irq(unsigned int irq); /* 失能中断,需等待中断执行完才返回 */
void disable_percpu_irq(unsigned int irq);
void enable_irq(unsigned int irq); /* 使能中断 */
注:
调用“disable_irq”函数前,必须确保不会产生新中断,因为该函数需等待中断执行完才返回,如果有新中断一直产生,会导致阻塞。
1.3.5 中断释放
void free_irq(unsigned int, void *);
设备退出时,必须释放中断。“free_irq”函数释放设备中断后,并会禁止设备中断,无需手动禁止。如果是共享中断,只有释放完最后一个设备才会禁止中断。
1.4 中断下半部实现
linux内核提供了3种下半部实现方式,分别是soft tirq(软中断)、tasklet、work queue(工作队列),三种方式应用在不同的场合下。
- 软中断用于重要场合,对执行时间要求比较高,倾向于提高系统性能
- tasklet和工作队列用于大多数普通驱动
- tasklet是在中断上下文执行,工作队列在内核线程执行,可以挂起(sleep)延迟处理。任务需挂起,用工作队列
1.4.1 软中断
linux内核软中断用结构体“struct softirq_action”描述,位于“kernel/include/linux/interrupt.h”中,从原型看就是一个软中断处理回调函数指针,函数实体就是“下半部”处理的任务,由驱动工程师实现。
/* softirq mask and active fields moved to irq_cpustat_t in* asm/hardirq.h to get better cache usage. KAO*/struct softirq_action
{void (*action)(struct softirq_action *);
};
“interrupt.h”枚举了常用软中断类型。
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ highfrequency threaded job scheduling. For almost all the purposestasklets are more than enough. F.e. all serial device BHs etal. should be converted to tasklets, not to softirqs.*/enum
{HI_SOFTIRQ=0, /* 最高优先级软中断 */TIMER_SOFTIRQ, /* 定时器软中断 */NET_TX_SOFTIRQ, /* 网络发送软中断 */NET_RX_SOFTIRQ, /* 网络接收软中断 */BLOCK_SOFTIRQ, /* 块操作软中断 */BLOCK_IOPOLL_SOFTIRQ, /* 块IO轮询软中断 */TASKLET_SOFTIRQ, /* tasklet软中断 */SCHED_SOFTIRQ, /* 调度软中断 */HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */ /* 高精度定时器软中断 */RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ /* RCU软中断 */NR_SOFTIRQS /* 软中断总数 */
};
软中断使用步骤
1)注册软中断
void open_softirq(int nr, void (*action)(struct softirq_action *));
参数 | 含义 |
---|---|
nr | 软中断类型,interrupt.h中枚举类型 |
action | 软中断回调处理函数 |
返回 | 无 |
软中断注册,必须采用“静态”方式,因为内核起来后会调用“softirq_init”初始化软中断。
2)触发软中断
在上半部调用“raise_softirq”函数通知内核执行下半部。
void raise_softirq(unsigned int nr);
参数 | 含义 |
---|---|
nr | 软中断类型,interrupt.h中枚举类型 |
返回 | 无 |
1.4.2 tasklet
tasklet本质是软中断,基于软中断封装实现的一种方式,tasklet的描述“struct tasklet_struct”结构体同样位于“kernel/include/linux/interrupt.h”中。
struct tasklet_struct
{struct tasklet_struct *next; /* 链式存储,表示下一tasklet节点 */unsigned long state; /* tasklet状态,TASKLET_STATE_SCHED表示被调度过程,TASKLET_STATE_RUN表示tasklet正在某个CPU上执行 */atomic_t count; /* tasklet引用数,原子操作 */void (*func)(unsigned long); /* 回调处理函数,下半部处理任务置于此 */unsigned long data; /* 传递给回调函数fun的参数 */
};
count成员与tasklet状态相关,如果count等于0,tasklet处于enable状态,大于0则处于disable状态。
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{atomic_inc(&t->count);smp_mb__after_atomic();
}static inline void tasklet_disable(struct tasklet_struct *t)
{tasklet_disable_nosync(t); /* 自减 */tasklet_unlock_wait(t);smp_mb();
}static inline void tasklet_enable(struct tasklet_struct *t)
{smp_mb__before_atomic();atomic_dec(&t->count); /* 自加 */
}
tasklet使用步骤
1)注册tasklet
使用tasklet机制,首先需定义一个“struct tasklet_struct”,可以动态和静态定义,然后调用“tasklet_init”函数初始化。
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
参数 | 含义 |
---|---|
t | 需初始化的tasklet结构体实体地址 |
func | 下半部回调处理函数 |
data | 传递给回调函数fun的参数 |
返回 | 无 |
linux内核“interrupt.h”中封装了一个初始化的宏“DECLARE_TASKLET”,也可以直接调用该宏初始化,传入参数与“tasklet_init”一致。
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
2)触发调度
在上半部调用“tasklet_schedule”函数通知内核执行下半部调度。
void tasklet_schedule(struct tasklet_struct *t)
参数 | 含义 |
---|---|
t | 需调度的tasklet结构体实体地址 |
返回 | 无 |
1.4.3 工作队列
工作队列与前两种方式最大不同是,下半部任务由内核创建一个线程进行处理。内核线程有可能被其他线程抢占,因此工作队列允许睡眠或者重新调度。如果下半部任务实时性要求不高,允许睡眠,则选择工作队列;否则选择软中断或者tasklet。工作队列是没有优先级的,多个工作队列时,是按照FIFO的方式进行处理。
工作队列的方式下,把下半部任务封装为工作项(work),linux内核用“struct work_strcut”结构体描述,位于“kernel/include/workqueue.h”中。
struct work_struct {atomic_long_t data; /* 传递给回调函数fun的参数 */struct list_head entry; /* 指针入口 */work_func_t func; /* 回调处理函数,下半部处理任务置于此 */
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};typedef void (*work_func_t)(struct work_struct *work);
一系列工作项(work)组成工作队列(work queue),“workqueue_struct”结构体描述,原型位于“kernel/kernel/workqueue.c”中。
/** The externally visible workqueue. It relays the issued work items to* the appropriate worker_pool through its pool_workqueues.*/
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; /* PW: only for unbound wqs */struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */#ifdef CONFIG_SYSFSstruct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endifchar name[WQ_NAME_LEN]; /* I: workqueue name */......
};
工作队列使用步骤
工作队列可以使用linux系统创建的队列和用户自定义队列,队列可以设定为延迟执行和非延迟执行。
1)注册工作项
使用工作队列机制,首先需定义一个“struct work_struct”工作项,可以动态和静态定义。
- 非延工作迟项注册
#define INIT_WORK(_work, _func) \__INIT_WORK((_work), (_func), 0)#define DECLARE_WORK(n, f) \struct work_struct n = __WORK_INITIALIZER(n, f)
- 延迟工作项注册
#define INIT_DELAYED_WORK(_work, _func) \__INIT_DELAYED_WORK(_work, _func, 0)#define DECLARE_DELAYED_WORK(n, f) \struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
参数 | 含义 |
---|---|
_work | 待注册的工作项,静态注册 |
_func | 下半部回调处理函数 |
n | 待注册的工作指针,动态注册 |
返回 | 无 |
注:
使用系统工作队列时,只需注册工作任务即可。
2)使用自定义队列
- 创建工作队列
如果使用用户自定义的工作队列,则首先需创建一个工作队列。创建工作队列,首先是定义一个“struct workqueue_struct”工作队列指针,调用“create_singlethread_workqueue”宏创建工作队列。
#define create_singlethread_workqueue(name) \alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)
参数 | 含义 |
---|---|
name | 工作队列名称 |
返回 | 工作队列首地址 |
例子:
struct workqueue_struct *pworkqueue = create_singlethread_workqueue("wq0");
- 绑定自定义工作队列
/* 非延迟工作队列绑定 */
bool queue_work(struct workqueue_struct *wq,struct work_struct *work)
{return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}/* 延迟工作队列绑定 */
bool queue_delayed_work(struct workqueue_struct *wq,struct delayed_work *dwork,unsigned long delay)
{return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}
参数 | 含义 |
---|---|
wq | 工作队列 |
work | 工作项 |
delay | 延时的执行的时钟节拍(非时间) |
返回 | 成功返回true |
例子:
struct work_struct work;
struct workqueue_struct *pwrokqueue;void work_handle(struct work_struct *pw)
{
/* todo */
}INIT_WORK(work, work_handle);
pworkqueue = create_singlethread_workqueue("wq0");
queue_work(pwrokqueue, &work); /* 绑定自定义工作队列 */
- 释放工作队列
void destroy_workqueue(struct workqueue_struct *wq)
参数 | 含义 |
---|---|
wq | 工作队列 |
返回 | 无 |
3)触发调度
- 触发非延迟工作项
bool schedule_work(struct work_struct *work)
- 触发延迟工作项
bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay)
参数 | 含义 |
---|---|
dwork | 工作项 |
delay | 延迟时钟节拍数 |
返回 | 成功返回true |
gq0{compatible = "gq0";gpios = <&gpio1, 10, GPIO_ACTIVE_LOW>;pinctrl-names = "default";pinctrl-0 = <&spi1_cs0_gpio>; /* gpio模式 */interrupt-parent = <&gpio1>; interrupts = <10, IRQ_TYPE_EDGE_BOTH>; /* 上升沿和下降沿触发 */status = "okay";};spi1_cs0_gpio: spi1_cs0_gpio {rockchip,pins =<1 10 RK_FUNC_GPIO &pcfg_pull_none>,};
2. 中断驱动编写
以gpio为例,编写一个gpio触发的中断驱动,并获取gpio状态值。使用的是GPIO1_B2端口。
2.1 实现方式
- GPIO上升沿和下降沿触发中断
- read函数通过等待队列挂起应用进程
- 中断后触发同步信号唤醒进程读取IO状态
2.2 添加设备树
GPIO1_B2引脚是复用引脚,可以复用为sp1的片选引脚(SP1_CSn0)。原设备树文件已添加 SPI1_CSn0的pin节点描述,在其他后增加GPIO属性描述。同时增加一个“gpioirq”的驱动节点信息。
- pin设备树
/* 在rk3399.dtsi 中添加 */
spi1 {......spi1_cs0: spi1-cs0 {rockchip,pins =<1 10 RK_FUNC_2 &pcfg_pull_up>;};spi1_cs0_gpio: spi1_cs0_gpio { /* 添加GPIO pin描述 */rockchip,pins =<1 10 RK_FUNC_GPIO &pcfg_pull_none>,};......}
- 驱动节点设备树
/* 在rk3399-firefly-port.dtsi 中添加 */gq0{compatible = "gq0";gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;pinctrl-names = "default";pinctrl-0 = <&spi1_cs0_gpio>; /* gpio模式 */interrupt-parent = <&gpio1>; interrupts = <10 IRQ_TYPE_EDGE_BOTH>; /* 上升沿和下降沿触发 */status = "okay";};
注:
设备数下的中断类型描述,位于“irq.h”中,与前面1.3.3节描述的中断类型值是一致的,只是名称不一样。
enum {
IRQ_TYPE_NONE = 0x00000000,
IRQ_TYPE_EDGE_RISING = 0x00000001,
IRQ_TYPE_EDGE_FALLING = 0x00000002,
IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING),
IRQ_TYPE_LEVEL_HIGH = 0x00000004,
IRQ_TYPE_LEVEL_LOW = 0x00000008,
IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW | IRQ_TYPE_LEVEL_HIGH),
IRQ_TYPE_SENSE_MASK = 0x0000000f,
IRQ_TYPE_DEFAULT = IRQ_TYPE_SENSE_MASK,
…
};
2.3 设备数据结构
struct gpioirq_dev
{struct cdev dev; /* 字符驱动 */dev_t dev_id; /* 设备ID */struct class *dev_class; /* 设备类 */int gpio; /* GPIO序号 */int irq; /* 中断序号 */wait_queue_head_t r_queue; /* 等待队列 */bool r_en; /* 可读标识 */struct fasync_struct *r_sync;/* 内核通知应用信号 */
};
2.4 中断函数
static irqreturn_t gq0_irq_handle(int irq, void *dev_id)
{struct gpioirq_dev *p;p = (struct gpioirq_dev *)dev_id;p->r_en = true;wake_up_interruptible(&(p->r_queue)); /* 唤醒休眠进程 *//* 通知应用进程数据可读* SIGIO:信号类型* POLL_IN:普通数据可读*/kill_fasync(&p->r_sync, SIGIO, POLL_IN); return IRQ_HANDLED;
}
2.5 状态读取函数
static ssize_t gq0_read(struct file *pfile, char __user *buf, size_t size, loff_t * offset)
{ int ret = 0;struct gpioirq_dev *p;char level = 0;p = pfile->private_data;wait_event_interruptible(p->r_queue, p->r_en); /* 进程休眠,等待中断 */level = gpio_get_value(p->gpio);ret = copy_to_user(buf, &level, 1);return ret;
}
2.6 中断注册
static int gq0_probe(struct platform_device *pdev)
{ struct device *dev; int ret = -1;dev_t id = 0;struct device_node *nd;nd = pdev->dev.of_node; /* 设备树节点 */if(nd == NULL){printk("get node faileed\n");return -1;}gq0.gpio = of_get_named_gpio(nd, "gpios", 0); /* 获取GPIO */if(gq0.gpio < 0){printk("get gpio failed\n");return -1;}if (!gpio_is_valid(gq0.gpio)) {printk("gpio [%d] is invalid\n", gq0.gpio);return -1;}ret = gpio_request(gq0.gpio, "gq0"); /* 申请GPIO */if(ret < 0){printk("gpio request failed\n");return ret;}ret = gpio_direction_input(gq0.gpio); //gq0.irq = gpio_to_irq(gq0.gpio); /* 中断号映射 */gq0.irq = irq_of_parse_and_map(nd, 0);ret = request_irq(gq0.irq, gq0_irq_handle, gq0.irq_mode, "gq0", &gq0);/* 注册中断 */if(ret<0){printk("request gq0 irq failed\n");free_irq(gq0.irq, &gq0);gpio_free(gq0.gpio);return ret;}......
}
2.7 platform 驱动
static struct of_device_id of_gq0_ids[] = {{.compatible = "gpioirq"}, /* 与节点设备树“compatible ”属性一致 */{ } };static struct platform_driver gq0_driver = { .driver = { .owner = THIS_MODULE, .name = DEV_NAME, .of_match_table = of_gq0_ids,}, .probe = gq0_probe, .remove = gq0_remove,
};module_platform_driver(gq0_driver); /* platform 驱动注册和注销 */
3. 源码
【1】https://github.com/Prry/rk3399
4. 参考
【1】https://blog.csdn.net/yhb1047818384/article/details/63687126
【2】http://www.wowotech.net/irq_subsystem/tasklet.html
【3】https://www.ibm.com/developerworks/cn/linux/l-cn-cncrrc-mngd-wkq/