From: Rafael J. Wysocki <rafael.j.wyso...@intel.com> The "freeze" sleep state, also known as suspend-to-idle, is entered without taking nonboot CPUs offline, right after devices have been suspended. It works by waiting for at least one wakeup source object to become "active" as a result of handling a hardware interrupt.
Of course, interrupts supposed to be able to wake up the system from suspend-to-idle cannot be disabled by suspend_device_irqs() and their interrupt handlers must be able to cope with interrupts coming after all devices have been suspended. In that case, they only need to call __pm_wakeup_event() for a single wakeup source object without trying to access hardware (that will be resumed later as part of the subsequent system resume). Make PCIe PME interrupts work this way. Register an additional wakeup source object for each PCIe PME service device. That object will be used to generate wakeups from suspend-to-idle. During system suspend, while suspending the PME service for given port, walk the bus below that port and see if any devices on that bus are configured for wakeup. If so, do not disable the PME interrupt, but call enable_device_irq_wake() for it, set a "suspend level" variable for the service to "wakeup" and make the interrupt handler behave in a special way, which is to call __pm_wakeup_event() with the service's wakeup source object as the first argument and switch the "suspend level" over to "noirq" when the interrupt is triggered. The "suspend level" is cleared and disable_device_irq_wake() is called for the PME interrupt while resuming the PME service. This change allows Wake-on-LAN to be used for wakeup from suspend-to-idle on my MSI Wind tesbed netbook. Signed-off-by: Rafael J. Wysocki <rafael.j.wyso...@intel.com> --- drivers/pci/pcie/pme.c | 74 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 11 deletions(-) Index: linux-pm/drivers/pci/pcie/pme.c =================================================================== --- linux-pm.orig/drivers/pci/pcie/pme.c +++ linux-pm/drivers/pci/pcie/pme.c @@ -20,6 +20,7 @@ #include <linux/device.h> #include <linux/pcieport_if.h> #include <linux/pm_runtime.h> +#include <linux/pm_wakeup.h> #include "../pci.h" #include "portdrv.h" @@ -41,11 +42,18 @@ static int __init pcie_pme_setup(char *s } __setup("pcie_pme=", pcie_pme_setup); +enum pme_suspend_level { + PME_SUSPEND_NONE = 0, + PME_SUSPEND_WAKEUP, + PME_SUSPEND_NOIRQ, +}; + struct pcie_pme_service_data { spinlock_t lock; struct pcie_device *srv; struct work_struct work; - bool noirq; /* Don't enable the PME interrupt used by this service. */ + struct wakeup_source *ws; + enum pme_suspend_level suspend_level; }; /** @@ -223,7 +231,7 @@ static void pcie_pme_work_fn(struct work spin_lock_irq(&data->lock); for (;;) { - if (data->noirq) + if (data->suspend_level == PME_SUSPEND_NOIRQ) break; pcie_capability_read_dword(port, PCI_EXP_RTSTA, &rtsta); @@ -234,6 +242,12 @@ static void pcie_pme_work_fn(struct work */ pcie_clear_root_pme_status(port); + if (data->suspend_level == PME_SUSPEND_WAKEUP) { + __pm_wakeup_event(data->ws, 0); + data->suspend_level = PME_SUSPEND_NOIRQ; + break; + } + spin_unlock_irq(&data->lock); pcie_pme_handle_request(port, rtsta & 0xffff); spin_lock_irq(&data->lock); @@ -250,7 +264,7 @@ static void pcie_pme_work_fn(struct work spin_lock_irq(&data->lock); } - if (!data->noirq) + if (data->suspend_level != PME_SUSPEND_NOIRQ) pcie_pme_interrupt_enable(port, true); spin_unlock_irq(&data->lock); @@ -360,6 +374,7 @@ static int pcie_pme_probe(struct pcie_de if (ret) { kfree(data); } else { + data->ws = wakeup_source_register(dev_name(&srv->device)); pcie_pme_mark_devices(port); pcie_pme_interrupt_enable(port, true); } @@ -367,6 +382,21 @@ static int pcie_pme_probe(struct pcie_de return ret; } +static bool pcie_pme_check_wakeup(struct pci_bus *bus) +{ + struct pci_dev *dev; + + if (!bus) + return false; + + list_for_each_entry(dev, &bus->devices, bus_list) + if (device_may_wakeup(&dev->dev) + || pcie_pme_check_wakeup(dev->subordinate)) + return true; + + return false; +} + /** * pcie_pme_suspend - Suspend PCIe PME service device. * @srv: PCIe service device to suspend. @@ -375,11 +405,26 @@ static int pcie_pme_suspend(struct pcie_ { struct pcie_pme_service_data *data = get_service_data(srv); struct pci_dev *port = srv->port; + bool wakeup; + if (device_may_wakeup(&port->dev)) { + wakeup = true; + } else { + down_read(&pci_bus_sem); + wakeup = pcie_pme_check_wakeup(port->subordinate); + up_read(&pci_bus_sem); + } spin_lock_irq(&data->lock); - pcie_pme_interrupt_enable(port, false); - pcie_clear_root_pme_status(port); - data->noirq = true; + if (wakeup) { + enable_device_irq_wake(srv->irq, srv); + data->suspend_level = PME_SUSPEND_WAKEUP; + } else { + struct pci_dev *port = srv->port; + + pcie_pme_interrupt_enable(port, false); + pcie_clear_root_pme_status(port); + data->suspend_level = PME_SUSPEND_NOIRQ; + } spin_unlock_irq(&data->lock); synchronize_irq(srv->irq); @@ -394,12 +439,16 @@ static int pcie_pme_suspend(struct pcie_ static int pcie_pme_resume(struct pcie_device *srv) { struct pcie_pme_service_data *data = get_service_data(srv); - struct pci_dev *port = srv->port; spin_lock_irq(&data->lock); - data->noirq = false; - pcie_clear_root_pme_status(port); - pcie_pme_interrupt_enable(port, true); + if (data->suspend_level == PME_SUSPEND_NOIRQ) { + struct pci_dev *port = srv->port; + + pcie_clear_root_pme_status(port); + pcie_pme_interrupt_enable(port, true); + } + disable_device_irq_wake(srv->irq, srv); + data->suspend_level = PME_SUSPEND_NONE; spin_unlock_irq(&data->lock); return 0; @@ -411,9 +460,12 @@ static int pcie_pme_resume(struct pcie_d */ static void pcie_pme_remove(struct pcie_device *srv) { + struct pcie_pme_service_data *data = get_service_data(srv); + pcie_pme_suspend(srv); free_irq(srv->irq, srv); - kfree(get_service_data(srv)); + wakeup_source_unregister(data->ws); + kfree(data); } static struct pcie_port_service_driver pcie_pme_driver = { -- To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to majord...@vger.kernel.org More majordomo info at http://vger.kernel.org/majordomo-info.html Please read the FAQ at http://www.tux.org/lkml/