linux/kernel/irq/pm.c
Rafael J. Wysocki 17f4803420 genirq / PM: Add flag for shared NO_SUSPEND interrupt lines
It currently is required that all users of NO_SUSPEND interrupt
lines pass the IRQF_NO_SUSPEND flag when requesting the IRQ or the
WARN_ON_ONCE() in irq_pm_install_action() will trigger.  That is
done to warn about situations in which unprepared interrupt handlers
may be run unnecessarily for suspended devices and may attempt to
access those devices by mistake.  However, it may cause drivers
that have no technical reasons for using IRQF_NO_SUSPEND to set
that flag just because they happen to share the interrupt line
with something like a timer.

Moreover, the generic handling of wakeup interrupts introduced by
commit 9ce7a25849 (genirq: Simplify wakeup mechanism) only works
for IRQs without any NO_SUSPEND users, so the drivers of wakeup
devices needing to use shared NO_SUSPEND interrupt lines for
signaling system wakeup generally have to detect wakeup in their
interrupt handlers.  Thus if they happen to share an interrupt line
with a NO_SUSPEND user, they also need to request that their
interrupt handlers be run after suspend_device_irqs().

In both cases the reason for using IRQF_NO_SUSPEND is not because
the driver in question has a genuine need to run its interrupt
handler after suspend_device_irqs(), but because it happens to
share the line with some other NO_SUSPEND user.  Otherwise, the
driver would do without IRQF_NO_SUSPEND just fine.

To make it possible to specify that condition explicitly, introduce
a new IRQ action handler flag for shared IRQs, IRQF_COND_SUSPEND,
that, when set, will indicate to the IRQ core that the interrupt
user is generally fine with suspending the IRQ, but it also can
tolerate handler invocations after suspend_device_irqs() and, in
particular, it is capable of detecting system wakeup and triggering
it as appropriate from its interrupt handler.

That will allow us to work around a problem with a shared timer
interrupt line on at91 platforms.

Link: http://marc.info/?l=linux-kernel&m=142252777602084&w=2
Link: http://marc.info/?t=142252775300011&r=1&w=2
Link: https://lkml.org/lkml/2014/12/15/552
Reported-by: Boris Brezillon <boris.brezillon@free-electrons.com>
Signed-off-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com>
Acked-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Acked-by: Mark Rutland <mark.rutland@arm.com>
2015-03-04 21:42:19 +01:00

206 lines
5 KiB
C

/*
* linux/kernel/irq/pm.c
*
* Copyright (C) 2009 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.
*
* This file contains power management functions related to interrupts.
*/
#include <linux/irq.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/suspend.h>
#include <linux/syscore_ops.h>
#include "internals.h"
bool irq_pm_check_wakeup(struct irq_desc *desc)
{
if (irqd_is_wakeup_armed(&desc->irq_data)) {
irqd_clear(&desc->irq_data, IRQD_WAKEUP_ARMED);
desc->istate |= IRQS_SUSPENDED | IRQS_PENDING;
desc->depth++;
irq_disable(desc);
pm_system_wakeup();
return true;
}
return false;
}
/*
* Called from __setup_irq() with desc->lock held after @action has
* been installed in the action chain.
*/
void irq_pm_install_action(struct irq_desc *desc, struct irqaction *action)
{
desc->nr_actions++;
if (action->flags & IRQF_FORCE_RESUME)
desc->force_resume_depth++;
WARN_ON_ONCE(desc->force_resume_depth &&
desc->force_resume_depth != desc->nr_actions);
if (action->flags & IRQF_NO_SUSPEND)
desc->no_suspend_depth++;
else if (action->flags & IRQF_COND_SUSPEND)
desc->cond_suspend_depth++;
WARN_ON_ONCE(desc->no_suspend_depth &&
(desc->no_suspend_depth +
desc->cond_suspend_depth) != desc->nr_actions);
}
/*
* Called from __free_irq() with desc->lock held after @action has
* been removed from the action chain.
*/
void irq_pm_remove_action(struct irq_desc *desc, struct irqaction *action)
{
desc->nr_actions--;
if (action->flags & IRQF_FORCE_RESUME)
desc->force_resume_depth--;
if (action->flags & IRQF_NO_SUSPEND)
desc->no_suspend_depth--;
else if (action->flags & IRQF_COND_SUSPEND)
desc->cond_suspend_depth--;
}
static bool suspend_device_irq(struct irq_desc *desc, int irq)
{
if (!desc->action || desc->no_suspend_depth)
return false;
if (irqd_is_wakeup_set(&desc->irq_data)) {
irqd_set(&desc->irq_data, IRQD_WAKEUP_ARMED);
/*
* We return true here to force the caller to issue
* synchronize_irq(). We need to make sure that the
* IRQD_WAKEUP_ARMED is visible before we return from
* suspend_device_irqs().
*/
return true;
}
desc->istate |= IRQS_SUSPENDED;
__disable_irq(desc, irq);
/*
* Hardware which has no wakeup source configuration facility
* requires that the non wakeup interrupts are masked at the
* chip level. The chip implementation indicates that with
* IRQCHIP_MASK_ON_SUSPEND.
*/
if (irq_desc_get_chip(desc)->flags & IRQCHIP_MASK_ON_SUSPEND)
mask_irq(desc);
return true;
}
/**
* suspend_device_irqs - disable all currently enabled interrupt lines
*
* During system-wide suspend or hibernation device drivers need to be
* prevented from receiving interrupts and this function is provided
* for this purpose.
*
* So we disable all interrupts and mark them IRQS_SUSPENDED except
* for those which are unused, those which are marked as not
* suspendable via an interrupt request with the flag IRQF_NO_SUSPEND
* set and those which are marked as active wakeup sources.
*
* The active wakeup sources are handled by the flow handler entry
* code which checks for the IRQD_WAKEUP_ARMED flag, suspends the
* interrupt and notifies the pm core about the wakeup.
*/
void suspend_device_irqs(void)
{
struct irq_desc *desc;
int irq;
for_each_irq_desc(irq, desc) {
unsigned long flags;
bool sync;
raw_spin_lock_irqsave(&desc->lock, flags);
sync = suspend_device_irq(desc, irq);
raw_spin_unlock_irqrestore(&desc->lock, flags);
if (sync)
synchronize_irq(irq);
}
}
EXPORT_SYMBOL_GPL(suspend_device_irqs);
static void resume_irq(struct irq_desc *desc, int irq)
{
irqd_clear(&desc->irq_data, IRQD_WAKEUP_ARMED);
if (desc->istate & IRQS_SUSPENDED)
goto resume;
/* Force resume the interrupt? */
if (!desc->force_resume_depth)
return;
/* Pretend that it got disabled ! */
desc->depth++;
resume:
desc->istate &= ~IRQS_SUSPENDED;
__enable_irq(desc, irq);
}
static void resume_irqs(bool want_early)
{
struct irq_desc *desc;
int irq;
for_each_irq_desc(irq, desc) {
unsigned long flags;
bool is_early = desc->action &&
desc->action->flags & IRQF_EARLY_RESUME;
if (!is_early && want_early)
continue;
raw_spin_lock_irqsave(&desc->lock, flags);
resume_irq(desc, irq);
raw_spin_unlock_irqrestore(&desc->lock, flags);
}
}
/**
* irq_pm_syscore_ops - enable interrupt lines early
*
* Enable all interrupt lines with %IRQF_EARLY_RESUME set.
*/
static void irq_pm_syscore_resume(void)
{
resume_irqs(true);
}
static struct syscore_ops irq_pm_syscore_ops = {
.resume = irq_pm_syscore_resume,
};
static int __init irq_pm_init_ops(void)
{
register_syscore_ops(&irq_pm_syscore_ops);
return 0;
}
device_initcall(irq_pm_init_ops);
/**
* resume_device_irqs - enable interrupt lines disabled by suspend_device_irqs()
*
* Enable all non-%IRQF_EARLY_RESUME interrupt lines previously
* disabled by suspend_device_irqs() that have the IRQS_SUSPENDED flag
* set as well as those with %IRQF_FORCE_RESUME.
*/
void resume_device_irqs(void)
{
resume_irqs(false);
}
EXPORT_SYMBOL_GPL(resume_device_irqs);