aboutsummaryrefslogtreecommitdiffstats
path: root/target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c
diff options
context:
space:
mode:
Diffstat (limited to 'target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c')
-rw-r--r--target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c449
1 files changed, 449 insertions, 0 deletions
diff --git a/target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c b/target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c
new file mode 100644
index 0000000..7d7fc24
--- /dev/null
+++ b/target/linux/adm5120/files-3.18/drivers/usb/host/adm5120-pm.c
@@ -0,0 +1,449 @@
+/*
+ * ADM5120 HCD (Host Controller Driver) for USB
+ *
+ * Copyright (C) 2007-2008 Gabor Juhos <juhosg@openwrt.org>
+ *
+ * This file was derived from fragments of the OHCI driver.
+ * (C) Copyright 1999 Roman Weissgaerber <weissg@vienna.at>
+ * (C) Copyright 2000-2004 David Brownell <dbrownell@users.sourceforge.net>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published
+ * by the Free Software Foundation.
+ *
+ */
+
+#define OHCI_SCHED_ENABLES \
+ (OHCI_CTRL_CLE|OHCI_CTRL_BLE|OHCI_CTRL_PLE|OHCI_CTRL_IE)
+
+#ifdef CONFIG_PM
+static int admhc_restart(struct admhcd *ahcd);
+
+static int admhc_rh_suspend(struct admhcd *ahcd, int autostop)
+__releases(ahcd->lock)
+__acquires(ahcd->lock)
+{
+ int status = 0;
+
+ ahcd->hc_control = admhc_readl(ahcd, &ahcd->regs->control);
+ switch (ahcd->hc_control & OHCI_CTRL_HCFS) {
+ case OHCI_USB_RESUME:
+ admhc_dbg(ahcd, "resume/suspend?\n");
+ ahcd->hc_control &= ~OHCI_CTRL_HCFS;
+ ahcd->hc_control |= OHCI_USB_RESET;
+ admhc_writel(ahcd, ahcd->hc_control, &ahcd->ahcd->regs->control);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+ /* FALL THROUGH */
+ case OHCI_USB_RESET:
+ status = -EBUSY;
+ admhc_dbg(ahcd, "needs reinit!\n");
+ goto done;
+ case OHCI_USB_SUSPEND:
+ if (!ahcd->autostop) {
+ admhc_dbg(ahcd, "already suspended\n");
+ goto done;
+ }
+ }
+ admhc_dbg(ahcd, "%s root hub\n",
+ autostop ? "auto-stop" : "suspend");
+
+ /* First stop any processing */
+ if (!autostop && (ahcd->hc_control & OHCI_SCHED_ENABLES)) {
+ ahcd->hc_control &= ~OHCI_SCHED_ENABLES;
+ admhc_writel(ahcd, ahcd->hc_control, &ahcd->ahcd->regs->control);
+ ahcd->hc_control = admhc_readl(ahcd, &ahcd->regs->control);
+ admhc_writel(ahcd, OHCI_INTR_SF, &ahcd->regs->intrstatus);
+
+ /* sched disables take effect on the next frame,
+ * then the last WDH could take 6+ msec
+ */
+ admhc_dbg(ahcd, "stopping schedules ...\n");
+ ahcd->autostop = 0;
+ spin_unlock_irq (&ahcd->lock);
+ msleep (8);
+ spin_lock_irq(&ahcd->lock);
+ }
+ dl_done_list (ahcd);
+ finish_unlinks (ahcd, admhc_frame_no(ahcd));
+
+ /* maybe resume can wake root hub */
+ if (device_may_wakeup(&admhcd_to_hcd(ahcd)->self.root_hub->dev) ||
+ autostop)
+ ahcd->hc_control |= OHCI_CTRL_RWE;
+ else {
+ admhc_writel(ahcd, OHCI_INTR_RHSC, &ahcd->regs->intrdisable);
+ ahcd->hc_control &= ~OHCI_CTRL_RWE;
+ }
+
+ /* Suspend hub ... this is the "global (to this bus) suspend" mode,
+ * which doesn't imply ports will first be individually suspended.
+ */
+ ahcd->hc_control &= ~OHCI_CTRL_HCFS;
+ ahcd->hc_control |= OHCI_USB_SUSPEND;
+ admhc_writel(ahcd, ahcd->hc_control, &ahcd->ahcd->regs->control);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+
+ /* no resumes until devices finish suspending */
+ if (!autostop) {
+ ahcd->next_statechange = jiffies + msecs_to_jiffies (5);
+ ahcd->autostop = 0;
+ }
+
+done:
+ 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;
+}
+
+/* caller has locked the root hub */
+static int admhc_rh_resume(struct admhcd *ahcd)
+__releases(ahcd->lock)
+__acquires(ahcd->lock)
+{
+ struct usb_hcd *hcd = admhcd_to_hcd (ahcd);
+ u32 temp, enables;
+ int status = -EINPROGRESS;
+ int autostopped = ahcd->autostop;
+
+ ahcd->autostop = 0;
+ ahcd->hc_control = admhc_readl(ahcd, &ahcd->regs->control);
+
+ if (ahcd->hc_control & (OHCI_CTRL_IR | OHCI_SCHED_ENABLES)) {
+ /* this can happen after resuming a swsusp snapshot */
+ if (hcd->state == HC_STATE_RESUMING) {
+ admhc_dbg(ahcd, "BIOS/SMM active, control %03x\n",
+ ahcd->hc_control);
+ status = -EBUSY;
+ /* this happens when pmcore resumes HC then root */
+ } else {
+ admhc_dbg(ahcd, "duplicate resume\n");
+ status = 0;
+ }
+ } else switch (ahcd->hc_control & OHCI_CTRL_HCFS) {
+ case OHCI_USB_SUSPEND:
+ ahcd->hc_control &= ~(OHCI_CTRL_HCFS|OHCI_SCHED_ENABLES);
+ ahcd->hc_control |= OHCI_USB_RESUME;
+ admhc_writel(ahcd, ahcd->hc_control, &ahcd->ahcd->regs->control);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+ admhc_dbg(ahcd, "%s root hub\n",
+ autostopped ? "auto-start" : "resume");
+ break;
+ case OHCI_USB_RESUME:
+ /* HCFS changes sometime after INTR_RD */
+ admhc_dbg(ahcd, "%swakeup root hub\n",
+ autostopped ? "auto-" : "");
+ break;
+ case OHCI_USB_OPER:
+ /* this can happen after resuming a swsusp snapshot */
+ admhc_dbg(ahcd, "snapshot resume? reinit\n");
+ status = -EBUSY;
+ break;
+ default: /* RESET, we lost power */
+ admhc_dbg(ahcd, "lost power\n");
+ status = -EBUSY;
+ }
+ if (status == -EBUSY) {
+ if (!autostopped) {
+ spin_unlock_irq (&ahcd->lock);
+ (void) ahcd_init (ahcd);
+ status = admhc_restart (ahcd);
+ spin_lock_irq(&ahcd->lock);
+ }
+ return status;
+ }
+ if (status != -EINPROGRESS)
+ return status;
+ if (autostopped)
+ goto skip_resume;
+ spin_unlock_irq (&ahcd->lock);
+
+ /* Some controllers (lucent erratum) need extra-long delays */
+ msleep (20 /* usb 11.5.1.10 */ + 12 /* 32 msec counter */ + 1);
+
+ temp = admhc_readl(ahcd, &ahcd->regs->control);
+ temp &= OHCI_CTRL_HCFS;
+ if (temp != OHCI_USB_RESUME) {
+ admhc_err (ahcd, "controller won't resume\n");
+ spin_lock_irq(&ahcd->lock);
+ return -EBUSY;
+ }
+
+ /* disable old schedule state, reinit from scratch */
+ admhc_writel(ahcd, 0, &ahcd->regs->ed_controlhead);
+ admhc_writel(ahcd, 0, &ahcd->regs->ed_controlcurrent);
+ admhc_writel(ahcd, 0, &ahcd->regs->ed_bulkhead);
+ admhc_writel(ahcd, 0, &ahcd->regs->ed_bulkcurrent);
+ admhc_writel(ahcd, 0, &ahcd->regs->ed_periodcurrent);
+ admhc_writel(ahcd, (u32) ahcd->hcca_dma, &ahcd->ahcd->regs->hcca);
+
+ /* Sometimes PCI D3 suspend trashes frame timings ... */
+ periodic_reinit(ahcd);
+
+ /* the following code is executed with ahcd->lock held and
+ * irqs disabled if and only if autostopped is true
+ */
+
+skip_resume:
+ /* interrupts might have been disabled */
+ admhc_writel(ahcd, OHCI_INTR_INIT, &ahcd->regs->int_enable);
+ if (ahcd->ed_rm_list)
+ admhc_writel(ahcd, OHCI_INTR_SF, &ahcd->regs->int_enable);
+
+ /* Then re-enable operations */
+ admhc_writel(ahcd, OHCI_USB_OPER, &ahcd->regs->control);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+ if (!autostopped)
+ msleep (3);
+
+ temp = ahcd->hc_control;
+ temp &= OHCI_CTRL_RWC;
+ temp |= OHCI_CONTROL_INIT | OHCI_USB_OPER;
+ ahcd->hc_control = temp;
+ admhc_writel(ahcd, temp, &ahcd->regs->control);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+
+ /* TRSMRCY */
+ if (!autostopped) {
+ msleep (10);
+ spin_lock_irq(&ahcd->lock);
+ }
+ /* now ahcd->lock is always held and irqs are always disabled */
+
+ /* keep it alive for more than ~5x suspend + resume costs */
+ ahcd->next_statechange = jiffies + STATECHANGE_DELAY;
+
+ /* maybe turn schedules back on */
+ enables = 0;
+ temp = 0;
+ if (!ahcd->ed_rm_list) {
+ if (ahcd->ed_controltail) {
+ admhc_writel(ahcd,
+ find_head (ahcd->ed_controltail)->dma,
+ &ahcd->regs->ed_controlhead);
+ enables |= OHCI_CTRL_CLE;
+ temp |= OHCI_CLF;
+ }
+ if (ahcd->ed_bulktail) {
+ admhc_writel(ahcd, find_head (ahcd->ed_bulktail)->dma,
+ &ahcd->regs->ed_bulkhead);
+ enables |= OHCI_CTRL_BLE;
+ temp |= OHCI_BLF;
+ }
+ }
+ if (hcd->self.bandwidth_isoc_reqs || hcd->self.bandwidth_int_reqs)
+ enables |= OHCI_CTRL_PLE|OHCI_CTRL_IE;
+ if (enables) {
+ admhc_dbg(ahcd, "restarting schedules ... %08x\n", enables);
+ ahcd->hc_control |= enables;
+ admhc_writel(ahcd, ahcd->hc_control, &ahcd->ahcd->regs->control);
+ if (temp)
+ admhc_writel(ahcd, temp, &ahcd->regs->cmdstatus);
+ (void) admhc_readl(ahcd, &ahcd->regs->control);
+ }
+
+ return 0;
+}
+
+static int admhc_bus_suspend(struct usb_hcd *hcd)
+{
+ struct admhcd *ahcd = hcd_to_admhcd(hcd);
+ int rc;
+
+ spin_lock_irq(&ahcd->lock);
+
+ if (unlikely(!HCD_HW_ACCESSIBLE(hcd)))
+ rc = -ESHUTDOWN;
+ else
+ rc = admhc_rh_suspend(ahcd, 0);
+ spin_unlock_irq(&ahcd->lock);
+ return rc;
+}
+
+static int admhc_bus_resume(struct usb_hcd *hcd)
+{
+ struct admhcd *ahcd = hcd_to_admhcd(hcd);
+ int rc;
+
+ if (time_before(jiffies, ahcd->next_statechange))
+ msleep(5);
+
+ spin_lock_irq(&ahcd->lock);
+
+ if (unlikely(!HCD_HW_ACCESSIBLE(hcd)))
+ rc = -ESHUTDOWN;
+ else
+ rc = admhc_rh_resume(ahcd);
+ spin_unlock_irq(&ahcd->lock);
+
+ /* poll until we know a device is connected or we autostop */
+ if (rc == 0)
+ usb_hcd_poll_rh_status(hcd);
+ return rc;
+}
+
+/* Carry out polling-, autostop-, and autoresume-related state changes */
+static int admhc_root_hub_state_changes(struct admhcd *ahcd, int changed,
+ int any_connected)
+{
+ int poll_rh = 1;
+
+ switch (ahcd->hc_control & OHCI_CTRL_HCFS) {
+
+ case OHCI_USB_OPER:
+ /* keep on polling until we know a device is connected
+ * and RHSC is enabled */
+ if (!ahcd->autostop) {
+ if (any_connected ||
+ !device_may_wakeup(&admhcd_to_hcd(ahcd)
+ ->self.root_hub->dev)) {
+ if (admhc_readl(ahcd, &ahcd->regs->int_enable) &
+ OHCI_INTR_RHSC)
+ poll_rh = 0;
+ } else {
+ ahcd->autostop = 1;
+ ahcd->next_statechange = jiffies + HZ;
+ }
+
+ /* if no devices have been attached for one second, autostop */
+ } else {
+ if (changed || any_connected) {
+ ahcd->autostop = 0;
+ ahcd->next_statechange = jiffies +
+ STATECHANGE_DELAY;
+ } else if (time_after_eq(jiffies,
+ ahcd->next_statechange)
+ && !ahcd->ed_rm_list
+ && !(ahcd->hc_control &
+ OHCI_SCHED_ENABLES)) {
+ ahcd_rh_suspend(ahcd, 1);
+ }
+ }
+ break;
+
+ /* if there is a port change, autostart or ask to be resumed */
+ case OHCI_USB_SUSPEND:
+ case OHCI_USB_RESUME:
+ if (changed) {
+ if (ahcd->autostop)
+ admhc_rh_resume(ahcd);
+ else
+ usb_hcd_resume_root_hub(admhcd_to_hcd(ahcd));
+ } else {
+ /* everything is idle, no need for polling */
+ poll_rh = 0;
+ }
+ break;
+ }
+ return poll_rh;
+}
+
+/*-------------------------------------------------------------------------*/
+
+/* must not be called from interrupt context */
+static int admhc_restart(struct admhcd *ahcd)
+{
+ int temp;
+ int i;
+ struct urb_priv *priv;
+
+ /* mark any devices gone, so they do nothing till khubd disconnects.
+ * recycle any "live" eds/tds (and urbs) right away.
+ * later, khubd disconnect processing will recycle the other state,
+ * (either as disconnect/reconnect, or maybe someday as a reset).
+ */
+ spin_lock_irq(&ahcd->lock);
+ admhc_disable(ahcd);
+ usb_root_hub_lost_power(admhcd_to_hcd(ahcd)->self.root_hub);
+ if (!list_empty(&ahcd->pending))
+ admhc_dbg(ahcd, "abort schedule...\n");
+ list_for_each_entry(priv, &ahcd->pending, pending) {
+ struct urb *urb = priv->td[0]->urb;
+ struct ed *ed = priv->ed;
+
+ switch (ed->state) {
+ case ED_OPER:
+ ed->state = ED_UNLINK;
+ ed->hwINFO |= cpu_to_hc32(ahcd, ED_DEQUEUE);
+ ed_deschedule (ahcd, ed);
+
+ ed->ed_next = ahcd->ed_rm_list;
+ ed->ed_prev = NULL;
+ ahcd->ed_rm_list = ed;
+ /* FALLTHROUGH */
+ case ED_UNLINK:
+ break;
+ default:
+ admhc_dbg(ahcd, "bogus ed %p state %d\n",
+ ed, ed->state);
+ }
+
+ if (!urb->unlinked)
+ urb->unlinked = -ESHUTDOWN;
+ }
+ finish_unlinks(ahcd, 0);
+ spin_unlock_irq(&ahcd->lock);
+
+ /* paranoia, in case that didn't work: */
+
+ /* empty the interrupt branches */
+ for (i = 0; i < NUM_INTS; i++) ahcd->load[i] = 0;
+ for (i = 0; i < NUM_INTS; i++) ahcd->hcca->int_table[i] = 0;
+
+ /* no EDs to remove */
+ ahcd->ed_rm_list = NULL;
+
+ /* empty control and bulk lists */
+ ahcd->ed_controltail = NULL;
+ ahcd->ed_bulktail = NULL;
+
+ if ((temp = admhc_run(ahcd)) < 0) {
+ admhc_err(ahcd, "can't restart, %d\n", temp);
+ return temp;
+ } else {
+ /* here we "know" root ports should always stay powered,
+ * and that if we try to turn them back on the root hub
+ * will respond to CSC processing.
+ */
+ i = ahcd->num_ports;
+ while (i--)
+ admhc_writel(ahcd, RH_PS_PSS,
+ &ahcd->regs->portstatus[i]);
+ admhc_dbg(ahcd, "restart complete\n");
+ }
+ return 0;
+}
+
+#else /* CONFIG_PM */
+
+static inline int admhc_rh_resume(struct admhcd *ahcd)
+{
+ return 0;
+}
+
+/* Carry out polling-related state changes.
+ * autostop isn't used when CONFIG_PM is turned off.
+ */
+static int admhc_root_hub_state_changes(struct admhcd *ahcd, int changed,
+ int any_connected)
+{
+ /* If INSM is enabled, don't poll */
+ if (admhc_readl(ahcd, &ahcd->regs->int_enable) & ADMHC_INTR_INSM)
+ return 0;
+
+ /* If no status changes are pending, enable status-change interrupts */
+ if (!changed) {
+ admhc_intr_enable(ahcd, ADMHC_INTR_INSM);
+ return 0;
+ }
+
+ return 1;
+}
+
+#endif /* CONFIG_PM */
+