Add support for the HSIC PHY present in the Qualcomm MSM9615 SoC.
This PHY is also present on other SoCs and would need some changes.

Signed-off-by: Neil Armstrong <narmstr...@baylibre.com>
---
 drivers/usb/phy/Kconfig             |  13 +
 drivers/usb/phy/Makefile            |   1 +
 drivers/usb/phy/phy-qcom-hsic-usb.c | 506 ++++++++++++++++++++++++++++++++++++
 3 files changed, 520 insertions(+)
 create mode 100644 drivers/usb/phy/phy-qcom-hsic-usb.c

diff --git a/drivers/usb/phy/Kconfig b/drivers/usb/phy/Kconfig
index c690474..b131e50 100644
--- a/drivers/usb/phy/Kconfig
+++ b/drivers/usb/phy/Kconfig
@@ -166,6 +166,19 @@ config USB_QCOM_8X16_PHY
          To compile this driver as a module, choose M here: the
          module will be called phy-qcom-8x16-usb.
 
+config USB_QCOM_HSIC_PHY
+       tristate "Qualcomm HSIC USB PHY controller support"
+       depends on ARCH_QCOM || COMPILE_TEST
+       depends on RESET_CONTROLLER && EXTCON
+       select USB_PHY
+       help
+         Enable this to support the HSIC USB transceiver on Qualcomm chipsets.
+         It handles PHY initialization, clock management, power management,
+         and workarounds required after resetting the hardware.
+
+         To compile this driver as a module, choose M here: the
+         module will be called phy-qcom-hsic-usb.
+
 config USB_MV_OTG
        tristate "Marvell USB OTG support"
        depends on USB_EHCI_MV && USB_MV_UDC && PM && USB_OTG
diff --git a/drivers/usb/phy/Makefile b/drivers/usb/phy/Makefile
index b433e5d..f250632 100644
--- a/drivers/usb/phy/Makefile
+++ b/drivers/usb/phy/Makefile
@@ -21,6 +21,7 @@ obj-$(CONFIG_USB_GPIO_VBUS)           += phy-gpio-vbus-usb.o
 obj-$(CONFIG_USB_ISP1301)              += phy-isp1301.o
 obj-$(CONFIG_USB_MSM_OTG)              += phy-msm-usb.o
 obj-$(CONFIG_USB_QCOM_8X16_PHY)        += phy-qcom-8x16-usb.o
+obj-$(CONFIG_USB_QCOM_HSIC_PHY)        += phy-qcom-hsic-usb.o
 obj-$(CONFIG_USB_MV_OTG)               += phy-mv-usb.o
 obj-$(CONFIG_USB_MXS_PHY)              += phy-mxs-usb.o
 obj-$(CONFIG_USB_ULPI)                 += phy-ulpi.o
diff --git a/drivers/usb/phy/phy-qcom-hsic-usb.c 
b/drivers/usb/phy/phy-qcom-hsic-usb.c
new file mode 100644
index 0000000..7d233cb
--- /dev/null
+++ b/drivers/usb/phy/phy-qcom-hsic-usb.c
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2015, Baylibre SAS
+ * Based on phy-qcom-hsic-usb.c
+ * Copyright (c) 2015, Linaro Limited
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 and
+ * only version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/extcon.h>
+#include <linux/gpio/consumer.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reboot.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+#include <linux/usb/ulpi.h>
+#include <linux/usb/hcd.h>
+#include <linux/usb/msm_hsusb_hw.h>
+#include <linux/regmap.h>
+#include <linux/gpio.h>
+#include <linux/of_gpio.h>
+#include <linux/mfd/syscon.h>
+
+#define HSPHY_ULPI_VIEWPORT    0x0170
+
+#define USB_PHY_VDD_DIG_VOL_MIN        1000000 /* uV */
+#define USB_PHY_VDD_DIG_VOL_MAX        1320000 /* uV */
+#define USB_PHY_SUSP_DIG_VOL   500000  /* uV */
+
+#define ULPI_IO_TIMEOUT_USEC   (10 * 1000)
+
+#define HSIC_DBG1_REG          0x38
+#define HSIC_CFG_REG           0x30
+#define HSIC_CFG1_REG          0x31
+#define HSIC_IO_CAL_PER_REG    0x33
+
+#define HSIC_CAL_PAD_CTL       0x20C8
+#define HSIC_LV_MODE           0x04
+#define HSIC_PAD_CALIBRATION   0xA8
+
+#define MSM_USB_BASE (qphy->regs)
+
+enum vdd_levels {
+       VDD_LEVEL_NONE = 0,
+       VDD_LEVEL_MIN,
+       VDD_LEVEL_MAX,
+};
+
+struct phy_hsic {
+       struct usb_phy                  phy;
+       void __iomem                    *regs;
+       struct clk                      *core_clk;
+       struct clk                      *alt_core_clk;
+       struct clk                      *phy_clk;
+       struct clk                      *cal_clk;
+       struct clk                      *iface_clk;
+       struct regulator                *vdd;
+
+       struct reset_control            *link_reset;
+
+       int vdd_levels[3];
+
+       struct usb_bus                  *host;
+
+       struct regmap                   *tlmm;
+       u32                             tlmm_cfg[4];
+
+       int                             hsic_gpios[2];
+       bool                            hsic_gpios_en;
+};
+
+static int ulpi_read(struct usb_phy *phy, u32 reg)
+{
+       struct phy_hsic *qphy = container_of(phy, struct phy_hsic, phy);
+       int cnt = 0;
+
+       /* initiate read operation */
+       writel(ULPI_RUN | ULPI_READ | ULPI_ADDR(reg),
+              USB_ULPI_VIEWPORT);
+
+       /* wait for completion */
+       while (cnt < ULPI_IO_TIMEOUT_USEC) {
+               if (!(readl(USB_ULPI_VIEWPORT) & ULPI_RUN))
+                       break;
+               udelay(1);
+               cnt++;
+       }
+
+       if (cnt >= ULPI_IO_TIMEOUT_USEC) {
+               dev_err(phy->dev, "ulpi_read: timeout %08x\n",
+                       readl(USB_ULPI_VIEWPORT));
+               return -ETIMEDOUT;
+       }
+       return ULPI_DATA_READ(readl(USB_ULPI_VIEWPORT));
+}
+
+static int ulpi_write(struct usb_phy *phy, u32 val, u32 reg)
+{
+       struct phy_hsic *qphy = container_of(phy, struct phy_hsic, phy);
+       int cnt = 0;
+
+       /* initiate write operation */
+       writel(ULPI_RUN | ULPI_WRITE |
+              ULPI_ADDR(reg) | ULPI_DATA(val),
+              USB_ULPI_VIEWPORT);
+
+       /* wait for completion */
+       while (cnt < ULPI_IO_TIMEOUT_USEC) {
+               if (!(readl(USB_ULPI_VIEWPORT) & ULPI_RUN))
+                       break;
+               udelay(1);
+               cnt++;
+       }
+
+       if (cnt >= ULPI_IO_TIMEOUT_USEC) {
+               dev_err(phy->dev, "ulpi_write: timeout\n");
+               return -ETIMEDOUT;
+       }
+       return 0;
+}
+
+static struct usb_phy_io_ops qcom_hsic_io_ops = {
+       .read = ulpi_read,
+       .write = ulpi_write,
+};
+
+static int phy_hsic_regulators_enable(struct phy_hsic *qphy)
+{
+       int ret;
+
+       ret = regulator_set_voltage(qphy->vdd,
+                               qphy->vdd_levels[VDD_LEVEL_MIN],
+                               qphy->vdd_levels[VDD_LEVEL_MAX]);
+       if (ret)
+               return ret;
+
+       ret = regulator_enable(qphy->vdd);
+       if (ret)
+               return ret;
+
+       return 0;
+}
+
+static void phy_hsic_regulators_disable(struct phy_hsic *qphy)
+{
+       regulator_disable(qphy->vdd);
+}
+
+static int phy_hsic_clock_reset(struct phy_hsic *qphy)
+{
+       /* Reset sequence */
+       if (!IS_ERR(qphy->link_reset))
+               reset_control_assert(qphy->link_reset);
+
+       clk_disable(qphy->core_clk);
+       clk_disable(qphy->alt_core_clk);
+
+       if (!IS_ERR(qphy->link_reset))
+               reset_control_deassert(qphy->link_reset);
+
+       usleep_range(10000, 12000);
+
+       clk_enable(qphy->core_clk);
+       clk_enable(qphy->alt_core_clk);
+
+       return 0;
+}
+
+static int phy_hsic_reset(struct phy_hsic *qphy)
+{
+       phy_hsic_clock_reset(qphy);
+
+       /* select ULPI phy and clear other status/control bits in PORTSC */
+       writel_relaxed(0x80000000, USB_PORTSC);
+
+       /* Be sure PORTSC is written */
+       mb();
+
+       if (qphy->tlmm &&
+           qphy->hsic_gpios[0] > 0 &&
+           qphy->hsic_gpios[1] > 0) {
+
+               /* Enable LV_MODE in HSIC_CAL_PAD_CTL register */
+               regmap_write(qphy->tlmm, HSIC_CAL_PAD_CTL, HSIC_LV_MODE);
+
+               /* Be sure register is written */
+               mb();
+
+               /* set periodic calibration interval to ~2.048sec */
+               ulpi_write(&qphy->phy, 0xFF, HSIC_IO_CAL_PER_REG);
+
+               /* Enable periodic IO calibration in HSIC_CFG register */
+               ulpi_write(&qphy->phy, 0xA8, HSIC_CFG_REG);
+
+               /* Configure GPIO pins for HSIC functionality mode */
+               if (!qphy->hsic_gpios_en) {
+                       gpio_request(qphy->hsic_gpios[0], "HSIC_GPIO0");
+                       gpio_request(qphy->hsic_gpios[1], "HSIC_GPIO1");
+                       qphy->hsic_gpios_en = true;
+               }
+
+               /* Set LV_MODE=0x1 and DCC=0x2 in HSIC_GPIO PAD_CTL register */
+               regmap_write(qphy->tlmm, qphy->tlmm_cfg[0],
+                                        qphy->tlmm_cfg[1]);
+               regmap_write(qphy->tlmm, qphy->tlmm_cfg[2],
+                                        qphy->tlmm_cfg[3]);
+
+               /* Enable HSIC mode in HSIC_CFG register */
+               ulpi_write(&qphy->phy, 0x01, HSIC_CFG1_REG);
+
+       } else {
+               /* Setup HSIC pads */
+               if (qphy->tlmm) {
+                       regmap_write(qphy->tlmm, qphy->tlmm_cfg[0],
+                                                qphy->tlmm_cfg[1]);
+                       regmap_write(qphy->tlmm, qphy->tlmm_cfg[2],
+                                                qphy->tlmm_cfg[3]);
+               }
+
+               /* programmable length of connect signaling (33.2ns) */
+               ulpi_write(&qphy->phy, 3, HSIC_DBG1_REG);
+
+               /* set periodic calibration interval to ~2.048sec */
+               ulpi_write(&qphy->phy, 0xFF, HSIC_IO_CAL_PER_REG);
+
+               /* Enable HSIC mode in HSIC_CFG register */
+               ulpi_write(&qphy->phy, 0xA9, HSIC_CFG_REG);
+       }
+
+       /* Disable auto resume */
+       ulpi_write(&qphy->phy, ULPI_IFC_CTRL_AUTORESUME,
+                       ULPI_CLR(ULPI_IFC_CTRL));
+
+       return 0;
+};
+
+static int phy_hsic_init(struct usb_phy *phy)
+{
+       struct phy_hsic *qphy = container_of(phy, struct phy_hsic, phy);
+
+       /* bursts of unspecified length. */
+       writel_relaxed(0, USB_AHBBURST);
+
+       /* Use the AHB transactor */
+       writel_relaxed(0x08, USB_AHBMODE);
+
+       /* Disable streaming mode and select host mode */
+       writel_relaxed(0x13, USB_USBMODE);
+
+       return 0;
+}
+
+static int phy_hsic_set_host(struct usb_otg *otg, struct usb_bus *host)
+{
+       struct phy_hsic *qphy =
+               container_of(otg->usb_phy, struct phy_hsic, phy);
+       struct usb_hcd *hcd;
+
+       dev_info(otg->usb_phy->dev, "%s()\n", __func__);
+
+       if (!host) {
+               if (!qphy->host)
+                       return -EINVAL;
+
+               hcd = bus_to_hcd(host);
+
+               usb_remove_hcd(hcd);
+
+               qphy->host = NULL;
+
+               dev_dbg(otg->usb_phy->dev, "host off\n");
+
+       } else {
+               if (qphy->host) {
+                       dev_err(otg->usb_phy->dev, "host already registered\n");
+                       return -EFAULT;
+               }
+
+               phy_hsic_init(otg->usb_phy);
+
+               hcd = bus_to_hcd(host);
+
+               usb_add_hcd(hcd, hcd->irq, IRQF_SHARED);
+
+               device_wakeup_enable(hcd->self.controller);
+
+               dev_dbg(otg->usb_phy->dev, "host on\n");
+
+               qphy->host = host;
+       }
+
+       return 0;
+}
+
+static int phy_hsic_read_devicetree(struct phy_hsic *qphy)
+{
+       struct regulator_bulk_data regs[1];
+       struct device *dev = qphy->phy.dev;
+       u32 tmp[3];
+       int ret;
+       int len;
+
+       qphy->core_clk = devm_clk_get(dev, "core");
+       if (IS_ERR(qphy->core_clk))
+               return PTR_ERR(qphy->core_clk);
+
+       qphy->alt_core_clk = devm_clk_get(dev, "alt-core");
+       if (IS_ERR(qphy->alt_core_clk))
+               return PTR_ERR(qphy->alt_core_clk);
+
+       qphy->phy_clk = devm_clk_get(dev, "phy");
+       if (IS_ERR(qphy->phy_clk))
+               return PTR_ERR(qphy->phy_clk);
+
+       qphy->cal_clk = devm_clk_get(dev, "cal");
+       if (IS_ERR(qphy->cal_clk))
+               return PTR_ERR(qphy->cal_clk);
+
+       qphy->iface_clk = devm_clk_get(dev, "iface");
+       if (IS_ERR(qphy->iface_clk))
+               return PTR_ERR(qphy->iface_clk);
+
+       regs[0].supply = "vddcx";
+
+       ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(regs), regs);
+       if (ret)
+               return ret;
+
+       qphy->vdd  = regs[0].consumer;
+
+       qphy->vdd_levels[VDD_LEVEL_NONE] = USB_PHY_SUSP_DIG_VOL;
+       qphy->vdd_levels[VDD_LEVEL_MIN] = USB_PHY_VDD_DIG_VOL_MIN;
+       qphy->vdd_levels[VDD_LEVEL_MAX] = USB_PHY_VDD_DIG_VOL_MAX;
+
+       if (of_get_property(dev->of_node, "qcom,vdd-levels", &len) &&
+           len == sizeof(tmp)) {
+               of_property_read_u32_array(dev->of_node, "qcom,vdd-levels",
+                                          tmp, len / sizeof(*tmp));
+               qphy->vdd_levels[VDD_LEVEL_NONE] = tmp[VDD_LEVEL_NONE];
+               qphy->vdd_levels[VDD_LEVEL_MIN] = tmp[VDD_LEVEL_MIN];
+               qphy->vdd_levels[VDD_LEVEL_MAX] = tmp[VDD_LEVEL_MAX];
+       }
+
+       qphy->link_reset = devm_reset_control_get(dev, "link");
+       if (IS_ERR(qphy->link_reset))
+               return PTR_ERR(qphy->link_reset);
+
+       qphy->tlmm = syscon_regmap_lookup_by_phandle(dev->of_node,
+                                                    "qcom,tlmm");
+       if (!IS_ERR(qphy->tlmm) &&
+           of_get_property(dev->of_node, "qcom,tlmm-cfg", &len) &&
+           len == (4 * sizeof(u32))) {
+               of_property_read_u32_array(dev->of_node, "qcom,tlmm-cfg",
+                                          qphy->tlmm_cfg, 4);
+               dev_info(dev, "got tlmm hsic pad cfg\n");
+
+               if (of_gpio_named_count(dev->of_node,
+                                       "qcom,hsic-gpios") == 2) {
+                       qphy->hsic_gpios[0] = of_get_named_gpio(dev->of_node,
+                                                       "qcom,hsic-gpios", 0);
+                       qphy->hsic_gpios[1] = of_get_named_gpio(dev->of_node,
+                                                       "qcom,hsic-gpios", 1);
+               }
+       } else
+               qphy->tlmm = NULL;
+
+       return 0;
+}
+
+static int phy_hsic_probe(struct platform_device *pdev)
+{
+       struct phy_hsic *qphy;
+       struct resource *res;
+       struct usb_phy *phy;
+       int ret;
+
+       qphy = devm_kzalloc(&pdev->dev, sizeof(*qphy), GFP_KERNEL);
+       if (!qphy)
+               return -ENOMEM;
+
+       qphy->phy.otg = devm_kzalloc(&pdev->dev, sizeof(struct usb_otg),
+                                    GFP_KERNEL);
+       if (!qphy->phy.otg)
+               return -ENOMEM;
+
+       platform_set_drvdata(pdev, qphy);
+
+       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+       if (!res)
+               return -EINVAL;
+
+       qphy->regs = devm_ioremap(&pdev->dev, res->start, resource_size(res));
+       if (!qphy->regs)
+               return -ENOMEM;
+
+       phy                     = &qphy->phy;
+       phy->dev                = &pdev->dev;
+       phy->label              = dev_name(&pdev->dev);
+       phy->io_ops             = &qcom_hsic_io_ops;
+       phy->type               = USB_PHY_TYPE_USB2;
+       phy->init               = phy_hsic_init;
+
+       phy->otg->usb_phy       = phy;
+       phy->otg->set_host      = phy_hsic_set_host;
+
+       ret = phy_hsic_read_devicetree(qphy);
+       if (ret < 0)
+               return ret;
+
+       ret = clk_prepare_enable(qphy->core_clk);
+       if (ret < 0)
+               return ret;
+
+       ret = clk_prepare_enable(qphy->phy_clk);
+       if (ret < 0)
+               goto off_alt;
+
+       ret = clk_prepare_enable(qphy->cal_clk);
+       if (ret < 0)
+               goto off_phy;
+
+       ret = clk_prepare_enable(qphy->iface_clk);
+       if (ret < 0)
+               goto off_cal;
+
+       ret = clk_prepare_enable(qphy->alt_core_clk);
+       if (ret < 0)
+               goto off_core;
+
+       ret = phy_hsic_regulators_enable(qphy);
+       if (ret)
+               goto off_clks;
+
+       ret = phy_hsic_reset(qphy);
+       if (ret)
+               goto off_clks;
+
+       ret = usb_add_phy_dev(&qphy->phy);
+       if (ret)
+               goto off_power;
+
+       return 0;
+
+off_power:
+       phy_hsic_regulators_disable(qphy);
+off_clks:
+       clk_disable_unprepare(qphy->iface_clk);
+off_cal:
+       clk_disable_unprepare(qphy->cal_clk);
+off_phy:
+       clk_disable_unprepare(qphy->phy_clk);
+off_alt:
+       clk_disable_unprepare(qphy->alt_core_clk);
+off_core:
+       clk_disable_unprepare(qphy->core_clk);
+       return ret;
+}
+
+static int phy_hsic_remove(struct platform_device *pdev)
+{
+       struct phy_hsic *qphy = platform_get_drvdata(pdev);
+
+       usb_remove_phy(&qphy->phy);
+
+       clk_disable_unprepare(qphy->iface_clk);
+       clk_disable_unprepare(qphy->cal_clk);
+       clk_disable_unprepare(qphy->phy_clk);
+       clk_disable_unprepare(qphy->alt_core_clk);
+       clk_disable_unprepare(qphy->core_clk);
+       phy_hsic_regulators_disable(qphy);
+       return 0;
+}
+
+static const struct of_device_id phy_hsic_dt_match[] = {
+       { .compatible = "qcom,usb-hsic-phy" },
+       { }
+};
+MODULE_DEVICE_TABLE(of, phy_hsic_dt_match);
+
+static struct platform_driver phy_hsic_driver = {
+       .probe  = phy_hsic_probe,
+       .remove = phy_hsic_remove,
+       .driver = {
+               .name = "phy-qcom-hsic-usb",
+               .of_match_table = phy_hsic_dt_match,
+       },
+};
+module_platform_driver(phy_hsic_driver);
+
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("Qualcomm HSIC USB transceiver driver");
-- 
1.9.1

Reply via email to