ChangeSet 1.1807.48.3, 2004/07/30 16:34:04-07:00, [EMAIL PROTECTED]

[PATCH] USB: add CONFIG_USB_SUSPEND

This is the core of the USB_SUSPEND functionality.  Please merge.

This adds an experimental CONFIG_USB_SUSPEND option, which supports the
USB "suspend" state.  Linux-USB hosts have previously ignored that state.

    -   New driver API calls, usb_suspend_device() and its
        sibling usb_resume_device().

    -   Access to those calls through sysfs, such as
                echo -n 2 > power/state
                echo -n 0 > power/state

That can be used to reduce the power consumption of any given USB device,
then re-activate it later.  Eventually, most USB device drivers should
probably suspend idle USB devices.

One problem with this patch:  USB drivers without suspend() callbacks
may badly misbehave.  Right now only hub drivers know suspend().  If the
driver core didn't self-deadlock when we try it, unbinding those drivers
from those devices (then re-enumerating on resume) would be perfect...
the current compromise is just to emit a warning message.

In conjunction with host controller driver support (already merged for
OHCI and EHCI), PCI host controllers will issue the PME# wakeup signal
when a USB keyboard starts remote wakeup signaling.  (But the keyboard
wasn't usable later, since HID doesn't try to suspend.)

I understand some ACPI patches are circulating, and maybe already in
the MM tree, to make a suspended system wake up given PME# signaling.
It'd be great if someone made that work transparently with USB, but
for now I'm told it'll need some sysfs setup first.

Signed-off-by: David Brownell <[EMAIL PROTECTED]>
Signed-off-by: Greg Kroah-Hartman <[EMAIL PROTECTED]>


 Documentation/DocBook/usb.tmpl |    1 
 drivers/usb/core/Kconfig       |   11 
 drivers/usb/core/hub.c         |  485 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 496 insertions(+), 1 deletion(-)


diff -Nru a/Documentation/DocBook/usb.tmpl b/Documentation/DocBook/usb.tmpl
--- a/Documentation/DocBook/usb.tmpl    2004-08-23 13:22:05 -07:00
+++ b/Documentation/DocBook/usb.tmpl    2004-08-23 13:22:05 -07:00
@@ -251,6 +251,7 @@
 !Edrivers/usb/core/message.c
 !Edrivers/usb/core/file.c
 !Edrivers/usb/core/usb.c
+!Edrivers/usb/core/hub.c
     </chapter>
 
     <chapter><title>Host Controller APIs</title>
diff -Nru a/drivers/usb/core/Kconfig b/drivers/usb/core/Kconfig
--- a/drivers/usb/core/Kconfig  2004-08-23 13:22:05 -07:00
+++ b/drivers/usb/core/Kconfig  2004-08-23 13:22:05 -07:00
@@ -60,3 +60,14 @@
 
          If you are unsure about this, say N here.
 
+config USB_SUSPEND
+       bool "USB suspend/resume (EXPERIMENTAL)"
+       depends on USB && PM && EXPERIMENTAL
+       help
+         If you say Y here, you can use driver calls or the sysfs
+         "power/state" file to suspend or resume individual USB
+         peripherals.  There are many related features, such as
+         remote wakeup and driver-specific suspend processing, that
+         may not yet work as expected.
+
+         If you are unsure about this, say N here.
diff -Nru a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
--- a/drivers/usb/core/hub.c    2004-08-23 13:22:05 -07:00
+++ b/drivers/usb/core/hub.c    2004-08-23 13:22:05 -07:00
@@ -1239,7 +1239,490 @@
 
 #ifdef CONFIG_USB_SUSPEND
 
-       /* no USB_SUSPEND yet! */
+/*
+ * 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.
+ *
+ * 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 wakes the USB host.
+ */
+static int hub_port_suspend(struct usb_device *hdev, int port)
+{
+       int                     status;
+       struct usb_device       *udev;
+
+       udev = hdev->children[port - 1];
+       // dev_dbg(hubdev(hdev), "suspend port %d\n", port);
+
+       /* enable remote wakeup when appropriate; this lets the device
+        * wake up the upstream hub (including maybe the root hub).
+        *
+        * NOTE:  OTG devices may issue remote wakeup (or SRP) even when
+        * we don't explicitly enable it here.
+        */
+       if (udev->actconfig
+                       // && FIXME (remote wakeup enabled on this bus)
+                       // ... currently assuming it's always appropriate
+                       && (udev->actconfig->desc.bmAttributes
+                               & USB_CONFIG_ATT_WAKEUP) != 0) {
+               status = usb_control_msg(udev, usb_sndctrlpipe(udev, 0),
+                               USB_REQ_SET_FEATURE, USB_RECIP_DEVICE,
+                               USB_DEVICE_REMOTE_WAKEUP, 0,
+                               NULL, 0,
+                               USB_CTRL_SET_TIMEOUT);
+               if (status)
+                       dev_dbg(&udev->dev,
+                               "won't remote wakeup, status %d\n",
+                               status);
+       }
+
+       /* see 7.1.7.6 */
+       status = set_port_feature(hdev, port, USB_PORT_FEAT_SUSPEND);
+       if (status) {
+               dev_dbg(hubdev(hdev),
+                       "can't suspend port %d, status %d\n",
+                       port, status);
+               /* paranoia:  "should not happen" */
+               (void) usb_control_msg(udev, usb_sndctrlpipe(udev, 0),
+                               USB_REQ_CLEAR_FEATURE, USB_RECIP_DEVICE,
+                               USB_DEVICE_REMOTE_WAKEUP, 0,
+                               NULL, 0,
+                               USB_CTRL_SET_TIMEOUT);
+       } else {
+               /* device has up to 10 msec to fully suspend */
+               dev_dbg(&udev->dev, "usb suspend\n");
+               udev->state = USB_STATE_SUSPENDED;
+               msleep(10);
+       }
+       return status;
+}
+
+/*
+ * Devices on USB hub ports have only one "suspend" state, corresponding
+ * to ACPI D2 (PM_SUSPEND_MEM), "may cause the device to lose some context".
+ * State transitions include:
+ *
+ *   - suspend, resume ... when the VBUS power link stays live
+ *   - suspend, disconnect ... VBUS lost
+ *
+ * Once VBUS drop breaks the circuit, the port it's using has to go through
+ * normal re-enumeration procedures, starting with enabling VBUS power.
+ * Other than re-initializing the hub (plug/unplug, except for root hubs),
+ * Linux (2.6) currently has NO mechanisms to initiate that:  no khubd
+ * timer, no SRP, no requests through sysfs.
+ */
+static int __usb_suspend_device (struct usb_device *udev, int port, u32 state)
+{
+       int     status;
+
+       if (port < 0)
+               return port;
+
+       /* NOTE:  udev->serialize released on all real returns! */
+
+       if (state <= udev->dev.power.power_state
+                       || state < PM_SUSPEND_MEM
+                       || udev->state == USB_STATE_SUSPENDED
+                       || udev->state == USB_STATE_NOTATTACHED) {
+               up(&udev->serialize);
+               return 0;
+       }
+
+       /* suspend interface drivers; if this is a hub, it
+        * suspends the child devices
+        */
+       if (udev->actconfig) {
+               int     i;
+
+               for (i = 0; i < udev->actconfig->desc.bNumInterfaces; i++) {
+                       struct usb_interface    *intf;
+                       struct usb_driver       *driver;
+
+                       intf = udev->actconfig->interface[i];
+                       if (state <= intf->dev.power.power_state)
+                               continue;
+                       if (!intf->dev.driver)
+                               continue;
+                       driver = to_usb_driver(intf->dev.driver);
+
+                       if (driver->suspend) {
+                               status = driver->suspend(intf, state);
+                               if (intf->dev.power.power_state != state
+                                               || status)
+                                       dev_err(&intf->dev,
+                                               "suspend %d fail, code %d\n",
+                                               state, status);
+                       }
+
+                       /* only drivers with suspend() can ever resume();
+                        * and after power loss, even they won't.
+                        * bus_rescan_devices() can rebind drivers later.
+                        *
+                        * FIXME the PM core self-deadlocks when unbinding
+                        * drivers during suspend/resume ... everything grabs
+                        * dpm_sem (not a spinlock, ugh).  we want to unbind,
+                        * since we know every driver's probe/disconnect works
+                        * even for drivers that can't suspend.
+                        */
+                       if (!driver->suspend || state > PM_SUSPEND_MEM) {
+#if 1
+                               dev_warn(&intf->dev, "resume is unsafe!\n");
+#else
+                               down_write(&usb_bus_type.rwsem);
+                               device_release_driver(&intf->dev);
+                               up_write(&usb_bus_type.rwsem);
+#endif
+                       }
+               }
+       }
+
+       /*
+        * FIXME this needs port power off call paths too, to help force
+        * USB into the "generic" PM model.  At least for devices on
+        * ports that aren't using ganged switching (usually root hubs).
+        *
+        * NOTE: SRP-capable links should adopt more aggressive poweroff
+        * policies (when HNP doesn't apply) once we have mechanisms to
+        * turn power back on!  (Likely not before 2.7...)
+        */
+       if (state > PM_SUSPEND_MEM) {
+               dev_warn(&udev->dev, "no poweroff yet, suspending instead\n");
+               state = PM_SUSPEND_MEM;
+       }
+
+       /* "global suspend" of the HC-to-USB interface (root hub), or
+        * "selective suspend" of just one hub-device link.
+        */
+       if (!udev->parent) {
+               struct usb_bus  *bus = udev->bus;
+               if (bus && bus->op->hub_suspend)
+                       status = bus->op->hub_suspend (bus);
+               else
+                       status = -EOPNOTSUPP;
+       } else
+               status = hub_port_suspend(udev->parent, port + 1);
+
+       if (status == 0)
+               udev->dev.power.power_state = state;
+       up(&udev->serialize);
+       return status;
+}
+
+/**
+ * usb_suspend_device - suspend a usb device
+ * @udev: device that's no longer in active use
+ * @state: PM_SUSPEND_MEM to suspend
+ * Context: must be able to sleep; device not locked
+ *
+ * Suspends a USB device that isn't in active use, conserving power.
+ * Devices may wake out of a suspend, if anything important happens,
+ * using the remote wakeup mechanism.  They may also be taken out of
+ * suspend by the host, using usb_resume_device().  It's also routine
+ * to disconnect devices while they are suspended.
+ *
+ * Suspending OTG devices may trigger HNP, if that's been enabled
+ * between a pair of dual-role devices.  That will change roles, such
+ * as from A-Host to A-Peripheral or from B-Host back to B-Peripheral.
+ *
+ * Returns 0 on success, else negative errno.
+ */
+int usb_suspend_device(struct usb_device *udev, u32 state)
+{
+       return __usb_suspend_device(udev, locktree(udev), state);
+}
+
+/*
+ * 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_port_resume(struct usb_device *udev)
+{
+       int     status;
+       u16     devstatus;
+
+       /* caller owns udev->serialize */
+       dev_dbg(&udev->dev, "usb resume\n");
+       udev->dev.power.power_state = PM_SUSPEND_ON;
+
+       /* usb ch9 identifies four variants of SUSPENDED, based on what
+        * state the device resumes to.  Linux currently won't see the
+        * first two on the host side; they'd be inside hub_port_init()
+        * during many timeouts, but khubd can't suspend until later.
+        */
+       udev->state = udev->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 the device didn't go crazy on resume,
+        * and device drivers will know about any resume quirks.
+        */
+       status = usb_get_status(udev, USB_RECIP_DEVICE, 0, &devstatus);
+       if (status < 0)
+               dev_dbg(&udev->dev,
+                       "gone after usb resume? status %d\n",
+                       status);
+       else if (udev->actconfig) {
+               unsigned        i;
+
+               le16_to_cpus(&devstatus);
+               if (devstatus & (1 << USB_DEVICE_REMOTE_WAKEUP)) {
+                       status = usb_control_msg(udev,
+                                       usb_sndctrlpipe(udev, 0),
+                                       USB_REQ_CLEAR_FEATURE,
+                                               USB_RECIP_DEVICE,
+                                       USB_DEVICE_REMOTE_WAKEUP, 0,
+                                       NULL, 0,
+                                       USB_CTRL_SET_TIMEOUT);
+                       if (status) {
+                               dev_dbg(&udev->dev, "disable remote "
+                                       "wakeup, status %d\n", status);
+                               status = 0;
+                       }
+               }
+
+               /* resume interface drivers; if this is a hub, it
+                * resumes the child devices
+                */
+               for (i = 0; i < udev->actconfig->desc.bNumInterfaces; i++) {
+                       struct usb_interface    *intf;
+                       struct usb_driver       *driver;
+
+                       intf = udev->actconfig->interface[i];
+                       if (intf->dev.power.power_state == PM_SUSPEND_ON)
+                               continue;
+                       if (!intf->dev.driver) {
+                               /* FIXME maybe force to alt 0 */
+                               continue;
+                       }
+                       driver = to_usb_driver(intf->dev.driver);
+
+                       /* bus_rescan_devices() may rebind drivers */
+                       if (!driver->resume)
+                               continue;
+
+                       /* can we do better than just logging errors? */
+                       status = driver->resume(intf);
+                       if (intf->dev.power.power_state != PM_SUSPEND_ON
+                                       || status)
+                               dev_dbg(&intf->dev,
+                                       "resume fail, state %d code %d\n",
+                                       intf->dev.power.power_state, status);
+               }
+               status = 0;
+
+       } else if (udev->devnum <= 0) {
+               dev_dbg(&udev->dev, "bogus resume!\n");
+               status = -EINVAL;
+       }
+       return status;
+}
+
+static int
+hub_port_resume(struct usb_device *hdev, int port)
+{
+       int                     status;
+       struct usb_device       *udev;
+
+       udev = hdev->children[port - 1];
+       // dev_dbg(hubdev(hdev), "resume port %d\n", port);
+
+       /* see 7.1.7.7; affects power usage, but not budgeting */
+       status = clear_port_feature(hdev, port, USB_PORT_FEAT_SUSPEND);
+       if (status) {
+               dev_dbg(&hdev->actconfig->interface[0]->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(&udev->dev, "RESUME\n");
+               msleep(25);
+
+#define LIVE_FLAGS     ( USB_PORT_STAT_POWER \
+                       | USB_PORT_STAT_ENABLE \
+                       | USB_PORT_STAT_CONNECTION)
+
+               /* Virtual root hubs can trigger on GET_PORT_STATUS to
+                * stop resume signaling.  Then finish the resume
+                * sequence.
+                */
+               devstatus = portchange = 0;
+               status = hub_port_status(hdev, port - 1,
+                               &devstatus, &portchange);
+               if (status < 0
+                               || (devstatus & LIVE_FLAGS) != LIVE_FLAGS
+                               || (devstatus & USB_PORT_STAT_SUSPEND) != 0
+                               ) {
+                       dev_dbg(&hdev->actconfig->interface[0]->dev,
+                               "port %d status %04x.%04x after resume, %d\n",
+                               port, portchange, devstatus, status);
+               } else {
+                       /* TRSMRCY = 10 msec */
+                       msleep(10);
+                       status = finish_port_resume(udev);
+               }
+       }
+       if (status < 0)
+               status = hub_port_disable(hdev, port);
+
+       return status;
+}
+
+static int hub_resume (struct usb_interface *intf);
+
+/**
+ * usb_resume_device - re-activate a suspended usb device
+ * @udev: device to re-activate
+ * Context: must be able to sleep; device not locked
+ *
+ * This will re-activate the suspended device, increasing power usage
+ * while letting drivers communicate again with its endpoints.
+ * USB resume explicitly guarantees that the power session between
+ * the host and the device is the same as it was when the device
+ * suspended.
+ *
+ * Returns 0 on success, else negative errno.
+ */
+int usb_resume_device(struct usb_device *udev)
+{
+       int                     port, status;
+
+       port = locktree(udev);
+       if (port < 0)
+               return port;
+
+       /* "global resume" of the HC-to-USB interface (root hub), or
+        * selective resume of one hub-to-device port
+        */
+       if (!udev->parent) {
+               struct usb_bus  *bus = udev->bus;
+               if (bus && bus->op->hub_resume)
+                       status = bus->op->hub_resume (bus);
+               else
+                       status = -EOPNOTSUPP;
+               if (status == 0) {
+                       /* TRSMRCY = 10 msec */
+                       msleep(10);
+                       status = hub_resume (bus->root_hub
+                                       ->actconfig->interface[0]);
+               }
+       } else if (udev->state == USB_STATE_SUSPENDED) {
+               status = hub_port_resume(udev->parent, port + 1);
+       } else {
+               status = 0;
+               udev->dev.power.power_state = PM_SUSPEND_ON;
+       }
+       if (status < 0) {
+               dev_dbg(&udev->dev, "can't resume, status %d\n",
+                       status);
+       }
+
+       up(&udev->serialize);
+
+       /* rebind drivers that had no suspend() */
+       bus_rescan_devices(&usb_bus_type);
+
+       return status;
+}
+
+static int remote_wakeup(struct usb_device *udev)
+{
+       int     status = 0;
+
+       /* don't repeat RESUME sequence if this device
+        * was already woken up by some other task
+        */
+       down(&udev->serialize);
+       if (udev->state == USB_STATE_SUSPENDED) {
+               dev_dbg(&udev->dev, "RESUME (wakeup)\n");
+               /* TRSMRCY = 10 msec */
+               msleep(10);
+               status = finish_port_resume(udev);
+       }
+       up(&udev->serialize);
+       return status;
+}
+
+static int hub_suspend(struct usb_interface *intf, u32 state)
+{
+       struct usb_hub          *hub = usb_get_intfdata (intf);
+       struct usb_device       *hdev = hub->hdev;
+       unsigned                port;
+       int                     status;
+
+       /* stop khubd and related activity */
+       hub_quiesce(hub);
+
+       /* then suspend every port */
+       for (port = 0; port < hdev->maxchild; port++) {
+               struct usb_device       *udev;
+
+               udev = hdev->children [port];
+               if (!udev)
+                       continue;
+               down(&udev->serialize);
+               status = __usb_suspend_device(udev, port, state);
+               if (status < 0)
+                       dev_dbg(&intf->dev, "suspend port %d --> %d\n",
+                               port, status);
+       }
+
+       intf->dev.power.power_state = state;
+       return 0;
+}
+
+static int hub_resume(struct usb_interface *intf)
+{
+       struct usb_device       *hdev = interface_to_usbdev(intf);
+       struct usb_hub          *hub = usb_get_intfdata (intf);
+       unsigned                port;
+       int                     status;
+
+       for (port = 0; port < hdev->maxchild; port++) {
+               struct usb_device       *udev;
+               u16                     portstat, portchange;
+
+               udev = hdev->children [port];
+               status = hub_port_status(hdev, port, &portstat, &portchange);
+               if (status == 0) {
+                       if (portchange & USB_PORT_STAT_C_SUSPEND) {
+                               clear_port_feature(hdev, port + 1,
+                                       USB_PORT_FEAT_C_SUSPEND);
+                               portchange &= ~USB_PORT_STAT_C_SUSPEND;
+                       }
+
+                       /* let khubd handle disconnects etc */
+                       if (portchange)
+                               continue;
+               }
+
+               if (!udev)
+                       continue;
+               down (&udev->serialize);
+               if (portstat & USB_PORT_STAT_SUSPEND)
+                       status = hub_port_resume(hdev, port + 1);
+               else {
+                       status = finish_port_resume(udev);
+                       if (status < 0)
+                               status = hub_port_disable(hdev, port);
+                       if (status < 0)
+                               dev_dbg(&intf->dev, "resume port %d --> %d\n",
+                                       port, status);
+               }
+               up(&udev->serialize);
+       }
+       intf->dev.power.power_state = PM_SUSPEND_ON;
+
+       hub_reactivate(hub);
+       return 0;
+}
 
 #else  /* !CONFIG_USB_SUSPEND */
 



-------------------------------------------------------
SF.Net email is sponsored by Shop4tech.com-Lowest price on Blank Media
100pk Sonic DVD-R 4x for only $29 -100pk Sonic DVD+R for only $33
Save 50% off Retail on Ink & Toner - Free Shipping and Free Gift.
http://www.shop4tech.com/z/Inkjet_Cartridges/9_108_r285
_______________________________________________
[EMAIL PROTECTED]
To unsubscribe, use the last form field at:
https://lists.sourceforge.net/lists/listinfo/linux-usb-devel

Reply via email to