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@xxxxxxxxxxxx> --- 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 -- To unsubscribe from this list: send the line "unsubscribe linux-usb" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html