Now that we're starting to finally remove all the wierd locking and device/interface life cycles from usbcore, it seems somehow appropriate to look start filling in some significant omissions ... like hooking usb-specific power management mechanisms up!
Please merge the attached patch, which includes just the essentials of USB suspend/resume, and remote wakeup.
- Dave
CONFIG_USB_SUSPEND makes USB devices listen to driver model power management calls. So one way to suspend a USB device (shrinking the power it draws from its hub) can 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 bDeviceClass MAY NOT contain the value 9
(indicating an external or root hub): only leaves in the USB tree may be
suspended so far, not interior nodes. (This is mostly due to limitations
in the PM framework -- parent/child relationships are disregarded except
during system-wide suspend! -- and is part of why this is EXPERIMENTAL.)
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, on the assumption that all the hubs
support it (or soon will).
Probably usbcore should reject urbs sent to suspended devices, but
for now they're failed by host controller hardware.
--- 1.3/drivers/usb/core/Kconfig Fri May 30 02:36:57 2003
+++ edited/drivers/usb/core/Kconfig Mon Apr 26 15:02:50 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 Mon Apr 26 15:27:26 2004
@@ -970,10 +970,258 @@
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's also global suspend, of the bus by the root hub, which acts
+ * like selective suspend of all root hub ports ... except that the host's
+ * USB controller might save even more power. Global suspend is normally
+ * used when the controller itself is 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 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;
+
+ dev = hubdev->children[port - 1];
+ if (!dev)
+ return -ENODEV;
+ if (dev->state == USB_STATE_SUSPENDED)
+ return 0;
+
+ /* FIXME to suspend a hub, first suspend everything below it;
+ * driver model isn't doing that yet except for system suspend
+ */
+ if (dev->maxchild) {
+ dev_dbg(&dev->dev, "can't suspend hubs yet\n");
+ return -EISDIR;
+ }
+
+ down(&dev->serialize);
+ // 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;
+}
+
+int usb_hub_suspend_device(struct usb_device *dev, u32 state)
+{
+ 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 hubs too: suspend children bottom-up,
+ * and resume from top down; 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);
+ 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);
+
+ /* 10.5.4.5 says be sure devices in the tree are still there. For
+ * now let's assume no subtle bugs, like losing current config
+ * or not being fully functional.
+ *
+ * FIXME if it's a hub, do this for ALL its children, top down:
+ * update power state, verify it's there, update USB state.
+ */
+ 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);
+
+ /* 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).
+ */
+ else if (dev->actconfig) {
+ dev->state = USB_STATE_CONFIGURED;
+ 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)
+ dev->state = USB_STATE_ADDRESS;
+ else {
+ 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;
+}
+
+int usb_hub_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;
+}
+
+#endif /* CONFIG_USB_SUSPEND */
+
/* USB 2.0 spec, 7.1.7.3 / fig 7-29:
*
* Between connect detection and reset signaling there must be a delay
@@ -1472,11 +1720,23 @@
}
if (portchange & USB_PORT_STAT_C_SUSPEND) {
- dev_dbg (&hub->intf->dev,
- "suspend change on port %d\n",
- i + 1);
- clear_port_feature(dev,
- i + 1, USB_PORT_FEAT_C_SUSPEND);
+ struct usb_device *kid;
+
+ /* USB_PORT_STAT_SUSPEND is clear */
+ kid = dev->children[i];
+ ret = finish_resume(kid);
+ if (ret < 0) {
+ dev_dbg (&hub->intf->dev,
+ "resume fail %d on port %d,\n",
+ ret, i + 1);
+ ret = hub_port_disable(dev, i);
+ } else {
+ dev_dbg (&hub->intf->dev,
+ "resume on port %d\n",
+ i + 1);
+ clear_port_feature(dev, i + 1,
+ USB_PORT_FEAT_C_SUSPEND);
+ }
}
if (portchange & USB_PORT_STAT_C_OVERCURRENT) {
--- 1.19/drivers/usb/core/hub.h Mon Apr 19 08:29:02 2004
+++ edited/drivers/usb/core/hub.h Mon Apr 26 15:44:16 2004
@@ -183,6 +183,14 @@
u16 devinfo;
};
+#ifdef CONFIG_USB_SUSPEND
+extern int usb_hub_suspend_device(struct usb_device *dev, u32 state);
+extern int usb_hub_resume_device (struct usb_device *dev);
+#else
+#define usb_hub_suspend_device(dev, state) 0
+#define usb_hub_resume_device(dev) 0
+#endif /* CONFIG_USB_SUSPEND */
+
extern void usb_hub_tt_clear_buffer (struct usb_device *dev, int pipe);
struct usb_hub {
--- 1.160/drivers/usb/core/usb.c Wed Apr 21 03:46:29 2004
+++ edited/drivers/usb/core/usb.c Mon Apr 26 15:44:16 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);
@@ -1446,8 +1447,11 @@
struct usb_interface *intf;
struct usb_driver *driver;
+ /* devices resume through their hub */
+ if (dev->driver == &usb_generic_driver)
+ return usb_hub_suspend_device (to_usb_device(dev), state);
+
if ((dev->driver == NULL) ||
- (dev->driver == &usb_generic_driver) ||
(dev->driver_data == &usb_generic_driver_data))
return 0;
@@ -1464,8 +1468,11 @@
struct usb_interface *intf;
struct usb_driver *driver;
+ /* devices resume through their hub */
+ if (dev->driver == &usb_generic_driver)
+ return usb_hub_resume_device (to_usb_device(dev));
+
if ((dev->driver == NULL) ||
- (dev->driver == &usb_generic_driver) ||
(dev->driver_data == &usb_generic_driver_data))
return 0;
