diff options
Diffstat (limited to 'drivers/usb/otg/msm72k_otg.c')
-rw-r--r-- | drivers/usb/otg/msm72k_otg.c | 283 |
1 files changed, 279 insertions, 4 deletions
diff --git a/drivers/usb/otg/msm72k_otg.c b/drivers/usb/otg/msm72k_otg.c index 46f468a912f..1cd52edcd0c 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, }, }; |