Here's an updated version of the patch I sent on Monday. Key changes:

 - 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);

Reply via email to