[PATCH 1/2] usb: phy: Add initial support for Qualcomm HSIC PHY

Neil Armstrong
2016-06-17T10:25:49Z

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 <narmstrong@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