USB: OTG: msm: Add support for power management
authorPavankumar Kondeti <pkondeti@codeaurora.org>
Tue, 7 Dec 2010 12:23:58 +0000 (17:53 +0530)
committerGreg Kroah-Hartman <gregkh@suse.de>
Fri, 10 Dec 2010 22:23:32 +0000 (14:23 -0800)
Implement runtime and system pm ops to put hardware into low power
mode (LPM). As part of LPM, USB clocks are turned off, PHY is put
into suspend state and PHY comparators are turned off if VBUS/Id
notifications are not required from PHY.

Signed-off-by: Pavankumar Kondeti <pkondeti@codeaurora.org>
Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de>
drivers/usb/otg/Kconfig
drivers/usb/otg/msm72k_otg.c
include/linux/usb/msm_hsusb.h
include/linux/usb/msm_hsusb_hw.h

index 915c729872f8ebb7b4024245918e8d53a5d05ef2..2810c2af71b0e879f2d20eb870de9cdb480cd2df 100644 (file)
@@ -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
index 46f468a912f4c41f45fbddb43a7a2721afe0d180..1cd52edcd0c27d51c122ebce119158b63cd0cd78 100644 (file)
@@ -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,
        },
 };
 
index b796df94ec72a0db646312d61d9adf3a6c1e253e..3675e03b15392073b8cdac519ee9470721f7f7c0 100644 (file)
@@ -88,6 +88,8 @@ struct msm_otg_platform_data {
  * @regs: ioremapped register base address.
  * @inputs: OTG state machine inputs(Id, SessValid etc).
  * @sm_work: OTG state machine work.
+ * @in_lpm: indicates low power mode (LPM) state.
+ * @async_int: Async interrupt arrived.
  *
  */
 struct msm_otg {
@@ -103,6 +105,8 @@ struct msm_otg {
 #define B_SESS_VLD     1
        unsigned long inputs;
        struct work_struct sm_work;
+       atomic_t in_lpm;
+       int async_int;
 };
 
 #endif
index b061cffcf048cf5d945f99e78fc77e81145f3b50..b92e17349c7b9f1b7c00ebd92f46de734daca87e 100644 (file)
@@ -44,6 +44,9 @@
 #define ULPI_DATA(n)          ((n) & 255)
 #define ULPI_DATA_READ(n)     (((n) >> 8) & 255)
 
+#define ASYNC_INTR_CTRL         (1 << 29) /* Enable async interrupt */
+#define ULPI_STP_CTRL           (1 << 30) /* Block communication with PHY */
+
 /* OTG definitions */
 #define OTGSC_INTSTS_MASK      (0x7f << 16)
 #define OTGSC_ID               (1 << 8)