summaryrefslogtreecommitdiffstats
path: root/drivers/usb/otg
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/usb/otg')
-rw-r--r--drivers/usb/otg/Kconfig5
-rw-r--r--drivers/usb/otg/msm72k_otg.c283
2 files changed, 282 insertions, 6 deletions
diff --git a/drivers/usb/otg/Kconfig b/drivers/usb/otg/Kconfig
index 915c729872f8..2810c2af71b0 100644
--- a/drivers/usb/otg/Kconfig
+++ b/drivers/usb/otg/Kconfig
@@ -88,7 +88,8 @@ config USB_MSM_OTG_72K
help
Enable this to support the USB OTG transceiver on MSM chips. It
handles PHY initialization, clock management, and workarounds
- required after resetting the hardware. This driver is required
- even for peripheral only or host only mode configuration.
+ required after resetting the hardware and power management.
+ This driver is required even for peripheral only or host only
+ mode configurations.
endif # USB || OTG
diff --git a/drivers/usb/otg/msm72k_otg.c b/drivers/usb/otg/msm72k_otg.c
index 46f468a912f4..1cd52edcd0c2 100644
--- a/drivers/usb/otg/msm72k_otg.c
+++ b/drivers/usb/otg/msm72k_otg.c
@@ -29,6 +29,7 @@
#include <linux/uaccess.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
+#include <linux/pm_runtime.h>
#include <linux/usb.h>
#include <linux/usb/otg.h>
@@ -251,6 +252,154 @@ static int msm_otg_reset(struct otg_transceiver *otg)
return 0;
}
+#define PHY_SUSPEND_TIMEOUT_USEC (500 * 1000)
+static int msm_otg_suspend(struct msm_otg *motg)
+{
+ struct otg_transceiver *otg = &motg->otg;
+ struct usb_bus *bus = otg->host;
+ struct msm_otg_platform_data *pdata = motg->pdata;
+ int cnt = 0;
+
+ if (atomic_read(&motg->in_lpm))
+ return 0;
+
+ disable_irq(motg->irq);
+ /*
+ * Interrupt Latch Register auto-clear feature is not present
+ * in all PHY versions. Latch register is clear on read type.
+ * Clear latch register to avoid spurious wakeup from
+ * low power mode (LPM).
+ */
+ ulpi_read(otg, 0x14);
+
+ /*
+ * PHY comparators are disabled when PHY enters into low power
+ * mode (LPM). Keep PHY comparators ON in LPM only when we expect
+ * VBUS/Id notifications from USB PHY. Otherwise turn off USB
+ * PHY comparators. This save significant amount of power.
+ */
+ if (pdata->otg_control == OTG_PHY_CONTROL)
+ ulpi_write(otg, 0x01, 0x30);
+
+ /*
+ * PLL is not turned off when PHY enters into low power mode (LPM).
+ * Disable PLL for maximum power savings.
+ */
+ ulpi_write(otg, 0x08, 0x09);
+
+ /*
+ * PHY may take some time or even fail to enter into low power
+ * mode (LPM). Hence poll for 500 msec and reset the PHY and link
+ * in failure case.
+ */
+ writel(readl(USB_PORTSC) | PORTSC_PHCD, USB_PORTSC);
+ while (cnt < PHY_SUSPEND_TIMEOUT_USEC) {
+ if (readl(USB_PORTSC) & PORTSC_PHCD)
+ break;
+ udelay(1);
+ cnt++;
+ }
+
+ if (cnt >= PHY_SUSPEND_TIMEOUT_USEC) {
+ dev_err(otg->dev, "Unable to suspend PHY\n");
+ msm_otg_reset(otg);
+ enable_irq(motg->irq);
+ return -ETIMEDOUT;
+ }
+
+ /*
+ * PHY has capability to generate interrupt asynchronously in low
+ * power mode (LPM). This interrupt is level triggered. So USB IRQ
+ * line must be disabled till async interrupt enable bit is cleared
+ * in USBCMD register. Assert STP (ULPI interface STOP signal) to
+ * block data communication from PHY.
+ */
+ writel(readl(USB_USBCMD) | ASYNC_INTR_CTRL | ULPI_STP_CTRL, USB_USBCMD);
+
+ clk_disable(motg->pclk);
+ clk_disable(motg->clk);
+ if (motg->core_clk)
+ clk_disable(motg->core_clk);
+
+ if (device_may_wakeup(otg->dev))
+ enable_irq_wake(motg->irq);
+ if (bus)
+ clear_bit(HCD_FLAG_HW_ACCESSIBLE, &(bus_to_hcd(bus))->flags);
+
+ atomic_set(&motg->in_lpm, 1);
+ enable_irq(motg->irq);
+
+ dev_info(otg->dev, "USB in low power mode\n");
+
+ return 0;
+}
+
+#define PHY_RESUME_TIMEOUT_USEC (100 * 1000)
+static int msm_otg_resume(struct msm_otg *motg)
+{
+ struct otg_transceiver *otg = &motg->otg;
+ struct usb_bus *bus = otg->host;
+ int cnt = 0;
+ unsigned temp;
+
+ if (!atomic_read(&motg->in_lpm))
+ return 0;
+
+ clk_enable(motg->pclk);
+ clk_enable(motg->clk);
+ if (motg->core_clk)
+ clk_enable(motg->core_clk);
+
+ temp = readl(USB_USBCMD);
+ temp &= ~ASYNC_INTR_CTRL;
+ temp &= ~ULPI_STP_CTRL;
+ writel(temp, USB_USBCMD);
+
+ /*
+ * PHY comes out of low power mode (LPM) in case of wakeup
+ * from asynchronous interrupt.
+ */
+ if (!(readl(USB_PORTSC) & PORTSC_PHCD))
+ goto skip_phy_resume;
+
+ writel(readl(USB_PORTSC) & ~PORTSC_PHCD, USB_PORTSC);
+ while (cnt < PHY_RESUME_TIMEOUT_USEC) {
+ if (!(readl(USB_PORTSC) & PORTSC_PHCD))
+ break;
+ udelay(1);
+ cnt++;
+ }
+
+ if (cnt >= PHY_RESUME_TIMEOUT_USEC) {
+ /*
+ * This is a fatal error. Reset the link and
+ * PHY. USB state can not be restored. Re-insertion
+ * of USB cable is the only way to get USB working.
+ */
+ dev_err(otg->dev, "Unable to resume USB."
+ "Re-plugin the cable\n");
+ msm_otg_reset(otg);
+ }
+
+skip_phy_resume:
+ if (device_may_wakeup(otg->dev))
+ disable_irq_wake(motg->irq);
+ if (bus)
+ set_bit(HCD_FLAG_HW_ACCESSIBLE, &(bus_to_hcd(bus))->flags);
+
+ if (motg->async_int) {
+ motg->async_int = 0;
+ pm_runtime_put(otg->dev);
+ enable_irq(motg->irq);
+ }
+
+ atomic_set(&motg->in_lpm, 0);
+
+ dev_info(otg->dev, "USB exited from low power mode\n");
+
+ return 0;
+}
+
static void msm_otg_start_host(struct otg_transceiver *otg, int on)
{
struct msm_otg *motg = container_of(otg, struct msm_otg, otg);
@@ -306,6 +455,7 @@ static int msm_otg_set_host(struct otg_transceiver *otg, struct usb_bus *host)
if (!host) {
if (otg->state == OTG_STATE_A_HOST) {
+ pm_runtime_get_sync(otg->dev);
msm_otg_start_host(otg, 0);
otg->host = NULL;
otg->state = OTG_STATE_UNDEFINED;
@@ -327,8 +477,10 @@ static int msm_otg_set_host(struct otg_transceiver *otg, struct usb_bus *host)
* Kick the state machine work, if peripheral is not supported
* or peripheral is already registered with us.
*/
- if (motg->pdata->mode == USB_HOST || otg->gadget)
+ if (motg->pdata->mode == USB_HOST || otg->gadget) {
+ pm_runtime_get_sync(otg->dev);
schedule_work(&motg->sm_work);
+ }
return 0;
}
@@ -376,6 +528,7 @@ static int msm_otg_set_peripheral(struct otg_transceiver *otg,
if (!gadget) {
if (otg->state == OTG_STATE_B_PERIPHERAL) {
+ pm_runtime_get_sync(otg->dev);
msm_otg_start_peripheral(otg, 0);
otg->gadget = NULL;
otg->state = OTG_STATE_UNDEFINED;
@@ -393,8 +546,10 @@ static int msm_otg_set_peripheral(struct otg_transceiver *otg,
* Kick the state machine work, if host is not supported
* or host is already registered with us.
*/
- if (motg->pdata->mode == USB_PERIPHERAL || otg->host)
+ if (motg->pdata->mode == USB_PERIPHERAL || otg->host) {
+ pm_runtime_get_sync(otg->dev);
schedule_work(&motg->sm_work);
+ }
return 0;
}
@@ -473,6 +628,7 @@ static void msm_otg_sm_work(struct work_struct *w)
msm_otg_start_peripheral(otg, 1);
otg->state = OTG_STATE_B_PERIPHERAL;
}
+ pm_runtime_put_sync(otg->dev);
break;
case OTG_STATE_B_PERIPHERAL:
dev_dbg(otg->dev, "OTG_STATE_B_PERIPHERAL state\n");
@@ -504,6 +660,13 @@ static irqreturn_t msm_otg_irq(int irq, void *data)
struct otg_transceiver *otg = &motg->otg;
u32 otgsc = 0;
+ if (atomic_read(&motg->in_lpm)) {
+ disable_irq_nosync(irq);
+ motg->async_int = 1;
+ pm_runtime_get(otg->dev);
+ return IRQ_HANDLED;
+ }
+
otgsc = readl(USB_OTGSC);
if (!(otgsc & (OTGSC_IDIS | OTGSC_BSVIS)))
return IRQ_NONE;
@@ -514,12 +677,14 @@ static irqreturn_t msm_otg_irq(int irq, void *data)
else
clear_bit(ID, &motg->inputs);
dev_dbg(otg->dev, "ID set/clear\n");
+ pm_runtime_get_noresume(otg->dev);
} else if ((otgsc & OTGSC_BSVIS) && (otgsc & OTGSC_BSVIE)) {
if (otgsc & OTGSC_BSV)
set_bit(B_SESS_VLD, &motg->inputs);
else
clear_bit(B_SESS_VLD, &motg->inputs);
dev_dbg(otg->dev, "BSV set/clear\n");
+ pm_runtime_get_noresume(otg->dev);
}
writel(otgsc, USB_OTGSC);
@@ -616,6 +781,7 @@ static ssize_t msm_otg_mode_write(struct file *file, const char __user *ubuf,
goto out;
}
+ pm_runtime_get_sync(otg->dev);
schedule_work(&motg->sm_work);
out:
return status;
@@ -770,8 +936,10 @@ static int __init msm_otg_probe(struct platform_device *pdev)
"not available\n");
}
- return 0;
+ pm_runtime_set_active(&pdev->dev);
+ pm_runtime_enable(&pdev->dev);
+ return 0;
free_irq:
free_irq(motg->irq, motg);
disable_clks:
@@ -796,23 +964,45 @@ static int __devexit msm_otg_remove(struct platform_device *pdev)
{
struct msm_otg *motg = platform_get_drvdata(pdev);
struct otg_transceiver *otg = &motg->otg;
+ int cnt = 0;
if (otg->host || otg->gadget)
return -EBUSY;
msm_otg_debugfs_cleanup();
cancel_work_sync(&motg->sm_work);
+
+ msm_otg_resume(motg);
+
device_init_wakeup(&pdev->dev, 0);
- otg_set_transceiver(NULL);
+ pm_runtime_disable(&pdev->dev);
+ otg_set_transceiver(NULL);
free_irq(motg->irq, motg);
+ /*
+ * Put PHY in low power mode.
+ */
+ ulpi_read(otg, 0x14);
+ ulpi_write(otg, 0x08, 0x09);
+
+ writel(readl(USB_PORTSC) | PORTSC_PHCD, USB_PORTSC);
+ while (cnt < PHY_SUSPEND_TIMEOUT_USEC) {
+ if (readl(USB_PORTSC) & PORTSC_PHCD)
+ break;
+ udelay(1);
+ cnt++;
+ }
+ if (cnt >= PHY_SUSPEND_TIMEOUT_USEC)
+ dev_err(otg->dev, "Unable to suspend PHY\n");
+
clk_disable(motg->pclk);
clk_disable(motg->clk);
if (motg->core_clk)
clk_disable(motg->core_clk);
iounmap(motg->regs);
+ pm_runtime_set_suspended(&pdev->dev);
clk_put(motg->phy_reset_clk);
clk_put(motg->pclk);
@@ -825,11 +1015,96 @@ static int __devexit msm_otg_remove(struct platform_device *pdev)
return 0;
}
+#ifdef CONFIG_PM_RUNTIME
+static int msm_otg_runtime_idle(struct device *dev)
+{
+ struct msm_otg *motg = dev_get_drvdata(dev);
+ struct otg_transceiver *otg = &motg->otg;
+
+ dev_dbg(dev, "OTG runtime idle\n");
+
+ /*
+ * It is observed some times that a spurious interrupt
+ * comes when PHY is put into LPM immediately after PHY reset.
+ * This 1 sec delay also prevents entering into LPM immediately
+ * after asynchronous interrupt.
+ */
+ if (otg->state != OTG_STATE_UNDEFINED)
+ pm_schedule_suspend(dev, 1000);
+
+ return -EAGAIN;
+}
+
+static int msm_otg_runtime_suspend(struct device *dev)
+{
+ struct msm_otg *motg = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "OTG runtime suspend\n");
+ return msm_otg_suspend(motg);
+}
+
+static int msm_otg_runtime_resume(struct device *dev)
+{
+ struct msm_otg *motg = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "OTG runtime resume\n");
+ return msm_otg_resume(motg);
+}
+#else
+#define msm_otg_runtime_idle NULL
+#define msm_otg_runtime_suspend NULL
+#define msm_otg_runtime_resume NULL
+#endif
+
+#ifdef CONFIG_PM
+static int msm_otg_pm_suspend(struct device *dev)
+{
+ struct msm_otg *motg = dev_get_drvdata(dev);
+
+ dev_dbg(dev, "OTG PM suspend\n");
+ return msm_otg_suspend(motg);
+}
+
+static int msm_otg_pm_resume(struct device *dev)
+{
+ struct msm_otg *motg = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(dev, "OTG PM resume\n");
+
+ ret = msm_otg_resume(motg);
+ if (ret)
+ return ret;
+
+ /*
+ * Runtime PM Documentation recommends bringing the
+ * device to full powered state upon resume.
+ */
+ pm_runtime_disable(dev);
+ pm_runtime_set_active(dev);
+ pm_runtime_enable(dev);
+
+ return 0;
+}
+#else
+#define msm_otg_pm_suspend NULL
+#define msm_otg_pm_resume NULL
+#endif
+
+static const struct dev_pm_ops msm_otg_dev_pm_ops = {
+ .runtime_suspend = msm_otg_runtime_suspend,
+ .runtime_resume = msm_otg_runtime_resume,
+ .runtime_idle = msm_otg_runtime_idle,
+ .suspend = msm_otg_pm_suspend,
+ .resume = msm_otg_pm_resume,
+};
+
static struct platform_driver msm_otg_driver = {
.remove = __devexit_p(msm_otg_remove),
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
+ .pm = &msm_otg_dev_pm_ops,
},
};