usb: gadget: Add HSIC Power Management

Add HSIC power management to support HSIC core Low Power Mode and
integrate it with System PM and Runtime PM mechanisms.

Change-Id: Id87d5740e7f229e599f8f6bb17a70ac1d8078bf4
Signed-off-by: Ofir Cohen <ofirc@codeaurora.org>
diff --git a/drivers/usb/gadget/ci13xxx_msm_hsic.c b/drivers/usb/gadget/ci13xxx_msm_hsic.c
index 135c84d..12b6774 100644
--- a/drivers/usb/gadget/ci13xxx_msm_hsic.c
+++ b/drivers/usb/gadget/ci13xxx_msm_hsic.c
@@ -35,14 +35,17 @@
 
 #define MSM_USB_BASE	(mhsic->regs)
 
-#define ULPI_IO_TIMEOUT_USEC	(10 * 1000)
-#define USB_PHY_VDD_DIG_VOL_MIN		1045000 /* uV */
-#define USB_PHY_VDD_DIG_VOL_MAX		1320000 /* uV */
-#define USB_PHY_VDD_DIG_LOAD		49360	/* uA */
-#define LINK_RESET_TIMEOUT_USEC		(250 * 1000)
-#define HSIC_CFG_REG 0x30
-#define HSIC_IO_CAL_PER_REG 0x33
-#define HSIC_DBG1_REG 0x38
+#define ULPI_IO_TIMEOUT_USEC			(10 * 1000)
+#define USB_PHY_VDD_DIG_VOL_SUSP_MIN	500000 /* uV */
+#define USB_PHY_VDD_DIG_VOL_MIN			1045000 /* uV */
+#define USB_PHY_VDD_DIG_VOL_MAX			1320000 /* uV */
+#define USB_PHY_VDD_DIG_LOAD			49360	/* uA */
+#define LINK_RESET_TIMEOUT_USEC			(250 * 1000)
+#define PHY_SUSPEND_TIMEOUT_USEC		(500 * 1000)
+#define PHY_RESUME_TIMEOUT_USEC			(100 * 1000)
+#define HSIC_CFG_REG					0x30
+#define HSIC_IO_CAL_PER_REG				0x33
+#define HSIC_DBG1_REG					0x38
 
 struct msm_hsic_per *the_mhsic;
 
@@ -57,6 +60,11 @@
 	bool				async_int;
 	void __iomem		*regs;
 	int					irq;
+	atomic_t			in_lpm;
+	struct wake_lock	wlock;
+	struct msm_xo_voter	*xo_handle;
+	struct workqueue_struct *wq;
+	struct work_struct	suspend_w;
 };
 
 static int msm_hsic_init_vddcx(struct msm_hsic_per *mhsic, int init)
@@ -302,6 +310,227 @@
 	}
 }
 
+
+#ifdef CONFIG_PM_SLEEP
+static int msm_hsic_suspend(struct msm_hsic_per *mhsic)
+{
+	int cnt = 0, ret;
+	u32 val;
+
+	if (atomic_read(&mhsic->in_lpm)) {
+		dev_dbg(mhsic->dev, "%s called while in lpm\n", __func__);
+		return 0;
+	}
+	disable_irq(mhsic->irq);
+
+	/*
+	 * 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.
+	 */
+	val = readl_relaxed(USB_PORTSC) | PORTSC_PHCD;
+	writel_relaxed(val, USB_PORTSC);
+
+	while (cnt < PHY_SUSPEND_TIMEOUT_USEC) {
+		if (readl_relaxed(USB_PORTSC) & PORTSC_PHCD)
+			break;
+		udelay(1);
+		cnt++;
+	}
+
+	if (cnt >= PHY_SUSPEND_TIMEOUT_USEC) {
+		dev_err(mhsic->dev, "Unable to suspend PHY\n");
+		msm_hsic_reset(mhsic);
+	}
+
+	/*
+	 * 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_relaxed(readl_relaxed(USB_USBCMD) | ASYNC_INTR_CTRL |
+				ULPI_STP_CTRL, USB_USBCMD);
+
+	/*
+	 * Ensure that hardware is put in low power mode before
+	 * clocks are turned OFF and VDD is allowed to minimize.
+	 */
+	mb();
+
+	clk_disable(mhsic->iface_clk);
+	clk_disable(mhsic->core_clk);
+	clk_disable(mhsic->phy_clk);
+	clk_disable(mhsic->cal_clk);
+
+	ret = msm_xo_mode_vote(mhsic->xo_handle, MSM_XO_MODE_OFF);
+	if (ret)
+		dev_err(mhsic->dev, "%s failed to devote for TCXO %d\n"
+				, __func__, ret);
+
+	ret = regulator_set_voltage(mhsic->hsic_vddcx,
+				USB_PHY_VDD_DIG_VOL_SUSP_MIN,
+				USB_PHY_VDD_DIG_VOL_MAX);
+	if (ret < 0)
+		dev_err(mhsic->dev, "unable to set vddcx voltage: min:0.5v max:1.32v\n");
+
+	if (device_may_wakeup(mhsic->dev))
+		enable_irq_wake(mhsic->irq);
+
+	atomic_set(&mhsic->in_lpm, 1);
+	enable_irq(mhsic->irq);
+	wake_unlock(&mhsic->wlock);
+
+	dev_info(mhsic->dev, "HSIC-USB in low power mode\n");
+
+	return 0;
+}
+
+static int msm_hsic_resume(struct msm_hsic_per *mhsic)
+{
+	int cnt = 0, ret;
+	unsigned temp;
+
+	if (!atomic_read(&mhsic->in_lpm)) {
+		dev_dbg(mhsic->dev, "%s called while not in lpm\n", __func__);
+		return 0;
+	}
+
+	wake_lock(&mhsic->wlock);
+	ret = regulator_set_voltage(mhsic->hsic_vddcx,
+				USB_PHY_VDD_DIG_VOL_MIN,
+				USB_PHY_VDD_DIG_VOL_MAX);
+	if (ret < 0)
+		dev_err(mhsic->dev,
+				"unable to set vddcx voltage: min:1.045v max:1.32v\n");
+
+	ret = msm_xo_mode_vote(mhsic->xo_handle, MSM_XO_MODE_ON);
+	if (ret)
+		dev_err(mhsic->dev, "%s failed to vote for TCXO %d\n",
+				__func__, ret);
+
+	clk_enable(mhsic->iface_clk);
+	clk_enable(mhsic->core_clk);
+	clk_enable(mhsic->phy_clk);
+	clk_enable(mhsic->cal_clk);
+
+	temp = readl_relaxed(USB_USBCMD);
+	temp &= ~ASYNC_INTR_CTRL;
+	temp &= ~ULPI_STP_CTRL;
+	writel_relaxed(temp, USB_USBCMD);
+
+	if (!(readl_relaxed(USB_PORTSC) & PORTSC_PHCD))
+		goto skip_phy_resume;
+
+	temp = readl_relaxed(USB_PORTSC) & ~PORTSC_PHCD;
+	writel_relaxed(temp, USB_PORTSC);
+	while (cnt < PHY_RESUME_TIMEOUT_USEC) {
+		if (!(readl_relaxed(USB_PORTSC) & PORTSC_PHCD) &&
+			(readl_relaxed(USB_ULPI_VIEWPORT) & ULPI_SYNC_STATE))
+			break;
+		udelay(1);
+		cnt++;
+	}
+
+	if (cnt >= PHY_RESUME_TIMEOUT_USEC) {
+		/*
+		 * This is a fatal error. Reset the link and
+		 * PHY to make hsic working.
+		 */
+		dev_err(mhsic->dev, "Unable to resume USB. Reset the hsic\n");
+		msm_hsic_reset(mhsic);
+	}
+skip_phy_resume:
+	if (device_may_wakeup(mhsic->dev))
+		disable_irq_wake(mhsic->irq);
+
+	atomic_set(&mhsic->in_lpm, 0);
+
+	if (mhsic->async_int) {
+		mhsic->async_int = false;
+		enable_irq(mhsic->irq);
+	}
+
+	dev_info(mhsic->dev, "HSIC-USB exited from low power mode\n");
+
+	return 0;
+}
+
+static int msm_hsic_pm_suspend(struct device *dev)
+{
+	struct msm_hsic_per *mhsic = dev_get_drvdata(dev);
+
+	dev_dbg(dev, "MSM HSIC Peripheral PM suspend\n");
+
+	return msm_hsic_suspend(mhsic);
+}
+
+#ifdef CONFIG_PM_RUNTIME
+static int msm_hsic_pm_resume(struct device *dev)
+{
+	dev_dbg(dev, "MSM HSIC Peripheral PM resume\n");
+
+	/*
+	 * Do not resume hardware as part of system resume,
+	 * rather, wait for the ASYNC INT from the h/w
+	 */
+	return 0;
+}
+#else
+static int msm_hsic_pm_resume(struct device *dev)
+{
+	struct msm_hsic_per *mhsic = dev_get_drvdata(dev);
+
+	dev_dbg(dev, "MSM HSIC Peripheral PM resume\n");
+
+	return msm_hsic_resume(mhsic);
+}
+#endif
+
+static void msm_hsic_pm_suspend_work(struct work_struct *w)
+{
+	pm_runtime_put_noidle(the_mhsic->dev);
+	pm_runtime_suspend(the_mhsic->dev);
+}
+#endif /* CONFIG_PM_SLEEP */
+
+#ifdef CONFIG_PM_RUNTIME
+static int msm_hsic_runtime_idle(struct device *dev)
+{
+	dev_dbg(dev, "MSM HSIC Peripheral runtime idle\n");
+
+	return 0;
+}
+
+static int msm_hsic_runtime_suspend(struct device *dev)
+{
+	struct msm_hsic_per *mhsic = dev_get_drvdata(dev);
+
+	dev_dbg(dev, "MSM HSIC Peripheral runtime suspend\n");
+
+	return msm_hsic_suspend(mhsic);
+}
+
+static int msm_hsic_runtime_resume(struct device *dev)
+{
+	struct msm_hsic_per *mhsic = dev_get_drvdata(dev);
+
+	dev_dbg(dev, "MSM HSIC Peripheral runtime resume\n");
+	pm_runtime_get_noresume(mhsic->dev);
+
+	return msm_hsic_resume(mhsic);
+}
+#endif
+
+#ifdef CONFIG_PM
+static const struct dev_pm_ops msm_hsic_dev_pm_ops = {
+	SET_SYSTEM_SLEEP_PM_OPS(msm_hsic_pm_suspend, msm_hsic_pm_resume)
+	SET_RUNTIME_PM_OPS(msm_hsic_runtime_suspend, msm_hsic_runtime_resume,
+				msm_hsic_runtime_idle)
+};
+#endif
+
 /**
  * Dummy match function - will be called only for HSIC msm
  * device (msm_device_gadget_hsic_peripheral).
@@ -320,6 +549,15 @@
 
 static irqreturn_t msm_udc_hsic_irq(int irq, void *data)
 {
+	struct msm_hsic_per *mhsic = data;
+
+	if (atomic_read(&mhsic->in_lpm)) {
+		disable_irq_nosync(mhsic->irq);
+		mhsic->async_int = true;
+		pm_request_resume(mhsic->dev);
+		return IRQ_HANDLED;
+	}
+
 	return udc_irq();
 }
 
@@ -338,6 +576,10 @@
 		dev_dbg(dev, "CI13XXX_CONTROLLER_CONNECT_EVENT received\n");
 		msm_hsic_start();
 		break;
+	case CI13XXX_CONTROLLER_SUSPEND_EVENT:
+		dev_dbg(dev, "CI13XXX_CONTROLLER_SUSPEND_EVENT received\n");
+		queue_work(mhsic->wq, &mhsic->suspend_w);
+		break;
 	default:
 		dev_dbg(dev, "unknown ci13xxx_udc event\n");
 		break;
@@ -378,6 +620,15 @@
 		goto error;
 	}
 
+	mhsic->wq = alloc_workqueue("mhsic_wq", WQ_UNBOUND | WQ_MEM_RECLAIM, 1);
+	if (!mhsic->wq) {
+		pr_err("%s: Unable to create workqueue mhsic wq\n",
+				__func__);
+		ret = -ENOMEM;
+		goto error;
+	}
+	INIT_WORK(&mhsic->suspend_w, msm_hsic_pm_suspend_work);
+
 	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 	if (!res) {
 		dev_err(&pdev->dev, "Unable to get memory resource\n");
@@ -392,6 +643,21 @@
 	}
 	dev_info(&pdev->dev, "HSIC Peripheral regs = %p\n", mhsic->regs);
 
+	mhsic->xo_handle = msm_xo_get(MSM_XO_CXO, "hsic_peripheral");
+	if (IS_ERR(mhsic->xo_handle)) {
+		dev_err(&pdev->dev, "%s not able to get the handle "
+			"to vote for TCXO\n", __func__);
+		ret = PTR_ERR(mhsic->xo_handle);
+		goto unmap;
+	}
+
+	ret = msm_xo_mode_vote(mhsic->xo_handle, MSM_XO_MODE_ON);
+	if (ret) {
+		dev_err(&pdev->dev, "%s failed to vote for TCXO %d\n",
+				__func__, ret);
+		goto free_xo_handle;
+	}
+
 	ret = msm_hsic_enable_clocks(pdev, mhsic, true);
 
 	if (ret) {
@@ -422,16 +688,21 @@
 
 	msm_hsic_connect_peripheral(&pdev->dev);
 
+	device_init_wakeup(&pdev->dev, 1);
+	wake_lock_init(&mhsic->wlock, WAKE_LOCK_SUSPEND, dev_name(&pdev->dev));
+	wake_lock(&mhsic->wlock);
+
 	ret = request_irq(mhsic->irq, msm_udc_hsic_irq,
-					  IRQF_SHARED, pdev->name, pdev);
+					  IRQF_SHARED, pdev->name, mhsic);
 	if (ret < 0) {
 		dev_err(&pdev->dev, "request_irq failed\n");
 		ret = -ENODEV;
 		goto udc_remove;
 	}
 
-	pm_runtime_no_callbacks(&pdev->dev);
+	pm_runtime_set_active(&pdev->dev);
 	pm_runtime_enable(&pdev->dev);
+	pm_runtime_get_sync(&pdev->dev);
 
 	return 0;
 udc_remove:
@@ -440,9 +711,13 @@
 	msm_hsic_init_vddcx(mhsic, 0);
 deinit_clocks:
 	msm_hsic_enable_clocks(pdev, mhsic, 0);
+	msm_xo_mode_vote(mhsic->xo_handle, MSM_XO_MODE_OFF);
+free_xo_handle:
+	msm_xo_put(mhsic->xo_handle);
 unmap:
 	iounmap(mhsic->regs);
 error:
+	destroy_workqueue(mhsic->wq);
 	kfree(mhsic);
 	return ret;
 }
@@ -452,19 +727,29 @@
 	struct msm_hsic_per *mhsic = platform_get_drvdata(pdev);
 
 	device_init_wakeup(&pdev->dev, 0);
+	pm_runtime_disable(&pdev->dev);
+	pm_runtime_set_suspended(&pdev->dev);
+
 	msm_hsic_init_vddcx(mhsic, 0);
 	msm_hsic_enable_clocks(pdev, mhsic, 0);
+	msm_xo_put(mhsic->xo_handle);
+	wake_lock_destroy(&mhsic->wlock);
+	destroy_workqueue(mhsic->wq);
 	udc_remove();
 	iounmap(mhsic->regs);
 	kfree(mhsic);
 
 	return 0;
 }
+
 static struct platform_driver msm_hsic_peripheral_driver = {
 	.probe	= msm_hsic_probe,
 	.remove	= __devexit_p(hsic_msm_remove),
 	.driver = {
 		.name = "msm_hsic_peripheral",
+#ifdef CONFIG_PM
+		.pm = &msm_hsic_dev_pm_ops,
+#endif
 	},
 };
 
diff --git a/drivers/usb/gadget/ci13xxx_udc.c b/drivers/usb/gadget/ci13xxx_udc.c
index b64a38a..562b36a 100644
--- a/drivers/usb/gadget/ci13xxx_udc.c
+++ b/drivers/usb/gadget/ci13xxx_udc.c
@@ -3176,6 +3176,9 @@
 				udc->suspended = 1;
 				spin_unlock(udc->lock);
 				udc->driver->suspend(&udc->gadget);
+				if (udc->udc_driver->notify_event)
+					udc->udc_driver->notify_event(udc,
+					  CI13XXX_CONTROLLER_SUSPEND_EVENT);
 				spin_lock(udc->lock);
 			}
 			isr_statistics.sli++;
diff --git a/drivers/usb/gadget/ci13xxx_udc.h b/drivers/usb/gadget/ci13xxx_udc.h
index 13b54c7..684517b 100644
--- a/drivers/usb/gadget/ci13xxx_udc.h
+++ b/drivers/usb/gadget/ci13xxx_udc.h
@@ -129,6 +129,7 @@
 
 #define CI13XXX_CONTROLLER_RESET_EVENT		0
 #define CI13XXX_CONTROLLER_CONNECT_EVENT	1
+#define CI13XXX_CONTROLLER_SUSPEND_EVENT	2
 	void	(*notify_event) (struct ci13xxx *udc, unsigned event);
 };