| /** | |
| * tusb1210.c - TUSB1210 USB ULPI PHY driver | |
| * | |
| * Copyright (C) 2015 Intel Corporation | |
| * | |
| * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> | |
| * | |
| * This program is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License version 2 as | |
| * published by the Free Software Foundation. | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/ulpi/driver.h> | |
| #include <linux/ulpi/regs.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/phy/ulpi_phy.h> | |
| #define TUSB1210_VENDOR_SPECIFIC2 0x80 | |
| #define TUSB1210_VENDOR_SPECIFIC2_IHSTX_SHIFT 0 | |
| #define TUSB1210_VENDOR_SPECIFIC2_ZHSDRV_SHIFT 4 | |
| #define TUSB1210_VENDOR_SPECIFIC2_DP_SHIFT 6 | |
| struct tusb1210 { | |
| struct ulpi *ulpi; | |
| struct phy *phy; | |
| struct gpio_desc *gpio_reset; | |
| struct gpio_desc *gpio_cs; | |
| u8 vendor_specific2; | |
| }; | |
| static int tusb1210_power_on(struct phy *phy) | |
| { | |
| struct tusb1210 *tusb = phy_get_drvdata(phy); | |
| gpiod_set_value_cansleep(tusb->gpio_reset, 1); | |
| gpiod_set_value_cansleep(tusb->gpio_cs, 1); | |
| /* Restore the optional eye diagram optimization value */ | |
| if (tusb->vendor_specific2) | |
| ulpi_write(tusb->ulpi, TUSB1210_VENDOR_SPECIFIC2, | |
| tusb->vendor_specific2); | |
| return 0; | |
| } | |
| static int tusb1210_power_off(struct phy *phy) | |
| { | |
| struct tusb1210 *tusb = phy_get_drvdata(phy); | |
| gpiod_set_value_cansleep(tusb->gpio_reset, 0); | |
| gpiod_set_value_cansleep(tusb->gpio_cs, 0); | |
| return 0; | |
| } | |
| static int tusb1210_set_mode(struct phy *phy, enum phy_mode mode) | |
| { | |
| struct tusb1210 *tusb = phy_get_drvdata(phy); | |
| int ret; | |
| ret = ulpi_read(tusb->ulpi, ULPI_OTG_CTRL); | |
| if (ret < 0) | |
| return ret; | |
| switch (mode) { | |
| case PHY_MODE_USB_HOST: | |
| ret |= (ULPI_OTG_CTRL_DRVVBUS_EXT | |
| | ULPI_OTG_CTRL_ID_PULLUP | |
| | ULPI_OTG_CTRL_DP_PULLDOWN | |
| | ULPI_OTG_CTRL_DM_PULLDOWN); | |
| ulpi_write(tusb->ulpi, ULPI_OTG_CTRL, ret); | |
| ret |= ULPI_OTG_CTRL_DRVVBUS; | |
| break; | |
| case PHY_MODE_USB_DEVICE: | |
| ret &= ~(ULPI_OTG_CTRL_DRVVBUS | |
| | ULPI_OTG_CTRL_DP_PULLDOWN | |
| | ULPI_OTG_CTRL_DM_PULLDOWN); | |
| ulpi_write(tusb->ulpi, ULPI_OTG_CTRL, ret); | |
| ret &= ~ULPI_OTG_CTRL_DRVVBUS_EXT; | |
| break; | |
| default: | |
| /* nothing */ | |
| return 0; | |
| } | |
| return ulpi_write(tusb->ulpi, ULPI_OTG_CTRL, ret); | |
| } | |
| static const struct phy_ops phy_ops = { | |
| .power_on = tusb1210_power_on, | |
| .power_off = tusb1210_power_off, | |
| .set_mode = tusb1210_set_mode, | |
| .owner = THIS_MODULE, | |
| }; | |
| static int tusb1210_probe(struct ulpi *ulpi) | |
| { | |
| struct tusb1210 *tusb; | |
| u8 val, reg; | |
| tusb = devm_kzalloc(&ulpi->dev, sizeof(*tusb), GFP_KERNEL); | |
| if (!tusb) | |
| return -ENOMEM; | |
| tusb->gpio_reset = devm_gpiod_get_optional(&ulpi->dev, "reset", | |
| GPIOD_OUT_LOW); | |
| if (IS_ERR(tusb->gpio_reset)) | |
| return PTR_ERR(tusb->gpio_reset); | |
| gpiod_set_value_cansleep(tusb->gpio_reset, 1); | |
| tusb->gpio_cs = devm_gpiod_get_optional(&ulpi->dev, "cs", | |
| GPIOD_OUT_LOW); | |
| if (IS_ERR(tusb->gpio_cs)) | |
| return PTR_ERR(tusb->gpio_cs); | |
| gpiod_set_value_cansleep(tusb->gpio_cs, 1); | |
| /* | |
| * VENDOR_SPECIFIC2 register in TUSB1210 can be used for configuring eye | |
| * diagram optimization and DP/DM swap. | |
| */ | |
| /* High speed output drive strength configuration */ | |
| device_property_read_u8(&ulpi->dev, "ihstx", &val); | |
| reg = val << TUSB1210_VENDOR_SPECIFIC2_IHSTX_SHIFT; | |
| /* High speed output impedance configuration */ | |
| device_property_read_u8(&ulpi->dev, "zhsdrv", &val); | |
| reg |= val << TUSB1210_VENDOR_SPECIFIC2_ZHSDRV_SHIFT; | |
| /* DP/DM swap control */ | |
| device_property_read_u8(&ulpi->dev, "datapolarity", &val); | |
| reg |= val << TUSB1210_VENDOR_SPECIFIC2_DP_SHIFT; | |
| if (reg) { | |
| ulpi_write(ulpi, TUSB1210_VENDOR_SPECIFIC2, reg); | |
| tusb->vendor_specific2 = reg; | |
| } | |
| tusb->phy = ulpi_phy_create(ulpi, &phy_ops); | |
| if (IS_ERR(tusb->phy)) | |
| return PTR_ERR(tusb->phy); | |
| tusb->ulpi = ulpi; | |
| phy_set_drvdata(tusb->phy, tusb); | |
| ulpi_set_drvdata(ulpi, tusb); | |
| return 0; | |
| } | |
| static void tusb1210_remove(struct ulpi *ulpi) | |
| { | |
| struct tusb1210 *tusb = ulpi_get_drvdata(ulpi); | |
| ulpi_phy_destroy(ulpi, tusb->phy); | |
| } | |
| #define TI_VENDOR_ID 0x0451 | |
| static const struct ulpi_device_id tusb1210_ulpi_id[] = { | |
| { TI_VENDOR_ID, 0x1507, }, /* TUSB1210 */ | |
| { TI_VENDOR_ID, 0x1508, }, /* TUSB1211 */ | |
| { }, | |
| }; | |
| MODULE_DEVICE_TABLE(ulpi, tusb1210_ulpi_id); | |
| static struct ulpi_driver tusb1210_driver = { | |
| .id_table = tusb1210_ulpi_id, | |
| .probe = tusb1210_probe, | |
| .remove = tusb1210_remove, | |
| .driver = { | |
| .name = "tusb1210", | |
| .owner = THIS_MODULE, | |
| }, | |
| }; | |
| module_ulpi_driver(tusb1210_driver); | |
| MODULE_AUTHOR("Intel Corporation"); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_DESCRIPTION("TUSB1210 ULPI PHY driver"); |