ChangeSet 1.1587.3.53, 2004/05/11 16:03:07-07:00, david-b@pacbell.net [PATCH] USB: OHCI root hub suspend/resume/wakeup This patch goes on top of the previous two, and the hcd-0506 patch: - Moves root hub suspend/resume code out of PCI-specific bus glue into generic hub code. That way it's easy to re-use it even for non-PCI implementations like SA1111, OMAP, and LH7A404. (Plus, given CONFIG_USB_SUSPEND, it can be invoked with sysfs.) - Root hub suspend is a lot more careful, as is root hub resume. Pending transactions are now shut down more consistently; and more registers are re-initialized on resume. - The PCI bus glue is now left with truly generic PCI stuff, plus some PMAC-specific stuff (which doesn't include irq disabling any more, hcd-0506 moves that up a level in the stack). - Remote wakeup support is basically working for the root hub. (given CONFIG_USB_SUSPEND to suspend devices and enable it). - Idle HCs will now automatically suspend themselves, and resume as necessary. This saves a certain amount of power on most systems, and matches what UHCI has been doing for a while. The large size of this patch is mostly because of moving that root hub suspend/resume code out of the PCI-specific glue. drivers/usb/host/ohci-hcd.c | 13 + drivers/usb/host/ohci-hub.c | 293 +++++++++++++++++++++++++++++++++++++++++++- drivers/usb/host/ohci-mem.c | 1 drivers/usb/host/ohci-pci.c | 168 +++---------------------- drivers/usb/host/ohci.h | 5 5 files changed, 333 insertions(+), 147 deletions(-) diff -Nru a/drivers/usb/host/ohci-hcd.c b/drivers/usb/host/ohci-hcd.c --- a/drivers/usb/host/ohci-hcd.c Fri May 14 15:28:27 2004 +++ b/drivers/usb/host/ohci-hcd.c Fri May 14 15:28:27 2004 @@ -117,6 +117,8 @@ /* For initializing controller (mask in an HCFS mode too) */ #define OHCI_CONTROL_INIT OHCI_CTRL_CBSR +#define OHCI_INTR_INIT \ + (OHCI_INTR_MIE | OHCI_INTR_UE | OHCI_INTR_RD | OHCI_INTR_WDH) /*-------------------------------------------------------------------------*/ @@ -515,8 +517,11 @@ writel (ohci->hc_control, &ohci->regs->control); ohci->hcd.state = USB_STATE_RUNNING; + /* wake on ConnectStatusChange, matching external hubs */ + writel (RH_HS_DRWE, &ohci->regs->roothub.status); + /* Choose the interrupts we care about now, others later on demand */ - mask = OHCI_INTR_MIE | OHCI_INTR_UE | OHCI_INTR_WDH; + mask = OHCI_INTR_INIT; writel (mask, &ohci->regs->intrstatus); writel (mask, &ohci->regs->intrenable); @@ -612,6 +617,11 @@ hc_reset (ohci); } + if (ints & OHCI_INTR_RD) { + ohci_vdbg (ohci, "resume detect\n"); + schedule_work(&ohci->rh_resume); + } + if (ints & OHCI_INTR_WDH) { if (HCD_IS_RUNNING(hcd->state)) writel (OHCI_INTR_WDH, ®s->intrdisable); @@ -657,6 +667,7 @@ ohci->hcd.state); ohci_dump (ohci, 1); + flush_scheduled_work(); if (HCD_IS_RUNNING(ohci->hcd.state)) hc_reset (ohci); diff -Nru a/drivers/usb/host/ohci-hub.c b/drivers/usb/host/ohci-hub.c --- a/drivers/usb/host/ohci-hub.c Fri May 14 15:28:27 2004 +++ b/drivers/usb/host/ohci-hub.c Fri May 14 15:28:27 2004 @@ -60,6 +60,261 @@ (temp & RH_PS_CCS) ? " CCS" : "" \ ); +/*-------------------------------------------------------------------------*/ + +#if defined(CONFIG_USB_SUSPEND) || defined(CONFIG_PM) + +#define OHCI_SCHED_ENABLES \ + (OHCI_CTRL_CLE|OHCI_CTRL_BLE|OHCI_CTRL_PLE|OHCI_CTRL_IE) + +static void dl_done_list (struct ohci_hcd *, struct pt_regs *); +static void finish_unlinks (struct ohci_hcd *, u16 , struct pt_regs *); + +static int ohci_hub_suspend (struct usb_hcd *hcd) +{ + struct ohci_hcd *ohci = hcd_to_ohci (hcd); + struct usb_device *root = hcd_to_bus (&ohci->hcd)->root_hub; + int status = 0; + + if (root->dev.power.power_state != 0) + return 0; + if (time_before (jiffies, ohci->next_statechange)) + return -EAGAIN; + + spin_lock_irq (&ohci->lock); + + ohci->hc_control = readl (&ohci->regs->control); + switch (ohci->hc_control & OHCI_CTRL_HCFS) { + case OHCI_USB_RESUME: + ohci_dbg (ohci, "resume/suspend?\n"); + ohci->hc_control &= ~OHCI_CTRL_HCFS; + ohci->hc_control |= OHCI_USB_RESET; + writel (ohci->hc_control, &ohci->regs->control); + (void) readl (&ohci->regs->control); + /* FALL THROUGH */ + case OHCI_USB_RESET: + status = -EBUSY; + ohci_dbg (ohci, "needs reinit!\n"); + goto done; + case OHCI_USB_SUSPEND: + ohci_dbg (ohci, "already suspended?\n"); + goto succeed; + } + ohci_dbg (ohci, "suspend root hub\n"); + + /* First stop any processing */ + ohci->hcd.state = USB_STATE_QUIESCING; + if (ohci->hc_control & OHCI_SCHED_ENABLES) { + int limit; + + ohci->hc_control &= ~OHCI_SCHED_ENABLES; + writel (ohci->hc_control, &ohci->regs->control); + ohci->hc_control = readl (&ohci->regs->control); + writel (OHCI_INTR_SF, &ohci->regs->intrstatus); + + /* sched disables take effect on the next frame, + * then the last WDH could take 6+ msec + */ + ohci_dbg (ohci, "stopping schedules ...\n"); + limit = 2000; + while (limit > 0) { + udelay (250); + limit =- 250; + if (readl (&ohci->regs->intrstatus) & OHCI_INTR_SF) + break; + } + dl_done_list (ohci, 0); + mdelay (7); + } + dl_done_list (ohci, 0); + finish_unlinks (ohci, OHCI_FRAME_NO(ohci->hcca), 0); + writel (readl (&ohci->regs->intrstatus), &ohci->regs->intrstatus); + + /* maybe resume can wake root hub */ + if (ohci->hcd.remote_wakeup) + ohci->hc_control |= OHCI_CTRL_RWE; + else + ohci->hc_control &= ~OHCI_CTRL_RWE; + + /* Suspend hub */ + ohci->hc_control &= ~OHCI_CTRL_HCFS; + ohci->hc_control |= OHCI_USB_SUSPEND; + writel (ohci->hc_control, &ohci->regs->control); + (void) readl (&ohci->regs->control); + + /* no resumes until devices finish suspending */ + ohci->next_statechange = jiffies + MSEC_TO_JIFFIES (5); + +succeed: + /* it's not USB_STATE_SUSPENDED unless access to this + * hub from the non-usb side (PCI, SOC, etc) stopped + */ + root->dev.power.power_state = 3; +done: + spin_unlock_irq (&ohci->lock); + return status; +} + +static inline struct ed *find_head (struct ed *ed) +{ + /* for bulk and control lists */ + while (ed->ed_prev) + ed = ed->ed_prev; + return ed; +} + +static int hc_restart (struct ohci_hcd *ohci); + +/* caller owns root->serialize */ +static int ohci_hub_resume (struct usb_hcd *hcd) +{ + struct ohci_hcd *ohci = hcd_to_ohci (hcd); + struct usb_device *root = hcd_to_bus (&ohci->hcd)->root_hub; + u32 temp, enables; + int status = -EINPROGRESS; + + if (!root->dev.power.power_state) + return 0; + if (time_before (jiffies, ohci->next_statechange)) + return -EAGAIN; + + spin_lock_irq (&ohci->lock); + ohci->hc_control = readl (&ohci->regs->control); + switch (ohci->hc_control & OHCI_CTRL_HCFS) { + case OHCI_USB_SUSPEND: + ohci->hc_control &= ~(OHCI_CTRL_HCFS|OHCI_SCHED_ENABLES); + ohci->hc_control |= OHCI_USB_RESUME; + writel (ohci->hc_control, &ohci->regs->control); + (void) readl (&ohci->regs->control); + ohci_dbg (ohci, "resume root hub\n"); + break; + case OHCI_USB_RESUME: + /* HCFS changes sometime after INTR_RD */ + ohci_info (ohci, "remote wakeup\n"); + break; + case OHCI_USB_OPER: + ohci_dbg (ohci, "odd resume\n"); + root->dev.power.power_state = 0; + status = 0; + break; + default: /* RESET, we lost power */ + ohci_dbg (ohci, "root hub hardware reset\n"); + status = -EBUSY; + } + spin_unlock_irq (&ohci->lock); + if (status == -EBUSY) + return hc_restart (ohci); + if (status != -EINPROGRESS) + return status; + + temp = roothub_a (ohci) & RH_A_NDP; + enables = 0; + while (temp--) { + u32 stat = readl (&ohci->regs->roothub.portstatus [temp]); + + /* force global, not selective, resume */ + if (!(stat & RH_PS_PSS)) + continue; + writel (RH_PS_POCI, &ohci->regs->roothub.portstatus [temp]); + } + + /* Some controllers (lucent) need extra-long delays */ + ohci->hcd.state = USB_STATE_RESUMING; + mdelay (20 /* usb 11.5.1.10 */ + 15); + + temp = readl (&ohci->regs->control); + temp &= OHCI_CTRL_HCFS; + if (temp != OHCI_USB_RESUME) { + ohci_err (ohci, "controller won't resume\n"); + return -EBUSY; + } + + /* disable old schedule state, reinit from scratch */ + writel (0, &ohci->regs->ed_controlhead); + writel (0, &ohci->regs->ed_controlcurrent); + writel (0, &ohci->regs->ed_bulkhead); + writel (0, &ohci->regs->ed_bulkcurrent); + writel (0, &ohci->regs->ed_periodcurrent); + writel ((u32) ohci->hcca_dma, &ohci->regs->hcca); + + periodic_reinit (ohci); + + /* interrupts might have been disabled */ + writel (OHCI_INTR_INIT, &ohci->regs->intrenable); + if (ohci->ed_rm_list) + writel (OHCI_INTR_SF, &ohci->regs->intrenable); + writel (readl (&ohci->regs->intrstatus), &ohci->regs->intrstatus); + + /* Then re-enable operations */ + writel (OHCI_USB_OPER, &ohci->regs->control); + (void) readl (&ohci->regs->control); + msec_delay (3); + + temp = OHCI_CONTROL_INIT | OHCI_USB_OPER; + if (ohci->hcd.can_wakeup) + temp |= OHCI_CTRL_RWC; + ohci->hc_control = temp; + writel (temp, &ohci->regs->control); + (void) readl (&ohci->regs->control); + + /* TRSMRCY */ + msec_delay (10); + root->dev.power.power_state = 0; + + /* keep it alive for ~5x suspend + resume costs */ + ohci->next_statechange = jiffies + MSEC_TO_JIFFIES (250); + + /* maybe turn schedules back on */ + enables = 0; + temp = 0; + if (!ohci->ed_rm_list) { + if (ohci->ed_controltail) { + writel (find_head (ohci->ed_controltail)->dma, + &ohci->regs->ed_controlhead); + enables |= OHCI_CTRL_CLE; + temp |= OHCI_CLF; + } + if (ohci->ed_bulktail) { + writel (find_head (ohci->ed_bulktail)->dma, + &ohci->regs->ed_bulkhead); + enables |= OHCI_CTRL_BLE; + temp |= OHCI_BLF; + } + } + if (hcd_to_bus (&ohci->hcd)->bandwidth_isoc_reqs + || hcd_to_bus (&ohci->hcd)->bandwidth_int_reqs) + enables |= OHCI_CTRL_PLE|OHCI_CTRL_IE; + if (enables) { + ohci_dbg (ohci, "restarting schedules ... %08x\n", enables); + ohci->hc_control |= enables; + writel (ohci->hc_control, &ohci->regs->control); + if (temp) + writel (status, &ohci->regs->cmdstatus); + (void) readl (&ohci->regs->control); + } + + ohci->hcd.state = USB_STATE_RUNNING; + return 0; +} + +static void ohci_rh_resume (void *_hcd) +{ + struct usb_hcd *hcd = _hcd; + + down (&hcd->self.root_hub->serialize); + (void) ohci_hub_resume (hcd); + up (&hcd->self.root_hub->serialize); +} + +#else + +static void ohci_rh_resume (void *_hcd) +{ + struct ohci_hcd *ohci = hcd_to_ohci (hcd); + ohci_dbg(ohci, "rh_resume ??\n"); +} + +#endif /* CONFIG_USB_SUSPEND || CONFIG_PM */ /*-------------------------------------------------------------------------*/ @@ -70,6 +325,7 @@ { struct ohci_hcd *ohci = hcd_to_ohci (hcd); int ports, i, changed = 0, length = 1; + int can_suspend = 1; ports = roothub_a (ohci) & RH_A_NDP; if (ports > MAX_ROOT_PORTS) { @@ -95,16 +351,44 @@ for (i = 0; i < ports; i++) { u32 status = roothub_portstatus (ohci, i); - status &= RH_PS_CSC | RH_PS_PESC | RH_PS_PSSC - | RH_PS_OCIC | RH_PS_PRSC; - if (status) { + if (status & (RH_PS_CSC | RH_PS_PESC | RH_PS_PSSC + | RH_PS_OCIC | RH_PS_PRSC)) { changed = 1; if (i < 7) buf [0] |= 1 << (i + 1); else buf [1] |= 1 << (i - 7); + continue; } + + /* can suspend if no ports are enabled; or if all all + * enabled ports are suspended AND remote wakeup is on. + */ + if (!(status & RH_PS_CCS)) + continue; + if ((status & RH_PS_PSS) && ohci->hcd.remote_wakeup) + continue; + can_suspend = 0; } + +#ifdef CONFIG_PM + /* save power by suspending idle root hubs; + * INTR_RD wakes us when there's work + */ + if (can_suspend + && !changed + && !ohci->ed_rm_list + && ((OHCI_CTRL_HCFS | OHCI_SCHED_ENABLES) + & ohci->hc_control) + == OHCI_USB_OPER + && down_trylock (&hcd->self.root_hub->serialize) == 0 + ) { + ohci_vdbg (ohci, "autosuspend\n"); + (void) ohci_hub_suspend (&ohci->hcd); + up (&hcd->self.root_hub->serialize); + } +#endif + return changed ? length : 0; } @@ -188,6 +472,9 @@ break; case USB_PORT_FEAT_SUSPEND: temp = RH_PS_POCI; + if ((ohci->hc_control & OHCI_CTRL_HCFS) + != OHCI_USB_OPER) + schedule_work (&ohci->rh_resume); break; case USB_PORT_FEAT_C_SUSPEND: temp = RH_PS_PSSC; diff -Nru a/drivers/usb/host/ohci-mem.c b/drivers/usb/host/ohci-mem.c --- a/drivers/usb/host/ohci-mem.c Fri May 14 15:28:27 2004 +++ b/drivers/usb/host/ohci-mem.c Fri May 14 15:28:27 2004 @@ -33,6 +33,7 @@ ohci->hcd.product_desc = "OHCI Host Controller"; spin_lock_init (&ohci->lock); INIT_LIST_HEAD (&ohci->pending); + INIT_WORK (&ohci->rh_resume, ohci_rh_resume, &ohci->hcd); return &ohci->hcd; } return 0; diff -Nru a/drivers/usb/host/ohci-pci.c b/drivers/usb/host/ohci-pci.c --- a/drivers/usb/host/ohci-pci.c Fri May 14 15:28:27 2004 +++ b/drivers/usb/host/ohci-pci.c Fri May 14 15:28:27 2004 @@ -36,6 +36,7 @@ struct ohci_hcd *ohci = hcd_to_ohci (hcd); ohci->regs = hcd->regs; + ohci->next_statechange = jiffies; return hc_reset (ohci); } @@ -118,74 +119,25 @@ static int ohci_pci_suspend (struct usb_hcd *hcd, u32 state) { struct ohci_hcd *ohci = hcd_to_ohci (hcd); - u16 cmd; - u32 tmp; - if ((ohci->hc_control & OHCI_CTRL_HCFS) != OHCI_USB_OPER) { - ohci_dbg (ohci, "can't suspend (state is %s)\n", - hcfs2string (ohci->hc_control & OHCI_CTRL_HCFS)); - return -EIO; - } + /* suspend root hub, hoping it keeps power during suspend */ + while (time_before (jiffies, ohci->next_statechange)) + msec_delay (100); + +#ifdef CONFIG_USB_SUSPEND + (void) usb_suspend_device (hcd->self.root_hub); +#else + /* FIXME lock root hub */ + (void) ohci_hub_suspend (hcd); +#endif - /* act as if usb suspend can always be used */ - ohci_dbg (ohci, "suspend to %d\n", state); + /* let things settle down a bit */ + msec_delay (100); - /* First stop processing */ - spin_lock_irq (&ohci->lock); - ohci->hc_control &= - ~(OHCI_CTRL_PLE|OHCI_CTRL_CLE|OHCI_CTRL_BLE|OHCI_CTRL_IE); - writel (ohci->hc_control, &ohci->regs->control); - writel (OHCI_INTR_SF, &ohci->regs->intrstatus); - (void) readl (&ohci->regs->intrstatus); - spin_unlock_irq (&ohci->lock); - - /* Wait a frame or two */ - mdelay (1); - if (!readl (&ohci->regs->intrstatus) & OHCI_INTR_SF) - mdelay (1); - #ifdef CONFIG_PMAC_PBOOK if (_machine == _MACH_Pmac) disable_irq ((to_pci_dev(hcd->self.controller))->irq); - /* else, 2.4 assumes shared irqs -- don't disable */ -#endif - - /* Enable remote wakeup */ - writel (readl (&ohci->regs->intrenable) | OHCI_INTR_RD, - &ohci->regs->intrenable); - - /* Suspend chip and let things settle down a bit */ - spin_lock_irq (&ohci->lock); - ohci->hc_control = OHCI_USB_SUSPEND; - writel (ohci->hc_control, &ohci->regs->control); - (void) readl (&ohci->regs->control); - spin_unlock_irq (&ohci->lock); - - set_current_state (TASK_UNINTERRUPTIBLE); - schedule_timeout (HZ/2); - - tmp = readl (&ohci->regs->control) | OHCI_CTRL_HCFS; - switch (tmp) { - case OHCI_USB_RESET: - case OHCI_USB_RESUME: - case OHCI_USB_OPER: - ohci_err (ohci, "can't suspend; hcfs %d\n", tmp); - break; - case OHCI_USB_SUSPEND: - ohci_dbg (ohci, "suspended\n"); - break; - } - /* In some rare situations, Apple's OHCI have happily trashed - * memory during sleep. We disable its bus master bit during - * suspend - */ - pci_read_config_word (to_pci_dev(hcd->self.controller), PCI_COMMAND, - &cmd); - cmd &= ~PCI_COMMAND_MASTER; - pci_write_config_word (to_pci_dev(hcd->self.controller), PCI_COMMAND, - cmd); -#ifdef CONFIG_PMAC_PBOOK { struct device_node *of_node; @@ -202,7 +154,6 @@ static int ohci_pci_resume (struct usb_hcd *hcd) { struct ohci_hcd *ohci = hcd_to_ohci (hcd); - int temp; int retval = 0; #ifdef CONFIG_PMAC_PBOOK @@ -215,92 +166,25 @@ pmac_call_feature (PMAC_FTR_USB_ENABLE, of_node, 0, 1); } #endif - /* did we suspend, or were we powered off? */ - ohci->hc_control = readl (&ohci->regs->control); - temp = ohci->hc_control & OHCI_CTRL_HCFS; - -#ifdef DEBUG - /* the registers may look crazy here */ - ohci_dump_status (ohci, 0, 0); -#endif - - /* Re-enable bus mastering */ - pci_set_master (to_pci_dev(ohci->hcd.self.controller)); - - switch (temp) { - case OHCI_USB_RESET: // lost power -restart: - ohci_info (ohci, "USB restart\n"); - retval = hc_restart (ohci); - break; - - case OHCI_USB_SUSPEND: // host wakeup - case OHCI_USB_RESUME: // remote wakeup - ohci_info (ohci, "USB continue from %s wakeup\n", - (temp == OHCI_USB_SUSPEND) - ? "host" : "remote"); - - /* we "should" only need RESUME if we're SUSPENDed ... */ - ohci->hc_control = OHCI_USB_RESUME; - writel (ohci->hc_control, &ohci->regs->control); - (void) readl (&ohci->regs->control); - /* Some controllers (lucent) need extra-long delays */ - mdelay (35); /* no schedule here ! */ - - temp = readl (&ohci->regs->control); - temp = ohci->hc_control & OHCI_CTRL_HCFS; - if (temp != OHCI_USB_RESUME) { - ohci_err (ohci, "controller won't resume\n"); - /* maybe we can reset */ - goto restart; - } - - /* Then re-enable operations */ - writel (OHCI_USB_OPER, &ohci->regs->control); - (void) readl (&ohci->regs->control); - mdelay (3); - - spin_lock_irq (&ohci->lock); - ohci->hc_control = OHCI_CONTROL_INIT | OHCI_USB_OPER; - if (!ohci->ed_rm_list) { - if (ohci->ed_controltail) - ohci->hc_control |= OHCI_CTRL_CLE; - if (ohci->ed_bulktail) - ohci->hc_control |= OHCI_CTRL_BLE; - } - if (hcd_to_bus (&ohci->hcd)->bandwidth_isoc_reqs - || hcd_to_bus (&ohci->hcd)->bandwidth_int_reqs) - ohci->hc_control |= OHCI_CTRL_PLE|OHCI_CTRL_IE; - hcd->state = USB_STATE_RUNNING; - writel (ohci->hc_control, &ohci->regs->control); - - /* trigger a start-frame interrupt (why?) */ - writel (OHCI_INTR_SF, &ohci->regs->intrstatus); - writel (OHCI_INTR_SF, &ohci->regs->intrenable); - - writel (OHCI_INTR_WDH, &ohci->regs->intrdisable); - (void) readl (&ohci->regs->intrdisable); - - /* Check for a pending done list */ - if (ohci->hcca->done_head) - dl_done_list (ohci, NULL); - - spin_unlock_irq (&ohci->lock); + /* resume root hub */ + while (time_before (jiffies, ohci->next_statechange)) + msec_delay (100); +#ifdef CONFIG_USB_SUSPEND + /* get extra cleanup even if remote wakeup isn't in use */ + retval = usb_resume_device (hcd->self.root_hub); +#else + down (&hcd->self.root_hub->serialize); + retval = ohci_hub_resume (hcd); + up (&hcd->self.root_hub->serialize); +#endif + if (retval == 0) { + hcd->self.controller->power.power_state = 0; #ifdef CONFIG_PMAC_PBOOK if (_machine == _MACH_Pmac) enable_irq (to_pci_dev(hcd->self.controller)->irq); #endif - - writel (OHCI_INTR_WDH, &ohci->regs->intrenable); - - /* assume there are TDs on the bulk and control lists */ - writel (OHCI_BLF | OHCI_CLF, &ohci->regs->cmdstatus); - break; - - default: - ohci_warn (ohci, "odd PCI resume\n"); } return retval; } diff -Nru a/drivers/usb/host/ohci.h b/drivers/usb/host/ohci.h --- a/drivers/usb/host/ohci.h Fri May 14 15:28:27 2004 +++ b/drivers/usb/host/ohci.h Fri May 14 15:28:27 2004 @@ -372,8 +372,11 @@ */ int load [NUM_INTS]; u32 hc_control; /* copy of hc control reg */ + unsigned long next_statechange; /* suspend/resume */ u32 fminterval; /* saved register */ + struct work_struct rh_resume; + unsigned long flags; /* for HC bugs */ #define OHCI_QUIRK_AMD756 0x01 /* erratum #4 */ #define OHCI_QUIRK_SUPERIO 0x02 /* natsemi */ @@ -404,7 +407,7 @@ #define FI 0x2edf /* 12000 bits per frame (-1) */ #define DEFAULT_FMINTERVAL ((((6 * (FI - 210)) / 7) << 16) | FI) -#define LSTHRESH 0x628 /* lowspeed bit threshold */ +#define LSTHRESH 0x628 /* lowspeed bit threshold */ static inline void periodic_reinit (struct ohci_hcd *ohci) {