From 53823639173cc9e9a261f68f4abefe62364b86c6 Mon Sep 17 00:00:00 2001 From: "Rafael J. Wysocki" Date: Sat, 23 Jan 2010 22:02:51 +0100 Subject: PM / Runtime: Add sysfs switch for disabling device run-time PM Add new device sysfs attribute, power/control, allowing the user space to block the run-time power management of the devices. If this attribute is set to "on", the driver of the device won't be able to power manage it at run time (without breaking the rules) and the device will always be in the full power state (except when the entire system goes into a sleep state). Signed-off-by: Rafael J. Wysocki Acked-by: Alan Stern --- include/linux/pm.h | 1 + include/linux/pm_runtime.h | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'include') diff --git a/include/linux/pm.h b/include/linux/pm.h index 198b8f9fe05e..25b1eca8049d 100644 --- a/include/linux/pm.h +++ b/include/linux/pm.h @@ -430,6 +430,7 @@ struct dev_pm_info { unsigned int request_pending:1; unsigned int deferred_resume:1; unsigned int run_wake:1; + unsigned int runtime_auto:1; enum rpm_request request; enum rpm_status runtime_status; int runtime_error; diff --git a/include/linux/pm_runtime.h b/include/linux/pm_runtime.h index 370ce0a6fe4a..7d773aac5314 100644 --- a/include/linux/pm_runtime.h +++ b/include/linux/pm_runtime.h @@ -28,6 +28,8 @@ extern int __pm_runtime_set_status(struct device *dev, unsigned int status); extern int pm_runtime_barrier(struct device *dev); extern void pm_runtime_enable(struct device *dev); extern void __pm_runtime_disable(struct device *dev, bool check_resume); +extern void pm_runtime_allow(struct device *dev); +extern void pm_runtime_forbid(struct device *dev); static inline bool pm_children_suspended(struct device *dev) { @@ -78,6 +80,8 @@ static inline int __pm_runtime_set_status(struct device *dev, static inline int pm_runtime_barrier(struct device *dev) { return 0; } static inline void pm_runtime_enable(struct device *dev) {} static inline void __pm_runtime_disable(struct device *dev, bool c) {} +static inline void pm_runtime_allow(struct device *dev) {} +static inline void pm_runtime_forbid(struct device *dev) {} static inline bool pm_children_suspended(struct device *dev) { return false; } static inline void pm_suspend_ignore_children(struct device *dev, bool en) {} -- cgit v1.2.3 From 5af84b82701a96be4b033aaa51d86c72e2ded061 Mon Sep 17 00:00:00 2001 From: "Rafael J. Wysocki" Date: Sat, 23 Jan 2010 22:23:32 +0100 Subject: PM: Asynchronous suspend and resume of devices Theoretically, the total time of system sleep transitions (suspend to RAM, hibernation) can be reduced by running suspend and resume callbacks of device drivers in parallel with each other. However, there are dependencies between devices such that we're not allowed to suspend the parent of a device before suspending the device itself. Analogously, we're not allowed to resume a device before resuming its parent. The most straightforward way to take these dependencies into accout is to start the async threads used for suspending and resuming devices at the core level, so that async_schedule() is called for each suspend and resume callback supposed to be executed asynchronously. For this purpose, introduce a new device flag, power.async_suspend, used to mark the devices whose suspend and resume callbacks are to be executed asynchronously (ie. in parallel with the main suspend/resume thread and possibly in parallel with each other) and helper function device_enable_async_suspend() allowing one to set power.async_suspend for given device (power.async_suspend is unset by default for all devices). For each device with the power.async_suspend flag set the PM core will use async_schedule() to execute its suspend and resume callbacks. The async threads started for different devices as a result of calling async_schedule() are synchronized with each other and with the main suspend/resume thread with the help of completions, in the following way: (1) There is a completion, power.completion, for each device object. (2) Each device's completion is reset before calling async_schedule() for the device or, in the case of devices with the power.async_suspend flags unset, before executing the device's suspend and resume callbacks. (3) During suspend, right before running the bus type, device type and device class suspend callbacks for the device, the PM core waits for the completions of all the device's children to be completed. (4) During resume, right before running the bus type, device type and device class resume callbacks for the device, the PM core waits for the completion of the device's parent to be completed. (5) The PM core completes power.completion for each device right after the bus type, device type and device class suspend (or resume) callbacks executed for the device have returned. Signed-off-by: Rafael J. Wysocki --- drivers/base/power/main.c | 115 ++++++++++++++++++++++++++++++++++++++++--- include/linux/device.h | 6 +++ include/linux/pm.h | 3 ++ include/linux/resume-trace.h | 7 +++ 4 files changed, 125 insertions(+), 6 deletions(-) (limited to 'include') diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c index 6ca5cdf63849..3b44c201ddad 100644 --- a/drivers/base/power/main.c +++ b/drivers/base/power/main.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "../base.h" #include "power.h" @@ -42,6 +43,7 @@ LIST_HEAD(dpm_list); static DEFINE_MUTEX(dpm_list_mtx); +static pm_message_t pm_transition; /* * Set once the preparation of devices for a PM transition has started, reset @@ -56,6 +58,7 @@ static bool transition_started; void device_pm_init(struct device *dev) { dev->power.status = DPM_ON; + init_completion(&dev->power.completion); pm_runtime_init(dev); } @@ -111,6 +114,7 @@ void device_pm_remove(struct device *dev) pr_debug("PM: Removing info for %s:%s\n", dev->bus ? dev->bus->name : "No Bus", kobject_name(&dev->kobj)); + complete_all(&dev->power.completion); mutex_lock(&dpm_list_mtx); list_del_init(&dev->power.entry); mutex_unlock(&dpm_list_mtx); @@ -187,6 +191,31 @@ static void initcall_debug_report(struct device *dev, ktime_t calltime, } } +/** + * dpm_wait - Wait for a PM operation to complete. + * @dev: Device to wait for. + * @async: If unset, wait only if the device's power.async_suspend flag is set. + */ +static void dpm_wait(struct device *dev, bool async) +{ + if (!dev) + return; + + if (async || dev->power.async_suspend) + wait_for_completion(&dev->power.completion); +} + +static int dpm_wait_fn(struct device *dev, void *async_ptr) +{ + dpm_wait(dev, *((bool *)async_ptr)); + return 0; +} + +static void dpm_wait_for_children(struct device *dev, bool async) +{ + device_for_each_child(dev, &async, dpm_wait_fn); +} + /** * pm_op - Execute the PM operation appropriate for given PM event. * @dev: Device to handle. @@ -466,17 +495,19 @@ static int legacy_resume(struct device *dev, int (*cb)(struct device *dev)) } /** - * device_resume - Execute "resume" callbacks for given device. + * __device_resume - Execute "resume" callbacks for given device. * @dev: Device to handle. * @state: PM transition of the system being carried out. + * @async: If true, the device is being resumed asynchronously. */ -static int device_resume(struct device *dev, pm_message_t state) +static int __device_resume(struct device *dev, pm_message_t state, bool async) { int error = 0; TRACE_DEVICE(dev); TRACE_RESUME(0); + dpm_wait(dev->parent, async); down(&dev->sem); if (dev->bus) { @@ -511,11 +542,36 @@ static int device_resume(struct device *dev, pm_message_t state) } End: up(&dev->sem); + complete_all(&dev->power.completion); TRACE_RESUME(error); return error; } +static void async_resume(void *data, async_cookie_t cookie) +{ + struct device *dev = (struct device *)data; + int error; + + error = __device_resume(dev, pm_transition, true); + if (error) + pm_dev_err(dev, pm_transition, " async", error); + put_device(dev); +} + +static int device_resume(struct device *dev) +{ + INIT_COMPLETION(dev->power.completion); + + if (dev->power.async_suspend && !pm_trace_is_enabled()) { + get_device(dev); + async_schedule(async_resume, dev); + return 0; + } + + return __device_resume(dev, pm_transition, false); +} + /** * dpm_resume - Execute "resume" callbacks for non-sysdev devices. * @state: PM transition of the system being carried out. @@ -530,6 +586,7 @@ static void dpm_resume(pm_message_t state) INIT_LIST_HEAD(&list); mutex_lock(&dpm_list_mtx); + pm_transition = state; while (!list_empty(&dpm_list)) { struct device *dev = to_device(dpm_list.next); @@ -540,7 +597,7 @@ static void dpm_resume(pm_message_t state) dev->power.status = DPM_RESUMING; mutex_unlock(&dpm_list_mtx); - error = device_resume(dev, state); + error = device_resume(dev); mutex_lock(&dpm_list_mtx); if (error) @@ -555,6 +612,7 @@ static void dpm_resume(pm_message_t state) } list_splice(&list, &dpm_list); mutex_unlock(&dpm_list_mtx); + async_synchronize_full(); dpm_show_time(starttime, state, NULL); } @@ -732,17 +790,24 @@ static int legacy_suspend(struct device *dev, pm_message_t state, return error; } +static int async_error; + /** * device_suspend - Execute "suspend" callbacks for given device. * @dev: Device to handle. * @state: PM transition of the system being carried out. + * @async: If true, the device is being suspended asynchronously. */ -static int device_suspend(struct device *dev, pm_message_t state) +static int __device_suspend(struct device *dev, pm_message_t state, bool async) { int error = 0; + dpm_wait_for_children(dev, async); down(&dev->sem); + if (async_error) + goto End; + if (dev->class) { if (dev->class->pm) { pm_dev_dbg(dev, state, "class "); @@ -773,12 +838,44 @@ static int device_suspend(struct device *dev, pm_message_t state) error = legacy_suspend(dev, state, dev->bus->suspend); } } + + if (!error) + dev->power.status = DPM_OFF; + End: up(&dev->sem); + complete_all(&dev->power.completion); return error; } +static void async_suspend(void *data, async_cookie_t cookie) +{ + struct device *dev = (struct device *)data; + int error; + + error = __device_suspend(dev, pm_transition, true); + if (error) { + pm_dev_err(dev, pm_transition, " async", error); + async_error = error; + } + + put_device(dev); +} + +static int device_suspend(struct device *dev) +{ + INIT_COMPLETION(dev->power.completion); + + if (dev->power.async_suspend) { + get_device(dev); + async_schedule(async_suspend, dev); + return 0; + } + + return __device_suspend(dev, pm_transition, false); +} + /** * dpm_suspend - Execute "suspend" callbacks for all non-sysdev devices. * @state: PM transition of the system being carried out. @@ -791,13 +888,15 @@ static int dpm_suspend(pm_message_t state) INIT_LIST_HEAD(&list); mutex_lock(&dpm_list_mtx); + pm_transition = state; + async_error = 0; while (!list_empty(&dpm_list)) { struct device *dev = to_device(dpm_list.prev); get_device(dev); mutex_unlock(&dpm_list_mtx); - error = device_suspend(dev, state); + error = device_suspend(dev); mutex_lock(&dpm_list_mtx); if (error) { @@ -805,13 +904,17 @@ static int dpm_suspend(pm_message_t state) put_device(dev); break; } - dev->power.status = DPM_OFF; if (!list_empty(&dev->power.entry)) list_move(&dev->power.entry, &list); put_device(dev); + if (async_error) + break; } list_splice(&list, dpm_list.prev); mutex_unlock(&dpm_list_mtx); + async_synchronize_full(); + if (!error) + error = async_error; if (!error) dpm_show_time(starttime, state, NULL); return error; diff --git a/include/linux/device.h b/include/linux/device.h index a62799f2ab00..70adc5f3f50a 100644 --- a/include/linux/device.h +++ b/include/linux/device.h @@ -472,6 +472,12 @@ static inline int device_is_registered(struct device *dev) return dev->kobj.state_in_sysfs; } +static inline void device_enable_async_suspend(struct device *dev) +{ + if (dev->power.status == DPM_ON) + dev->power.async_suspend = true; +} + void driver_init(void); /* diff --git a/include/linux/pm.h b/include/linux/pm.h index 25b1eca8049d..9c16cd20fc96 100644 --- a/include/linux/pm.h +++ b/include/linux/pm.h @@ -26,6 +26,7 @@ #include #include #include +#include /* * Callbacks for platform drivers to implement. @@ -412,9 +413,11 @@ struct dev_pm_info { pm_message_t power_state; unsigned int can_wakeup:1; unsigned int should_wakeup:1; + unsigned async_suspend:1; enum dpm_state status; /* Owned by the PM core */ #ifdef CONFIG_PM_SLEEP struct list_head entry; + struct completion completion; #endif #ifdef CONFIG_PM_RUNTIME struct timer_list suspend_timer; diff --git a/include/linux/resume-trace.h b/include/linux/resume-trace.h index c9ba2fdf807d..bc8c3881c729 100644 --- a/include/linux/resume-trace.h +++ b/include/linux/resume-trace.h @@ -6,6 +6,11 @@ extern int pm_trace_enabled; +static inline int pm_trace_is_enabled(void) +{ + return pm_trace_enabled; +} + struct device; extern void set_trace_device(struct device *); extern void generate_resume_trace(const void *tracedata, unsigned int user); @@ -17,6 +22,8 @@ extern void generate_resume_trace(const void *tracedata, unsigned int user); #else +static inline int pm_trace_is_enabled(void) { return 0; } + #define TRACE_DEVICE(dev) do { } while (0) #define TRACE_RESUME(dev) do { } while (0) -- cgit v1.2.3 From 5a2eb8585f3b38e01e30aacaa8b985a1520a993d Mon Sep 17 00:00:00 2001 From: "Rafael J. Wysocki" Date: Sat, 23 Jan 2010 22:25:23 +0100 Subject: PM: Add facility for advanced testing of async suspend/resume Add configuration switch CONFIG_PM_ADVANCED_DEBUG for compiling in extra PM debugging/testing code allowing one to access some PM-related attributes of devices from the user space via sysfs. If CONFIG_PM_ADVANCED_DEBUG is set, add sysfs attribute power/async for every device allowing the user space to access the device's power.async_suspend flag and modify it, if desired. Signed-off-by: Rafael J. Wysocki --- Documentation/ABI/testing/sysfs-devices-power | 26 ++++++++++++++ drivers/base/power/sysfs.c | 49 +++++++++++++++++++++++++++ include/linux/device.h | 11 ++++++ kernel/power/Kconfig | 14 ++++++++ 4 files changed, 100 insertions(+) (limited to 'include') diff --git a/Documentation/ABI/testing/sysfs-devices-power b/Documentation/ABI/testing/sysfs-devices-power index 431bfd7e65c7..6123c523bfd7 100644 --- a/Documentation/ABI/testing/sysfs-devices-power +++ b/Documentation/ABI/testing/sysfs-devices-power @@ -51,3 +51,29 @@ Description: drivers. Changing this attribute to "on" prevents the driver from power managing the device at run time. Doing that while the device is suspended causes it to be woken up. + +What: /sys/devices/.../power/async +Date: January 2009 +Contact: Rafael J. Wysocki +Description: + The /sys/devices/.../async attribute allows the user space to + enable or diasble the device's suspend and resume callbacks to + be executed asynchronously (ie. in separate threads, in parallel + with the main suspend/resume thread) during system-wide power + transitions (eg. suspend to RAM, hibernation). + + All devices have one of the following two values for the + power/async file: + + + "enabled\n" to permit the asynchronous suspend/resume; + + "disabled\n" to forbid it; + + The value of this attribute may be changed by writing either + "enabled", or "disabled" to it. + + It generally is unsafe to permit the asynchronous suspend/resume + of a device unless it is certain that all of the PM dependencies + of the device are known to the PM core. However, for some + devices this attribute is set to "enabled" by bus type code or + device drivers and in that cases it should be safe to leave the + default value. diff --git a/drivers/base/power/sysfs.c b/drivers/base/power/sysfs.c index c011ff15632c..86fd9373447e 100644 --- a/drivers/base/power/sysfs.c +++ b/drivers/base/power/sysfs.c @@ -54,6 +54,24 @@ * wakeup events internally (unless they are disabled), keeping * their hardware in low power modes whenever they're unused. This * saves runtime power, without requiring system-wide sleep states. + * + * async - Report/change current async suspend setting for the device + * + * Asynchronous suspend and resume of the device during system-wide power + * state transitions can be enabled by writing "enabled" to this file. + * Analogously, if "disabled" is written to this file, the device will be + * suspended and resumed synchronously. + * + * All devices have one of the following two values for power/async: + * + * + "enabled\n" to permit the asynchronous suspend/resume of the device; + * + "disabled\n" to forbid it; + * + * NOTE: It generally is unsafe to permit the asynchronous suspend/resume + * of a device unless it is certain that all of the PM dependencies of the + * device are known to the PM core. However, for some devices this + * attribute is set to "enabled" by bus type code or device drivers and in + * that cases it should be safe to leave the default value. */ static const char enabled[] = "enabled"; @@ -125,12 +143,43 @@ wake_store(struct device * dev, struct device_attribute *attr, static DEVICE_ATTR(wakeup, 0644, wake_show, wake_store); +#ifdef CONFIG_PM_SLEEP_ADVANCED_DEBUG +static ssize_t async_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%s\n", + device_async_suspend_enabled(dev) ? enabled : disabled); +} + +static ssize_t async_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t n) +{ + char *cp; + int len = n; + + cp = memchr(buf, '\n', n); + if (cp) + len = cp - buf; + if (len == sizeof enabled - 1 && strncmp(buf, enabled, len) == 0) + device_enable_async_suspend(dev); + else if (len == sizeof disabled - 1 && strncmp(buf, disabled, len) == 0) + device_disable_async_suspend(dev); + else + return -EINVAL; + return n; +} + +static DEVICE_ATTR(async, 0644, async_show, async_store); +#endif /* CONFIG_PM_SLEEP_ADVANCED_DEBUG */ static struct attribute * power_attrs[] = { #ifdef CONFIG_PM_RUNTIME &dev_attr_control.attr, #endif &dev_attr_wakeup.attr, +#ifdef CONFIG_PM_SLEEP_ADVANCED_DEBUG + &dev_attr_async.attr, +#endif NULL, }; static struct attribute_group pm_attr_group = { diff --git a/include/linux/device.h b/include/linux/device.h index 70adc5f3f50a..b30527db3ac0 100644 --- a/include/linux/device.h +++ b/include/linux/device.h @@ -478,6 +478,17 @@ static inline void device_enable_async_suspend(struct device *dev) dev->power.async_suspend = true; } +static inline void device_disable_async_suspend(struct device *dev) +{ + if (dev->power.status == DPM_ON) + dev->power.async_suspend = false; +} + +static inline bool device_async_suspend_enabled(struct device *dev) +{ + return !!dev->power.async_suspend; +} + void driver_init(void); /* diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig index 4c9cffcf69c7..5c36ea9d55d2 100644 --- a/kernel/power/Kconfig +++ b/kernel/power/Kconfig @@ -27,6 +27,15 @@ config PM_DEBUG code. This is helpful when debugging and reporting PM bugs, like suspend support. +config PM_ADVANCED_DEBUG + bool "Extra PM attributes in sysfs for low-level debugging/testing" + depends on PM_DEBUG + default n + ---help--- + Add extra sysfs attributes allowing one to access some Power Management + fields of device objects from user space. If you are not a kernel + developer interested in debugging/testing Power Management, say "no". + config PM_VERBOSE bool "Verbose Power Management debugging" depends on PM_DEBUG @@ -85,6 +94,11 @@ config PM_SLEEP depends on SUSPEND || HIBERNATION || XEN_SAVE_RESTORE default y +config PM_SLEEP_ADVANCED_DEBUG + bool + depends on PM_ADVANCED_DEBUG + default n + config SUSPEND bool "Suspend to RAM and standby" depends on PM && ARCH_SUSPEND_POSSIBLE -- cgit v1.2.3 From f8824cee405c62ba465b85365201166d9cf86a14 Mon Sep 17 00:00:00 2001 From: "Rafael J. Wysocki" Date: Wed, 27 Jan 2010 23:47:38 +0100 Subject: PM: Allow device drivers to use dpm_wait() There are some dependencies between devices (in particular, between EHCI USB controllers and their OHCI/UHCI siblings) which are not reflected by the structure of the device tree. With synchronous suspend and resume these dependencies are taken into accout automatically, because the devices in question are always registered in the right order, but to meet these constraints with asynchronous suspend and resume the drivers of these devices will need to use dpm_wait() in their suspend/resume routines, so introduce a helper function allowing them to do that. Signed-off-by: Rafael J. Wysocki --- drivers/base/power/main.c | 11 +++++++++++ include/linux/pm.h | 2 ++ 2 files changed, 13 insertions(+) (limited to 'include') diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c index 6efef9fb23a1..0e26a6f6fd48 100644 --- a/drivers/base/power/main.c +++ b/drivers/base/power/main.c @@ -1046,3 +1046,14 @@ void __suspend_report_result(const char *function, void *fn, int ret) printk(KERN_ERR "%s(): %pF returns %d\n", function, fn, ret); } EXPORT_SYMBOL_GPL(__suspend_report_result); + +/** + * device_pm_wait_for_dev - Wait for suspend/resume of a device to complete. + * @dev: Device to wait for. + * @subordinate: Device that needs to wait for @dev. + */ +void device_pm_wait_for_dev(struct device *subordinate, struct device *dev) +{ + dpm_wait(dev, subordinate->power.async_suspend); +} +EXPORT_SYMBOL_GPL(device_pm_wait_for_dev); diff --git a/include/linux/pm.h b/include/linux/pm.h index 9c16cd20fc96..e80df06ad22a 100644 --- a/include/linux/pm.h +++ b/include/linux/pm.h @@ -512,6 +512,7 @@ extern void __suspend_report_result(const char *function, void *fn, int ret); __suspend_report_result(__func__, fn, ret); \ } while (0) +extern void device_pm_wait_for_dev(struct device *sub, struct device *dev); #else /* !CONFIG_PM_SLEEP */ #define device_pm_lock() do {} while (0) @@ -524,6 +525,7 @@ static inline int dpm_suspend_start(pm_message_t state) #define suspend_report_result(fn, ret) do {} while (0) +static inline void device_pm_wait_for_dev(struct device *a, struct device *b) {} #endif /* !CONFIG_PM_SLEEP */ /* How to reorder dpm_list after device_move() */ -- cgit v1.2.3 From 6d19c009cc780c63de25a046509ebc9473809fd6 Mon Sep 17 00:00:00 2001 From: Alan Stern Date: Fri, 12 Feb 2010 12:21:11 +0100 Subject: USB: implement non-tree resume ordering constraints for PCI host controllers This patch (as1331) adds non-tree ordering constraints needed for proper resume of PCI USB host controllers from hibernation. The main issue is that non-high-speed devices must not be resumed before the high-speed root hub, because it is the ehci_bus_resume() routine which takes care of handing the device connection over to the companion controller. If the device resume is attempted before the handover then the device won't be found and it will be treated as though it had disconnected. The patch adds a new field to the usb_bus structure; for each full/low-speed bus this field will contain a pointer to the companion high-speed bus (if one exists). It is used during normal device resume; if the hs_companion pointer isn't NULL then we wait for the root-hub device on the hs_companion bus. A secondary issue is that an EHCI controlller shouldn't be resumed before any of its companions. On some machines I have observed handovers failing if the companion controller is reinitialized after the handover. Thus, the EHCI resume routine must wait for the companion controllers to be resumed. The patch also fixes a small bug in usb_hcd_pci_probe(); an error path jumps to the wrong label, causing a memory leak. [rjw: Fixed compilation for CONFIG_PM_SLEEP unset.] Signed-off-by: Alan Stern Acked-by: Greg Kroah-Hartman Signed-off-by: Rafael J. Wysocki --- drivers/usb/core/driver.c | 8 +++ drivers/usb/core/hcd-pci.c | 127 ++++++++++++++++++++++++++++++++++++++++++++- include/linux/usb.h | 1 + 3 files changed, 135 insertions(+), 1 deletion(-) (limited to 'include') diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c index 60a45f1e3a67..f2f055eb6831 100644 --- a/drivers/usb/core/driver.c +++ b/drivers/usb/core/driver.c @@ -1022,6 +1022,14 @@ static int usb_resume_device(struct usb_device *udev, pm_message_t msg) goto done; } + /* Non-root devices on a full/low-speed bus must wait for their + * companion high-speed root hub, in case a handoff is needed. + */ + if (!(msg.event & PM_EVENT_AUTO) && udev->parent && + udev->bus->hs_companion) + device_pm_wait_for_dev(&udev->dev, + &udev->bus->hs_companion->root_hub->dev); + if (udev->quirks & USB_QUIRK_RESET_RESUME) udev->reset_resume = 1; diff --git a/drivers/usb/core/hcd-pci.c b/drivers/usb/core/hcd-pci.c index 2dcf906df569..15286533c15a 100644 --- a/drivers/usb/core/hcd-pci.c +++ b/drivers/usb/core/hcd-pci.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,122 @@ /* PCI-based HCs are common, but plenty of non-PCI HCs are used too */ +#ifdef CONFIG_PM_SLEEP + +/* Coordinate handoffs between EHCI and companion controllers + * during system resume + */ + +static DEFINE_MUTEX(companions_mutex); + +#define CL_UHCI PCI_CLASS_SERIAL_USB_UHCI +#define CL_OHCI PCI_CLASS_SERIAL_USB_OHCI +#define CL_EHCI PCI_CLASS_SERIAL_USB_EHCI + +enum companion_action { + SET_HS_COMPANION, CLEAR_HS_COMPANION, WAIT_FOR_COMPANIONS +}; + +static void companion_common(struct pci_dev *pdev, struct usb_hcd *hcd, + enum companion_action action) +{ + struct pci_dev *companion; + struct usb_hcd *companion_hcd; + unsigned int slot = PCI_SLOT(pdev->devfn); + + /* Iterate through other PCI functions in the same slot. + * If pdev is OHCI or UHCI then we are looking for EHCI, and + * vice versa. + */ + companion = NULL; + for (;;) { + companion = pci_get_device(PCI_ANY_ID, PCI_ANY_ID, companion); + if (!companion) + break; + if (companion->bus != pdev->bus || + PCI_SLOT(companion->devfn) != slot) + continue; + + companion_hcd = pci_get_drvdata(companion); + if (!companion_hcd) + continue; + + /* For SET_HS_COMPANION, store a pointer to the EHCI bus in + * the OHCI/UHCI companion bus structure. + * For CLEAR_HS_COMPANION, clear the pointer to the EHCI bus + * in the OHCI/UHCI companion bus structure. + * For WAIT_FOR_COMPANIONS, wait until the OHCI/UHCI + * companion controllers have fully resumed. + */ + + if ((pdev->class == CL_OHCI || pdev->class == CL_UHCI) && + companion->class == CL_EHCI) { + /* action must be SET_HS_COMPANION */ + dev_dbg(&companion->dev, "HS companion for %s\n", + dev_name(&pdev->dev)); + hcd->self.hs_companion = &companion_hcd->self; + + } else if (pdev->class == CL_EHCI && + (companion->class == CL_OHCI || + companion->class == CL_UHCI)) { + switch (action) { + case SET_HS_COMPANION: + dev_dbg(&pdev->dev, "HS companion for %s\n", + dev_name(&companion->dev)); + companion_hcd->self.hs_companion = &hcd->self; + break; + case CLEAR_HS_COMPANION: + companion_hcd->self.hs_companion = NULL; + break; + case WAIT_FOR_COMPANIONS: + device_pm_wait_for_dev(&pdev->dev, + &companion->dev); + break; + } + } + } +} + +static void set_hs_companion(struct pci_dev *pdev, struct usb_hcd *hcd) +{ + mutex_lock(&companions_mutex); + dev_set_drvdata(&pdev->dev, hcd); + companion_common(pdev, hcd, SET_HS_COMPANION); + mutex_unlock(&companions_mutex); +} + +static void clear_hs_companion(struct pci_dev *pdev, struct usb_hcd *hcd) +{ + mutex_lock(&companions_mutex); + dev_set_drvdata(&pdev->dev, NULL); + + /* If pdev is OHCI or UHCI, just clear its hs_companion pointer */ + if (pdev->class == CL_OHCI || pdev->class == CL_UHCI) + hcd->self.hs_companion = NULL; + + /* Otherwise search for companion buses and clear their pointers */ + else + companion_common(pdev, hcd, CLEAR_HS_COMPANION); + mutex_unlock(&companions_mutex); +} + +static void wait_for_companions(struct pci_dev *pdev, struct usb_hcd *hcd) +{ + /* Only EHCI controllers need to wait. + * No locking is needed because a controller cannot be resumed + * while one of its companions is getting unbound. + */ + if (pdev->class == CL_EHCI) + companion_common(pdev, hcd, WAIT_FOR_COMPANIONS); +} + +#else /* !CONFIG_PM_SLEEP */ + +static inline void set_hs_companion(struct pci_dev *d, struct usb_hcd *h) {} +static inline void clear_hs_companion(struct pci_dev *d, struct usb_hcd *h) {} +static inline void wait_for_companions(struct pci_dev *d, struct usb_hcd *h) {} + +#endif /* !CONFIG_PM_SLEEP */ /*-------------------------------------------------------------------------*/ @@ -123,7 +240,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) if (region == PCI_ROM_RESOURCE) { dev_dbg(&dev->dev, "no i/o regions available\n"); retval = -EBUSY; - goto err1; + goto err2; } } @@ -132,6 +249,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) retval = usb_add_hcd(hcd, dev->irq, IRQF_DISABLED | IRQF_SHARED); if (retval != 0) goto err4; + set_hs_companion(dev, hcd); return retval; err4: @@ -142,6 +260,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) } else release_region(hcd->rsrc_start, hcd->rsrc_len); err2: + clear_hs_companion(dev, hcd); usb_put_hcd(hcd); err1: pci_disable_device(dev); @@ -180,6 +299,7 @@ void usb_hcd_pci_remove(struct pci_dev *dev) } else { release_region(hcd->rsrc_start, hcd->rsrc_len); } + clear_hs_companion(dev, hcd); usb_put_hcd(hcd); pci_disable_device(dev); } @@ -344,6 +464,11 @@ static int resume_common(struct device *dev, bool hibernated) clear_bit(HCD_FLAG_SAW_IRQ, &hcd->flags); if (hcd->driver->pci_resume) { + /* This call should be made only during system resume, + * not during runtime resume. + */ + wait_for_companions(pci_dev, hcd); + retval = hcd->driver->pci_resume(hcd, hibernated); if (retval) { dev_err(dev, "PCI post-resume error %d!\n", retval); diff --git a/include/linux/usb.h b/include/linux/usb.h index d7ace1b80f09..332eaea61021 100644 --- a/include/linux/usb.h +++ b/include/linux/usb.h @@ -339,6 +339,7 @@ struct usb_bus { struct usb_devmap devmap; /* device address allocation map */ struct usb_device *root_hub; /* Root hub */ + struct usb_bus *hs_companion; /* Companion EHCI bus, if any */ struct list_head bus_list; /* list of busses */ int bandwidth_allocated; /* on this bus: how much of the time -- cgit v1.2.3