系统休眠(System Suspend)和设备中断处理
一、设备IRQ的suspend和resume
二、关于IRQF_NO_SUSPEND Flag
三、系统中断唤醒接口:enable_irq_wake() 和 disable_irq_wake()
四、Interrupts and Suspend-to-Idle
五、IRQF_NO_SUSPEND 标志和enable_irq_wake函数不能同时使用
一、设备IRQ的suspend和resume
解决问题:
在系统休眠过程中,如何suspend设备中断(IRQ)?
在从休眠中唤醒的过程中,如何resume设备IRQ?
一般而言,在系统suspend过程后期,各个设备的IRQ (interrupt request line)被disable。
具体的时间点在各个设备的late suspend阶段之后。
static int suspend_enter(suspend_state_t state, bool *wakeup){
……
error = dpm_suspend_late(PMSG_SUSPEND);-----late suspend阶段
error = platform_suspend_prepare_late(state);
// 下面的代码中会disable各个设备的irq
error = dpm_suspend_noirq(PMSG_SUSPEND);----进入noirq的阶段
error = platform_suspend_prepare_noirq(state);
……
}
在dpm_suspend_noirq函数中,会针对系统中的每一个device,
依次调用device_suspend_noirq来执行该设备noirq情况下的suspend callback函数,
当然,在此之前会调用suspend_device_irqs函数来disable所有设备的irq。
做法理由:
在各个设备驱动完成late suspend后,已被suspend的设备不应再触发中断。
如果还有一些设备没有被正确suspend,则最好mask该设备的irq,阻止中断句柄执行。
过去代码中对设备共享IRQ的处理不是很好,
问题:
在共享IRQ的设备们完成suspend后,
若有中断触发,但这时设备驱动的interrupt handler并没有准备好。
有些场景下,interrupt handler访问已经suspend设备的IO地址空间,导致不可预知问题。
解法:
引入设备noirq阶段的callback函数和suspend_device_irqs()。
系统resume过程中,在各个设备的early resume过程之前,
各个设备的IRQ会被重新打开:
static int suspend_enter(suspend_state_t state, bool *wakeup){
……
platform_resume_noirq(state);---首先执行noirq阶段的resume
dpm_resume_noirq(PMSG_RESUME);--在这里会恢复irq,然后进入early resume阶段
platform_resume_early(state);
dpm_resume_early(PMSG_RESUME);
……
}
在dpm_resume_noirq函数中,会调用各个设备驱动的noirq callback,
在此之后,调用resume_device_irqs函数,完成各个设备irq的enable。
同样问题的解法:
引入设备noirq阶段的callback函数和resume_device_irqs()。
suspend_device_irqs
resume_device_irqs
各个设备irq的enable/disable
dpm_suspend_noirq
dpm_resume_noirq
调用各个设备驱动的noirq callback,准备中断相关的资源,避免旧问题出现
二、关于IRQF_NO_SUSPEND Flag
有些中断需要在整个系统的suspend-resume过程中,保持能触发的状态。
(包括在noirq阶段,包括将nonboot CPU推送到offline状态
以及系统resume后,将其重新设置为online的阶段)
例子:timer中断,此外IPI以及一些特殊目的设备中断也需要如此。
在中断申请时,IRQF_NO_SUSPEND flag用来告知IRQ subsystem,是前文描述的中断:
需要在系统的suspend-resume过程中保持enable状态。
有了该flag,suspend_device_irqs并不会disable该IRQ,
从而让该中断在随后的suspend和resume过程中,保持中断使能。
当然,这并不能保证该中断可以将系统唤醒,若想要唤醒,则调用enable_irq_wake。
注意:
IRQF_NO_SUSPEND flag影响使用该IRQ的所有外设
如果一个IRQ被多个外设共享(ARM中不会这么用),且各个外设都注册了对应的handler,
如果其一在申请中断时使用IRQF_NO_SUSPEND flag,
则在系统suspend时(指suspend_device_irqs之后,按理说各个IRQ已经被disable),
所有该IRQ上的各个设备的interrupt handler都可以被触发,
即便有些设备在调用request_irq(或者其他中断注册函数)时没有设定IRQF_NO_SUSPEND。
避免同时使用IRQF_NO_SUSPEND 和IRQF_SHARED。
使用了IRQF_SHARED,导致IRQF_NO_SUSPEND也被共享
三、系统中断唤醒接口:enable_irq_wake() 和 disable_irq_wake()
有些中断可以将系统从睡眠状态中唤醒,称之“可以唤醒系统的中断”,
当然,“可以唤醒系统的中断”需要配置才能使能唤醒系统功能。
这样的中断一般在工作状态时作为普通I/O interrupt使用,
但在即将使能唤醒系统功能时,才会发起一些特别的配置和设定。
外设的中断信号被送到“通用的中断信号处理模块”和“特定中断信号接收模块”。
正常工作时,turn on“通用的中断信号处理模块”的处理逻辑,
turn off“特定中断信号接收模块” 的处理逻辑。
但是,在系统进入睡眠状态时,有可能“通用的中断信号处理模块”已经off,
这时,需要使能“特定中断信号接收模块”来接收中断信号,从而让系统suspend-resume模块
(它往往是suspend状态时唯一能够工作的HW block)
可以被该中断信号正常唤醒。
一旦唤醒,最好turn off“特定中断信号接收模块”,让外设的中断处理回到正常的工作模式,
同时,也避免系统suspend-resume模块收到不必要的干扰。
IRQ子系统提供了两个接口来完成该功能:
enable_irq_wake()用来打开该外设中断线通往系统电源管理模块之路,
(也就是上面的suspend-resume模块)
disable_irq_wake()用来关闭该外设中断线通往系统电源管理模块路径上的各种HW block。
调用enable_irq_wake会影响系统suspend过程中的suspend_device_irqs处理:
static bool suspend_device_irq(struct irq_desc *desc){
……
if (irqd_is_wakeup_set(&desc->irq_data)) {
irqd_set(&desc->irq_data, IRQD_WAKEUP_ARMED);
return true;
}
省略Disable 中断的代码
}
一旦调用enable_irq_wake设定了该设备的中断作为系统suspend的唤醒源,
那么该外设的中断不会被disable,只是被标记一个IRQD_WAKEUP_ARMED的标记。
对于那些不是wakeup source的中断,
在suspend_device_irq 函数中会标记IRQS_SUSPENDED并disable该设备的irq。
在系统唤醒过程中(resume_device_irqs),被diable的中断会重新enable。
当然,如果在suspend的过程中发生了某些事件(例如wakeup source产生了有效信号),
从而导致本次suspend abort,那么这个abort事件会通知到PM core模块。
事件并不会被立刻通知到PM core模块,
一般而言,suspend thread会在某些点上去检查pending的wakeup event。
在系统suspend的过程中,每一个来自wakeup source的中断
都会终止suspend过程或者将系统唤醒(如果系统已经进入suspend状态)。
但是,在执行了suspend_device_irqs之后,普通的中断被屏蔽了,
这时,即便HW触发了中断信号也无法执行其interrupt handler。
作为wakeup source的IRQ会怎样呢?
虽然它的中断没有被mask掉,但是其interrupt handler也不会执行
(这时HW Signal只用来唤醒系统)。
唯一有机会执行的handler是标记IRQF_NO_SUSPEND flag的IRQ,
因为它们的中断始终是enable的。
这些中断不应该调用enable_irq_wake进行唤醒源的设定。
总结:
IRQD_WAKEUP_ARMED标记,代表是wakeup source的中断
系统suspend的过程中,该类中断句柄也不会执行
唯一执行handler的是IRQF_NO_SUSPEND 标记
所以:IRQF_NO_SUSPEND 标志和enable_irq_wake函数不能同时使用
四、Interrupts and Suspend-to-Idle
Suspend-to-idle (也被称为"freeze" 状态)
是一个相对比较新的系统电源管理状态:
static int suspend_enter(suspend_state_t state, bool *wakeup)
{
……
各个设备的late suspend阶段
各个设备的noirq suspend阶段
if (state == PM_SUSPEND_FREEZE) {
freeze_enter();
goto Platform_wake;
}
……
}
Freeze和suspend的前面的操作基本是一样的:
首先冻结系统中的进程,然后suspend系统中的device,
不一样的地方在noirq suspend完成后,
freeze不会disable那些non-BSP的处理器和syscore suspend阶段,
而是调用freeze_enter函数,把所有的处理器推送到idle状态。
这时,任何的enable的中断都可以将系统唤醒。
意味着那些标记IRQF_NO_SUSPEND的中断有能力将处理器从idle状态唤醒
(其IRQ没有在suspend_device_irqs过程中被mask掉)
注意:
这种信号并不会触发一个系统唤醒信号(因为它不是一个唤醒源)
普通中断由于其IRQ被disable,因此无法唤醒idle状态中的处理器。
那些能够唤醒系统的wakeup interrupt呢?
由于其中断没有被mask掉,因此可以将系统从suspend-to-idle状态中唤醒。
整个过程和将系统从suspend状态中唤醒一样,
唯一不同:
将系统从freeze状态唤醒走的中断处理路径,用的是IRQF_NO_SUSPEND的handler
将系统从suspend状态唤醒走的唤醒处理路径,用的是IRQD_WAKEUP_ARMED,
需要电源管理HW BLOCK中特别的中断处理逻辑的参与。
五、IRQF_NO_SUSPEND 标志和enable_irq_wake函数不能同时使用
针对一个设备,在申请中断的时候使用IRQF_NO_SUSPEND flag,
又同时调用enable_irq_wake设定唤醒源是不合理的
原因如下:
如果IRQ没有共享,使用IRQF_NO_SUSPEND flag说明想要在整个系统suspend-resume过程中
(包括suspend_device_irqs之后的阶段)保持中断打开以便正常调用其interrupt handler。
调用enable_irq_wake函数则说明想要将该设备的irq信号设为中断源,不期望调用它的handler。
这两个需求是互斥的。
IRQF_NO_SUSPEND 标志和enable_irq_wake函数都不是针对一个interrupt handler,
而是针对该IRQ上注册的所有的handler。
在一个IRQ上共享唤醒源以及no suspend中断源是比较荒谬的。
不过,在非常特殊的场合下,
一个IRQ可以被设定为wakeup source,同时也设定IRQF_NO_SUSPEND 标志。
为了代码逻辑正确,该设备的驱动代码需要满足一些特别的需求。