I thought I'd post the latest version of my patch supporting USB suspend, and (with EHCI and OHCI) remote wakeup. It's had some small changes from feedback on the earlier patches (mostly from Alan Stern). The changes mostly affect the hub driver. With sysfs, for any USB device including a hub, this lets you
echo -n 3 > /sys/bus/usb/devices/$DEVICE/power/state
to suspend it. Use "0" to resume.
IMO this isn't ready to merge yet, but it worked well the last time I tested it. Except that it's particularly annoying not to have HID driver support for the driver suspend/resume calls, the failure mode is particularly rude. And there are still those general Linux-wide PM integration issues to solve. :)
- Dave
--- a/drivers/usb/core/Kconfig Fri May 28 09:05:27 2004 +++ b/drivers/usb/core/Kconfig Fri May 28 09:05:27 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 && PM && 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 and driver-specific + suspend processing, that may not yet work as expected. + + If you are unsure about this, say N here. + --- a/drivers/usb/core/hcd.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/core/hcd.c Fri May 28 09:05:27 2004 @@ -1432,6 +1432,32 @@ /*-------------------------------------------------------------------------*/ +#ifdef CONFIG_USB_SUSPEND + +static int hcd_hub_suspend (struct usb_bus *bus) +{ + struct usb_hcd *hcd; + + hcd = container_of (bus, struct usb_hcd, self); + if (hcd->driver->hub_suspend) + return hcd->driver->hub_suspend (hcd); + return 0; +} + +static int hcd_hub_resume (struct usb_bus *bus) +{ + struct usb_hcd *hcd; + + hcd = container_of (bus, struct usb_hcd, self); + if (hcd->driver->hub_resume) + return hcd->driver->hub_resume (hcd); + return 0; +} + +#endif + +/*-------------------------------------------------------------------------*/ + /* called by khubd, rmmod, apmd, or other thread for hcd-private cleanup. * we're guaranteed that the device is fully quiesced. also, that each * endpoint has been hcd_endpoint_disabled. @@ -1486,6 +1512,10 @@ .buffer_alloc = hcd_buffer_alloc, .buffer_free = hcd_buffer_free, .disable = hcd_endpoint_disable, +#ifdef CONFIG_USB_SUSPEND + .hub_suspend = hcd_hub_suspend, + .hub_resume = hcd_hub_resume, +#endif }; EXPORT_SYMBOL (usb_hcd_operations); diff -Nru a/drivers/usb/core/hcd.h b/drivers/usb/core/hcd.h --- a/drivers/usb/core/hcd.h Fri May 28 09:05:26 2004 +++ b/drivers/usb/core/hcd.h Fri May 28 09:05:26 2004 @@ -152,6 +152,10 @@ void *addr, dma_addr_t dma); void (*disable)(struct usb_device *udev, int bEndpointAddress); + + /* global suspend/resume of bus */ + int (*hub_suspend)(struct usb_bus *); + int (*hub_resume)(struct usb_bus *); }; /* each driver provides one of these, and hardware init support */ @@ -203,6 +207,8 @@ int (*hub_control) (struct usb_hcd *hcd, u16 typeReq, u16 wValue, u16 wIndex, char *buf, u16 wLength); + int (*hub_suspend)(struct usb_hcd *); + int (*hub_resume)(struct usb_hcd *); }; extern void usb_hcd_giveback_urb (struct usb_hcd *hcd, struct urb *urb, struct pt_regs *regs); --- a/drivers/usb/core/hub.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/core/hub.c Fri May 28 09:05:27 2004 @@ -9,6 +9,11 @@ */ #include <linux/config.h> +#ifdef CONFIG_USB_DEBUG + #define DEBUG +#else + #undef DEBUG +#endif #include <linux/kernel.h> #include <linux/errno.h> #include <linux/module.h> @@ -19,11 +24,6 @@ #include <linux/slab.h> #include <linux/smp_lock.h> #include <linux/ioctl.h> -#ifdef CONFIG_USB_DEBUG - #define DEBUG -#else - #undef DEBUG -#endif #include <linux/usb.h> #include <linux/usbdevice_fs.h> #include <linux/suspend.h> @@ -266,10 +266,16 @@ } resubmit: +#ifdef CONFIG_PM + if (hub->intf->dev.power.power_state) { + urb->status = -EHOSTUNREACH; + goto done; + } +#endif if ((status = usb_submit_urb (hub->urb, GFP_ATOMIC)) != 0 /* ENODEV means we raced disconnect() */ && status != -ENODEV) - dev_err (&hub->intf->dev, "resubmit --> %d\n", urb->status); + dev_err (&hub->intf->dev, "resubmit --> %d\n", status); if (status == 0) hub->urb_active = 1; done: @@ -969,10 +975,499 @@ if (ret) dev_err(hubdev(hub), "cannot disable port %d (err = %d)\n", port + 1, ret); - return ret; } + +#ifdef CONFIG_USB_SUSPEND + +/* grab device/port lock, returning index of that port (zero based). + * protects the upstream link used by this device from concurrent tasks + * for tree operations like suspend, resume, reset, disconnect. + */ +static int locktree (struct usb_device *dev) +{ + int t; + struct usb_device *hub; + + if (!dev) + return -ENODEV; + + /* root hub is always the first lock in the series */ + hub = dev->parent; + if (!hub) { + down(&dev->serialize); + return 0; + } + + /* on the path from root to us, lock everything from + * top down, dropping parent locks when not needed + */ + t = locktree (hub); + if (t < 0) + return t; + for (t = 0; t < hub->maxchild; t++) { + if (hub->children[t] == dev) { + /* when everyone grabs locks top->bottom, + * non-overlapping work may be concurrent + */ + down(&dev->serialize); + up(&hub->serialize); + return t; + } + } + up(&hub->serialize); + return -ENODEV; +} + +/* + * 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, int port) +{ + int status; + struct usb_device *dev; + + dev = hubdev->children[port - 1]; + // 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). + * + * FIXME some root hubs won't do wakeup; don't enable wakeup + * unless the topmost hub in this recursion can do it too. + */ + 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(&hubdev->actconfig->interface[0]->dev, + "can't suspend port %d, status %d\n", + port, status); + /* paranoia: "should not happen" */ + (void) 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); + } 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(msecs_to_jiffies(10)); + } + return status; +} + +static int __usb_suspend_device (struct usb_device *dev, int port, u32 state) +{ + int status; + + if (port < 0) + return port; + + /* NOTE: dev->serialize released on all real returns! */ + + if (dev->state == USB_STATE_SUSPENDED) { + up(&dev->serialize); + return 0; + } + + /* suspend interface drivers; if this is a hub, it + * suspends the child devices + */ + if (dev->actconfig) { + int i; + + for (i = 0; i < dev->actconfig->desc.bNumInterfaces; i++) { + struct usb_interface *intf; + struct usb_driver *driver; + + intf = dev->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) + continue; + + /* can we do better than just logging errors? */ + status = driver->suspend(intf, state); + if (status || intf->dev.power.power_state != state) + dev_err(&intf->dev, "suspend fail, code %d\n", + status); + } + } + + /* "global suspend" of the HC-to-USB interface (root hub), or + * "selective suspend" of just one hub-device link. + */ + if (!dev->parent) { + struct usb_bus *bus = dev->bus; + if (bus && bus->op->hub_suspend) + status = bus->op->hub_suspend (bus); + else + status = -EOPNOTSUPP; + } else + status = hub_port_suspend(dev->parent, port + 1); + + if (status == 0) + dev->dev.power.power_state = state; + 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) +{ + return __usb_suspend_device (dev, locktree (dev), 3); +} + +/* + * 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; + + /* caller owns dev->serialize */ + dev_dbg(&dev->dev, "usb resume\n"); + dev->dev.power.power_state = 0; + + /* 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... + */ + 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) { + unsigned i; + + le16_to_cpus(&devstatus); + if (devstatus & (1 << USB_DEVICE_REMOTE_WAKEUP)) { + 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; + } + } + + /* resume interface drivers; if this is a hub, it + * resumes the child devices + */ + for (i = 0; i < dev->actconfig->desc.bNumInterfaces; i++) { + struct usb_interface *intf; + struct usb_driver *driver; + + intf = dev->actconfig->interface[i]; + if (!intf->dev.power.power_state) + continue; + if (!intf->dev.driver) + continue; + driver = to_usb_driver(intf->dev.driver); + if (!driver->resume) + continue; + + /* can we do better than just logging errors? */ + status = driver->resume(intf); + if (status || intf->dev.power.power_state) + dev_dbg(&intf->dev, "resume fail, code %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, int port) +{ + int status; + struct usb_device *dev; + + dev = hubdev->children[port - 1]; + // 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(&hubdev->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(&dev->dev, "RESUME\n"); + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(msecs_to_jiffies(25)); + +#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(&hubdev->actconfig->interface[0]->dev, + "port %d status %04x.%04x after resume, %d\n", + port, portchange, devstatus, status); + } else { + /* TRSMRCY = 10 msec */ + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(msecs_to_jiffies(10)); + status = finish_resume(dev); + } + } + if (status < 0) + status = hub_port_disable(hubdev, port); + + return status; +} + +static int hub_resume (struct usb_interface *intf); + +/** + * 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) +{ + int port, status; + + port = locktree (dev); + 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 (!dev->parent) { + struct usb_bus *bus = dev->bus; + if (bus && bus->op->hub_resume) + status = bus->op->hub_resume (bus); + else + status = -EOPNOTSUPP; + if (status == 0) { + /* TRSMRCY = 10 msec */ + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(msecs_to_jiffies(10)); + + status = hub_resume (bus->root_hub + ->actconfig->interface[0]); + } + } else if (dev->state == USB_STATE_SUSPENDED) { + status = hub_port_resume(dev->parent, port + 1); + } else { + status = 0; + dev->dev.power.power_state = 0; + } + if (status < 0) { + dev_dbg (&dev->dev, "can't resume, status %d\n", + status); + } + + up(&dev->serialize); + return status; +} + +static inline int remote_wakeup (struct usb_device *dev) +{ + int status = 0; + + /* don't repeat RESUME sequence if this device + * was already woken up by some other task + */ + locktree(dev); + if (dev->state == USB_STATE_SUSPENDED) { + dev_dbg(&dev->dev, "RESUME (wakeup)\n"); + /* TRSMRCY = 10 msec */ + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(msecs_to_jiffies(10)); + status = finish_resume(dev); + } + up(&dev->serialize); + return status; +} + +static int hub_suspend (struct usb_interface *intf, u32 state) +{ + struct usb_device *hub = interface_to_usbdev (intf); + struct usb_device *dev; + unsigned port; + int status; + + for (port = 0; port < hub->maxchild; port++) { + dev = hub->children [port]; + if (!dev) + continue; + down (&dev->serialize); + status = __usb_suspend_device (dev, 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 *hub = interface_to_usbdev (intf); + struct usb_device *dev; + struct usb_hub *hubstate = usb_get_intfdata (intf); + unsigned port; + int status; + + for (port = 0; port < hub->maxchild; port++) { + u16 portstat, portchange; + + dev = hub->children [port]; + status = hub_port_status(hub, port, &portstat, &portchange); + if (status == 0) { + if (portchange & USB_PORT_STAT_C_SUSPEND) { + clear_port_feature(hub, port + 1, + USB_PORT_FEAT_C_SUSPEND); + portchange &= ~USB_PORT_STAT_C_SUSPEND; + } + + /* let khubd handle disconnects etc */ + if (portchange) + continue; + } + + if (!dev) + continue; + down (&dev->serialize); + if (portstat & USB_PORT_STAT_SUSPEND) + status = hub_port_resume(hub, port + 1); + else { + status = finish_resume (dev); + if (status < 0) + status = hub_port_disable(hub, port); + if (status < 0) + dev_dbg(&intf->dev, "resume port %d --> %d\n", + port, status); + } + up (&dev->serialize); + } + intf->dev.power.power_state = 0; + + /* resubmit the urb (iff it's safe) */ + if (hubstate->urb->status == -EHOSTUNREACH) { + status = usb_submit_urb (hubstate->urb, GFP_NOIO); + if (status < 0) + dev_err (&intf->dev, + "resume/resubmit --> %d\n", + status); + } else + status = -EHOSTUNREACH; + return 0; +} + +#else /* !CONFIG_USB_SUSPEND */ + +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; +} + +#define hub_suspend 0 +#define hub_resume 0 +#define remote_wakeup(x) 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 @@ -1480,11 +1975,17 @@ } if (portchange & USB_PORT_STAT_C_SUSPEND) { + clear_port_feature(dev, i + 1, + USB_PORT_FEAT_C_SUSPEND); + if (dev->children[i]) + ret = remote_wakeup(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) + ret = hub_port_disable(dev, i); } if (portchange & USB_PORT_STAT_C_OVERCURRENT) { @@ -1563,13 +2064,12 @@ .name = "hub", .probe = hub_probe, .disconnect = hub_disconnect, + .suspend = hub_suspend, + .resume = hub_resume, .ioctl = hub_ioctl, .id_table = hub_id_table, }; -/* - * This should be a separate module. - */ int usb_hub_init(void) { pid_t pid; --- a/drivers/usb/core/usb.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/core/usb.c Fri May 28 09:05:27 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); @@ -1447,13 +1455,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; @@ -1465,13 +1476,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; @@ -1487,8 +1501,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 --- a/drivers/usb/host/ehci-hcd.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/host/ehci-hcd.c Fri May 28 09:05:27 2004 @@ -1029,6 +1029,8 @@ */ .hub_status_data = ehci_hub_status_data, .hub_control = ehci_hub_control, + .hub_suspend = ehci_hub_suspend, + .hub_resume = ehci_hub_resume, }; /*-------------------------------------------------------------------------*/ --- a/drivers/usb/host/ohci-omap.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/host/ohci-omap.c Fri May 28 09:05:27 2004 @@ -565,6 +565,10 @@ */ .hub_status_data = ohci_hub_status_data, .hub_control = ohci_hub_control, +#ifdef CONFIG_USB_SUSPEND + .hub_suspend = ohci_hub_suspend, + .hub_resume = ohci_hub_resume, +#endif }; /*-------------------------------------------------------------------------*/ --- a/drivers/usb/host/ohci-pci.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/host/ohci-pci.c Fri May 28 09:05:27 2004 @@ -238,6 +238,10 @@ */ .hub_status_data = ohci_hub_status_data, .hub_control = ohci_hub_control, +#ifdef CONFIG_USB_SUSPEND + .hub_suspend = ohci_hub_suspend, + .hub_resume = ohci_hub_resume, +#endif }; /*-------------------------------------------------------------------------*/ --- a/drivers/usb/host/ohci-sa1111.c Fri May 28 09:05:27 2004 +++ b/drivers/usb/host/ohci-sa1111.c Fri May 28 09:05:27 2004 @@ -348,6 +348,10 @@ */ .hub_status_data = ohci_hub_status_data, .hub_control = ohci_hub_control, +#ifdef CONFIG_USB_SUSPEND + .hub_suspend = ohci_hub_suspend, + .hub_resume = ohci_hub_resume, +#endif }; /*-------------------------------------------------------------------------*/ --- a/include/linux/usb.h Fri May 28 09:05:26 2004 +++ b/include/linux/usb.h Fri May 28 09:05:26 2004 @@ -930,6 +930,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);