分类: LINUX
2015-08-19 14:43:28
作者: 一、前言
当外设触发一次中断后,一个大概的处理过程是:
1、具体CPU architecture相关的模块会进行现场保护,然后调用machine driver对应的中断处理handler
2、machine driver对应的中断处理handler中会根据硬件的信息获取HW interrupt ID,并且通过irq domain模块翻译成IRQ number
3、调用该IRQ number对应的high level irq event handler,在这个high level的handler中,会通过和interupt controller交互,进行中断处理的flow control(处理中断的嵌套、抢占等),当然最终会遍历该中断描述符的IRQ action list,调用外设的specific handler来处理该中断
4、具体CPU architecture相关的模块会进行现场恢复。
注:这份文档充满了猜测和空想,很多地方描述可能是有问题的,不过我还是把它发出来,抛砖引玉,希望可以引发大家讨论。
一、如何进入high level irq event handler
1、从具体CPU architecture的中断处理到machine相关的处理模块
我们以上升沿为例描述边缘中断的处理过程(下降沿的触发是类似的)。当interrupt controller检测到了上升沿信号,会将该上升沿状态(pending)锁存在寄存器中,并通过中断的signal向CPU触发中断。需要注意:这时候,外设和interrupt controller之间的interrupt request信号线会保持高电平,这也就意味着interrupt controller不可能检测到新的中断信号(本身是高电平,无法形成上升沿)。这个高电平信号会一直保持到软件ack该中断(调用irq chip的irq_ack callback函数)。ack之后,中断控制器才有可能继续探测上升沿,触发下一次中断。
ARM+GIC组成的系统不符合这个类型。虽然GIC提供了IAR(Interrupt Acknowledge Register)寄存器来让ARM来ack中断,但是,在调用high level handler之前,中断处理程序需要通过读取IAR寄存器获得HW interrpt ID并转换成IRQ number,因此实际上,对于GIC的irq chip,它是无法提供本场景中的irq_ack函数的。很多GPIO type的interrupt controller符合上面的条件,它们会提供pending状态寄存器,读可以获取pending状态,而向pending状态寄存器写1可以ack该中断,让interrupt controller可以继续触发下一次中断。
handle_edge_irq代码如下:
void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock); -----------------(0)desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);----参考上一章的描述
if (unlikely(irqd_irq_disabled(&desc->irq_data) ||-----------(1)
irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {
if (!irq_check_poll(desc)) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
}
kstat_incr_irqs_this_cpu(irq, desc); ---更新该IRQ统计信息
desc->irq_data.chip->irq_ack(&desc->irq_data); ---------(2)do {
if (unlikely(!desc->action)) { -----------------(3)
mask_irq(desc);
goto out_unlock;
}
if (unlikely(desc->istate & IRQS_PENDING)) { ---------(4)
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc);
}handle_irq_event(desc); -------------------(5)
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data)); -------------(6)out_unlock:
raw_spin_unlock(&desc->lock); -----------------(7)
}
(0) 这时候,中断仍然是关闭的,因此不会有来自本CPU的并发,使用raw spin lock就防止其他CPU上对该IRQ的中断描述符的访问。针对该spin lock,我们直观的感觉是raw_spin_lock和(7)中的raw_spin_unlock是成对的,实际上并不是,handle_irq_event中的代码是这样的:
irqreturn_t handle_irq_event(struct irq_desc *desc)
{raw_spin_unlock(&desc->lock); -------和上面的(0)对应
处理具体的action list
raw_spin_lock(&desc->lock);--------和上面的(7)对应
}
实际上,由于在handle_irq_event中处理action list的耗时还是比较长的,因此处理具体的action list的时候并没有持有中断描述符的spin lock。在如果那样的话,其他CPU在对中断描述符进行操作的时候需要spin的时间会很长的。
(1)判断是否需要执行下面的action list的处理。这里分成几种情况:
a、该中断事件已经被其他的CPU处理了
b、该中断被其他的CPU disable了
c、该中断描述符没有注册specific handler。这个比较简单,如果没有irqaction,根本没有必要调用action list的处理
如果该中断事件已经被其他的CPU处理了,那么我们仅仅是设定pending状态(为了委托正在处理的该中断的那个CPU进行处理),mask_ack_irq该中断并退出就OK了,并不做具体的处理。另外正在处理该中断的CPU会检查pending状态,并进行处理的。同样的,如果该中断被其他的CPU disable了,本就不应该继续执行该中断的specific handler,我们也是设定pending状态,mask and ack中断就退出了。当其他CPU的代码离开临界区,enable 该中断的时候,软件会检测pending状态并resend该中断。
这里的irq_check_poll代码如下:
static bool irq_check_poll(struct irq_desc *desc)
{
if (!(desc->istate & IRQS_POLL_INPROGRESS))
return false;
return irq_wait_for_poll(desc);
}
IRQS_POLL_INPROGRESS标识了该IRQ正在被polling(上一章有描述),如果没有被轮询,那么返回false,进行正常的设定pending标记、mask and ack中断。如果正在被轮询,那么需要等待poll结束。
(2)ack该中断。对于中断控制器,一旦被ack,表示该外设的中断被enable,硬件上已经准备好触发下一次中断了。再次触发的中断会被调度到其他的CPU上。现在,我们可以再次回到步骤(1)中,为什么这里用mask and ack而不是单纯的ack呢?如果单纯的ack则意味着后续中断还是会触发,这时候怎么处理?在pending+in progress的情况下,我们要怎么处理?记录pending的次数,有意义吗?由于中断是完全异步的,也有可能pending的标记可能在另外的CPU上已经修改为replay的标记,这时候怎么办?当事情变得复杂的时候,那一定是本来方向就错了,因此,mask and ack就是最好的策略,我已经记录了pending状态,不再考虑pending嵌套的情况。
(3)在调用specific handler处理具体的中断的时候,由于不持有中断描述符的spin lock,因此其他CPU上有可能会注销其specific handler,因此do while循环之后,desc->action有可能是NULL,如果是这样,那么mask irq,然后退出就OK了
(4)如果中断描述符处于pending状态,那么一定是其他CPU上又触发了该interrupt source的中断,并设定了pending状态,“委托”本CPU进行处理,这时候,需要把之前mask住的中断进行unmask的操作。一旦unmask了该interrupt source,后续的中断可以继续触发,由其他的CPU处理(仍然是设定中断描述符的pending状态,委托当前正在处理该中断请求的那个CPU进行处理)。
(5)处理该中断请求事件
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;desc->istate &= ~IRQS_PENDING;----CPU已经准备处理该中断了,因此,清除pending状态
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);--设定INPROGRESS的flag
raw_spin_unlock(&desc->lock);ret = handle_irq_event_percpu(desc, action); ---遍历action list,调用specific handler
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);---处理完成,清除INPROGRESS标记
return ret;
}
(6)只要有pending标记,就说明该中断还在pending状态,需要继续处理。当然,如果有其他的CPU disable了该interrupt source,那么本次中断结束处理。
2、电平触发的handler
使用handle_level_irq这个handler的硬件中断系统行为如下:
我们以高电平触发为例。当interrupt controller检测到了高电平信号,并通过中断的signal向CPU触发中断。这时候,对中断控制器进行ack并不能改变interrupt request signal上的电平状态,一直要等到执行具体的中断服务程序(specific handler),对外设进行ack的时候,电平信号才会恢复成低电平。在对外设ack之前,中断状态一直是pending的,如果没有mask中断,那么中断控制器就会assert CPU。
handle_level_irq的代码如下:
void handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc); ---------------------(1)if (unlikely(irqd_irq_inprogress(&desc->irq_data)))---------(2)
if (!irq_check_poll(desc))
goto out_unlock;desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);--和retrigger中断以及自动探测IRQ相关
kstat_incr_irqs_this_cpu(irq, desc);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {-----(3)
desc->istate |= IRQS_PENDING;
goto out_unlock;
}handle_irq_event(desc);
cond_unmask_irq(desc); --------------(4)
out_unlock:
raw_spin_unlock(&desc->lock);
}
(1)考虑CPU<------>interrupt controller<------>device这样的连接方式中,我们认为high level handler主要是和interrupt controller交互,而specific handler(request_irq注册的那个)是和device进行交互。Level类型的中断的特点就是只要外设interrupt request line的电平状态是有效状态,对于interrupt controller,该外设的interrupt总是active的。由于外设检测到了事件(比如数据到来了),因此assert了指定的电平信号,这个电平信号会一直保持,直到软件清除了外设的状态寄存器。但是,high level irq event handler这个层面只能操作Interrupt controller,不能操作具体外设的寄存器(那应该属于具体外设的specific interrupt handler处理内容,该handler会挂入中断描述符中的IRQ action list)。直到在具体的中断服务程序(specific handler中)操作具体外设的寄存器,才能让这个asserted电平信号消息。
正是因为level trigger的这个特点,因此,在high level handler中首先mask并ack该IRQ。这一点和边缘触发的high level handler有显著的不同,在handle_edge_irq中,我们仅仅是ack了中断,并没有mask,因为边缘触发的中断稍纵即逝,一旦mask了该中断,容易造成中断丢失。而对于电平中断,我们不得不mask住该中断,如果不mask住,只要CPU ack中断,中断控制器将持续的assert CPU中断(因为有效电平状态一直保持)。如果我们mask住该中断,中断控制器将不再转发该interrupt source来的中断,因此,所有的CPU都不会感知到该中断,直到软件unmask。这里的ack是针对interrupt controller的ack,本身ack就是为了clear interrupt controller对该IRQ的状态寄存器,不过由于外部的电平仍然是有效信号,其实未必能清除interrupt controller的中断状态,不过这是和中断控制器硬件实现相关的。
(2)对于电平触发的high level handler,我们一开始就mask并ack了中断,因此后续specific handler因该是串行化执行的,为何要判断in progress标记呢?不要忘记spurious interrupt,那里会直接调用handler来处理spurious interrupt。
(3)这里有两个场景
a、没有注册specific handler。如果没有注册handler,那么保持mask并设定pending标记(这个pending标记有什么作用还没有想明白)。
b、该中断被其他的CPU disable了。如果该中断被其他的CPU disable了,本就不应该继续执行该中断的specific handler,我们也是设定pending状态,mask and ack中断就退出了。当其他CPU的代码离开临界区,enable 该中断的时候,软件会检测pending状态并resend该中断。
(4)为何是有条件的unmask该IRQ?正常的话当然是umask就OK了,不过有些threaded interrupt(这个概念在下一份文档中描述)要求是one shot的(首次中断,specific handler中开了一枪,wakeup了irq handler thread,如果允许中断嵌套,那么在specific handler会多次开枪,这也就不是one shot了,有些IRQ的handler thread要求是one shot,也就是不能嵌套specific handler)。
3、支持EOI的handler
TODO
2015-04-30 20:22
Level irq的IRQS_PENDING状态来自于下面这个patch:
commit d4dc0f90d243fb54cfbca6601c9a7c5a758e437f
Author: Thomas Gleixner <tglx@linutronix.de>
Date: Wed Apr 25 12:54:54 2012 +0200
genirq: Allow check_wakeup_irqs to notice level-triggered interrupts
Level triggered interrupts do not cause IRQS_PENDING to be set when
they fire while "disabled" as the 'pending' state is always present in
the level - they automatically refire where re-enabled.
However the IRQS_PENDING flag is also used to abort a suspend cycle -
if any 'is_wakeup_set' interrupt is PENDING, check_wakeup_irqs() will
cause suspend to abort. Without IRQS_PENDING, suspend won't abort.
Consequently, level-triggered interrupts that fire during the 'noirq'
phase of suspend do not currently abort suspend.
So set IRQS_PENDING even for level triggered interrupts, and make sure
to clear the flag in check_irq_resend.
从注释里面可以看出来:
? 这个patch是为了解决一个level irq suspend状态错误的问题。
? 如果这是一个edge irq,假设在中断上半部disable_irq(),然后启动workqueue处理下半部,假设这个时候来了第二个同样的edge irq,系统设置IRQS_PENDING状态之后退出来,假设workqueue运行到一半还没有结束的时候系统进入suspend流程,那么在函数check_wakeup_irqs()会判断这个irq,如果这个irq是一个可以唤醒系统的中断,系统就不会suspend,而是直接退出suspend流程。
? 如果这是一个level irq,kernel之前的处理办法是先mask当前正在处理的这个irq,如果有相同的irq第二次进来,假设当前irq已经被disable了,那么就什么也不做直接退出。这样的结果是如果系统接下来进入suspend流程,假设这个irq是可以唤醒系统的,它的状态不是pending,所以系统真正进入suspend。这样子从逻辑上面来讲就是错误的,因为一个可以唤醒系统的中断源已经有效了,系统不应该进入suspend。
? 为了解决这个bug,在level irq重入的时候,如果状态已经是disable了,就设置IRQS_PENDING,然后在check_wakeup_irqs函数里面一旦检测到这种状态之后就会退出suspend流程。当最后enable_irq()函数被呼叫的时候,通过呼叫check_irq_resend()来去掉level irq的pending位。
请教几个问题:
1.有没有同时支持上升沿和下降沿的中断控制器?当然我觉得不可能有同时支持高电平和低电平出发的中断控制器。
2.gpio type 中断,如果是上升沿中断,是不是要使用下拉电阻或者根本不使用上下拉?如果使用了上拉电阻会怎么样?
3.gpio type 中断,如果是高电平,我觉得肯定要使用下拉电阻,将电平钳制在低电平。
3.gpio type 中断,如果是下降沿,是不是要使用上拉电阻,或者不使用。如果使用了下拉电阻会怎么样?
4.gpio type 中断,如果是低电平,我觉得肯定要使用上拉电阻,将电平钳制在高电平。
5.如果gpio中断内部使用了上拉电阻,如果外部在使用上拉电阻,会出现什么副作用吗。我觉得有,两个上拉电源之间会互相影响。
5.如果gpio中断内部使用了下拉电阻,如果外部在使用下拉电阻,会使pin脚的输入电阻变低,有可能高电平信号拉不起来。
6.如果外设使用边沿触发。比如上升沿,那么它的高电平维持时间有要求吗,由高电平变低电平,是外设自己拉低的?还是interrupt controler控制?
这都是我在工作中碰到的疑问。有些设置不好,会造成中断触发失败,有些会造成功耗过大。在这里想和大家交流一下。
而漏电的方向是从CPU漏到device还是反过来漏?这个主要看电源域吗?
一般怎么样去查证项目中有没有GPIO上面的漏电呢?
一般而言,漏电(leakage current)是一个芯片的DC参数之一,例如Iozl和Iozh这两个参数,分别表示引脚处于高阻(High-Impedance)状态时,外加高电平和低电平的电流值。由于是高阻状态,因此这时候的leakage current应该是很小的(例如1uA)。当然,我想你这里说的不是这种漏电。
我们调试嵌入式设备总是从功能开始,然后性能,特别是功耗,例如待机或者关机电流,我们都是希望能够满足功能的情况下越小越好。当大刀阔斧的针对各个HW block的的检查过后,最后往往会纠缠在GPIO状态的调整上,正确的GPIO设定往往能节省几个毫安的电流(例如如果一个GPIO的状态的错误设定可能导致0.1mA的电流,那么精细的调整10个GPIO可以节省1个mA)
具体电流是source(用你的描述就是从CPU漏到device)还是sink(从device漏到CPU)是和实际的电路连接、cpu pin的特性以及外设芯片引脚的特性相关。例如CPU的一个GPIO是tri state,假设在CPU处于suspend的时候,将该pin设定为high-impedance状态,如果对端连接的是外设芯片的enable引脚(低电平有效),为了稳定的电路状态,可能需要连接一个上拉电阻,确保外设芯片处于disable状态,以便节省功耗。如果CPU处于suspend的时候,将该pin设定为低电平,那么上拉电阻上将有一个不小的电流消耗(假设上拉到3V,上拉电阻是10k,那么CPU在该GPIO上的sink current大约要0.3mA)。如果调整输出成高电平,也不会有这个sink current。
具体的电路形形色色,这里无法每一个都描述了。
2015-03-18 17:23
@linuxer
应该没有特别的作用,就是代码简洁吧?!要不是为了mark一下IRQS_PENDING,表明此中断曾经来过??
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {-----(3)
if(desc->action)//啰嗦吧?!
desc->istate |= IRQS_PENDING;
goto out_unlock;
}
ps:写的真的挺好的,看第二遍就理解很清楚了!
2015-03-24 23:59
哦,我没有说清楚。之前没有接触过gic,所以看第一遍的时候,我概要的看了关于中断的所有文章,不懂也看完了一遍。
等回头再看第二遍的时候,发现总体都看明白了。
谢谢wowotech的付出。
这种抽象层的东西就是理解起来比较困难,因此实际的系统是千奇百怪的,因此,要要足够的底层知识的累积才可以悟到上层逻辑的精妙。
-------------------------------
这里没有精确的表达我的意思,本来我想说的是:目前我还不掌握足够的底层知识,需要积累到一定程度才能领会上层逻辑的精妙。因此,我准备先潜心研究一下具体的中断控制器再回头看看怎么修改这份文档。目前考虑先看看ARM+GIC系统,X86+Multi APIC系统和PowerPC+MPIC系统
上一篇:IRQ number和中断描述符
下一篇:驱动申请中断API
Copyright 2001-2010 ChinaUnix.net All Rights Reserved 北京皓辰网域网络信息技术有限公司. 版权所有
16024965号-6