ChangeSet 1.1587.3.46, 2004/05/11 15:33:38-07:00, david-b@pacbell.net [PATCH] USB: EHCI power management updates This patch updates EHCI suspend/resume so that its essential components work on a few different implementations: - make root hub suspend/resume work - make remote wakeup work (given CONFIG_USB_SUSPEND patch) - separate root hub suspend/resume from PCI suspend/resume - say if controller supports remote wakeup (on this system) - sysfs register dump unavailable if controller is suspended Plus a handful of minor cleanups. Please merge, along with the "hcd-0506.patch" I sent last week. Tested by modifying sysfs power/state files, since ACPI doesn't work on this system (so I can't test system suspend/resume): - For root hub(*) ... suspend/resume works, also remote wakeup - PCI controller ... suspend/resume works, remote wakeup signals PME# (according to "lspci -vv"), but that's ignored on my test sytem Regardless of whether USB was active, "echo 1 > /proc/acpi/sleep" produced a system that wouldn't resume, and the same result came from "echo standby > /sys/power/state". So that's about as far as I can take this testing for now. - Dave (*) Doing this relies on the CONFIG_USB_SUSPEND patch. Otherwise no USB devices respond to sysfs power/state updates. The PCI suspend/resume is a superset of this. drivers/usb/host/ehci-dbg.c | 30 ++++++- drivers/usb/host/ehci-hcd.c | 136 ++++++++++++++++---------------- drivers/usb/host/ehci-hub.c | 182 ++++++++++++++++++++++++++++++++++++++++++-- drivers/usb/host/ehci.h | 19 +++- 4 files changed, 287 insertions(+), 80 deletions(-) diff -Nru a/drivers/usb/host/ehci-dbg.c b/drivers/usb/host/ehci-dbg.c --- a/drivers/usb/host/ehci-dbg.c Fri May 14 15:29:01 2004 +++ b/drivers/usb/host/ehci-dbg.c Fri May 14 15:29:01 2004 @@ -170,6 +170,20 @@ itd->index[6], itd->index[7]); } +static void __attribute__((__unused__)) +dbg_sitd (const char *label, struct ehci_hcd *ehci, struct ehci_sitd *sitd) +{ + ehci_dbg (ehci, "%s [%d] sitd %p, next %08x, urb %p\n", + label, sitd->frame, sitd, le32_to_cpu(sitd->hw_next), sitd->urb); + ehci_dbg (ehci, + " addr %08x sched %04x result %08x buf %08x %08x\n", + le32_to_cpu(sitd->hw_fullspeed_ep), + le32_to_cpu(sitd->hw_uframe), + le32_to_cpu(sitd->hw_results), + le32_to_cpu(sitd->hw_buf [0]), + le32_to_cpu(sitd->hw_buf [1])); +} + static int __attribute__((__unused__)) dbg_status_buf (char *buf, unsigned len, char *label, u32 status) { @@ -625,11 +639,20 @@ spin_lock_irqsave (&ehci->lock, flags); + if (bus->controller->power.power_state) { + size = scnprintf (next, size, + "bus %s, device %s (driver " DRIVER_VERSION ")\n" + "SUSPENDED (no register access)\n", + hcd->self.controller->bus->name, + hcd->self.controller->bus_id); + goto done; + } + /* Capability Registers */ i = HC_VERSION(readl (&ehci->caps->hc_capbase)); temp = scnprintf (next, size, - "bus %s device %s\n" - "EHCI %x.%02x, hcd state %d (driver " DRIVER_VERSION ")\n", + "bus %s, device %s (driver " DRIVER_VERSION ")\n" + "EHCI %x.%02x, hcd state %d\n", hcd->self.controller->bus->name, hcd->self.controller->bus_id, i >> 8, i & 0x0ff, ehci->hcd.state); @@ -672,7 +695,7 @@ next += temp; for (i = 0; i < HCS_N_PORTS (ehci->hcs_params); i++) { - temp = dbg_port_buf (scratch, sizeof scratch, label, i, + temp = dbg_port_buf (scratch, sizeof scratch, label, i + 1, readl (&ehci->regs->port_status [i])); temp = scnprintf (next, size, fmt, temp, scratch); size -= temp; @@ -701,6 +724,7 @@ next += temp; #endif +done: spin_unlock_irqrestore (&ehci->lock, flags); return PAGE_SIZE - size; diff -Nru a/drivers/usb/host/ehci-hcd.c b/drivers/usb/host/ehci-hcd.c --- a/drivers/usb/host/ehci-hcd.c Fri May 14 15:29:01 2004 +++ b/drivers/usb/host/ehci-hcd.c Fri May 14 15:29:01 2004 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000-2002 by David Brownell + * Copyright (c) 2000-2004 by David Brownell * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -69,6 +69,7 @@ * * HISTORY: * + * 2004-05-10 Root hub and PCI suspend/resume support; remote wakeup. (db) * 2004-02-24 Replace pci_* with generic dma_* API calls (dsaxena@plexity.net) * 2003-12-29 Rewritten high speed iso transfer support (by Michal Sojka, * , updates by DB). @@ -96,7 +97,7 @@ * 2001-June Works with usb-storage and NEC EHCI on 2.4 */ -#define DRIVER_VERSION "2003-Dec-29" +#define DRIVER_VERSION "2004-May-10" #define DRIVER_AUTHOR "David Brownell" #define DRIVER_DESC "USB 2.0 'Enhanced' Host Controller (EHCI) Driver" @@ -128,7 +129,7 @@ module_param (log2_irq_thresh, int, S_IRUGO); MODULE_PARM_DESC (log2_irq_thresh, "log2 IRQ latency, 1-64 microframes"); -#define INTR_MASK (STS_IAA | STS_FATAL | STS_ERR | STS_INT) +#define INTR_MASK (STS_IAA | STS_FATAL | STS_PCD | STS_ERR | STS_INT) /*-------------------------------------------------------------------------*/ @@ -201,6 +202,7 @@ dbg_cmd (ehci, "reset", command); writel (command, &ehci->regs->command); ehci->hcd.state = USB_STATE_HALT; + ehci->next_statechange = jiffies; return handshake (&ehci->regs->command, CMD_RESET, 0, 250 * 1000); } @@ -241,6 +243,8 @@ /*-------------------------------------------------------------------------*/ +static void ehci_work(struct ehci_hcd *ehci, struct pt_regs *regs); + #include "ehci-hub.c" #include "ehci-mem.c" #include "ehci-q.c" @@ -248,8 +252,6 @@ /*-------------------------------------------------------------------------*/ -static void ehci_work(struct ehci_hcd *ehci, struct pt_regs *regs); - static void ehci_watchdog (unsigned long param) { struct ehci_hcd *ehci = (struct ehci_hcd *) param; @@ -428,12 +430,17 @@ #ifdef CONFIG_PCI if (hcd->self.controller->bus == &pci_bus_type) { struct pci_dev *pdev; + u16 port_wake; pdev = to_pci_dev(hcd->self.controller); /* Serial Bus Release Number is at PCI 0x60 offset */ pci_read_config_byte(pdev, 0x60, &sbrn); + /* port wake capability, reported by boot firmware */ + pci_read_config_word(pdev, 0x62, &port_wake); + hcd->can_wakeup = (port_wake & 1) != 0; + /* help hc dma work well with cachelines */ pci_set_mwi (pdev); @@ -615,41 +622,26 @@ /* suspend/resume, section 4.3 */ +/* These routines rely on PCI to handle powerdown and wakeup, and + * transceivers that don't need any software attention to set up + * the right sort of wakeup. + */ + static int ehci_suspend (struct usb_hcd *hcd, u32 state) { struct ehci_hcd *ehci = hcd_to_ehci (hcd); - int ports; - int i; - - ehci_dbg (ehci, "suspend to %d\n", state); - ports = HCS_N_PORTS (ehci->hcs_params); + while (time_before (jiffies, ehci->next_statechange)) + msec_delay (100); - // FIXME: This assumes what's probably a D3 level suspend... - - // FIXME: usb wakeup events on this bus should resume the machine. - // pci config register PORTWAKECAP controls which ports can do it; - // bios may have initted the register... - - /* suspend each port, then stop the hc */ - for (i = 0; i < ports; i++) { - int temp = readl (&ehci->regs->port_status [i]); - - if ((temp & PORT_PE) == 0 - || (temp & PORT_OWNER) != 0) - continue; - ehci_dbg (ehci, "suspend port %d", i); - temp |= PORT_SUSPEND; - writel (temp, &ehci->regs->port_status [i]); - } - - if (hcd->state == USB_STATE_RUNNING) - ehci_ready (ehci); - writel (readl (&ehci->regs->command) & ~CMD_RUN, &ehci->regs->command); - -// save pci FLADJ value +#ifdef CONFIG_USB_SUSPEND + (void) usb_suspend_device (hcd->self.root_hub); +#else + /* FIXME lock root hub */ + (void) ehci_hub_suspend (hcd); +#endif - /* who tells PCI to reduce power consumption? */ + // save (PCI) FLADJ in case of Vaux power loss return 0; } @@ -657,40 +649,22 @@ static int ehci_resume (struct usb_hcd *hcd) { struct ehci_hcd *ehci = hcd_to_ehci (hcd); - int ports; - int i; + int retval; - ehci_dbg (ehci, "resume\n"); + // maybe restore (PCI) FLADJ - ports = HCS_N_PORTS (ehci->hcs_params); + while (time_before (jiffies, ehci->next_statechange)) + msec_delay (100); - // FIXME: if controller didn't retain state, - // return and let generic code clean it up - // test configured_flag ? - - /* resume HC and each port */ -// restore pci FLADJ value - // khubd and drivers will set HC running, if needed; - hcd->state = USB_STATE_RUNNING; - // FIXME Philips/Intel/... etc don't really have a "READY" - // state ... turn on CMD_RUN too - for (i = 0; i < ports; i++) { - int temp = readl (&ehci->regs->port_status [i]); - - if ((temp & PORT_PE) == 0 - || (temp & PORT_SUSPEND) != 0) - continue; - ehci_dbg (ehci, "resume port %d", i); - temp |= PORT_RESUME; - writel (temp, &ehci->regs->port_status [i]); - readl (&ehci->regs->command); /* unblock posted writes */ - - wait_ms (20); - temp &= ~PORT_RESUME; - writel (temp, &ehci->regs->port_status [i]); - } - readl (&ehci->regs->command); /* unblock posted writes */ - return 0; +#ifdef CONFIG_USB_SUSPEND + retval = usb_resume_device (hcd->self.root_hub); +#else + /* FIXME lock root hub */ + retval = ehci_hub_resume (hcd); +#endif + if (retval == 0) + hcd->self.controller->power.power_state = 0; + return retval; } #endif @@ -752,7 +726,7 @@ bh = 0; #ifdef EHCI_VERBOSE_DEBUG - /* unrequested/ignored: Port Change Detect, Frame List Rollover */ + /* unrequested/ignored: Frame List Rollover */ dbg_status (ehci, "irq", status); #endif @@ -774,6 +748,34 @@ bh = 1; } + /* remote wakeup [4.3.1] */ + if ((status & STS_PCD) && ehci->hcd.remote_wakeup) { + unsigned i = HCS_N_PORTS (ehci->hcs_params); + + /* resume root hub? */ + status = readl (&ehci->regs->command); + if (!(status & CMD_RUN)) + writel (status | CMD_RUN, &ehci->regs->command); + + while (i--) { + status = readl (&ehci->regs->port_status [i]); + if (status & PORT_OWNER) + continue; + if (!(status & PORT_RESUME) + || ehci->reset_done [i] != 0) + continue; + + /* start 20 msec resume signaling from this port, + * and make khubd collect PORT_STAT_C_SUSPEND to + * stop that signaling. + */ + ehci->reset_done [i] = jiffies + MSEC_TO_JIFFIES (20); + mod_timer (&ehci->hcd.rh_timer, + ehci->reset_done [i] + 1); + ehci_dbg (ehci, "port %d remote wakeup\n", i + 1); + } + } + /* PCI errors [4.15.2.4] */ if (unlikely ((status & STS_FATAL) != 0)) { ehci_err (ehci, "fatal error\n"); @@ -814,7 +816,6 @@ struct ehci_hcd *ehci = hcd_to_ehci (hcd); struct list_head qtd_list; - urb->transfer_flags &= ~EHCI_STATE_UNLINK; INIT_LIST_HEAD (&qtd_list); switch (usb_pipetype (urb->pipe)) { @@ -914,7 +915,6 @@ // wait till next completion, do it then. // completion irqs can wait up to 1024 msec, - urb->transfer_flags |= EHCI_STATE_UNLINK; break; } spin_unlock_irqrestore (&ehci->lock, flags); diff -Nru a/drivers/usb/host/ehci-hub.c b/drivers/usb/host/ehci-hub.c --- a/drivers/usb/host/ehci-hub.c Fri May 14 15:29:01 2004 +++ b/drivers/usb/host/ehci-hub.c Fri May 14 15:29:01 2004 @@ -28,6 +28,131 @@ /*-------------------------------------------------------------------------*/ +#ifdef CONFIG_PM + +static int ehci_hub_suspend (struct usb_hcd *hcd) +{ + struct ehci_hcd *ehci = hcd_to_ehci (hcd); + struct usb_device *root = hcd_to_bus (&ehci->hcd)->root_hub; + int port; + int status = 0; + + if (root->dev.power.power_state != 0) + return 0; + if (time_before (jiffies, ehci->next_statechange)) + return -EAGAIN; + + port = HCS_N_PORTS (ehci->hcs_params); + spin_lock_irq (&ehci->lock); + + /* suspend any active/unsuspended ports, maybe allow wakeup */ + while (port--) { + u32 t1 = readl (&ehci->regs->port_status [port]); + u32 t2 = t1; + + if ((t1 & PORT_PE) && !(t1 & PORT_OWNER)) + t2 |= PORT_SUSPEND; + if (ehci->hcd.remote_wakeup) + t2 |= PORT_WKOC_E|PORT_WKDISC_E|PORT_WKCONN_E; + else + t2 &= ~(PORT_WKOC_E|PORT_WKDISC_E|PORT_WKCONN_E); + + if (t1 != t2) { + ehci_vdbg (ehci, "port %d, %08x -> %08x\n", + port + 1, t1, t2); + writel (t2, &ehci->regs->port_status [port]); + } + } + + /* stop schedules, then turn off HC and clean any completed work */ + if (hcd->state == USB_STATE_RUNNING) + ehci_ready (ehci); + ehci->command = readl (&ehci->regs->command); + writel (ehci->command & ~CMD_RUN, &ehci->regs->command); + if (ehci->reclaim) + ehci->reclaim_ready = 1; + ehci_work (ehci, 0); + (void) handshake (&ehci->regs->status, STS_HALT, STS_HALT, 2000); + + root->dev.power.power_state = 3; + ehci->next_statechange = jiffies + MSEC_TO_JIFFIES(10); + spin_unlock_irq (&ehci->lock); + return status; +} + + +/* caller owns root->serialize, and should reset/reinit on error */ +static int ehci_hub_resume (struct usb_hcd *hcd) +{ + struct ehci_hcd *ehci = hcd_to_ehci (hcd); + struct usb_device *root = hcd_to_bus (&ehci->hcd)->root_hub; + u32 temp; + int i; + + if (!root->dev.power.power_state) + return 0; + if (time_before (jiffies, ehci->next_statechange)) + return -EAGAIN; + + /* re-init operational registers in case we lost power */ + if (readl (&ehci->regs->intr_enable) == 0) { + writel (INTR_MASK, &ehci->regs->intr_enable); + writel (0, &ehci->regs->segment); + writel (ehci->periodic_dma, &ehci->regs->frame_list); + writel ((u32)ehci->async->qh_dma, &ehci->regs->async_next); + /* FIXME will this work even (pci) vAUX was lost? */ + } + + /* restore CMD_RUN, framelist size, and irq threshold */ + writel (ehci->command, &ehci->regs->command); + + /* take ports out of suspend */ + i = HCS_N_PORTS (ehci->hcs_params); + while (i--) { + temp = readl (&ehci->regs->port_status [i]); + temp &= ~(PORT_WKOC_E|PORT_WKDISC_E|PORT_WKCONN_E); + if (temp & PORT_SUSPEND) { + ehci->reset_done [i] = jiffies + MSEC_TO_JIFFIES (20); + temp |= PORT_RESUME; + } + writel (temp, &ehci->regs->port_status [i]); + } + i = HCS_N_PORTS (ehci->hcs_params); + wait_ms (20); + while (i--) { + temp = readl (&ehci->regs->port_status [i]); + if ((temp & PORT_SUSPEND) == 0) + continue; + temp &= ~PORT_RESUME; + writel (temp, &ehci->regs->port_status [i]); + ehci_vdbg (ehci, "resumed port %d\n", i + 1); + } + (void) readl (&ehci->regs->command); + + /* maybe re-activate the schedule(s) */ + temp = 0; + if (ehci->async->qh_next.qh) + temp |= CMD_ASE; + if (ehci->periodic_sched) + temp |= CMD_PSE; + if (temp) + writel (ehci->command | temp, &ehci->regs->command); + + root->dev.power.power_state = 0; + ehci->next_statechange = jiffies + MSEC_TO_JIFFIES(5); + ehci->hcd.state = USB_STATE_RUNNING; + return 0; +} + +#else + +#define ehci_hub_suspend 0 +#define ehci_hub_resume 0 + +#endif /* CONFIG_PM */ + +/*-------------------------------------------------------------------------*/ + static int check_reset_complete ( struct ehci_hcd *ehci, int index, @@ -99,7 +224,11 @@ } if (!(temp & PORT_CONNECT)) ehci->reset_done [i] = 0; - if ((temp & (PORT_CSC | PORT_PEC | PORT_OCC)) != 0) { + if ((temp & (PORT_CSC | PORT_PEC | PORT_OCC)) != 0 + // PORT_STAT_C_SUSPEND? + || ((temp & PORT_RESUME) != 0 + && time_after (jiffies, + ehci->reset_done [i]))) { if (i < 7) buf [0] |= 1 << (i + 1); else @@ -143,6 +272,8 @@ /*-------------------------------------------------------------------------*/ +#define PORT_WAKE_BITS (PORT_WKOC_E|PORT_WKDISC_E|PORT_WKCONN_E) + static int ehci_hub_control ( struct usb_hcd *hcd, u16 typeReq, @@ -194,8 +325,20 @@ &ehci->regs->port_status [wIndex]); break; case USB_PORT_FEAT_SUSPEND: + if (temp & PORT_RESET) + goto error; + if (temp & PORT_SUSPEND) { + if ((temp & PORT_PE) == 0) + goto error; + /* resume signaling for 20 msec */ + writel ((temp & ~PORT_WAKE_BITS) | PORT_RESUME, + &ehci->regs->port_status [wIndex]); + ehci->reset_done [wIndex] = jiffies + + MSEC_TO_JIFFIES (20); + } + break; case USB_PORT_FEAT_C_SUSPEND: - /* ? */ + /* we auto-clear this feature */ break; case USB_PORT_FEAT_POWER: if (HCS_PPC (ehci->hcs_params)) @@ -239,15 +382,37 @@ status |= 1 << USB_PORT_FEAT_C_CONNECTION; if (temp & PORT_PEC) status |= 1 << USB_PORT_FEAT_C_ENABLE; - // USB_PORT_FEAT_C_SUSPEND if (temp & PORT_OCC) status |= 1 << USB_PORT_FEAT_C_OVER_CURRENT; + /* whoever resumes must GetPortStatus to complete it!! */ + if ((temp & PORT_RESUME) + && time_after (jiffies, + ehci->reset_done [wIndex])) { + status |= 1 << USB_PORT_FEAT_C_SUSPEND; + ehci->reset_done [wIndex] = 0; + + /* stop resume signaling */ + temp = readl (&ehci->regs->port_status [wIndex]); + writel (temp & ~PORT_RESUME, + &ehci->regs->port_status [wIndex]); + retval = handshake ( + &ehci->regs->port_status [wIndex], + PORT_RESUME, 0, 2000 /* 2msec */); + if (retval != 0) { + ehci_err (ehci, "port %d resume error %d\n", + wIndex + 1, retval); + goto error; + } + temp &= ~(PORT_SUSPEND|PORT_RESUME|(3<<10)); + } + /* whoever resets must GetPortStatus to complete it!! */ if ((temp & PORT_RESET) && time_after (jiffies, ehci->reset_done [wIndex])) { status |= 1 << USB_PORT_FEAT_C_RESET; + ehci->reset_done [wIndex] = 0; /* force reset to complete */ writel (temp & ~PORT_RESET, @@ -275,7 +440,7 @@ } if (temp & PORT_PE) status |= 1 << USB_PORT_FEAT_ENABLE; - if (temp & PORT_SUSPEND) + if (temp & (PORT_SUSPEND|PORT_RESUME)) status |= 1 << USB_PORT_FEAT_SUSPEND; if (temp & PORT_OC) status |= 1 << USB_PORT_FEAT_OVER_CURRENT; @@ -312,6 +477,11 @@ switch (wValue) { case USB_PORT_FEAT_SUSPEND: + if ((temp & PORT_PE) == 0 + || (temp & PORT_RESET) != 0) + goto error; + if (ehci->hcd.remote_wakeup) + temp |= PORT_WAKE_BITS; writel (temp | PORT_SUSPEND, &ehci->regs->port_status [wIndex]); break; @@ -321,6 +491,8 @@ &ehci->regs->port_status [wIndex]); break; case USB_PORT_FEAT_RESET: + if (temp & PORT_RESUME) + goto error; /* line status bits may report this as low speed, * which can be fine if this root hub has a * transaction translator built in. @@ -342,7 +514,7 @@ * usb 2.0 spec says 50 ms resets on root */ ehci->reset_done [wIndex] = jiffies - + ((50 /* msec */ * HZ) / 1000); + + MSEC_TO_JIFFIES (50); } writel (temp, &ehci->regs->port_status [wIndex]); break; diff -Nru a/drivers/usb/host/ehci.h b/drivers/usb/host/ehci.h --- a/drivers/usb/host/ehci.h Fri May 14 15:29:01 2004 +++ b/drivers/usb/host/ehci.h Fri May 14 15:29:01 2004 @@ -84,6 +84,8 @@ struct notifier_block reboot_notifier; unsigned long actions; unsigned stamp; + unsigned long next_statechange; + u32 command; unsigned is_arc_rh_tt:1; /* ARC roothub with TT */ @@ -99,8 +101,6 @@ /* unwrap an HCD pointer to get an EHCI_HCD pointer */ #define hcd_to_ehci(hcd_ptr) container_of(hcd_ptr, struct ehci_hcd, hcd) -/* NOTE: urb->transfer_flags expected to not use this bit !!! */ -#define EHCI_STATE_UNLINK 0x8000 /* urb being unlinked */ enum ehci_timer_action { TIMER_IO_WATCHDOG, @@ -221,7 +221,7 @@ u32 segment; /* address bits 63:32 if needed */ /* PERIODICLISTBASE: offset 0x14 */ u32 frame_list; /* points to periodic list */ - /* ASYNCICLISTADDR: offset 0x18 */ + /* ASYNCLISTADDR: offset 0x18 */ u32 async_next; /* address of next async queue head */ u32 reserved [9]; @@ -237,7 +237,10 @@ #define PORT_WKDISC_E (1<<21) /* wake on disconnect (enable) */ #define PORT_WKCONN_E (1<<20) /* wake on connect (enable) */ /* 19:16 for port testing */ -/* 15:14 for using port indicator leds (if HCS_INDICATOR allows) */ +#define PORT_LED_OFF (0<<14) +#define PORT_LED_AMBER (1<<14) +#define PORT_LED_GREEN (2<<14) +#define PORT_LED_MASK (3<<14) #define PORT_OWNER (1<<13) /* true: companion hc owns this port */ #define PORT_POWER (1<<12) /* true: has power (see PPC) */ #define PORT_USB11(x) (((x)&(3<<10))==(1<<10)) /* USB 1.1 device */ @@ -592,6 +595,14 @@ #endif /*-------------------------------------------------------------------------*/ + +#define MSEC_TO_JIFFIES(msec) ((HZ * (msec) + 999) / 1000) + +static inline void msec_delay(int msec) +{ + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(MSEC_TO_JIFFIES(msec)); +} #ifndef DEBUG #define STUB_DEBUG_FILES