- Suspends hubs, if their children have been suspended first. The device model sysfs support happily breaks the rule that children suspend before parents, and resume after them...
- Exports new usb_{suspend,resume}_device() functions, so that device
drivers can automate "suspend my device until needed" policies.This adds CONFIG_USB_SUSPEND, hooking USB devices into the driver model power management framework. So one way to suspend a USB device (which shrinks the power it draws from its hub) may now optionally be:
echo -n 1 > /sys/bus/usb/devices/$ID/power/state
This works only for real devices, with a .../$ID/bDeviceClass file, not interfaces. For the moment this won't suspend or resume root hubs (needs a new usbcore api hook).
Works with external hubs and ohci-hcd. The host can initiate resume by "echo -n 0 ...". Remote wakeup support is enabled on all devices (like keyboards, mice) that support it.
- Dave
--- 1.3/drivers/usb/core/Kconfig Fri May 30 02:36:57 2003
+++ edited/drivers/usb/core/Kconfig Thu Apr 29 08:48:39 2004
@@ -60,3 +60,14 @@
If you are unsure about this, say N here.
+config USB_SUSPEND
+ bool "USB suspend/resume (EXPERIMENTAL)"
+ depends on USB && EXPERIMENTAL
+ help
+ If you say Y here, you can use the sysfs "power/state" file
+ to suspend or resume individual usb devices. There are many
+ related features, such as remote wakeup or suspending hubs
+ (particularly root hubs), that may not work properly yet.
+
+ If you are unsure about this, say N here.
+
--- 1.94/drivers/usb/core/hub.c Tue Apr 20 20:44:45 2004
+++ edited/drivers/usb/core/hub.c Thu Apr 29 13:14:42 2004
@@ -970,10 +970,302 @@
if (ret)
dev_err(hubdev(hub), "cannot disable port %d (err = %d)\n",
port + 1, ret);
-
return ret;
}
+
+#ifdef CONFIG_USB_SUSPEND
+
+/*
+ * Selective port suspend reduces power; most suspended devices draw
+ * less than 500 uA. It's also used in OTG, along with remote wakeup.
+ * All devices below the suspended port are also suspended.
+ *
+ * There are also various kinds of hub-wide suspend, sometimes with the
+ * ability to wake up the next hub (or the host itself). When root hubs
+ * suspend, "global" (it's a _really_ small world) suspend may be mentioned;
+ * that suggests using additional power saving techniques.
+ *
+ * Devices leave suspend state when the host wakes them up. Some devices
+ * also support "remote wakeup", where the device can activate the USB
+ * tree above them to deliver data, such as a keypress or packet. In
+ * some cases, this involves waking up the USB host.
+ */
+static int
+hub_port_suspend(struct usb_device *hubdev, struct usb_hub *hub, int port)
+{
+ int status;
+ struct usb_device *dev;
+ unsigned i;
+
+ dev = hubdev->children[port - 1];
+ if (!dev)
+ return -ENODEV;
+ if (dev->state == USB_STATE_SUSPENDED)
+ return 0;
+
+ down(&dev->serialize);
+
+ /* it's OK to suspend hubs IFF children are already suspended */
+ for (i = 0; i < dev->maxchild; i++) {
+ if (!dev->children[i])
+ continue;
+ if (dev->children[i]->state == USB_STATE_SUSPENDED)
+ continue;
+
+ /* FIXME only system-wide suspend ensures that the
+ * children are suspended by this point.
+ */
+ dev_dbg(&dev->children[i]->dev, "child not suspended\n");
+ up(&dev->serialize);
+ return -EISDIR;
+ }
+ // dev_dbg(&hub->intf->dev, "suspend port %d\n", port);
+
+ /* enable remote wakeup when appropriate; this lets the device
+ * wake up the upstream hub (including maybe the root hub).
+ */
+ if (dev->actconfig
+ && (dev->actconfig->desc.bmAttributes
+ & USB_CONFIG_ATT_WAKEUP) != 0) {
+ status = usb_control_msg(dev, usb_sndctrlpipe(dev, 0),
+ USB_REQ_SET_FEATURE, USB_RECIP_DEVICE,
+ USB_DEVICE_REMOTE_WAKEUP, 0,
+ NULL, 0,
+ USB_CTRL_SET_TIMEOUT);
+ if (status)
+ dev_dbg(&dev->dev,
+ "won't remote wakeup, status %d\n",
+ status);
+ }
+
+ /* see 7.1.7.6 */
+ status = set_port_feature(hubdev, port, USB_PORT_FEAT_SUSPEND);
+ if (status)
+ dev_dbg(&hub->intf->dev,
+ "can't suspend port %d, status %d\n",
+ port, status);
+ else {
+ /* device has up to 10 msec to fully suspend */
+ dev_dbg(&dev->dev, "usb suspend\n");
+ dev->state = USB_STATE_SUSPENDED;
+ set_current_state(TASK_UNINTERRUPTIBLE);
+ schedule_timeout((HZ * 10)/1000);
+ }
+
+ up(&dev->serialize);
+ return status;
+}
+
+/**
+ * usb_suspend_device - suspend a usb device
+ * @dev: device that's no longer in active use
+ * Context: must be able to sleep; dev->serialize not held
+ *
+ * Suspends a USB device that isn't in active use, conserving power.
+ * Devices can wake out of a suspend, if anything important happens,
+ * using the remote wakeup mechanism.
+ *
+ * Returns 0 on success, else negative errno.
+ */
+int usb_suspend_device (struct usb_device *dev)
+{
+ struct usb_device *hubdev = dev->parent;
+ struct usb_hub *hub;
+ int i, status = -ENODEV;
+
+ /* for now, all nonzero states map to "USB Suspend" */
+
+ /* FIXME support root hubs too */
+ if (!hubdev)
+ return -EOPNOTSUPP;
+
+ down(&hubdev->serialize);
+ hub = usb_get_intfdata (hubdev->actconfig->interface[0]);
+ for (i = 0; i < hubdev->maxchild; i++) {
+ if (hubdev->children[i] == dev) {
+ status = hub_port_suspend(hubdev, hub, i + 1);
+ if (status < 0)
+ dev_dbg (&hubdev->dev, "can't suspend device "
+ "on port %d, status %d\n",
+ i + 1, status);
+ break;
+ }
+ }
+ up(&hubdev->serialize);
+ return status;
+}
+
+/*
+ * hardware resume signaling is finished, either because of selective
+ * resume (by host) or remote wakeup (by device) ... now see what changed
+ * in the tree that's rooted at this device.
+ */
+static int finish_resume (struct usb_device *dev)
+{
+ int status;
+ u16 devstatus;
+
+ /* TRSMRCY = 10 msec ... THEN it's OK to talk to this tree */
+ dev->dev.power.power_state = 0;
+ set_current_state(TASK_UNINTERRUPTIBLE);
+ schedule_timeout((HZ * 10)/1000);
+
+ /* usb ch9 identifies four variants of SUSPENDED, based on
+ * what state the device resumes to. Linux currently won't
+ * use the first two (they'd be inside hub_port_init).
+ */
+ dev->state = dev->actconfig
+ ? USB_STATE_CONFIGURED
+ : USB_STATE_ADDRESS;
+
+ /* 10.5.4.5 says be sure devices in the tree are still there.
+ * For now let's assume no subtle bugs...
+ *
+ * FIXME if it's a hub, do this for ALL its children, top down:
+ * update power and USB state, verify it's there.
+ */
+ status = usb_get_status(dev, USB_RECIP_DEVICE, 0, &devstatus);
+ if (status < 0)
+ dev_dbg(&dev->dev,
+ "gone after usb resume? status %d\n",
+ status);
+ else if (dev->actconfig) {
+ if ((dev->actconfig->desc.bmAttributes
+ & USB_CONFIG_ATT_WAKEUP) != 0) {
+ status = usb_control_msg(dev,
+ usb_sndctrlpipe(dev, 0),
+ USB_REQ_CLEAR_FEATURE,
+ USB_RECIP_DEVICE,
+ USB_DEVICE_REMOTE_WAKEUP, 0,
+ NULL, 0,
+ USB_CTRL_SET_TIMEOUT);
+ if (status) {
+ dev_dbg(&dev->dev, "disable remote "
+ "wakeup, status %d\n", status);
+ status = 0;
+ }
+ }
+ } else if (dev->devnum <= 0) {
+ dev_dbg(&dev->dev, "bogus resume!\n");
+ status = -EINVAL;
+ }
+ return status;
+}
+
+static int
+hub_port_resume(struct usb_device *hubdev, struct usb_hub *hub, int port)
+{
+ int status;
+ struct usb_device *dev;
+
+ dev = hubdev->children[port - 1];
+ if (!dev)
+ return -ENODEV;
+ if (dev->state != USB_STATE_SUSPENDED)
+ return -EINVAL;
+
+ down(&dev->serialize);
+ // dev_dbg(&hub->intf->dev, "resume port %d\n", port);
+
+ /* see 7.1.7.7; affects power usage, but not budgeting */
+ status = clear_port_feature(hubdev, port, USB_PORT_FEAT_SUSPEND);
+ if (status) {
+ dev_dbg(&hub->intf->dev,
+ "can't resume port %d, status %d\n",
+ port, status);
+ } else {
+ u16 devstatus;
+ u16 portchange;
+
+ /* drive resume for at least 20 msec */
+ dev_dbg(&dev->dev, "usb resume\n");
+ set_current_state(TASK_UNINTERRUPTIBLE);
+ schedule_timeout((HZ * 25)/1000);
+
+#define LIVE_FLAGS ( USB_PORT_STAT_POWER \
+ | USB_PORT_STAT_ENABLE \
+ | USB_PORT_STAT_CONNECTION)
+
+ /* Root hubs can use the GET_PORT_STATUS request to
+ * stop resume signaling. Then finish the resume
+ * sequence.
+ */
+ devstatus = portchange = 0;
+ status = hub_port_status(hubdev, port - 1,
+ &devstatus, &portchange);
+ if (status < 0
+ || (devstatus & LIVE_FLAGS) != LIVE_FLAGS
+ || (devstatus & USB_PORT_STAT_SUSPEND) != 0
+ ) {
+ dev_dbg(&dev->dev,
+ "port %d status %04x.%04x after resume, %d\n",
+ port, portchange, devstatus, status);
+ } else
+ status = finish_resume(dev);
+ if (status < 0)
+ status = hub_port_disable(hubdev, port);
+ }
+
+ up(&dev->serialize);
+ return status;
+}
+
+/**
+ * usb_resume_device - re-activate a suspended usb device
+ * @dev: device to re-activate
+ * Context: must be able to sleep; dev->serialize not held
+ *
+ * This will re-activate the suspended device, increasing power usage
+ * while letting drivers communicate again with its endpoints.
+ *
+ * Returns 0 on success, else negative errno.
+ */
+int usb_resume_device (struct usb_device *dev)
+{
+ struct usb_device *hubdev = dev->parent;
+ struct usb_hub *hub;
+ int i, status = -ENODEV;
+
+ /* FIXME support root hubs too */
+ if (!hubdev)
+ return -EOPNOTSUPP;
+
+ down(&hubdev->serialize);
+ hub = usb_get_intfdata(hubdev->actconfig->interface[0]);
+ for (i = 0; i < hubdev->maxchild; i++) {
+ if (hubdev->children[i] == dev) {
+ status = hub_port_resume(hubdev, hub, i + 1);
+ break;
+ }
+ }
+ up(&hubdev->serialize);
+ return status;
+}
+
+#else
+
+static inline int finish_resume (struct usb_device *dev)
+{
+ return 0;
+}
+
+int usb_suspend_device (struct usb_device *dev)
+{
+ return 0;
+}
+
+int usb_resume_device (struct usb_device *dev)
+{
+ return 0;
+}
+
+#endif /* CONFIG_USB_SUSPEND */
+
+EXPORT_SYMBOL(usb_suspend_device);
+EXPORT_SYMBOL(usb_resume_device);
+
+
/* USB 2.0 spec, 7.1.7.3 / fig 7-29:
*
* Between connect detection and reset signaling there must be a delay
@@ -1472,11 +1764,17 @@
}
if (portchange & USB_PORT_STAT_C_SUSPEND) {
+ clear_port_feature(dev, i + 1,
+ USB_PORT_FEAT_C_SUSPEND);
+ if (dev->children[i])
+ ret = finish_resume(dev->children[i]);
+ else
+ ret = -ENODEV;
dev_dbg (&hub->intf->dev,
- "suspend change on port %d\n",
- i + 1);
- clear_port_feature(dev,
- i + 1, USB_PORT_FEAT_C_SUSPEND);
+ "resume on port %d, status %d\n",
+ i + 1, ret);
+ if (ret < 0)
+ hub_port_disable(dev, i + 1);
}
if (portchange & USB_PORT_STAT_C_OVERCURRENT) {
--- 1.162/drivers/usb/core/usb.c Thu Apr 15 05:19:20 2004
+++ edited/drivers/usb/core/usb.c Thu Apr 29 08:48:39 2004
@@ -48,6 +48,7 @@
#include "hcd.h"
#include "usb.h"
+#include "hub.h"
extern int usb_hub_init(void);
extern void usb_hub_cleanup(void);
@@ -1441,13 +1442,16 @@
usb_pipein (pipe) ? DMA_FROM_DEVICE : DMA_TO_DEVICE);
}
-static int usb_device_suspend(struct device *dev, u32 state)
+static int usb_generic_suspend(struct device *dev, u32 state)
{
struct usb_interface *intf;
struct usb_driver *driver;
+ /* USB has just one active suspend state, using the hub */
+ if (dev->driver == &usb_generic_driver)
+ return usb_suspend_device (to_usb_device(dev));
+
if ((dev->driver == NULL) ||
- (dev->driver == &usb_generic_driver) ||
(dev->driver_data == &usb_generic_driver_data))
return 0;
@@ -1459,13 +1463,16 @@
return 0;
}
-static int usb_device_resume(struct device *dev)
+static int usb_generic_resume(struct device *dev)
{
struct usb_interface *intf;
struct usb_driver *driver;
+ /* devices resume through their hub */
+ if (dev->driver == &usb_generic_driver)
+ return usb_resume_device (to_usb_device(dev));
+
if ((dev->driver == NULL) ||
- (dev->driver == &usb_generic_driver) ||
(dev->driver_data == &usb_generic_driver_data))
return 0;
@@ -1481,8 +1488,8 @@
.name = "usb",
.match = usb_device_match,
.hotplug = usb_hotplug,
- .suspend = usb_device_suspend,
- .resume = usb_device_resume,
+ .suspend = usb_generic_suspend,
+ .resume = usb_generic_resume,
};
#ifndef MODULE
--- 1.106/include/linux/usb.h Thu Apr 15 07:43:58 2004
+++ edited/include/linux/usb.h Thu Apr 29 08:48:39 2004
@@ -941,6 +941,11 @@
void *data, int len, int *actual_length,
int timeout);
+/* selective suspend/resume */
+extern int usb_suspend_device(struct usb_device *dev);
+extern int usb_resume_device(struct usb_device *dev);
+
+
/* wrappers around usb_control_msg() for the most common standard requests */
extern int usb_get_descriptor(struct usb_device *dev, unsigned char desctype,
unsigned char descindex, void *buf, int size);
