mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
11 KiB
440 lines
11 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* MUSB OTG driver virtual root hub support |
|
* |
|
* Copyright 2005 Mentor Graphics Corporation |
|
* Copyright (C) 2005-2006 by Texas Instruments |
|
* Copyright (C) 2006-2007 Nokia Corporation |
|
*/ |
|
|
|
#include <linux/module.h> |
|
#include <linux/kernel.h> |
|
#include <linux/sched.h> |
|
#include <linux/errno.h> |
|
#include <linux/time.h> |
|
#include <linux/timer.h> |
|
|
|
#include <asm/unaligned.h> |
|
|
|
#include "musb_core.h" |
|
|
|
void musb_host_finish_resume(struct work_struct *work) |
|
{ |
|
struct musb *musb; |
|
unsigned long flags; |
|
u8 power; |
|
|
|
musb = container_of(work, struct musb, finish_resume_work.work); |
|
|
|
spin_lock_irqsave(&musb->lock, flags); |
|
|
|
power = musb_readb(musb->mregs, MUSB_POWER); |
|
power &= ~MUSB_POWER_RESUME; |
|
musb_dbg(musb, "root port resume stopped, power %02x", power); |
|
musb_writeb(musb->mregs, MUSB_POWER, power); |
|
|
|
/* |
|
* ISSUE: DaVinci (RTL 1.300) disconnects after |
|
* resume of high speed peripherals (but not full |
|
* speed ones). |
|
*/ |
|
musb->is_active = 1; |
|
musb->port1_status &= ~(USB_PORT_STAT_SUSPEND | MUSB_PORT_STAT_RESUME); |
|
musb->port1_status |= USB_PORT_STAT_C_SUSPEND << 16; |
|
usb_hcd_poll_rh_status(musb->hcd); |
|
/* NOTE: it might really be A_WAIT_BCON ... */ |
|
musb->xceiv->otg->state = OTG_STATE_A_HOST; |
|
|
|
spin_unlock_irqrestore(&musb->lock, flags); |
|
} |
|
|
|
int musb_port_suspend(struct musb *musb, bool do_suspend) |
|
{ |
|
struct usb_otg *otg = musb->xceiv->otg; |
|
u8 power; |
|
void __iomem *mbase = musb->mregs; |
|
|
|
if (!is_host_active(musb)) |
|
return 0; |
|
|
|
/* NOTE: this doesn't necessarily put PHY into low power mode, |
|
* turning off its clock; that's a function of PHY integration and |
|
* MUSB_POWER_ENSUSPEND. PHY may need a clock (sigh) to detect |
|
* SE0 changing to connect (J) or wakeup (K) states. |
|
*/ |
|
power = musb_readb(mbase, MUSB_POWER); |
|
if (do_suspend) { |
|
int retries = 10000; |
|
|
|
if (power & MUSB_POWER_RESUME) |
|
return -EBUSY; |
|
|
|
if (!(power & MUSB_POWER_SUSPENDM)) { |
|
power |= MUSB_POWER_SUSPENDM; |
|
musb_writeb(mbase, MUSB_POWER, power); |
|
|
|
/* Needed for OPT A tests */ |
|
power = musb_readb(mbase, MUSB_POWER); |
|
while (power & MUSB_POWER_SUSPENDM) { |
|
power = musb_readb(mbase, MUSB_POWER); |
|
if (retries-- < 1) |
|
break; |
|
} |
|
} |
|
|
|
musb_dbg(musb, "Root port suspended, power %02x", power); |
|
|
|
musb->port1_status |= USB_PORT_STAT_SUSPEND; |
|
switch (musb->xceiv->otg->state) { |
|
case OTG_STATE_A_HOST: |
|
musb->xceiv->otg->state = OTG_STATE_A_SUSPEND; |
|
musb->is_active = otg->host->b_hnp_enable; |
|
if (musb->is_active) |
|
mod_timer(&musb->otg_timer, jiffies |
|
+ msecs_to_jiffies( |
|
OTG_TIME_A_AIDL_BDIS)); |
|
musb_platform_try_idle(musb, 0); |
|
break; |
|
case OTG_STATE_B_HOST: |
|
musb->xceiv->otg->state = OTG_STATE_B_WAIT_ACON; |
|
musb->is_active = otg->host->b_hnp_enable; |
|
musb_platform_try_idle(musb, 0); |
|
break; |
|
default: |
|
musb_dbg(musb, "bogus rh suspend? %s", |
|
usb_otg_state_string(musb->xceiv->otg->state)); |
|
} |
|
} else if (power & MUSB_POWER_SUSPENDM) { |
|
power &= ~MUSB_POWER_SUSPENDM; |
|
power |= MUSB_POWER_RESUME; |
|
musb_writeb(mbase, MUSB_POWER, power); |
|
|
|
musb_dbg(musb, "Root port resuming, power %02x", power); |
|
|
|
musb->port1_status |= MUSB_PORT_STAT_RESUME; |
|
schedule_delayed_work(&musb->finish_resume_work, |
|
msecs_to_jiffies(USB_RESUME_TIMEOUT)); |
|
} |
|
return 0; |
|
} |
|
|
|
void musb_port_reset(struct musb *musb, bool do_reset) |
|
{ |
|
u8 power; |
|
void __iomem *mbase = musb->mregs; |
|
|
|
if (musb->xceiv->otg->state == OTG_STATE_B_IDLE) { |
|
musb_dbg(musb, "HNP: Returning from HNP; no hub reset from b_idle"); |
|
musb->port1_status &= ~USB_PORT_STAT_RESET; |
|
return; |
|
} |
|
|
|
if (!is_host_active(musb)) |
|
return; |
|
|
|
/* NOTE: caller guarantees it will turn off the reset when |
|
* the appropriate amount of time has passed |
|
*/ |
|
power = musb_readb(mbase, MUSB_POWER); |
|
if (do_reset) { |
|
/* |
|
* If RESUME is set, we must make sure it stays minimum 20 ms. |
|
* Then we must clear RESUME and wait a bit to let musb start |
|
* generating SOFs. If we don't do this, OPT HS A 6.8 tests |
|
* fail with "Error! Did not receive an SOF before suspend |
|
* detected". |
|
*/ |
|
if (power & MUSB_POWER_RESUME) { |
|
long remain = (unsigned long) musb->rh_timer - jiffies; |
|
|
|
if (musb->rh_timer > 0 && remain > 0) { |
|
/* take into account the minimum delay after resume */ |
|
schedule_delayed_work( |
|
&musb->deassert_reset_work, remain); |
|
return; |
|
} |
|
|
|
musb_writeb(mbase, MUSB_POWER, |
|
power & ~MUSB_POWER_RESUME); |
|
|
|
/* Give the core 1 ms to clear MUSB_POWER_RESUME */ |
|
schedule_delayed_work(&musb->deassert_reset_work, |
|
msecs_to_jiffies(1)); |
|
return; |
|
} |
|
|
|
power &= 0xf0; |
|
musb_writeb(mbase, MUSB_POWER, |
|
power | MUSB_POWER_RESET); |
|
|
|
musb->port1_status |= USB_PORT_STAT_RESET; |
|
musb->port1_status &= ~USB_PORT_STAT_ENABLE; |
|
schedule_delayed_work(&musb->deassert_reset_work, |
|
msecs_to_jiffies(50)); |
|
} else { |
|
musb_dbg(musb, "root port reset stopped"); |
|
musb_platform_pre_root_reset_end(musb); |
|
musb_writeb(mbase, MUSB_POWER, |
|
power & ~MUSB_POWER_RESET); |
|
musb_platform_post_root_reset_end(musb); |
|
|
|
power = musb_readb(mbase, MUSB_POWER); |
|
if (power & MUSB_POWER_HSMODE) { |
|
musb_dbg(musb, "high-speed device connected"); |
|
musb->port1_status |= USB_PORT_STAT_HIGH_SPEED; |
|
} |
|
|
|
musb->port1_status &= ~USB_PORT_STAT_RESET; |
|
musb->port1_status |= USB_PORT_STAT_ENABLE |
|
| (USB_PORT_STAT_C_RESET << 16) |
|
| (USB_PORT_STAT_C_ENABLE << 16); |
|
usb_hcd_poll_rh_status(musb->hcd); |
|
|
|
musb->vbuserr_retry = VBUSERR_RETRY_COUNT; |
|
} |
|
} |
|
|
|
void musb_root_disconnect(struct musb *musb) |
|
{ |
|
struct usb_otg *otg = musb->xceiv->otg; |
|
|
|
musb->port1_status = USB_PORT_STAT_POWER |
|
| (USB_PORT_STAT_C_CONNECTION << 16); |
|
|
|
usb_hcd_poll_rh_status(musb->hcd); |
|
musb->is_active = 0; |
|
|
|
switch (musb->xceiv->otg->state) { |
|
case OTG_STATE_A_SUSPEND: |
|
if (otg->host->b_hnp_enable) { |
|
musb->xceiv->otg->state = OTG_STATE_A_PERIPHERAL; |
|
musb->g.is_a_peripheral = 1; |
|
break; |
|
} |
|
fallthrough; |
|
case OTG_STATE_A_HOST: |
|
musb->xceiv->otg->state = OTG_STATE_A_WAIT_BCON; |
|
musb->is_active = 0; |
|
break; |
|
case OTG_STATE_A_WAIT_VFALL: |
|
musb->xceiv->otg->state = OTG_STATE_B_IDLE; |
|
break; |
|
default: |
|
musb_dbg(musb, "host disconnect (%s)", |
|
usb_otg_state_string(musb->xceiv->otg->state)); |
|
} |
|
} |
|
EXPORT_SYMBOL_GPL(musb_root_disconnect); |
|
|
|
|
|
/*---------------------------------------------------------------------*/ |
|
|
|
/* Caller may or may not hold musb->lock */ |
|
int musb_hub_status_data(struct usb_hcd *hcd, char *buf) |
|
{ |
|
struct musb *musb = hcd_to_musb(hcd); |
|
int retval = 0; |
|
|
|
/* called in_irq() via usb_hcd_poll_rh_status() */ |
|
if (musb->port1_status & 0xffff0000) { |
|
*buf = 0x02; |
|
retval = 1; |
|
} |
|
return retval; |
|
} |
|
|
|
static int musb_has_gadget(struct musb *musb) |
|
{ |
|
/* |
|
* In host-only mode we start a connection right away. In OTG mode |
|
* we have to wait until we loaded a gadget. We don't really need a |
|
* gadget if we operate as a host but we should not start a session |
|
* as a device without a gadget or else we explode. |
|
*/ |
|
#ifdef CONFIG_USB_MUSB_HOST |
|
return 1; |
|
#else |
|
return musb->port_mode == MUSB_HOST; |
|
#endif |
|
} |
|
|
|
int musb_hub_control( |
|
struct usb_hcd *hcd, |
|
u16 typeReq, |
|
u16 wValue, |
|
u16 wIndex, |
|
char *buf, |
|
u16 wLength) |
|
{ |
|
struct musb *musb = hcd_to_musb(hcd); |
|
u32 temp; |
|
int retval = 0; |
|
unsigned long flags; |
|
bool start_musb = false; |
|
|
|
spin_lock_irqsave(&musb->lock, flags); |
|
|
|
if (unlikely(!HCD_HW_ACCESSIBLE(hcd))) { |
|
spin_unlock_irqrestore(&musb->lock, flags); |
|
return -ESHUTDOWN; |
|
} |
|
|
|
/* hub features: always zero, setting is a NOP |
|
* port features: reported, sometimes updated when host is active |
|
* no indicators |
|
*/ |
|
switch (typeReq) { |
|
case ClearHubFeature: |
|
case SetHubFeature: |
|
switch (wValue) { |
|
case C_HUB_OVER_CURRENT: |
|
case C_HUB_LOCAL_POWER: |
|
break; |
|
default: |
|
goto error; |
|
} |
|
break; |
|
case ClearPortFeature: |
|
if ((wIndex & 0xff) != 1) |
|
goto error; |
|
|
|
switch (wValue) { |
|
case USB_PORT_FEAT_ENABLE: |
|
break; |
|
case USB_PORT_FEAT_SUSPEND: |
|
musb_port_suspend(musb, false); |
|
break; |
|
case USB_PORT_FEAT_POWER: |
|
if (!hcd->self.is_b_host) |
|
musb_platform_set_vbus(musb, 0); |
|
break; |
|
case USB_PORT_FEAT_C_CONNECTION: |
|
case USB_PORT_FEAT_C_ENABLE: |
|
case USB_PORT_FEAT_C_OVER_CURRENT: |
|
case USB_PORT_FEAT_C_RESET: |
|
case USB_PORT_FEAT_C_SUSPEND: |
|
break; |
|
default: |
|
goto error; |
|
} |
|
musb_dbg(musb, "clear feature %d", wValue); |
|
musb->port1_status &= ~(1 << wValue); |
|
break; |
|
case GetHubDescriptor: |
|
{ |
|
struct usb_hub_descriptor *desc = (void *)buf; |
|
|
|
desc->bDescLength = 9; |
|
desc->bDescriptorType = USB_DT_HUB; |
|
desc->bNbrPorts = 1; |
|
desc->wHubCharacteristics = cpu_to_le16( |
|
HUB_CHAR_INDV_PORT_LPSM /* per-port power switching */ |
|
| HUB_CHAR_NO_OCPM /* no overcurrent reporting */ |
|
); |
|
desc->bPwrOn2PwrGood = 5; /* msec/2 */ |
|
desc->bHubContrCurrent = 0; |
|
|
|
/* workaround bogus struct definition */ |
|
desc->u.hs.DeviceRemovable[0] = 0x02; /* port 1 */ |
|
desc->u.hs.DeviceRemovable[1] = 0xff; |
|
} |
|
break; |
|
case GetHubStatus: |
|
temp = 0; |
|
*(__le32 *) buf = cpu_to_le32(temp); |
|
break; |
|
case GetPortStatus: |
|
if (wIndex != 1) |
|
goto error; |
|
|
|
put_unaligned(cpu_to_le32(musb->port1_status |
|
& ~MUSB_PORT_STAT_RESUME), |
|
(__le32 *) buf); |
|
|
|
/* port change status is more interesting */ |
|
musb_dbg(musb, "port status %08x", musb->port1_status); |
|
break; |
|
case SetPortFeature: |
|
if ((wIndex & 0xff) != 1) |
|
goto error; |
|
|
|
switch (wValue) { |
|
case USB_PORT_FEAT_POWER: |
|
/* NOTE: this controller has a strange state machine |
|
* that involves "requesting sessions" according to |
|
* magic side effects from incompletely-described |
|
* rules about startup... |
|
* |
|
* This call is what really starts the host mode; be |
|
* very careful about side effects if you reorder any |
|
* initialization logic, e.g. for OTG, or change any |
|
* logic relating to VBUS power-up. |
|
*/ |
|
if (!hcd->self.is_b_host && musb_has_gadget(musb)) |
|
start_musb = true; |
|
break; |
|
case USB_PORT_FEAT_RESET: |
|
musb_port_reset(musb, true); |
|
break; |
|
case USB_PORT_FEAT_SUSPEND: |
|
musb_port_suspend(musb, true); |
|
break; |
|
case USB_PORT_FEAT_TEST: |
|
if (unlikely(is_host_active(musb))) |
|
goto error; |
|
|
|
wIndex >>= 8; |
|
switch (wIndex) { |
|
case USB_TEST_J: |
|
pr_debug("USB_TEST_J\n"); |
|
temp = MUSB_TEST_J; |
|
break; |
|
case USB_TEST_K: |
|
pr_debug("USB_TEST_K\n"); |
|
temp = MUSB_TEST_K; |
|
break; |
|
case USB_TEST_SE0_NAK: |
|
pr_debug("USB_TEST_SE0_NAK\n"); |
|
temp = MUSB_TEST_SE0_NAK; |
|
break; |
|
case USB_TEST_PACKET: |
|
pr_debug("USB_TEST_PACKET\n"); |
|
temp = MUSB_TEST_PACKET; |
|
musb_load_testpacket(musb); |
|
break; |
|
case USB_TEST_FORCE_ENABLE: |
|
pr_debug("USB_TEST_FORCE_ENABLE\n"); |
|
temp = MUSB_TEST_FORCE_HOST |
|
| MUSB_TEST_FORCE_HS; |
|
|
|
musb_writeb(musb->mregs, MUSB_DEVCTL, |
|
MUSB_DEVCTL_SESSION); |
|
break; |
|
case 6: |
|
pr_debug("TEST_FIFO_ACCESS\n"); |
|
temp = MUSB_TEST_FIFO_ACCESS; |
|
break; |
|
default: |
|
goto error; |
|
} |
|
musb_writeb(musb->mregs, MUSB_TESTMODE, temp); |
|
break; |
|
default: |
|
goto error; |
|
} |
|
musb_dbg(musb, "set feature %d", wValue); |
|
musb->port1_status |= 1 << wValue; |
|
break; |
|
|
|
default: |
|
error: |
|
/* "protocol stall" on error */ |
|
retval = -EPIPE; |
|
} |
|
spin_unlock_irqrestore(&musb->lock, flags); |
|
|
|
if (start_musb) |
|
musb_start(musb); |
|
|
|
return retval; |
|
}
|
|
|