diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
commit | 2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch) | |
tree | 848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/phy/ti/phy-tusb1210.c | |
parent | Initial commit. (diff) | |
download | linux-upstream.tar.xz linux-upstream.zip |
Adding upstream version 6.1.76.upstream/6.1.76upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | drivers/phy/ti/phy-tusb1210.c | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/drivers/phy/ti/phy-tusb1210.c b/drivers/phy/ti/phy-tusb1210.c new file mode 100644 index 000000000..669c13d6e --- /dev/null +++ b/drivers/phy/ti/phy-tusb1210.c @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * tusb1210.c - TUSB1210 USB ULPI PHY driver + * + * Copyright (C) 2015 Intel Corporation + * + * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> + */ +#include <linux/module.h> +#include <linux/bitfield.h> +#include <linux/delay.h> +#include <linux/ulpi/driver.h> +#include <linux/ulpi/regs.h> +#include <linux/gpio/consumer.h> +#include <linux/phy/ulpi_phy.h> +#include <linux/power_supply.h> +#include <linux/workqueue.h> + +#define TUSB1211_POWER_CONTROL 0x3d +#define TUSB1211_POWER_CONTROL_SET 0x3e +#define TUSB1211_POWER_CONTROL_CLEAR 0x3f +#define TUSB1211_POWER_CONTROL_SW_CONTROL BIT(0) +#define TUSB1211_POWER_CONTROL_DET_COMP BIT(1) +#define TUSB1211_POWER_CONTROL_DP_VSRC_EN BIT(6) + +#define TUSB1210_VENDOR_SPECIFIC2 0x80 +#define TUSB1210_VENDOR_SPECIFIC2_IHSTX_MASK GENMASK(3, 0) +#define TUSB1210_VENDOR_SPECIFIC2_ZHSDRV_MASK GENMASK(5, 4) +#define TUSB1210_VENDOR_SPECIFIC2_DP_MASK BIT(6) + +#define TUSB1211_VENDOR_SPECIFIC3 0x85 +#define TUSB1211_VENDOR_SPECIFIC3_SET 0x86 +#define TUSB1211_VENDOR_SPECIFIC3_CLEAR 0x87 +#define TUSB1211_VENDOR_SPECIFIC3_SW_USB_DET BIT(4) +#define TUSB1211_VENDOR_SPECIFIC3_CHGD_IDP_SRC_EN BIT(6) + +#define TUSB1210_RESET_TIME_MS 50 + +#define TUSB1210_CHG_DET_MAX_RETRIES 5 + +/* TUSB1210 charger detection work states */ +enum tusb1210_chg_det_state { + TUSB1210_CHG_DET_CONNECTING, + TUSB1210_CHG_DET_START_DET, + TUSB1210_CHG_DET_READ_DET, + TUSB1210_CHG_DET_FINISH_DET, + TUSB1210_CHG_DET_CONNECTED, + TUSB1210_CHG_DET_DISCONNECTING, + TUSB1210_CHG_DET_DISCONNECTING_DONE, + TUSB1210_CHG_DET_DISCONNECTED, +}; + +struct tusb1210 { + struct ulpi *ulpi; + struct phy *phy; + struct gpio_desc *gpio_reset; + struct gpio_desc *gpio_cs; + u8 otg_ctrl; + u8 vendor_specific2; +#ifdef CONFIG_POWER_SUPPLY + enum power_supply_usb_type chg_type; + enum tusb1210_chg_det_state chg_det_state; + int chg_det_retries; + struct delayed_work chg_det_work; + struct notifier_block psy_nb; + struct power_supply *psy; + struct power_supply *charger; +#endif +}; + +static int tusb1210_ulpi_write(struct tusb1210 *tusb, u8 reg, u8 val) +{ + int ret; + + ret = ulpi_write(tusb->ulpi, reg, val); + if (ret) + dev_err(&tusb->ulpi->dev, "error %d writing val 0x%02x to reg 0x%02x\n", + ret, val, reg); + + return ret; +} + +static int tusb1210_ulpi_read(struct tusb1210 *tusb, u8 reg, u8 *val) +{ + int ret; + + ret = ulpi_read(tusb->ulpi, reg); + if (ret >= 0) { + *val = ret; + ret = 0; + } else { + dev_err(&tusb->ulpi->dev, "error %d reading reg 0x%02x\n", ret, reg); + } + + return ret; +} + +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); + + msleep(TUSB1210_RESET_TIME_MS); + + /* Restore the optional eye diagram optimization value */ + tusb1210_ulpi_write(tusb, 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, int submode) +{ + struct tusb1210 *tusb = phy_get_drvdata(phy); + int ret; + u8 reg; + + ret = tusb1210_ulpi_read(tusb, ULPI_OTG_CTRL, ®); + if (ret < 0) + return ret; + + switch (mode) { + case PHY_MODE_USB_HOST: + reg |= (ULPI_OTG_CTRL_DRVVBUS_EXT + | ULPI_OTG_CTRL_ID_PULLUP + | ULPI_OTG_CTRL_DP_PULLDOWN + | ULPI_OTG_CTRL_DM_PULLDOWN); + tusb1210_ulpi_write(tusb, ULPI_OTG_CTRL, reg); + reg |= ULPI_OTG_CTRL_DRVVBUS; + break; + case PHY_MODE_USB_DEVICE: + reg &= ~(ULPI_OTG_CTRL_DRVVBUS + | ULPI_OTG_CTRL_DP_PULLDOWN + | ULPI_OTG_CTRL_DM_PULLDOWN); + tusb1210_ulpi_write(tusb, ULPI_OTG_CTRL, reg); + reg &= ~ULPI_OTG_CTRL_DRVVBUS_EXT; + break; + default: + /* nothing */ + return 0; + } + + tusb->otg_ctrl = reg; + return tusb1210_ulpi_write(tusb, ULPI_OTG_CTRL, reg); +} + +#ifdef CONFIG_POWER_SUPPLY +static const char * const tusb1210_chg_det_states[] = { + "CHG_DET_CONNECTING", + "CHG_DET_START_DET", + "CHG_DET_READ_DET", + "CHG_DET_FINISH_DET", + "CHG_DET_CONNECTED", + "CHG_DET_DISCONNECTING", + "CHG_DET_DISCONNECTING_DONE", + "CHG_DET_DISCONNECTED", +}; + +static void tusb1210_reset(struct tusb1210 *tusb) +{ + gpiod_set_value_cansleep(tusb->gpio_reset, 0); + usleep_range(200, 500); + gpiod_set_value_cansleep(tusb->gpio_reset, 1); +} + +static void tusb1210_chg_det_set_type(struct tusb1210 *tusb, + enum power_supply_usb_type type) +{ + dev_dbg(&tusb->ulpi->dev, "charger type: %d\n", type); + tusb->chg_type = type; + tusb->chg_det_retries = 0; + power_supply_changed(tusb->psy); +} + +static void tusb1210_chg_det_set_state(struct tusb1210 *tusb, + enum tusb1210_chg_det_state new_state, + int delay_ms) +{ + if (delay_ms) + dev_dbg(&tusb->ulpi->dev, "chg_det new state %s in %d ms\n", + tusb1210_chg_det_states[new_state], delay_ms); + + tusb->chg_det_state = new_state; + mod_delayed_work(system_long_wq, &tusb->chg_det_work, + msecs_to_jiffies(delay_ms)); +} + +static void tusb1210_chg_det_handle_ulpi_error(struct tusb1210 *tusb) +{ + tusb1210_reset(tusb); + if (tusb->chg_det_retries < TUSB1210_CHG_DET_MAX_RETRIES) { + tusb->chg_det_retries++; + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_START_DET, + TUSB1210_RESET_TIME_MS); + } else { + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_FINISH_DET, + TUSB1210_RESET_TIME_MS); + } +} + +/* + * Boards using a TUSB121x for charger-detection have 3 power_supply class devs: + * + * tusb1211-charger-detect(1) -> charger -> fuel-gauge + * + * To determine if an USB charger is connected to the board, the online prop of + * the charger psy needs to be read. Since the tusb1211-charger-detect psy is + * the start of the supplier -> supplied-to chain, power_supply_am_i_supplied() + * cannot be used here. + * + * Instead, below is a list of the power_supply names of known chargers for + * these boards and the charger psy is looked up by name from this list. + * + * (1) modelling the external USB charger + */ +static const char * const tusb1210_chargers[] = { + "bq24190-charger", +}; + +static bool tusb1210_get_online(struct tusb1210 *tusb) +{ + union power_supply_propval val; + int i; + + for (i = 0; i < ARRAY_SIZE(tusb1210_chargers) && !tusb->charger; i++) + tusb->charger = power_supply_get_by_name(tusb1210_chargers[i]); + + if (!tusb->charger) + return false; + + if (power_supply_get_property(tusb->charger, POWER_SUPPLY_PROP_ONLINE, &val)) + return false; + + return val.intval; +} + +static void tusb1210_chg_det_work(struct work_struct *work) +{ + struct tusb1210 *tusb = container_of(work, struct tusb1210, chg_det_work.work); + bool vbus_present = tusb1210_get_online(tusb); + int ret; + u8 val; + + dev_dbg(&tusb->ulpi->dev, "chg_det state %s vbus_present %d\n", + tusb1210_chg_det_states[tusb->chg_det_state], vbus_present); + + switch (tusb->chg_det_state) { + case TUSB1210_CHG_DET_CONNECTING: + tusb->chg_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + tusb->chg_det_retries = 0; + /* Power on USB controller for ulpi_read()/_write() */ + ret = pm_runtime_resume_and_get(tusb->ulpi->dev.parent); + if (ret < 0) { + dev_err(&tusb->ulpi->dev, "error %d runtime-resuming\n", ret); + /* Should never happen, skip charger detection */ + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_CONNECTED, 0); + return; + } + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_START_DET, 0); + break; + case TUSB1210_CHG_DET_START_DET: + /* + * Use the builtin charger detection FSM to keep things simple. + * This only detects DCP / SDP. This is good enough for the few + * boards which actually rely on the phy for charger detection. + */ + mutex_lock(&tusb->phy->mutex); + ret = tusb1210_ulpi_write(tusb, TUSB1211_VENDOR_SPECIFIC3_SET, + TUSB1211_VENDOR_SPECIFIC3_SW_USB_DET); + mutex_unlock(&tusb->phy->mutex); + if (ret) { + tusb1210_chg_det_handle_ulpi_error(tusb); + break; + } + + /* Wait 400 ms for the charger detection FSM to finish */ + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_READ_DET, 400); + break; + case TUSB1210_CHG_DET_READ_DET: + mutex_lock(&tusb->phy->mutex); + ret = tusb1210_ulpi_read(tusb, TUSB1211_POWER_CONTROL, &val); + mutex_unlock(&tusb->phy->mutex); + if (ret) { + tusb1210_chg_det_handle_ulpi_error(tusb); + break; + } + + if (val & TUSB1211_POWER_CONTROL_DET_COMP) + tusb1210_chg_det_set_type(tusb, POWER_SUPPLY_USB_TYPE_DCP); + else + tusb1210_chg_det_set_type(tusb, POWER_SUPPLY_USB_TYPE_SDP); + + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_FINISH_DET, 0); + break; + case TUSB1210_CHG_DET_FINISH_DET: + mutex_lock(&tusb->phy->mutex); + + /* Set SW_CONTROL to stop the charger-det FSM */ + ret = tusb1210_ulpi_write(tusb, TUSB1211_POWER_CONTROL_SET, + TUSB1211_POWER_CONTROL_SW_CONTROL); + + /* Clear DP_VSRC_EN which may have been enabled by the charger-det FSM */ + ret |= tusb1210_ulpi_write(tusb, TUSB1211_POWER_CONTROL_CLEAR, + TUSB1211_POWER_CONTROL_DP_VSRC_EN); + + /* Clear CHGD_IDP_SRC_EN (may have been enabled by the charger-det FSM) */ + ret |= tusb1210_ulpi_write(tusb, TUSB1211_VENDOR_SPECIFIC3_CLEAR, + TUSB1211_VENDOR_SPECIFIC3_CHGD_IDP_SRC_EN); + + /* If any of the above fails reset the phy */ + if (ret) { + tusb1210_reset(tusb); + msleep(TUSB1210_RESET_TIME_MS); + } + + /* Restore phy-parameters and OTG_CTRL register */ + tusb1210_ulpi_write(tusb, ULPI_OTG_CTRL, tusb->otg_ctrl); + tusb1210_ulpi_write(tusb, TUSB1210_VENDOR_SPECIFIC2, + tusb->vendor_specific2); + + mutex_unlock(&tusb->phy->mutex); + + pm_runtime_put(tusb->ulpi->dev.parent); + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_CONNECTED, 0); + break; + case TUSB1210_CHG_DET_CONNECTED: + if (!vbus_present) + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_DISCONNECTING, 0); + break; + case TUSB1210_CHG_DET_DISCONNECTING: + /* + * The phy seems to take approx. 600ms longer then the charger + * chip (which is used to get vbus_present) to determine Vbus + * session end. Wait 800ms to ensure the phy has detected and + * signalled Vbus session end. + */ + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_DISCONNECTING_DONE, 800); + break; + case TUSB1210_CHG_DET_DISCONNECTING_DONE: + /* + * The phy often stops reacting to ulpi_read()/_write requests + * after a Vbus-session end. Reset it to work around this. + */ + tusb1210_reset(tusb); + tusb1210_chg_det_set_type(tusb, POWER_SUPPLY_USB_TYPE_UNKNOWN); + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_DISCONNECTED, 0); + break; + case TUSB1210_CHG_DET_DISCONNECTED: + if (vbus_present) + tusb1210_chg_det_set_state(tusb, TUSB1210_CHG_DET_CONNECTING, 0); + break; + } +} + +static int tusb1210_psy_notifier(struct notifier_block *nb, + unsigned long event, void *ptr) +{ + struct tusb1210 *tusb = container_of(nb, struct tusb1210, psy_nb); + struct power_supply *psy = ptr; + + if (psy != tusb->psy && psy->desc->type == POWER_SUPPLY_TYPE_USB) + queue_delayed_work(system_long_wq, &tusb->chg_det_work, 0); + + return NOTIFY_OK; +} + +static int tusb1210_psy_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct tusb1210 *tusb = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = tusb1210_get_online(tusb); + break; + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = tusb->chg_type; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + if (tusb->chg_type == POWER_SUPPLY_USB_TYPE_DCP) + val->intval = 2000000; + else + val->intval = 500000; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_usb_type tusb1210_psy_usb_types[] = { + POWER_SUPPLY_USB_TYPE_SDP, + POWER_SUPPLY_USB_TYPE_DCP, + POWER_SUPPLY_USB_TYPE_UNKNOWN, +}; + +static const enum power_supply_property tusb1210_psy_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static const struct power_supply_desc tusb1210_psy_desc = { + .name = "tusb1211-charger-detect", + .type = POWER_SUPPLY_TYPE_USB, + .usb_types = tusb1210_psy_usb_types, + .num_usb_types = ARRAY_SIZE(tusb1210_psy_usb_types), + .properties = tusb1210_psy_props, + .num_properties = ARRAY_SIZE(tusb1210_psy_props), + .get_property = tusb1210_psy_get_prop, +}; + +/* Setup charger detection if requested, on errors continue without chg-det */ +static void tusb1210_probe_charger_detect(struct tusb1210 *tusb) +{ + struct power_supply_config psy_cfg = { .drv_data = tusb }; + struct device *dev = &tusb->ulpi->dev; + int ret; + + if (!device_property_read_bool(dev->parent, "linux,phy_charger_detect")) + return; + + if (tusb->ulpi->id.product != 0x1508) { + dev_err(dev, "error charger detection is only supported on the TUSB1211\n"); + return; + } + + ret = tusb1210_ulpi_read(tusb, ULPI_OTG_CTRL, &tusb->otg_ctrl); + if (ret) + return; + + tusb->psy = power_supply_register(dev, &tusb1210_psy_desc, &psy_cfg); + if (IS_ERR(tusb->psy)) + return; + + /* + * Delay initial run by 2 seconds to allow the charger driver, + * which is used to determine vbus_present, to load. + */ + tusb->chg_det_state = TUSB1210_CHG_DET_DISCONNECTED; + INIT_DELAYED_WORK(&tusb->chg_det_work, tusb1210_chg_det_work); + queue_delayed_work(system_long_wq, &tusb->chg_det_work, 2 * HZ); + + tusb->psy_nb.notifier_call = tusb1210_psy_notifier; + power_supply_reg_notifier(&tusb->psy_nb); +} + +static void tusb1210_remove_charger_detect(struct tusb1210 *tusb) +{ + + if (!IS_ERR_OR_NULL(tusb->psy)) { + power_supply_unreg_notifier(&tusb->psy_nb); + cancel_delayed_work_sync(&tusb->chg_det_work); + power_supply_unregister(tusb->psy); + } + + if (tusb->charger) + power_supply_put(tusb->charger); +} +#else +static void tusb1210_probe_charger_detect(struct tusb1210 *tusb) { } +static void tusb1210_remove_charger_detect(struct tusb1210 *tusb) { } +#endif + +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; + int ret; + + tusb = devm_kzalloc(&ulpi->dev, sizeof(*tusb), GFP_KERNEL); + if (!tusb) + return -ENOMEM; + + tusb->ulpi = ulpi; + + 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. + */ + + ret = tusb1210_ulpi_read(tusb, TUSB1210_VENDOR_SPECIFIC2, ®); + if (ret) + return ret; + + /* High speed output drive strength configuration */ + if (!device_property_read_u8(&ulpi->dev, "ihstx", &val)) + u8p_replace_bits(®, val, (u8)TUSB1210_VENDOR_SPECIFIC2_IHSTX_MASK); + + /* High speed output impedance configuration */ + if (!device_property_read_u8(&ulpi->dev, "zhsdrv", &val)) + u8p_replace_bits(®, val, (u8)TUSB1210_VENDOR_SPECIFIC2_ZHSDRV_MASK); + + /* DP/DM swap control */ + if (!device_property_read_u8(&ulpi->dev, "datapolarity", &val)) + u8p_replace_bits(®, val, (u8)TUSB1210_VENDOR_SPECIFIC2_DP_MASK); + + ret = tusb1210_ulpi_write(tusb, TUSB1210_VENDOR_SPECIFIC2, reg); + if (ret) + return ret; + + tusb->vendor_specific2 = reg; + + tusb1210_probe_charger_detect(tusb); + + tusb->phy = ulpi_phy_create(ulpi, &phy_ops); + if (IS_ERR(tusb->phy)) { + ret = PTR_ERR(tusb->phy); + goto err_remove_charger; + } + + phy_set_drvdata(tusb->phy, tusb); + ulpi_set_drvdata(ulpi, tusb); + return 0; + +err_remove_charger: + tusb1210_remove_charger_detect(tusb); + return ret; +} + +static void tusb1210_remove(struct ulpi *ulpi) +{ + struct tusb1210 *tusb = ulpi_get_drvdata(ulpi); + + ulpi_phy_destroy(ulpi, tusb->phy); + tusb1210_remove_charger_detect(tusb); +} + +#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"); |