f03c17fc9a
This fixes some bugs in EHCI suspend/resume that joined us over the past few releases (as usbcore, PCI, pmcore, and other components evolved): - Removes suspend and resume recursion from the EHCI driver, getting rid of the USB_SUSPEND special casing. - Updates the wakeup mechanism to work again; there's a newish usbcore call it needs to use. - Provide simpler tests for "do we need to restart from scratch", to address another case where PCI Vaux was lost. (In this case it was restoring a swsusp snapshot, but there could be others.) Un-exports a symbol that was temporarily exported. A notable change from previous version is that this doesn't move the spinlock init, so there's still a resume/reinit path bug. Signed-off-by: David Brownell <dbrownell@users.sourceforge.net> Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de> Signed-off-by: Linus Torvalds <torvalds@osdl.org>
572 lines
15 KiB
C
572 lines
15 KiB
C
/*
|
|
* Copyright (C) 2001-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
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
/* this file is part of ehci-hcd.c */
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
/*
|
|
* EHCI Root Hub ... the nonsharable stuff
|
|
*
|
|
* Registers don't need cpu_to_le32, that happens transparently
|
|
*/
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
#ifdef CONFIG_PM
|
|
|
|
static int ehci_bus_suspend (struct usb_hcd *hcd)
|
|
{
|
|
struct ehci_hcd *ehci = hcd_to_ehci (hcd);
|
|
int port;
|
|
|
|
if (time_before (jiffies, ehci->next_statechange))
|
|
msleep(5);
|
|
|
|
port = HCS_N_PORTS (ehci->hcs_params);
|
|
spin_lock_irq (&ehci->lock);
|
|
|
|
/* stop schedules, clean any completed work */
|
|
if (HC_IS_RUNNING(hcd->state)) {
|
|
ehci_quiesce (ehci);
|
|
hcd->state = HC_STATE_QUIESCING;
|
|
}
|
|
ehci->command = readl (&ehci->regs->command);
|
|
if (ehci->reclaim)
|
|
ehci->reclaim_ready = 1;
|
|
ehci_work(ehci, NULL);
|
|
|
|
/* suspend any active/unsuspended ports, maybe allow wakeup */
|
|
while (port--) {
|
|
u32 __iomem *reg = &ehci->regs->port_status [port];
|
|
u32 t1 = readl (reg) & ~PORT_RWC_BITS;
|
|
u32 t2 = t1;
|
|
|
|
if ((t1 & PORT_PE) && !(t1 & PORT_OWNER))
|
|
t2 |= PORT_SUSPEND;
|
|
if (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, reg);
|
|
}
|
|
}
|
|
|
|
/* turn off now-idle HC */
|
|
del_timer_sync (&ehci->watchdog);
|
|
ehci_halt (ehci);
|
|
hcd->state = HC_STATE_SUSPENDED;
|
|
|
|
ehci->next_statechange = jiffies + msecs_to_jiffies(10);
|
|
spin_unlock_irq (&ehci->lock);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* caller has locked the root hub, and should reset/reinit on error */
|
|
static int ehci_bus_resume (struct usb_hcd *hcd)
|
|
{
|
|
struct ehci_hcd *ehci = hcd_to_ehci (hcd);
|
|
u32 temp;
|
|
int i;
|
|
int intr_enable;
|
|
|
|
if (time_before (jiffies, ehci->next_statechange))
|
|
msleep(5);
|
|
spin_lock_irq (&ehci->lock);
|
|
|
|
/* Ideally and we've got a real resume here, and no port's power
|
|
* was lost. (For PCI, that means Vaux was maintained.) But we
|
|
* could instead be restoring a swsusp snapshot -- so that BIOS was
|
|
* the last user of the controller, not reset/pm hardware keeping
|
|
* state we gave to it.
|
|
*/
|
|
|
|
/* re-init operational registers in case we lost power */
|
|
if (readl (&ehci->regs->intr_enable) == 0) {
|
|
/* at least some APM implementations will try to deliver
|
|
* IRQs right away, so delay them until we're ready.
|
|
*/
|
|
intr_enable = 1;
|
|
writel (0, &ehci->regs->segment);
|
|
writel (ehci->periodic_dma, &ehci->regs->frame_list);
|
|
writel ((u32)ehci->async->qh_dma, &ehci->regs->async_next);
|
|
} else
|
|
intr_enable = 0;
|
|
ehci_dbg(ehci, "resume root hub%s\n",
|
|
intr_enable ? " after power loss" : "");
|
|
|
|
/* 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_RWC_BITS
|
|
| PORT_WKOC_E | PORT_WKDISC_E | PORT_WKCONN_E);
|
|
if (temp & PORT_SUSPEND) {
|
|
ehci->reset_done [i] = jiffies + msecs_to_jiffies (20);
|
|
temp |= PORT_RESUME;
|
|
}
|
|
writel (temp, &ehci->regs->port_status [i]);
|
|
}
|
|
i = HCS_N_PORTS (ehci->hcs_params);
|
|
mdelay (20);
|
|
while (i--) {
|
|
temp = readl (&ehci->regs->port_status [i]);
|
|
if ((temp & PORT_SUSPEND) == 0)
|
|
continue;
|
|
temp &= ~(PORT_RWC_BITS | 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) {
|
|
ehci->command |= temp;
|
|
writel (ehci->command, &ehci->regs->command);
|
|
}
|
|
|
|
ehci->next_statechange = jiffies + msecs_to_jiffies(5);
|
|
hcd->state = HC_STATE_RUNNING;
|
|
|
|
/* Now we can safely re-enable irqs */
|
|
if (intr_enable)
|
|
writel (INTR_MASK, &ehci->regs->intr_enable);
|
|
|
|
spin_unlock_irq (&ehci->lock);
|
|
return 0;
|
|
}
|
|
|
|
#else
|
|
|
|
#define ehci_bus_suspend NULL
|
|
#define ehci_bus_resume NULL
|
|
|
|
#endif /* CONFIG_PM */
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
static int check_reset_complete (
|
|
struct ehci_hcd *ehci,
|
|
int index,
|
|
int port_status
|
|
) {
|
|
if (!(port_status & PORT_CONNECT)) {
|
|
ehci->reset_done [index] = 0;
|
|
return port_status;
|
|
}
|
|
|
|
/* if reset finished and it's still not enabled -- handoff */
|
|
if (!(port_status & PORT_PE)) {
|
|
|
|
/* with integrated TT, there's nobody to hand it to! */
|
|
if (ehci_is_TDI(ehci)) {
|
|
ehci_dbg (ehci,
|
|
"Failed to enable port %d on root hub TT\n",
|
|
index+1);
|
|
return port_status;
|
|
}
|
|
|
|
ehci_dbg (ehci, "port %d full speed --> companion\n",
|
|
index + 1);
|
|
|
|
// what happens if HCS_N_CC(params) == 0 ?
|
|
port_status |= PORT_OWNER;
|
|
port_status &= ~PORT_RWC_BITS;
|
|
writel (port_status, &ehci->regs->port_status [index]);
|
|
|
|
} else
|
|
ehci_dbg (ehci, "port %d high speed\n", index + 1);
|
|
|
|
return port_status;
|
|
}
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
|
|
/* build "status change" packet (one or two bytes) from HC registers */
|
|
|
|
static int
|
|
ehci_hub_status_data (struct usb_hcd *hcd, char *buf)
|
|
{
|
|
struct ehci_hcd *ehci = hcd_to_ehci (hcd);
|
|
u32 temp, status = 0;
|
|
int ports, i, retval = 1;
|
|
unsigned long flags;
|
|
|
|
/* if !USB_SUSPEND, root hub timers won't get shut down ... */
|
|
if (!HC_IS_RUNNING(hcd->state))
|
|
return 0;
|
|
|
|
/* init status to no-changes */
|
|
buf [0] = 0;
|
|
ports = HCS_N_PORTS (ehci->hcs_params);
|
|
if (ports > 7) {
|
|
buf [1] = 0;
|
|
retval++;
|
|
}
|
|
|
|
/* no hub change reports (bit 0) for now (power, ...) */
|
|
|
|
/* port N changes (bit N)? */
|
|
spin_lock_irqsave (&ehci->lock, flags);
|
|
for (i = 0; i < ports; i++) {
|
|
temp = readl (&ehci->regs->port_status [i]);
|
|
if (temp & PORT_OWNER) {
|
|
/* don't report this in GetPortStatus */
|
|
if (temp & PORT_CSC) {
|
|
temp &= ~PORT_RWC_BITS;
|
|
temp |= PORT_CSC;
|
|
writel (temp, &ehci->regs->port_status [i]);
|
|
}
|
|
continue;
|
|
}
|
|
if (!(temp & PORT_CONNECT))
|
|
ehci->reset_done [i] = 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
|
|
buf [1] |= 1 << (i - 7);
|
|
status = STS_PCD;
|
|
}
|
|
}
|
|
/* FIXME autosuspend idle root hubs */
|
|
spin_unlock_irqrestore (&ehci->lock, flags);
|
|
return status ? retval : 0;
|
|
}
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
static void
|
|
ehci_hub_descriptor (
|
|
struct ehci_hcd *ehci,
|
|
struct usb_hub_descriptor *desc
|
|
) {
|
|
int ports = HCS_N_PORTS (ehci->hcs_params);
|
|
u16 temp;
|
|
|
|
desc->bDescriptorType = 0x29;
|
|
desc->bPwrOn2PwrGood = 10; /* ehci 1.0, 2.3.9 says 20ms max */
|
|
desc->bHubContrCurrent = 0;
|
|
|
|
desc->bNbrPorts = ports;
|
|
temp = 1 + (ports / 8);
|
|
desc->bDescLength = 7 + 2 * temp;
|
|
|
|
/* two bitmaps: ports removable, and usb 1.0 legacy PortPwrCtrlMask */
|
|
memset (&desc->bitmap [0], 0, temp);
|
|
memset (&desc->bitmap [temp], 0xff, temp);
|
|
|
|
temp = 0x0008; /* per-port overcurrent reporting */
|
|
if (HCS_PPC (ehci->hcs_params))
|
|
temp |= 0x0001; /* per-port power control */
|
|
else
|
|
temp |= 0x0002; /* no power switching */
|
|
#if 0
|
|
// re-enable when we support USB_PORT_FEAT_INDICATOR below.
|
|
if (HCS_INDICATOR (ehci->hcs_params))
|
|
temp |= 0x0080; /* per-port indicators (LEDs) */
|
|
#endif
|
|
desc->wHubCharacteristics = (__force __u16)cpu_to_le16 (temp);
|
|
}
|
|
|
|
/*-------------------------------------------------------------------------*/
|
|
|
|
#define PORT_WAKE_BITS (PORT_WKOC_E|PORT_WKDISC_E|PORT_WKCONN_E)
|
|
|
|
static int ehci_hub_control (
|
|
struct usb_hcd *hcd,
|
|
u16 typeReq,
|
|
u16 wValue,
|
|
u16 wIndex,
|
|
char *buf,
|
|
u16 wLength
|
|
) {
|
|
struct ehci_hcd *ehci = hcd_to_ehci (hcd);
|
|
int ports = HCS_N_PORTS (ehci->hcs_params);
|
|
u32 temp, status;
|
|
unsigned long flags;
|
|
int retval = 0;
|
|
|
|
/*
|
|
* FIXME: support SetPortFeatures USB_PORT_FEAT_INDICATOR.
|
|
* HCS_INDICATOR may say we can change LEDs to off/amber/green.
|
|
* (track current state ourselves) ... blink for diagnostics,
|
|
* power, "this is the one", etc. EHCI spec supports this.
|
|
*/
|
|
|
|
spin_lock_irqsave (&ehci->lock, flags);
|
|
switch (typeReq) {
|
|
case ClearHubFeature:
|
|
switch (wValue) {
|
|
case C_HUB_LOCAL_POWER:
|
|
case C_HUB_OVER_CURRENT:
|
|
/* no hub-wide feature/status flags */
|
|
break;
|
|
default:
|
|
goto error;
|
|
}
|
|
break;
|
|
case ClearPortFeature:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
wIndex--;
|
|
temp = readl (&ehci->regs->port_status [wIndex]);
|
|
if (temp & PORT_OWNER)
|
|
break;
|
|
|
|
switch (wValue) {
|
|
case USB_PORT_FEAT_ENABLE:
|
|
writel (temp & ~PORT_PE,
|
|
&ehci->regs->port_status [wIndex]);
|
|
break;
|
|
case USB_PORT_FEAT_C_ENABLE:
|
|
writel((temp & ~PORT_RWC_BITS) | PORT_PEC,
|
|
&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 */
|
|
temp &= ~(PORT_RWC_BITS | PORT_WAKE_BITS);
|
|
writel (temp | PORT_RESUME,
|
|
&ehci->regs->port_status [wIndex]);
|
|
ehci->reset_done [wIndex] = jiffies
|
|
+ msecs_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))
|
|
writel (temp & ~(PORT_RWC_BITS | PORT_POWER),
|
|
&ehci->regs->port_status [wIndex]);
|
|
break;
|
|
case USB_PORT_FEAT_C_CONNECTION:
|
|
writel((temp & ~PORT_RWC_BITS) | PORT_CSC,
|
|
&ehci->regs->port_status [wIndex]);
|
|
break;
|
|
case USB_PORT_FEAT_C_OVER_CURRENT:
|
|
writel((temp & ~PORT_RWC_BITS) | PORT_OCC,
|
|
&ehci->regs->port_status [wIndex]);
|
|
break;
|
|
case USB_PORT_FEAT_C_RESET:
|
|
/* GetPortStatus clears reset */
|
|
break;
|
|
default:
|
|
goto error;
|
|
}
|
|
readl (&ehci->regs->command); /* unblock posted write */
|
|
break;
|
|
case GetHubDescriptor:
|
|
ehci_hub_descriptor (ehci, (struct usb_hub_descriptor *)
|
|
buf);
|
|
break;
|
|
case GetHubStatus:
|
|
/* no hub-wide feature/status flags */
|
|
memset (buf, 0, 4);
|
|
//cpu_to_le32s ((u32 *) buf);
|
|
break;
|
|
case GetPortStatus:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
wIndex--;
|
|
status = 0;
|
|
temp = readl (&ehci->regs->port_status [wIndex]);
|
|
|
|
// wPortChange bits
|
|
if (temp & PORT_CSC)
|
|
status |= 1 << USB_PORT_FEAT_C_CONNECTION;
|
|
if (temp & PORT_PEC)
|
|
status |= 1 << USB_PORT_FEAT_C_ENABLE;
|
|
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_RWC_BITS | 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_RWC_BITS | PORT_RESET),
|
|
&ehci->regs->port_status [wIndex]);
|
|
/* REVISIT: some hardware needs 550+ usec to clear
|
|
* this bit; seems too long to spin routinely...
|
|
*/
|
|
retval = handshake (
|
|
&ehci->regs->port_status [wIndex],
|
|
PORT_RESET, 0, 750);
|
|
if (retval != 0) {
|
|
ehci_err (ehci, "port %d reset error %d\n",
|
|
wIndex + 1, retval);
|
|
goto error;
|
|
}
|
|
|
|
/* see what we found out */
|
|
temp = check_reset_complete (ehci, wIndex,
|
|
readl (&ehci->regs->port_status [wIndex]));
|
|
}
|
|
|
|
// don't show wPortStatus if it's owned by a companion hc
|
|
if (!(temp & PORT_OWNER)) {
|
|
if (temp & PORT_CONNECT) {
|
|
status |= 1 << USB_PORT_FEAT_CONNECTION;
|
|
// status may be from integrated TT
|
|
status |= ehci_port_speed(ehci, temp);
|
|
}
|
|
if (temp & PORT_PE)
|
|
status |= 1 << USB_PORT_FEAT_ENABLE;
|
|
if (temp & (PORT_SUSPEND|PORT_RESUME))
|
|
status |= 1 << USB_PORT_FEAT_SUSPEND;
|
|
if (temp & PORT_OC)
|
|
status |= 1 << USB_PORT_FEAT_OVER_CURRENT;
|
|
if (temp & PORT_RESET)
|
|
status |= 1 << USB_PORT_FEAT_RESET;
|
|
if (temp & PORT_POWER)
|
|
status |= 1 << USB_PORT_FEAT_POWER;
|
|
}
|
|
|
|
#ifndef EHCI_VERBOSE_DEBUG
|
|
if (status & ~0xffff) /* only if wPortChange is interesting */
|
|
#endif
|
|
dbg_port (ehci, "GetStatus", wIndex + 1, temp);
|
|
// we "know" this alignment is good, caller used kmalloc()...
|
|
*((__le32 *) buf) = cpu_to_le32 (status);
|
|
break;
|
|
case SetHubFeature:
|
|
switch (wValue) {
|
|
case C_HUB_LOCAL_POWER:
|
|
case C_HUB_OVER_CURRENT:
|
|
/* no hub-wide feature/status flags */
|
|
break;
|
|
default:
|
|
goto error;
|
|
}
|
|
break;
|
|
case SetPortFeature:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
wIndex--;
|
|
temp = readl (&ehci->regs->port_status [wIndex]);
|
|
if (temp & PORT_OWNER)
|
|
break;
|
|
|
|
temp &= ~PORT_RWC_BITS;
|
|
switch (wValue) {
|
|
case USB_PORT_FEAT_SUSPEND:
|
|
if ((temp & PORT_PE) == 0
|
|
|| (temp & PORT_RESET) != 0)
|
|
goto error;
|
|
if (hcd->remote_wakeup)
|
|
temp |= PORT_WAKE_BITS;
|
|
writel (temp | PORT_SUSPEND,
|
|
&ehci->regs->port_status [wIndex]);
|
|
break;
|
|
case USB_PORT_FEAT_POWER:
|
|
if (HCS_PPC (ehci->hcs_params))
|
|
writel (temp | PORT_POWER,
|
|
&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.
|
|
*/
|
|
if ((temp & (PORT_PE|PORT_CONNECT)) == PORT_CONNECT
|
|
&& !ehci_is_TDI(ehci)
|
|
&& PORT_USB11 (temp)) {
|
|
ehci_dbg (ehci,
|
|
"port %d low speed --> companion\n",
|
|
wIndex + 1);
|
|
temp |= PORT_OWNER;
|
|
} else {
|
|
ehci_vdbg (ehci, "port %d reset\n", wIndex + 1);
|
|
temp |= PORT_RESET;
|
|
temp &= ~PORT_PE;
|
|
|
|
/*
|
|
* caller must wait, then call GetPortStatus
|
|
* usb 2.0 spec says 50 ms resets on root
|
|
*/
|
|
ehci->reset_done [wIndex] = jiffies
|
|
+ msecs_to_jiffies (50);
|
|
}
|
|
writel (temp, &ehci->regs->port_status [wIndex]);
|
|
break;
|
|
default:
|
|
goto error;
|
|
}
|
|
readl (&ehci->regs->command); /* unblock posted writes */
|
|
break;
|
|
|
|
default:
|
|
error:
|
|
/* "stall" on error */
|
|
retval = -EPIPE;
|
|
}
|
|
spin_unlock_irqrestore (&ehci->lock, flags);
|
|
return retval;
|
|
}
|