字符设备驱动 — 4 异常与中断
异常与中断
中断属于异常的一种
异常会直接打断 CPU 的运行,而各种中断会被传到中断控制器,由中断控制器来选择优先级最高的中断并通知 CPU
处理流程
arm 对异常(中断)处理流程:
-
初始化:
- 设置中断源,让它可以产生中断
- 设置中断控制器(可以屏蔽中断、优先级)
- 设置 CPU 总开关(使能中断)
-
执行其他程序:正常程序
-
产生中断:比如按下按键 -> 中断控制器 -> CPU
-
CPU 在每执行完一条指令都会检查 有/无 中断/异常 产生
-
CPU 发现有 中断/异常 产生,开始处理
对于不同的异常(中断),跳去不同的地址程序
-
这些函数做什么事情:
- 保护现场(各种寄存器)
- 处理异常(中断);分辨中断源,再调用不同的处理函数
- 恢复现场
3、4、5 都是由硬件完成的
异常向量表
异常向量表:异常向量表就是把所有异常类型码及其对应的异常向量(入口函数)按一定的规律存在一个区域内,这个存储区域就叫做异常向量表
中断向量表:把系统中所有的中断类型码(中断号)及其对应的中断向量(中断处理函数)按一定的规律存放在一个区域内,这个存储区域就叫中断向量表
中断向量表包含在异常向量表中
异常向量表中的 “ _irq ” 代表中断
异常向量表:
异常向量表,每一条指令对应一种异常
发生复位时,CPU 就去执行第1条指令:b reset
发生中断时,CPU 就去执行 “ ldr pc,_irq ” 这条指令
这些指令存放的位置是固定的。比如对于 ARM9 芯片中断向量的地址是 0x18(这里指的是偏移位置),当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。
查看
查看 /proc/interrupts 可以获取系统中的中断信息
第一列:中断号
第二列:向 CPU 产生中断的次数
再后面就是中断的信息
中断函数
申请中断:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
-
**irq:**要申请的硬件中断号
-
**handler:**中断处理函数(上半部)
typedef irqreturn_t (*irq_handler_t)(int, void *);
-
**flags:**中断处理的属性
-
IRQF_DISABLED:表示进入中断处理程序时禁止任何中断,包括其它CPU,默认只是屏蔽当前对应所有CPU的中断线
-
IRQF_SHARED:表示中断线是共享的,也就是说同一个中断线上会对应多个中断处理程序,在中断处理程序中,通过第5个参数dev来判断到底是哪个发生了中断
-
-
**name:**中断名
-
**dev:**中断共享时用到,用来判断是哪个中断触发
释放中断:
void free_irq(unsigned int irq, void *dev_id)
irq:硬件中断号
dev_id:如果中断是共享的,则仅删除dev_id所对应的处理程序,直到最后一个中断被删除的时候才禁用此中断线
屏蔽中断:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq 会等待中断处理完成,所以如果在中断的上半部调用,则会造成系统死锁,这种情况下,只能调用disable_irq_nosync
#define local_irq_save(flags) ...
void local_irq_disable(void);
这两个函数或宏将屏蔽 CPU 内所有的中断
与之对应的恢复所有的中断也有两个函数或宏
#define local_irq_restore(flags) ...
void local_irq_enable(void);
Liunx系统对于中断的处理
《中断学习(一) —— Linux中断流程以及处理机制》 - 一个不知道干嘛的小萌新 - 博客园 (cnblogs.com)
ARM程序的运行流程:
首先需要明白,ARM 芯片属于精简指令集计算机(RISC)
特点:1)对于内存只有读、写指令
2)对数据的运算是在 CPU 的内部完成的
3)CPU 复杂度较小,易于设计
eg:a = a + b,在 ARM 中分为4步来实现:
- 把内存 a 的值读入 CPU 寄存器 R0
- 把内存 b 的值读入 CPU 寄存器 R1
- 把 R0、R1 的值相加,存入 R0
- 把 R0 的值写入内存 a
进程与线程的切换(保护现场):
进程、线程的核心是栈,栈是一种串列形状的数据结构,这种数据结构的特点是后入先出
程序被中断,如何保存现场:
- 程序A正常运行中,突然中断触发。程序A被打断这个时候会暂停程序A运行,将程序A中所有的寄存器保存下来。这也就保存现场(保存现场并不局限于中断,也包括程序之间的切换:函数调用)
- 这些寄存器会被保存在内存中,而这块内存就被称为栈
- 然后ARM去执行中断程序
- 等中断程序执行完了,从栈中恢复刚被保存的寄存器值
进程与线程之间的切换也大致如此
中断的拓展
Linux中有硬件中断和软件中断
对于硬件中断的两个原则:
- 不能嵌套(防止过多的保存现场导致栈溢出)(早期的 Linux 支持中断嵌套)
- 越快越好(上半部、下半部,上半部简单的接收数据(中断触发会被屏蔽,硬件自动屏蔽),将耗时的处理放在下半部(可以被其他硬件中断打断))(在中断处理的过程中是不能进行进程调度的,进程调度要靠定时器中断来实现,所以中断处理需要越快越好)
硬件中断
对于按键等硬件产生的中断称为 “ 硬件中断 ”,每个硬件中断都有自己的处理函数
当硬件触发中断后,会自动跳转到异常向量表的 irq 中,然后判断中断源,再跳转到对应的中断服务函数中,这些都是由硬件完成的
软件中断
人为制造的中断
软件中断何时产生:
由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断
软件中断何时处理:
Linux系统中,各种硬件中断发生的很频繁,至少定时器中断每 10ms 发生1次(心跳),所以在处理完硬件中断后,再去处理软件中断
-
硬件中断 A 处理过程中,没有其他中断产生:
- 中断 A 发生,count ++ 等于 1
- 硬件屏蔽中断,中断 A 上半部运行处理,处理完成,count – 等于 0
- count ++ 等于 1,硬件开启接收硬件中断,执行软件中断也就是所谓的中断下半部
- count – 等于 0,然后退出
-
硬件中断 A 处理过程中,又发生了中断 A:
- 第一次中断A发生,执行到第 ⑥ 时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表,这时 count 等于 1,并且中断A下半部的代码并未执行
- CPU 又从 ① 开始再次执行中断 A 的上半部代码,在 ① 中 count++ 等于2,到 ③ 后 count-- 等于1
- 在第 ④ 步发现 count 等于 1,所以直接结束当前第 2 次中断的处理
- 重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理,当第 2 次中断处理完毕, CPU会继续去执行第 ⑦ 步(但是只会处理一次下半部)
可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只执行了一次
-
硬件中断 A 处理过程中,又发生了中断 B:
- 第一次中断A发生,执行到第 ⑥ 时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表,这时 count 等于 1,并且中断下半部的代码并未执行
- CPU 又从 ① 开始执行中断 B 的上半部代码,在 ① 中 count++ 等于2,到 ③ 后 count-- 等于1
- 在第 ④ 步发现 count 等于 1,所以直接结束当前第 2 次中断的处理
- 重点来了,第 2 次中断发生后,打断了第一次中断的第 ⑦ 步处理。当第 2 次中断处理完毕, CPU会继续去执行第 ⑦ 步
在第 ⑦ 步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。 所以,多个中断的下半部,是汇集在一起处理的
中断不能休眠原因:
如果在中断的上半部睡眠,会发生什么:
在中断上半部执行的时候,硬件是屏蔽了中断接收的。所以操作系统接收不到任何中断(包括时钟中断),系统就会瘫痪(调度器依赖的时钟节拍也会没有)
如果在中断的下半部睡眠,会发生什么:
不能睡眠的关键是因为上下文
操作系统以进程调度为单位,进程的运行在进程的上下文中,以进程描述符作为管理的数据结构,进程可以睡眠的原因是操作系统可以切换不同进程的上下文,进行调度操作,这些操作都以进程描述符为支持,中断运行在中断上下文,没有一个所谓的中断描述符来描述它,它不是操作系统调度的单位,一旦在中断上下文中睡眠,首先无法切换上下文(因为没有中断描述符,当前上下文的状态得不到保存),其次,没有人来唤醒它,因为它不是操作系统的调度单位,此外,中断的发生是非常非常频繁的,在一个中断睡眠期间,其它中断发生并睡眠了,那很容易就造成中断栈溢出导致系统崩溃
注意:如果中断下半部使用的是工作队列机制,它是可以休眠的
执行中断下半部的三种机制
软中断
参考:
Linux中断子系统之软中断、tasklet和工作队列_linux 软中断-CSDN博客
初始化
内核中有一个软件中断数组
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
softirq_action 这个结构体用于描述一个软件中断,默认情况下,系统上只能使用 32 个软中断
struct softirq_action
{
void (*action)(struct softirq_action *);
};
action是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程
软件中断必须先注册,内核才能执行软中断,open_softirq 函数即用于该目的,它在 softirq_vec 表中指定的位置写入新的软中断
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
软中断的类型是通过枚举静态定义的,当前有10种软中断类型,从0到9,0的优先级最高,当系统调度到软中断时会最先执行
enum
{
HI_SOFTIRQ=0, /* 高优先级tasklet软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 发送网络包软中断 */
NET_RX_SOFTIRQ, /* 接收网络包软中断 */
BLOCK_SOFTIRQ, /* 用于处理块设备「block device」的软中断 */
IRQ_POLL_SOFTIRQ, /* 用于执行IOPOLL的回调函数 */
TASKLET_SOFTIRQ, /* tasklet软中断 */
SCHED_SOFTIRQ, /* 用于进程调度与负载均衡的软中断 */
HRTIMER_SOFTIRQ, /* 用于高精度定时器的软中断 */
RCU_SOFTIRQ, /* 用于RCU服务的软中断 */
NR_SOFTIRQS
};
在 start_kernel 函数会调用 softirq_init();
函数来初始化软件中断:
void __init softirq_init(void)
{
...
/* 注册tasklet相关的两个软中断 */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
触发
系统通过一个 irq_cpustat_t 的数据结构来记录软中断触发与否,可以将其理解为软中断状态寄存器,该寄存器实际是一个 unsigned int 类型的 _softirq_pending 变量,32 bit 可以对应32个软中断,当然目前只使用了 bit0 到 bit9 这10个
typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
触发软中断其实就是在 _softirq_pending 相应的bit位上写1,触发软中断的接口有两个 raise_softirq 和 raise_softirq_irqodd,前者在接口内部会关本地中断,而后者需要保证调用时已经处于关中断状态
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();
}
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
void __raise_softirq_irqoff(unsigned int nr)
{
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
执行
因为每个cpu都有一个软中断状态寄存器__softirq_pending
,因此每个CPU可以并行执行自己的软中断队列,软中断的执行时机有3个:
- 触发软中断后(执行
raise_softirq
和raise_softirq_irqoff
时); - 硬中断退出时(irq_exit时);
- 任何调用local_bh_enable打开本地软中断的时机
有几种方法可开启软中断处理,但这些都归结为调用 do_softirq 函数
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags); /* 保存IF标志的状态值,并禁用本地CPU上的中断 */
pending = local_softirq_pending();
if (pending && !ksoftirqd_running(pending))
do_softirq_own_stack();
local_irq_restore(flags); /* 恢复保存的IF标志的状态值并返回 */
}
in_interrupt() 该函数确认当前不处于中断上下文中(当然,即不涉及硬件中断),如果处于中断上下文,则立即结束,因为软中断用于执行 ISR 中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用
通过 local_softirq_pending,确定当前 CPU 软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART; /* 循环计数器的值初始化为10 */
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;
/*
* Mask out PF_MEMALLOC as the current task context is borrowedfor the
* softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
* again if the socket is related to swapping.
*/
current->flags &= ~PF_MEMALLOC;
/* 将本CPU的__softirq_pending变量复制到局部变量中 */
pending = local_softirq_pending();
account_irq_enter_time(current);
/* 关软中断,防止再次进入软中断执行逻辑 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* 将__softirq_pending清零,以便可以激活新的软中断 */
set_softirq_pending(0);
/* 开启硬件中断 */
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec;
prev_count = preempt_count();
/* 对softirq的执行次数计数,这是/proc/softirqs的数据来源 */
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
/* 某软中断对应的action回调 */
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}
if (__this_cpu_read(ksoftirqd) == current)
rcu_softirq_qs();
/* 处理完一波软中断后,关闭硬件中断 */
local_irq_disable();
/* 读一下当前的__softirq_pending,看这段时间有没有触发新的软中断 */
pending = local_softirq_pending();
/* 有新的软中断需要处理:
* 如果时间没超过2ms && 当前没有进程需要调度 && 这种循环没有超过10次,
* 此时重新跳到while循环处依次执行软中断的action回调;
* 否则唤醒ksoftirqd线程执行软中断处理。
*/
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
/* 离开软中断上下文时,开启软中断 */
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}
__do_softirq函数只做固定时间或固定次数的循环,然后就返回了,避免出现延迟用户进程的执行,如果还有其余挂起的软中断,内核线程ksoftirqd将会在预期时间里处理它们
内核线程 ksoftirqd
系统中的每个处理器都分配了自身的守护进程,名为 ksoftirqd
static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
wake_up_process唤醒进程后运行run_ksoftirqd()函数
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
tasklet(小任务)机制
tasklet 机制主要用在下半部要做的事耗时不是很长
tasklet 是基于软件中断来实现的,上面软件中断处理的流程图使用的就是 tasket 机制
tasklet 的使用非常的简单,我们只需要定义 tasklet 及其处理函数并将它们将关联
// tasklet 结构体
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
void my_tasklet_func(unsigned long); // 定义一个处理函数
DECLARE_TASKLET(my_tasklet, my_tasklet_fun, data);
// 代码 DECLARE_TASKLET 实现了定义名称为 my_tasklet 的 tasklet 并将其与 my_tasklet_func 这个函数绑定,而传入这个函数的参数为 data
tasklet_schedule(&my_tasklet) // 使系统在适当的时候进行调度运行
使用模板:
根据上面软件中断的那个流程图可以写出以下的使用模板
void xxx_do_tasklet(unsigned long);
struct tasklet_struct xxx_tasklet;
DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);
void xxx_do_tasklet(unsigned long)
{
……
}
// 中断服务程序
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
……
// 使系统在合适的时候调用驱动的下半部
tasklet_schedule(&xxx_tasklet);
……
}
int _init xxx_init(void)
{
……
// 申请中断,中断的上半部
result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLED,”xxx”,NULL);
……
return IRQ_HANDLED;
}
void _exit xxx_exit(void)
{
……
free_irq(xxx_irq,xxx_irq_interrupt);
……
}
工作队列机制
tasklet 机制在中断下半部执行的过程中,虽然是开中断的,期间可以处理各类中断,但是毕竟整个中断的处理还没有走完,这期间 APP 是无法运行的
如果中断要做的事情太过于耗时,就不能使用 tasket 机制(软件中断)来做,而应该用内核线程来做:在中断的上半部唤醒内核线程。这样内核线程和 APP 都一样竞争执行,APP 有机会执行,系统不会卡顿
上述的那个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
kworker 线程要去 “ 工作队列(work queue)”上取出一个一个 “ 工作(work)”,来执行它里面的函数
使用步骤:
-
创建 work:
void my_func(); DECLARE_WORK(my_work, my_func, &data); // DECLARE_WORK(my_work, my_func); // 第三个参数可以不要也可以写为空 static DECLARE_WORK(aer_recover_work, aer_recover_work_func); // 在编译时创建函数,并将函数入口地址和参数地址赋给它
如果不想要在编译时就用DECLARE_WORK ( ) 创建并初始化工作结构体变量,也可以在程序运行时再用INIT_WORK ( ) 创建
void my_func(); struct work_struct my_work; INIT_WORK(&my_work, my_func, &data);
-
将 work 提交给 work queue
schedule_work(&my_work); // 一般是中断的上半部调用
谁来调用 work:
不用我们管,schedule_work 函数不仅仅是把 work 放入队列,还会把 kworker 线程唤醒,此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数
使用模板:
struct work_struct xxx_wq;
// 中断下半部
void xxx_do_work(unsigned long)
{
……
}
// 中断上半部
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
……
schedule_work(&xxx_wq); // 将 work 放入工作队列,并且唤醒内核的 kworker 线程
……
}
int init(void)
{
……
result = request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,“xxx”,NULL);
……
INIT_WORK(&xxx_wq,(void(*)(void*))xxx_do_work,NULL);
…….
}
void xxx_exit(void)
{
……
free_irq(xxx_irq,xxx_interrupt);
……
}
新技术:threaded_irq
你可以只提供 thread_fn,系统会为这个函数创建一个内核线程,发生中断时,内核线程就会执行这个函数
之前的 work 都是由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着,但是在多个 CPU 的系统中,就会浪费资源
新技术 threaded_irq,为每一个中断创建一个内核线程,多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率
重要结构体
irq_desc
理解:
外部中断1和外部中断n共享 GPIO 的B号中断,而多个 GPIO 又汇聚到 GIC 的A号中断,最后由GIC中断 CPU,那么软件的处理就是反过来的,先读取 GIC 得知A号中断,再细分出 GPIO 的B号中断,最后在判断出具体是哪一个外部设备
所以中断处理函数的来源有三:
-
GIC 的处理函数(GIC:中断控制器)
irq_desc[A].handle_irq 细分出中断 后B ,调用对应的 irq_desc[B].handle_irq
显然中断 A 是 CPU 感受到的顶层的中断,GIC 中断 CPU 时,CPU 读取 GIC 状态得到中断 A
-
GPIO 的处理函数
比如对于GPIO模块向 GIC 发出的中断B ,它的处理函数是 irq_desc[B].handle_irq
判断中断具体有哪一个外设产生
-
外部设备提供的处理函数
判断设备是否发生了中断,如何处理中断
对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数,所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数,一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行(并不判断是哪一个外部设备而是循环执行所有的处理函数,这些处理函数会自行判断自己是否产生了中断),这个链表就是 action 链表
irq_action
当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体,里面保存 name、dev_id 等,最重要的是 handler、thread_fn、thread
**handler:**中断处理的上半部
thread_fn: 对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程,在内核线程里,会调用 thread_fn 函数
dev_id:
-
中断处理函数执行时,可以使用
-
卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项
所以在共享中断中必须提供 dev_id,非共享中断可以不提供
irq_data
中转站,里面有 irq_chip 指针、irq_domain 指针、都是指向别的结构体
irq:软件中断号
hwirq:硬件中断号
GPIO 中断B是软件中断号,GPIO里的第X号中断,这是硬件中断号
irq_domain 会把本地的 hwirq 映射为全局的 irq,比如GPIO控制器里有第1号中断,UART模块里也有第1号中断,这两个 “ 第1号中断 ”是不一样的,它们属于不同的 “ 域 ” —— irq_domain
irq_domain
如何在设备树中指定中断,设备树的中断如何被转换为 irq 时,irq_domain 将会起到极大的作用
eg:
在设备树中会有这样的属性:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用 gpio1 里的第 5 号中断,hwirq 就是 5
但是我们在驱动中会使用 request_irq(irq,handler)这样的函数来注册中断,irq 是软件中断号
gpio1 对应的 irq_domain 会将 hwirq 转换为 irq
irq_domian 结构体中有一个 irq_domain_ops 结构体,里面有各种的操作函数
**xlate:**用来解析设备树的中断属性,提取出 hwirq、type 等信息
**map:**把 hwirq 转换为 irq
irq_chip
我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数
但是对于外部设备相关的清中断操作,还是需要我们自己做的
就像上面图里的 “ 外部设备 1 “、“ 外部设备 n ”,外设备千变万化,内核里可没有对应的清除中断操作
设备树指定中断
概述
在硬件上,“ 中断控制器 ” 只有 GIC 这一个,但是在软件上也可以把 “ GPIO ” 称为 “ 中断控制器 ”
在设备树中,中断控制器必须有一个属性:interrupt-controller,表明它是 “ 中断控制器 ”
还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个 cell
如果中断控制器有级联关系 ,下级的中断控制器还需要表明它的 “ interrupt-parent ” 是谁
interrupts 的第2个参数取值:(这个参数可以写为宏,也可以写为具体的值)
#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
新写法:interrupts-extended
一个“interrupts-extended”属性就可以既指定 “ interrupt-parent ”,也指定 “ interrupts ”
interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;
示例
在代码中获取中断
对于platform_device
这类节点可以转换为 platform_device 结构体
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type,
unsigned int num);
// 第2个参数:获取资源的类型 IORESOURCE_IRQ:中断资源
// 第3个参数:这类资源中的哪一个
对于I2C、SPI设备
一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里
一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里
调用 of_irq_get 获得中断号
如果你的设备节点既不能转换为 platform_device,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号
对于GPIO
eg
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
user {
label = "User Button";
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
gpio-key,wakeup;
linux,code = <KEY_1>;
};
};
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);
irq = gpiod_to_irq(bdata->gpiod);
按键中断驱动示例
设备树
gpio_keys_100ask {
compatible = "100ask,gpio_key";
gpios = <&gpio5 1 GPIO_ACTIVE_LOW
&gpio4 14 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&key1_100ask &key2_100ask>;
};
驱动代码
/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;
unsigned flags = GPIOF_IN;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
count = of_gpio_count(node); // 计算 gpio 的引脚数目
if (!count)
{
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++)
{
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag); // 获取 gpio 里的第i个引脚
if (gpio_keys_100ask[i].gpio < 0)
{
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
if (flag & OF_GPIO_ACTIVE_LOW)
flags |= GPIOF_ACTIVE_LOW;
err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}
for (i = 0; i < count; i++)
{
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}
return 0;
}