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