From ace9429bb58fd418f0c81d4c2835699bddf6bde6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:27:49 +0200 Subject: Adding upstream version 6.6.15. Signed-off-by: Daniel Baumann --- drivers/usb/typec/tcpm/Kconfig | 91 + drivers/usb/typec/tcpm/Makefile | 12 + drivers/usb/typec/tcpm/fusb302.c | 1847 ++++++ drivers/usb/typec/tcpm/fusb302_reg.h | 177 + drivers/usb/typec/tcpm/maxim_contaminant.c | 387 ++ drivers/usb/typec/tcpm/qcom/Makefile | 6 + drivers/usb/typec/tcpm/qcom/qcom_pmic_typec.c | 381 ++ .../usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.c | 526 ++ .../usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.h | 119 + drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.c | 560 ++ drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.h | 195 + drivers/usb/typec/tcpm/tcpci.c | 909 +++ drivers/usb/typec/tcpm/tcpci_maxim.h | 89 + drivers/usb/typec/tcpm/tcpci_maxim_core.c | 519 ++ drivers/usb/typec/tcpm/tcpci_mt6360.c | 230 + drivers/usb/typec/tcpm/tcpci_mt6370.c | 205 + drivers/usb/typec/tcpm/tcpci_rt1711h.c | 423 ++ drivers/usb/typec/tcpm/tcpm.c | 6675 ++++++++++++++++++++ drivers/usb/typec/tcpm/wcove.c | 702 ++ 19 files changed, 14053 insertions(+) create mode 100644 drivers/usb/typec/tcpm/Kconfig create mode 100644 drivers/usb/typec/tcpm/Makefile create mode 100644 drivers/usb/typec/tcpm/fusb302.c create mode 100644 drivers/usb/typec/tcpm/fusb302_reg.h create mode 100644 drivers/usb/typec/tcpm/maxim_contaminant.c create mode 100644 drivers/usb/typec/tcpm/qcom/Makefile create mode 100644 drivers/usb/typec/tcpm/qcom/qcom_pmic_typec.c create mode 100644 drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.c create mode 100644 drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.h create mode 100644 drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.c create mode 100644 drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.h create mode 100644 drivers/usb/typec/tcpm/tcpci.c create mode 100644 drivers/usb/typec/tcpm/tcpci_maxim.h create mode 100644 drivers/usb/typec/tcpm/tcpci_maxim_core.c create mode 100644 drivers/usb/typec/tcpm/tcpci_mt6360.c create mode 100644 drivers/usb/typec/tcpm/tcpci_mt6370.c create mode 100644 drivers/usb/typec/tcpm/tcpci_rt1711h.c create mode 100644 drivers/usb/typec/tcpm/tcpm.c create mode 100644 drivers/usb/typec/tcpm/wcove.c (limited to 'drivers/usb/typec/tcpm') diff --git a/drivers/usb/typec/tcpm/Kconfig b/drivers/usb/typec/tcpm/Kconfig new file mode 100644 index 0000000000..0b2993fef5 --- /dev/null +++ b/drivers/usb/typec/tcpm/Kconfig @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: GPL-2.0 + +config TYPEC_TCPM + tristate "USB Type-C Port Controller Manager" + depends on USB + select USB_ROLE_SWITCH + select POWER_SUPPLY + help + The Type-C Port Controller Manager provides a USB PD and USB Type-C + state machine for use with Type-C Port Controllers. + +if TYPEC_TCPM + +config TYPEC_TCPCI + tristate "Type-C Port Controller Interface driver" + depends on I2C + select REGMAP_I2C + help + Type-C Port Controller driver for TCPCI-compliant controller. + +if TYPEC_TCPCI + +config TYPEC_RT1711H + tristate "Richtek RT1711H Type-C chip driver" + help + Richtek RT1711H Type-C chip driver that works with + Type-C Port Controller Manager to provide USB PD and USB + Type-C functionalities. + +config TYPEC_MT6360 + tristate "Mediatek MT6360 Type-C driver" + depends on MFD_MT6360 + help + Mediatek MT6360 is a multi-functional IC that includes + USB Type-C. It works with Type-C Port Controller Manager + to provide USB PD and USB Type-C functionalities. + +config TYPEC_TCPCI_MT6370 + tristate "MediaTek MT6370 Type-C driver" + depends on MFD_MT6370 + help + MediaTek MT6370 is a multi-functional IC that includes + USB Type-C. It works with Type-C Port Controller Manager + to provide USB PD and USB Type-C functionalities. + + This driver can also be built as a module. The module + will be called "tcpci_mt6370". + +config TYPEC_TCPCI_MAXIM + tristate "Maxim TCPCI based Type-C chip driver" + help + MAXIM TCPCI based Type-C/PD chip driver. Works with + with Type-C Port Controller Manager. + +endif # TYPEC_TCPCI + +config TYPEC_FUSB302 + tristate "Fairchild FUSB302 Type-C chip driver" + depends on I2C + depends on EXTCON || !EXTCON + help + The Fairchild FUSB302 Type-C chip driver that works with + Type-C Port Controller Manager to provide USB PD and USB + Type-C functionalities. + +config TYPEC_WCOVE + tristate "Intel WhiskeyCove PMIC USB Type-C PHY driver" + depends on ACPI + depends on MFD_INTEL_PMC_BXT + depends on BXT_WC_PMIC_OPREGION + help + This driver adds support for USB Type-C on Intel Broxton platforms + that have Intel Whiskey Cove PMIC. The driver works with USB Type-C + Port Controller Manager to provide USB PD and Type-C functionalities. + + To compile this driver as module, choose M here: the module will be + called typec_wcove.ko + +config TYPEC_QCOM_PMIC + tristate "Qualcomm PMIC USB Type-C Port Controller Manager driver" + depends on ARCH_QCOM || COMPILE_TEST + depends on DRM || DRM=n + help + A Type-C port and Power Delivery driver which aggregates two + discrete pieces of silicon in the PM8150b PMIC block: the + Type-C port controller and the Power Delivery PHY. + + This driver enables Type-C role switching, orientation, Alternate + mode and Power Delivery support both for VBUS and VCONN. + +endif # TYPEC_TCPM diff --git a/drivers/usb/typec/tcpm/Makefile b/drivers/usb/typec/tcpm/Makefile new file mode 100644 index 0000000000..7a8cad0c0b --- /dev/null +++ b/drivers/usb/typec/tcpm/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_TYPEC_TCPM) += tcpm.o +obj-$(CONFIG_TYPEC_FUSB302) += fusb302.o +obj-$(CONFIG_TYPEC_WCOVE) += typec_wcove.o +typec_wcove-y := wcove.o +obj-$(CONFIG_TYPEC_TCPCI) += tcpci.o +obj-$(CONFIG_TYPEC_RT1711H) += tcpci_rt1711h.o +obj-$(CONFIG_TYPEC_MT6360) += tcpci_mt6360.o +obj-$(CONFIG_TYPEC_TCPCI_MT6370) += tcpci_mt6370.o +obj-$(CONFIG_TYPEC_TCPCI_MAXIM) += tcpci_maxim.o +tcpci_maxim-y += tcpci_maxim_core.o maxim_contaminant.o +obj-$(CONFIG_TYPEC_QCOM_PMIC) += qcom/ diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c new file mode 100644 index 0000000000..bc21006e97 --- /dev/null +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -0,0 +1,1847 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2016-2017 Google, Inc + * + * Fairchild FUSB302 Type-C Chip Driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fusb302_reg.h" + +/* + * When the device is SNK, BC_LVL interrupt is used to monitor cc pins + * for the current capability offered by the SRC. As FUSB302 chip fires + * the BC_LVL interrupt on PD signalings, cc lvl should be handled after + * a delay to avoid measuring on PD activities. The delay is slightly + * longer than PD_T_PD_DEBPUNCE (10-20ms). + */ +#define T_BC_LVL_DEBOUNCE_DELAY_MS 30 + +enum toggling_mode { + TOGGLING_MODE_OFF, + TOGGLING_MODE_DRP, + TOGGLING_MODE_SNK, + TOGGLING_MODE_SRC, +}; + +enum src_current_status { + SRC_CURRENT_DEFAULT, + SRC_CURRENT_MEDIUM, + SRC_CURRENT_HIGH, +}; + +static const u8 ra_mda_value[] = { + [SRC_CURRENT_DEFAULT] = 4, /* 210mV */ + [SRC_CURRENT_MEDIUM] = 9, /* 420mV */ + [SRC_CURRENT_HIGH] = 18, /* 798mV */ +}; + +static const u8 rd_mda_value[] = { + [SRC_CURRENT_DEFAULT] = 38, /* 1638mV */ + [SRC_CURRENT_MEDIUM] = 38, /* 1638mV */ + [SRC_CURRENT_HIGH] = 61, /* 2604mV */ +}; + +#define LOG_BUFFER_ENTRIES 1024 +#define LOG_BUFFER_ENTRY_SIZE 128 + +struct fusb302_chip { + struct device *dev; + struct i2c_client *i2c_client; + struct tcpm_port *tcpm_port; + struct tcpc_dev tcpc_dev; + + struct regulator *vbus; + + spinlock_t irq_lock; + struct work_struct irq_work; + bool irq_suspended; + bool irq_while_suspended; + struct gpio_desc *gpio_int_n; + int gpio_int_n_irq; + struct extcon_dev *extcon; + + struct workqueue_struct *wq; + struct delayed_work bc_lvl_handler; + + /* lock for sharing chip states */ + struct mutex lock; + + /* chip status */ + enum toggling_mode toggling_mode; + enum src_current_status src_current_status; + bool intr_togdone; + bool intr_bc_lvl; + bool intr_comp_chng; + + /* port status */ + bool vconn_on; + bool vbus_on; + bool charge_on; + bool vbus_present; + enum typec_cc_polarity cc_polarity; + enum typec_cc_status cc1; + enum typec_cc_status cc2; + u32 snk_pdo[PDO_MAX_OBJECTS]; + +#ifdef CONFIG_DEBUG_FS + struct dentry *dentry; + /* lock for log buffer access */ + struct mutex logbuffer_lock; + int logbuffer_head; + int logbuffer_tail; + u8 *logbuffer[LOG_BUFFER_ENTRIES]; +#endif +}; + +/* + * Logging + */ + +#ifdef CONFIG_DEBUG_FS +static bool fusb302_log_full(struct fusb302_chip *chip) +{ + return chip->logbuffer_tail == + (chip->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; +} + +__printf(2, 0) +static void _fusb302_log(struct fusb302_chip *chip, const char *fmt, + va_list args) +{ + char tmpbuffer[LOG_BUFFER_ENTRY_SIZE]; + u64 ts_nsec = local_clock(); + unsigned long rem_nsec; + + if (!chip->logbuffer[chip->logbuffer_head]) { + chip->logbuffer[chip->logbuffer_head] = + kzalloc(LOG_BUFFER_ENTRY_SIZE, GFP_KERNEL); + if (!chip->logbuffer[chip->logbuffer_head]) + return; + } + + vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args); + + mutex_lock(&chip->logbuffer_lock); + + if (fusb302_log_full(chip)) { + chip->logbuffer_head = max(chip->logbuffer_head - 1, 0); + strscpy(tmpbuffer, "overflow", sizeof(tmpbuffer)); + } + + if (chip->logbuffer_head < 0 || + chip->logbuffer_head >= LOG_BUFFER_ENTRIES) { + dev_warn(chip->dev, + "Bad log buffer index %d\n", chip->logbuffer_head); + goto abort; + } + + if (!chip->logbuffer[chip->logbuffer_head]) { + dev_warn(chip->dev, + "Log buffer index %d is NULL\n", chip->logbuffer_head); + goto abort; + } + + rem_nsec = do_div(ts_nsec, 1000000000); + scnprintf(chip->logbuffer[chip->logbuffer_head], + LOG_BUFFER_ENTRY_SIZE, "[%5lu.%06lu] %s", + (unsigned long)ts_nsec, rem_nsec / 1000, + tmpbuffer); + chip->logbuffer_head = (chip->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; + +abort: + mutex_unlock(&chip->logbuffer_lock); +} + +__printf(2, 3) +static void fusb302_log(struct fusb302_chip *chip, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + _fusb302_log(chip, fmt, args); + va_end(args); +} + +static int fusb302_debug_show(struct seq_file *s, void *v) +{ + struct fusb302_chip *chip = s->private; + int tail; + + mutex_lock(&chip->logbuffer_lock); + tail = chip->logbuffer_tail; + while (tail != chip->logbuffer_head) { + seq_printf(s, "%s\n", chip->logbuffer[tail]); + tail = (tail + 1) % LOG_BUFFER_ENTRIES; + } + if (!seq_has_overflowed(s)) + chip->logbuffer_tail = tail; + mutex_unlock(&chip->logbuffer_lock); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(fusb302_debug); + +static void fusb302_debugfs_init(struct fusb302_chip *chip) +{ + char name[NAME_MAX]; + + mutex_init(&chip->logbuffer_lock); + snprintf(name, NAME_MAX, "fusb302-%s", dev_name(chip->dev)); + chip->dentry = debugfs_create_dir(name, usb_debug_root); + debugfs_create_file("log", S_IFREG | 0444, chip->dentry, chip, + &fusb302_debug_fops); +} + +static void fusb302_debugfs_exit(struct fusb302_chip *chip) +{ + debugfs_remove(chip->dentry); +} + +#else + +static void fusb302_log(const struct fusb302_chip *chip, + const char *fmt, ...) { } +static void fusb302_debugfs_init(const struct fusb302_chip *chip) { } +static void fusb302_debugfs_exit(const struct fusb302_chip *chip) { } + +#endif + +static int fusb302_i2c_write(struct fusb302_chip *chip, + u8 address, u8 data) +{ + int ret = 0; + + ret = i2c_smbus_write_byte_data(chip->i2c_client, address, data); + if (ret < 0) + fusb302_log(chip, "cannot write 0x%02x to 0x%02x, ret=%d", + data, address, ret); + + return ret; +} + +static int fusb302_i2c_block_write(struct fusb302_chip *chip, u8 address, + u8 length, const u8 *data) +{ + int ret = 0; + + if (length <= 0) + return ret; + + ret = i2c_smbus_write_i2c_block_data(chip->i2c_client, address, + length, data); + if (ret < 0) + fusb302_log(chip, "cannot block write 0x%02x, len=%d, ret=%d", + address, length, ret); + + return ret; +} + +static int fusb302_i2c_read(struct fusb302_chip *chip, + u8 address, u8 *data) +{ + int ret = 0; + + ret = i2c_smbus_read_byte_data(chip->i2c_client, address); + *data = (u8)ret; + if (ret < 0) + fusb302_log(chip, "cannot read %02x, ret=%d", address, ret); + + return ret; +} + +static int fusb302_i2c_block_read(struct fusb302_chip *chip, u8 address, + u8 length, u8 *data) +{ + int ret = 0; + + if (length <= 0) + return ret; + + ret = i2c_smbus_read_i2c_block_data(chip->i2c_client, address, + length, data); + if (ret < 0) { + fusb302_log(chip, "cannot block read 0x%02x, len=%d, ret=%d", + address, length, ret); + goto done; + } + if (ret != length) { + fusb302_log(chip, "only read %d/%d bytes from 0x%02x", + ret, length, address); + ret = -EIO; + } + +done: + return ret; +} + +static int fusb302_i2c_mask_write(struct fusb302_chip *chip, u8 address, + u8 mask, u8 value) +{ + int ret = 0; + u8 data; + + ret = fusb302_i2c_read(chip, address, &data); + if (ret < 0) + return ret; + data &= ~mask; + data |= value; + ret = fusb302_i2c_write(chip, address, data); + if (ret < 0) + return ret; + + return ret; +} + +static int fusb302_i2c_set_bits(struct fusb302_chip *chip, u8 address, + u8 set_bits) +{ + return fusb302_i2c_mask_write(chip, address, 0x00, set_bits); +} + +static int fusb302_i2c_clear_bits(struct fusb302_chip *chip, u8 address, + u8 clear_bits) +{ + return fusb302_i2c_mask_write(chip, address, clear_bits, 0x00); +} + +static int fusb302_sw_reset(struct fusb302_chip *chip) +{ + int ret = 0; + + ret = fusb302_i2c_write(chip, FUSB_REG_RESET, + FUSB_REG_RESET_SW_RESET); + if (ret < 0) + fusb302_log(chip, "cannot sw reset the chip, ret=%d", ret); + else + fusb302_log(chip, "sw reset"); + + return ret; +} + +static int fusb302_enable_tx_auto_retries(struct fusb302_chip *chip, u8 retry_count) +{ + int ret = 0; + + ret = fusb302_i2c_set_bits(chip, FUSB_REG_CONTROL3, retry_count | + FUSB_REG_CONTROL3_AUTO_RETRY); + + return ret; +} + +/* + * initialize interrupt on the chip + * - unmasked interrupt: VBUS_OK + */ +static int fusb302_init_interrupt(struct fusb302_chip *chip) +{ + int ret = 0; + + ret = fusb302_i2c_write(chip, FUSB_REG_MASK, + 0xFF & ~FUSB_REG_MASK_VBUSOK); + if (ret < 0) + return ret; + ret = fusb302_i2c_write(chip, FUSB_REG_MASKA, 0xFF); + if (ret < 0) + return ret; + ret = fusb302_i2c_write(chip, FUSB_REG_MASKB, 0xFF); + if (ret < 0) + return ret; + ret = fusb302_i2c_clear_bits(chip, FUSB_REG_CONTROL0, + FUSB_REG_CONTROL0_INT_MASK); + if (ret < 0) + return ret; + + return ret; +} + +static int fusb302_set_power_mode(struct fusb302_chip *chip, u8 power_mode) +{ + int ret = 0; + + ret = fusb302_i2c_write(chip, FUSB_REG_POWER, power_mode); + + return ret; +} + +static int tcpm_init(struct tcpc_dev *dev) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + u8 data; + + ret = fusb302_sw_reset(chip); + if (ret < 0) + return ret; + ret = fusb302_enable_tx_auto_retries(chip, FUSB_REG_CONTROL3_N_RETRIES_3); + if (ret < 0) + return ret; + ret = fusb302_init_interrupt(chip); + if (ret < 0) + return ret; + ret = fusb302_set_power_mode(chip, FUSB_REG_POWER_PWR_ALL); + if (ret < 0) + return ret; + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &data); + if (ret < 0) + return ret; + chip->vbus_present = !!(data & FUSB_REG_STATUS0_VBUSOK); + ret = fusb302_i2c_read(chip, FUSB_REG_DEVICE_ID, &data); + if (ret < 0) + return ret; + fusb302_log(chip, "fusb302 device ID: 0x%02x", data); + + return ret; +} + +static int tcpm_get_vbus(struct tcpc_dev *dev) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + + mutex_lock(&chip->lock); + ret = chip->vbus_present ? 1 : 0; + mutex_unlock(&chip->lock); + + return ret; +} + +static int tcpm_get_current_limit(struct tcpc_dev *dev) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int current_limit = 0; + unsigned long timeout; + + if (!chip->extcon) + return 0; + + /* + * USB2 Charger detection may still be in progress when we get here, + * this can take upto 600ms, wait 800ms max. + */ + timeout = jiffies + msecs_to_jiffies(800); + do { + if (extcon_get_state(chip->extcon, EXTCON_CHG_USB_SDP) == 1) + current_limit = 500; + + if (extcon_get_state(chip->extcon, EXTCON_CHG_USB_CDP) == 1 || + extcon_get_state(chip->extcon, EXTCON_CHG_USB_ACA) == 1) + current_limit = 1500; + + if (extcon_get_state(chip->extcon, EXTCON_CHG_USB_DCP) == 1) + current_limit = 2000; + + msleep(50); + } while (current_limit == 0 && time_before(jiffies, timeout)); + + return current_limit; +} + +static int fusb302_set_src_current(struct fusb302_chip *chip, + enum src_current_status status) +{ + int ret = 0; + + chip->src_current_status = status; + switch (status) { + case SRC_CURRENT_DEFAULT: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL0, + FUSB_REG_CONTROL0_HOST_CUR_MASK, + FUSB_REG_CONTROL0_HOST_CUR_DEF); + break; + case SRC_CURRENT_MEDIUM: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL0, + FUSB_REG_CONTROL0_HOST_CUR_MASK, + FUSB_REG_CONTROL0_HOST_CUR_MED); + break; + case SRC_CURRENT_HIGH: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL0, + FUSB_REG_CONTROL0_HOST_CUR_MASK, + FUSB_REG_CONTROL0_HOST_CUR_HIGH); + break; + default: + break; + } + + return ret; +} + +static int fusb302_set_toggling(struct fusb302_chip *chip, + enum toggling_mode mode) +{ + int ret = 0; + + /* first disable toggling */ + ret = fusb302_i2c_clear_bits(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_TOGGLE); + if (ret < 0) + return ret; + /* mask interrupts for SRC or SNK */ + ret = fusb302_i2c_set_bits(chip, FUSB_REG_MASK, + FUSB_REG_MASK_BC_LVL | + FUSB_REG_MASK_COMP_CHNG); + if (ret < 0) + return ret; + chip->intr_bc_lvl = false; + chip->intr_comp_chng = false; + /* configure toggling mode: none/snk/src/drp */ + switch (mode) { + case TOGGLING_MODE_OFF: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_MODE_MASK, + FUSB_REG_CONTROL2_MODE_NONE); + if (ret < 0) + return ret; + break; + case TOGGLING_MODE_SNK: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_MODE_MASK, + FUSB_REG_CONTROL2_MODE_UFP); + if (ret < 0) + return ret; + break; + case TOGGLING_MODE_SRC: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_MODE_MASK, + FUSB_REG_CONTROL2_MODE_DFP); + if (ret < 0) + return ret; + break; + case TOGGLING_MODE_DRP: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_MODE_MASK, + FUSB_REG_CONTROL2_MODE_DRP); + if (ret < 0) + return ret; + break; + default: + break; + } + + if (mode == TOGGLING_MODE_OFF) { + /* mask TOGDONE interrupt */ + ret = fusb302_i2c_set_bits(chip, FUSB_REG_MASKA, + FUSB_REG_MASKA_TOGDONE); + if (ret < 0) + return ret; + chip->intr_togdone = false; + } else { + /* Datasheet says vconn MUST be off when toggling */ + WARN(chip->vconn_on, "Vconn is on during toggle start"); + /* unmask TOGDONE interrupt */ + ret = fusb302_i2c_clear_bits(chip, FUSB_REG_MASKA, + FUSB_REG_MASKA_TOGDONE); + if (ret < 0) + return ret; + chip->intr_togdone = true; + /* start toggling */ + ret = fusb302_i2c_set_bits(chip, FUSB_REG_CONTROL2, + FUSB_REG_CONTROL2_TOGGLE); + if (ret < 0) + return ret; + /* during toggling, consider cc as Open */ + chip->cc1 = TYPEC_CC_OPEN; + chip->cc2 = TYPEC_CC_OPEN; + } + chip->toggling_mode = mode; + + return ret; +} + +static const char * const typec_cc_status_name[] = { + [TYPEC_CC_OPEN] = "Open", + [TYPEC_CC_RA] = "Ra", + [TYPEC_CC_RD] = "Rd", + [TYPEC_CC_RP_DEF] = "Rp-def", + [TYPEC_CC_RP_1_5] = "Rp-1.5", + [TYPEC_CC_RP_3_0] = "Rp-3.0", +}; + +static const enum src_current_status cc_src_current[] = { + [TYPEC_CC_OPEN] = SRC_CURRENT_DEFAULT, + [TYPEC_CC_RA] = SRC_CURRENT_DEFAULT, + [TYPEC_CC_RD] = SRC_CURRENT_DEFAULT, + [TYPEC_CC_RP_DEF] = SRC_CURRENT_DEFAULT, + [TYPEC_CC_RP_1_5] = SRC_CURRENT_MEDIUM, + [TYPEC_CC_RP_3_0] = SRC_CURRENT_HIGH, +}; + +static int tcpm_set_cc(struct tcpc_dev *dev, enum typec_cc_status cc) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + u8 switches0_mask = FUSB_REG_SWITCHES0_CC1_PU_EN | + FUSB_REG_SWITCHES0_CC2_PU_EN | + FUSB_REG_SWITCHES0_CC1_PD_EN | + FUSB_REG_SWITCHES0_CC2_PD_EN; + u8 rd_mda, switches0_data = 0x00; + int ret = 0; + + mutex_lock(&chip->lock); + switch (cc) { + case TYPEC_CC_OPEN: + break; + case TYPEC_CC_RD: + switches0_data |= FUSB_REG_SWITCHES0_CC1_PD_EN | + FUSB_REG_SWITCHES0_CC2_PD_EN; + break; + case TYPEC_CC_RP_DEF: + case TYPEC_CC_RP_1_5: + case TYPEC_CC_RP_3_0: + switches0_data |= (chip->cc_polarity == TYPEC_POLARITY_CC1) ? + FUSB_REG_SWITCHES0_CC1_PU_EN : + FUSB_REG_SWITCHES0_CC2_PU_EN; + break; + default: + fusb302_log(chip, "unsupported cc value %s", + typec_cc_status_name[cc]); + ret = -EINVAL; + goto done; + } + + fusb302_log(chip, "cc := %s", typec_cc_status_name[cc]); + + ret = fusb302_set_toggling(chip, TOGGLING_MODE_OFF); + if (ret < 0) { + fusb302_log(chip, "cannot set toggling mode, ret=%d", ret); + goto done; + } + + ret = fusb302_i2c_mask_write(chip, FUSB_REG_SWITCHES0, + switches0_mask, switches0_data); + if (ret < 0) { + fusb302_log(chip, "cannot set pull-up/-down, ret = %d", ret); + goto done; + } + /* reset the cc status */ + chip->cc1 = TYPEC_CC_OPEN; + chip->cc2 = TYPEC_CC_OPEN; + + /* adjust current for SRC */ + ret = fusb302_set_src_current(chip, cc_src_current[cc]); + if (ret < 0) { + fusb302_log(chip, "cannot set src current %s, ret=%d", + typec_cc_status_name[cc], ret); + goto done; + } + + /* enable/disable interrupts, BC_LVL for SNK and COMP_CHNG for SRC */ + switch (cc) { + case TYPEC_CC_RP_DEF: + case TYPEC_CC_RP_1_5: + case TYPEC_CC_RP_3_0: + rd_mda = rd_mda_value[cc_src_current[cc]]; + ret = fusb302_i2c_write(chip, FUSB_REG_MEASURE, rd_mda); + if (ret < 0) { + fusb302_log(chip, + "cannot set SRC measure value, ret=%d", + ret); + goto done; + } + ret = fusb302_i2c_mask_write(chip, FUSB_REG_MASK, + FUSB_REG_MASK_BC_LVL | + FUSB_REG_MASK_COMP_CHNG, + FUSB_REG_MASK_BC_LVL); + if (ret < 0) { + fusb302_log(chip, "cannot set SRC interrupt, ret=%d", + ret); + goto done; + } + chip->intr_comp_chng = true; + chip->intr_bc_lvl = false; + break; + case TYPEC_CC_RD: + ret = fusb302_i2c_mask_write(chip, FUSB_REG_MASK, + FUSB_REG_MASK_BC_LVL | + FUSB_REG_MASK_COMP_CHNG, + FUSB_REG_MASK_COMP_CHNG); + if (ret < 0) { + fusb302_log(chip, "cannot set SRC interrupt, ret=%d", + ret); + goto done; + } + chip->intr_bc_lvl = true; + chip->intr_comp_chng = false; + break; + default: + break; + } +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static int tcpm_get_cc(struct tcpc_dev *dev, enum typec_cc_status *cc1, + enum typec_cc_status *cc2) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + + mutex_lock(&chip->lock); + *cc1 = chip->cc1; + *cc2 = chip->cc2; + fusb302_log(chip, "cc1=%s, cc2=%s", typec_cc_status_name[*cc1], + typec_cc_status_name[*cc2]); + mutex_unlock(&chip->lock); + + return 0; +} + +static int tcpm_set_polarity(struct tcpc_dev *dev, + enum typec_cc_polarity polarity) +{ + return 0; +} + +static int tcpm_set_vconn(struct tcpc_dev *dev, bool on) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + u8 switches0_data = 0x00; + u8 switches0_mask = FUSB_REG_SWITCHES0_VCONN_CC1 | + FUSB_REG_SWITCHES0_VCONN_CC2; + + mutex_lock(&chip->lock); + if (chip->vconn_on == on) { + fusb302_log(chip, "vconn is already %s", on ? "On" : "Off"); + goto done; + } + if (on) { + switches0_data = (chip->cc_polarity == TYPEC_POLARITY_CC1) ? + FUSB_REG_SWITCHES0_VCONN_CC2 : + FUSB_REG_SWITCHES0_VCONN_CC1; + } + ret = fusb302_i2c_mask_write(chip, FUSB_REG_SWITCHES0, + switches0_mask, switches0_data); + if (ret < 0) + goto done; + chip->vconn_on = on; + fusb302_log(chip, "vconn := %s", on ? "On" : "Off"); +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static int tcpm_set_vbus(struct tcpc_dev *dev, bool on, bool charge) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + + mutex_lock(&chip->lock); + if (chip->vbus_on == on) { + fusb302_log(chip, "vbus is already %s", on ? "On" : "Off"); + } else { + if (on) + ret = regulator_enable(chip->vbus); + else + ret = regulator_disable(chip->vbus); + if (ret < 0) { + fusb302_log(chip, "cannot %s vbus regulator, ret=%d", + on ? "enable" : "disable", ret); + goto done; + } + chip->vbus_on = on; + fusb302_log(chip, "vbus := %s", on ? "On" : "Off"); + } + if (chip->charge_on == charge) + fusb302_log(chip, "charge is already %s", + charge ? "On" : "Off"); + else + chip->charge_on = charge; + +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static int fusb302_pd_tx_flush(struct fusb302_chip *chip) +{ + return fusb302_i2c_set_bits(chip, FUSB_REG_CONTROL0, + FUSB_REG_CONTROL0_TX_FLUSH); +} + +static int fusb302_pd_rx_flush(struct fusb302_chip *chip) +{ + return fusb302_i2c_set_bits(chip, FUSB_REG_CONTROL1, + FUSB_REG_CONTROL1_RX_FLUSH); +} + +static int fusb302_pd_set_auto_goodcrc(struct fusb302_chip *chip, bool on) +{ + if (on) + return fusb302_i2c_set_bits(chip, FUSB_REG_SWITCHES1, + FUSB_REG_SWITCHES1_AUTO_GCRC); + return fusb302_i2c_clear_bits(chip, FUSB_REG_SWITCHES1, + FUSB_REG_SWITCHES1_AUTO_GCRC); +} + +static int fusb302_pd_set_interrupts(struct fusb302_chip *chip, bool on) +{ + int ret = 0; + u8 mask_interrupts = FUSB_REG_MASK_COLLISION; + u8 maska_interrupts = FUSB_REG_MASKA_RETRYFAIL | + FUSB_REG_MASKA_HARDSENT | + FUSB_REG_MASKA_TX_SUCCESS | + FUSB_REG_MASKA_HARDRESET; + u8 maskb_interrupts = FUSB_REG_MASKB_GCRCSENT; + + ret = on ? + fusb302_i2c_clear_bits(chip, FUSB_REG_MASK, mask_interrupts) : + fusb302_i2c_set_bits(chip, FUSB_REG_MASK, mask_interrupts); + if (ret < 0) + return ret; + ret = on ? + fusb302_i2c_clear_bits(chip, FUSB_REG_MASKA, maska_interrupts) : + fusb302_i2c_set_bits(chip, FUSB_REG_MASKA, maska_interrupts); + if (ret < 0) + return ret; + ret = on ? + fusb302_i2c_clear_bits(chip, FUSB_REG_MASKB, maskb_interrupts) : + fusb302_i2c_set_bits(chip, FUSB_REG_MASKB, maskb_interrupts); + return ret; +} + +static int tcpm_set_pd_rx(struct tcpc_dev *dev, bool on) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + + mutex_lock(&chip->lock); + ret = fusb302_pd_rx_flush(chip); + if (ret < 0) { + fusb302_log(chip, "cannot flush pd rx buffer, ret=%d", ret); + goto done; + } + ret = fusb302_pd_tx_flush(chip); + if (ret < 0) { + fusb302_log(chip, "cannot flush pd tx buffer, ret=%d", ret); + goto done; + } + ret = fusb302_pd_set_auto_goodcrc(chip, on); + if (ret < 0) { + fusb302_log(chip, "cannot turn %s auto GCRC, ret=%d", + on ? "on" : "off", ret); + goto done; + } + ret = fusb302_pd_set_interrupts(chip, on); + if (ret < 0) { + fusb302_log(chip, "cannot turn %s pd interrupts, ret=%d", + on ? "on" : "off", ret); + goto done; + } + fusb302_log(chip, "pd := %s", on ? "on" : "off"); +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static const char * const typec_role_name[] = { + [TYPEC_SINK] = "Sink", + [TYPEC_SOURCE] = "Source", +}; + +static const char * const typec_data_role_name[] = { + [TYPEC_DEVICE] = "Device", + [TYPEC_HOST] = "Host", +}; + +static int tcpm_set_roles(struct tcpc_dev *dev, bool attached, + enum typec_role pwr, enum typec_data_role data) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + u8 switches1_mask = FUSB_REG_SWITCHES1_POWERROLE | + FUSB_REG_SWITCHES1_DATAROLE; + u8 switches1_data = 0x00; + + mutex_lock(&chip->lock); + if (pwr == TYPEC_SOURCE) + switches1_data |= FUSB_REG_SWITCHES1_POWERROLE; + if (data == TYPEC_HOST) + switches1_data |= FUSB_REG_SWITCHES1_DATAROLE; + ret = fusb302_i2c_mask_write(chip, FUSB_REG_SWITCHES1, + switches1_mask, switches1_data); + if (ret < 0) { + fusb302_log(chip, "unable to set pd header %s, %s, ret=%d", + typec_role_name[pwr], typec_data_role_name[data], + ret); + goto done; + } + fusb302_log(chip, "pd header := %s, %s", typec_role_name[pwr], + typec_data_role_name[data]); +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static int tcpm_start_toggling(struct tcpc_dev *dev, + enum typec_port_type port_type, + enum typec_cc_status cc) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + enum toggling_mode mode = TOGGLING_MODE_OFF; + int ret = 0; + + switch (port_type) { + case TYPEC_PORT_SRC: + mode = TOGGLING_MODE_SRC; + break; + case TYPEC_PORT_SNK: + mode = TOGGLING_MODE_SNK; + break; + case TYPEC_PORT_DRP: + mode = TOGGLING_MODE_DRP; + break; + } + + mutex_lock(&chip->lock); + ret = fusb302_set_src_current(chip, cc_src_current[cc]); + if (ret < 0) { + fusb302_log(chip, "unable to set src current %s, ret=%d", + typec_cc_status_name[cc], ret); + goto done; + } + ret = fusb302_set_toggling(chip, mode); + if (ret < 0) { + fusb302_log(chip, + "unable to start drp toggling, ret=%d", ret); + goto done; + } + fusb302_log(chip, "start drp toggling"); +done: + mutex_unlock(&chip->lock); + + return ret; +} + +static int fusb302_pd_send_message(struct fusb302_chip *chip, + const struct pd_message *msg) +{ + int ret = 0; + u8 buf[40]; + u8 pos = 0; + int len; + + /* SOP tokens */ + buf[pos++] = FUSB302_TKN_SYNC1; + buf[pos++] = FUSB302_TKN_SYNC1; + buf[pos++] = FUSB302_TKN_SYNC1; + buf[pos++] = FUSB302_TKN_SYNC2; + + len = pd_header_cnt_le(msg->header) * 4; + /* plug 2 for header */ + len += 2; + if (len > 0x1F) { + fusb302_log(chip, + "PD message too long %d (incl. header)", len); + return -EINVAL; + } + /* packsym tells the FUSB302 chip that the next X bytes are payload */ + buf[pos++] = FUSB302_TKN_PACKSYM | (len & 0x1F); + memcpy(&buf[pos], &msg->header, sizeof(msg->header)); + pos += sizeof(msg->header); + + len -= 2; + memcpy(&buf[pos], msg->payload, len); + pos += len; + + /* CRC */ + buf[pos++] = FUSB302_TKN_JAMCRC; + /* EOP */ + buf[pos++] = FUSB302_TKN_EOP; + /* turn tx off after sending message */ + buf[pos++] = FUSB302_TKN_TXOFF; + /* start transmission */ + buf[pos++] = FUSB302_TKN_TXON; + + ret = fusb302_i2c_block_write(chip, FUSB_REG_FIFOS, pos, buf); + if (ret < 0) + return ret; + fusb302_log(chip, "sending PD message header: %x", msg->header); + fusb302_log(chip, "sending PD message len: %d", len); + + return ret; +} + +static int fusb302_pd_send_hardreset(struct fusb302_chip *chip) +{ + return fusb302_i2c_set_bits(chip, FUSB_REG_CONTROL3, + FUSB_REG_CONTROL3_SEND_HARDRESET); +} + +static const char * const transmit_type_name[] = { + [TCPC_TX_SOP] = "SOP", + [TCPC_TX_SOP_PRIME] = "SOP'", + [TCPC_TX_SOP_PRIME_PRIME] = "SOP''", + [TCPC_TX_SOP_DEBUG_PRIME] = "DEBUG'", + [TCPC_TX_SOP_DEBUG_PRIME_PRIME] = "DEBUG''", + [TCPC_TX_HARD_RESET] = "HARD_RESET", + [TCPC_TX_CABLE_RESET] = "CABLE_RESET", + [TCPC_TX_BIST_MODE_2] = "BIST_MODE_2", +}; + +static int tcpm_pd_transmit(struct tcpc_dev *dev, enum tcpm_transmit_type type, + const struct pd_message *msg, unsigned int negotiated_rev) +{ + struct fusb302_chip *chip = container_of(dev, struct fusb302_chip, + tcpc_dev); + int ret = 0; + + mutex_lock(&chip->lock); + switch (type) { + case TCPC_TX_SOP: + /* nRetryCount 3 in P2.0 spec, whereas 2 in PD3.0 spec */ + ret = fusb302_enable_tx_auto_retries(chip, negotiated_rev > PD_REV20 ? + FUSB_REG_CONTROL3_N_RETRIES_2 : + FUSB_REG_CONTROL3_N_RETRIES_3); + if (ret < 0) + fusb302_log(chip, "Cannot update retry count ret=%d", ret); + + ret = fusb302_pd_send_message(chip, msg); + if (ret < 0) + fusb302_log(chip, + "cannot send PD message, ret=%d", ret); + break; + case TCPC_TX_HARD_RESET: + ret = fusb302_pd_send_hardreset(chip); + if (ret < 0) + fusb302_log(chip, + "cannot send hardreset, ret=%d", ret); + break; + default: + fusb302_log(chip, "type %s not supported", + transmit_type_name[type]); + ret = -EINVAL; + } + mutex_unlock(&chip->lock); + + return ret; +} + +static enum typec_cc_status fusb302_bc_lvl_to_cc(u8 bc_lvl) +{ + if (bc_lvl == FUSB_REG_STATUS0_BC_LVL_1230_MAX) + return TYPEC_CC_RP_3_0; + if (bc_lvl == FUSB_REG_STATUS0_BC_LVL_600_1230) + return TYPEC_CC_RP_1_5; + if (bc_lvl == FUSB_REG_STATUS0_BC_LVL_200_600) + return TYPEC_CC_RP_DEF; + return TYPEC_CC_OPEN; +} + +static void fusb302_bc_lvl_handler_work(struct work_struct *work) +{ + struct fusb302_chip *chip = container_of(work, struct fusb302_chip, + bc_lvl_handler.work); + int ret = 0; + u8 status0; + u8 bc_lvl; + enum typec_cc_status cc_status; + + mutex_lock(&chip->lock); + if (!chip->intr_bc_lvl) { + fusb302_log(chip, "BC_LVL interrupt is turned off, abort"); + goto done; + } + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &status0); + if (ret < 0) + goto done; + fusb302_log(chip, "BC_LVL handler, status0=0x%02x", status0); + if (status0 & FUSB_REG_STATUS0_ACTIVITY) { + fusb302_log(chip, "CC activities detected, delay handling"); + mod_delayed_work(chip->wq, &chip->bc_lvl_handler, + msecs_to_jiffies(T_BC_LVL_DEBOUNCE_DELAY_MS)); + goto done; + } + bc_lvl = status0 & FUSB_REG_STATUS0_BC_LVL_MASK; + cc_status = fusb302_bc_lvl_to_cc(bc_lvl); + if (chip->cc_polarity == TYPEC_POLARITY_CC1) { + if (chip->cc1 != cc_status) { + fusb302_log(chip, "cc1: %s -> %s", + typec_cc_status_name[chip->cc1], + typec_cc_status_name[cc_status]); + chip->cc1 = cc_status; + tcpm_cc_change(chip->tcpm_port); + } + } else { + if (chip->cc2 != cc_status) { + fusb302_log(chip, "cc2: %s -> %s", + typec_cc_status_name[chip->cc2], + typec_cc_status_name[cc_status]); + chip->cc2 = cc_status; + tcpm_cc_change(chip->tcpm_port); + } + } + +done: + mutex_unlock(&chip->lock); +} + +static void init_tcpc_dev(struct tcpc_dev *fusb302_tcpc_dev) +{ + fusb302_tcpc_dev->init = tcpm_init; + fusb302_tcpc_dev->get_vbus = tcpm_get_vbus; + fusb302_tcpc_dev->get_current_limit = tcpm_get_current_limit; + fusb302_tcpc_dev->set_cc = tcpm_set_cc; + fusb302_tcpc_dev->get_cc = tcpm_get_cc; + fusb302_tcpc_dev->set_polarity = tcpm_set_polarity; + fusb302_tcpc_dev->set_vconn = tcpm_set_vconn; + fusb302_tcpc_dev->set_vbus = tcpm_set_vbus; + fusb302_tcpc_dev->set_pd_rx = tcpm_set_pd_rx; + fusb302_tcpc_dev->set_roles = tcpm_set_roles; + fusb302_tcpc_dev->start_toggling = tcpm_start_toggling; + fusb302_tcpc_dev->pd_transmit = tcpm_pd_transmit; +} + +static const char * const cc_polarity_name[] = { + [TYPEC_POLARITY_CC1] = "Polarity_CC1", + [TYPEC_POLARITY_CC2] = "Polarity_CC2", +}; + +static int fusb302_set_cc_polarity_and_pull(struct fusb302_chip *chip, + enum typec_cc_polarity cc_polarity, + bool pull_up, bool pull_down) +{ + int ret = 0; + u8 switches0_data = 0x00; + u8 switches1_mask = FUSB_REG_SWITCHES1_TXCC1_EN | + FUSB_REG_SWITCHES1_TXCC2_EN; + u8 switches1_data = 0x00; + + if (pull_down) + switches0_data |= FUSB_REG_SWITCHES0_CC1_PD_EN | + FUSB_REG_SWITCHES0_CC2_PD_EN; + + if (cc_polarity == TYPEC_POLARITY_CC1) { + switches0_data |= FUSB_REG_SWITCHES0_MEAS_CC1; + if (chip->vconn_on) + switches0_data |= FUSB_REG_SWITCHES0_VCONN_CC2; + if (pull_up) + switches0_data |= FUSB_REG_SWITCHES0_CC1_PU_EN; + switches1_data = FUSB_REG_SWITCHES1_TXCC1_EN; + } else { + switches0_data |= FUSB_REG_SWITCHES0_MEAS_CC2; + if (chip->vconn_on) + switches0_data |= FUSB_REG_SWITCHES0_VCONN_CC1; + if (pull_up) + switches0_data |= FUSB_REG_SWITCHES0_CC2_PU_EN; + switches1_data = FUSB_REG_SWITCHES1_TXCC2_EN; + } + ret = fusb302_i2c_write(chip, FUSB_REG_SWITCHES0, switches0_data); + if (ret < 0) + return ret; + ret = fusb302_i2c_mask_write(chip, FUSB_REG_SWITCHES1, + switches1_mask, switches1_data); + if (ret < 0) + return ret; + chip->cc_polarity = cc_polarity; + + return ret; +} + +static int fusb302_handle_togdone_snk(struct fusb302_chip *chip, + u8 togdone_result) +{ + int ret = 0; + u8 status0; + u8 bc_lvl; + enum typec_cc_polarity cc_polarity; + enum typec_cc_status cc_status_active, cc1, cc2; + + /* set polarity and pull_up, pull_down */ + cc_polarity = (togdone_result == FUSB_REG_STATUS1A_TOGSS_SNK1) ? + TYPEC_POLARITY_CC1 : TYPEC_POLARITY_CC2; + ret = fusb302_set_cc_polarity_and_pull(chip, cc_polarity, false, true); + if (ret < 0) { + fusb302_log(chip, "cannot set cc polarity %s, ret=%d", + cc_polarity_name[cc_polarity], ret); + return ret; + } + /* fusb302_set_cc_polarity() has set the correct measure block */ + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &status0); + if (ret < 0) + return ret; + bc_lvl = status0 & FUSB_REG_STATUS0_BC_LVL_MASK; + cc_status_active = fusb302_bc_lvl_to_cc(bc_lvl); + /* restart toggling if the cc status on the active line is OPEN */ + if (cc_status_active == TYPEC_CC_OPEN) { + fusb302_log(chip, "restart toggling as CC_OPEN detected"); + ret = fusb302_set_toggling(chip, chip->toggling_mode); + return ret; + } + /* update tcpm with the new cc value */ + cc1 = (cc_polarity == TYPEC_POLARITY_CC1) ? + cc_status_active : TYPEC_CC_OPEN; + cc2 = (cc_polarity == TYPEC_POLARITY_CC2) ? + cc_status_active : TYPEC_CC_OPEN; + if ((chip->cc1 != cc1) || (chip->cc2 != cc2)) { + chip->cc1 = cc1; + chip->cc2 = cc2; + tcpm_cc_change(chip->tcpm_port); + } + /* turn off toggling */ + ret = fusb302_set_toggling(chip, TOGGLING_MODE_OFF); + if (ret < 0) { + fusb302_log(chip, + "cannot set toggling mode off, ret=%d", ret); + return ret; + } + /* unmask bc_lvl interrupt */ + ret = fusb302_i2c_clear_bits(chip, FUSB_REG_MASK, FUSB_REG_MASK_BC_LVL); + if (ret < 0) { + fusb302_log(chip, + "cannot unmask bc_lcl interrupt, ret=%d", ret); + return ret; + } + chip->intr_bc_lvl = true; + fusb302_log(chip, "detected cc1=%s, cc2=%s", + typec_cc_status_name[cc1], + typec_cc_status_name[cc2]); + + return ret; +} + +/* On error returns < 0, otherwise a typec_cc_status value */ +static int fusb302_get_src_cc_status(struct fusb302_chip *chip, + enum typec_cc_polarity cc_polarity, + enum typec_cc_status *cc) +{ + u8 ra_mda = ra_mda_value[chip->src_current_status]; + u8 rd_mda = rd_mda_value[chip->src_current_status]; + u8 switches0_data, status0; + int ret; + + /* Step 1: Set switches so that we measure the right CC pin */ + switches0_data = (cc_polarity == TYPEC_POLARITY_CC1) ? + FUSB_REG_SWITCHES0_CC1_PU_EN | FUSB_REG_SWITCHES0_MEAS_CC1 : + FUSB_REG_SWITCHES0_CC2_PU_EN | FUSB_REG_SWITCHES0_MEAS_CC2; + ret = fusb302_i2c_write(chip, FUSB_REG_SWITCHES0, switches0_data); + if (ret < 0) + return ret; + + fusb302_i2c_read(chip, FUSB_REG_SWITCHES0, &status0); + fusb302_log(chip, "get_src_cc_status switches: 0x%0x", status0); + + /* Step 2: Set compararator volt to differentiate between Open and Rd */ + ret = fusb302_i2c_write(chip, FUSB_REG_MEASURE, rd_mda); + if (ret < 0) + return ret; + + usleep_range(50, 100); + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &status0); + if (ret < 0) + return ret; + + fusb302_log(chip, "get_src_cc_status rd_mda status0: 0x%0x", status0); + if (status0 & FUSB_REG_STATUS0_COMP) { + *cc = TYPEC_CC_OPEN; + return 0; + } + + /* Step 3: Set compararator input to differentiate between Rd and Ra. */ + ret = fusb302_i2c_write(chip, FUSB_REG_MEASURE, ra_mda); + if (ret < 0) + return ret; + + usleep_range(50, 100); + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &status0); + if (ret < 0) + return ret; + + fusb302_log(chip, "get_src_cc_status ra_mda status0: 0x%0x", status0); + if (status0 & FUSB_REG_STATUS0_COMP) + *cc = TYPEC_CC_RD; + else + *cc = TYPEC_CC_RA; + + return 0; +} + +static int fusb302_handle_togdone_src(struct fusb302_chip *chip, + u8 togdone_result) +{ + /* + * - set polarity (measure cc, vconn, tx) + * - set pull_up, pull_down + * - set cc1, cc2, and update to tcpm_port + * - set I_COMP interrupt on + */ + int ret = 0; + u8 rd_mda = rd_mda_value[chip->src_current_status]; + enum toggling_mode toggling_mode = chip->toggling_mode; + enum typec_cc_polarity cc_polarity; + enum typec_cc_status cc1, cc2; + + /* + * The toggle-engine will stop in a src state if it sees either Ra or + * Rd. Determine the status for both CC pins, starting with the one + * where toggling stopped, as that is where the switches point now. + */ + if (togdone_result == FUSB_REG_STATUS1A_TOGSS_SRC1) + ret = fusb302_get_src_cc_status(chip, TYPEC_POLARITY_CC1, &cc1); + else + ret = fusb302_get_src_cc_status(chip, TYPEC_POLARITY_CC2, &cc2); + if (ret < 0) + return ret; + /* we must turn off toggling before we can measure the other pin */ + ret = fusb302_set_toggling(chip, TOGGLING_MODE_OFF); + if (ret < 0) { + fusb302_log(chip, "cannot set toggling mode off, ret=%d", ret); + return ret; + } + /* get the status of the other pin */ + if (togdone_result == FUSB_REG_STATUS1A_TOGSS_SRC1) + ret = fusb302_get_src_cc_status(chip, TYPEC_POLARITY_CC2, &cc2); + else + ret = fusb302_get_src_cc_status(chip, TYPEC_POLARITY_CC1, &cc1); + if (ret < 0) + return ret; + + /* determine polarity based on the status of both pins */ + if (cc1 == TYPEC_CC_RD && + (cc2 == TYPEC_CC_OPEN || cc2 == TYPEC_CC_RA)) { + cc_polarity = TYPEC_POLARITY_CC1; + } else if (cc2 == TYPEC_CC_RD && + (cc1 == TYPEC_CC_OPEN || cc1 == TYPEC_CC_RA)) { + cc_polarity = TYPEC_POLARITY_CC2; + } else { + fusb302_log(chip, "unexpected CC status cc1=%s, cc2=%s, restarting toggling", + typec_cc_status_name[cc1], + typec_cc_status_name[cc2]); + return fusb302_set_toggling(chip, toggling_mode); + } + /* set polarity and pull_up, pull_down */ + ret = fusb302_set_cc_polarity_and_pull(chip, cc_polarity, true, false); + if (ret < 0) { + fusb302_log(chip, "cannot set cc polarity %s, ret=%d", + cc_polarity_name[cc_polarity], ret); + return ret; + } + /* update tcpm with the new cc value */ + if ((chip->cc1 != cc1) || (chip->cc2 != cc2)) { + chip->cc1 = cc1; + chip->cc2 = cc2; + tcpm_cc_change(chip->tcpm_port); + } + /* set MDAC to Rd threshold, and unmask I_COMP for unplug detection */ + ret = fusb302_i2c_write(chip, FUSB_REG_MEASURE, rd_mda); + if (ret < 0) + return ret; + /* unmask comp_chng interrupt */ + ret = fusb302_i2c_clear_bits(chip, FUSB_REG_MASK, + FUSB_REG_MASK_COMP_CHNG); + if (ret < 0) { + fusb302_log(chip, + "cannot unmask comp_chng interrupt, ret=%d", ret); + return ret; + } + chip->intr_comp_chng = true; + fusb302_log(chip, "detected cc1=%s, cc2=%s", + typec_cc_status_name[cc1], + typec_cc_status_name[cc2]); + + return ret; +} + +static int fusb302_handle_togdone(struct fusb302_chip *chip) +{ + int ret = 0; + u8 status1a; + u8 togdone_result; + + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS1A, &status1a); + if (ret < 0) + return ret; + togdone_result = (status1a >> FUSB_REG_STATUS1A_TOGSS_POS) & + FUSB_REG_STATUS1A_TOGSS_MASK; + switch (togdone_result) { + case FUSB_REG_STATUS1A_TOGSS_SNK1: + case FUSB_REG_STATUS1A_TOGSS_SNK2: + return fusb302_handle_togdone_snk(chip, togdone_result); + case FUSB_REG_STATUS1A_TOGSS_SRC1: + case FUSB_REG_STATUS1A_TOGSS_SRC2: + return fusb302_handle_togdone_src(chip, togdone_result); + case FUSB_REG_STATUS1A_TOGSS_AA: + /* doesn't support */ + fusb302_log(chip, "AudioAccessory not supported"); + fusb302_set_toggling(chip, chip->toggling_mode); + break; + default: + fusb302_log(chip, "TOGDONE with an invalid state: %d", + togdone_result); + fusb302_set_toggling(chip, chip->toggling_mode); + break; + } + return ret; +} + +static int fusb302_pd_reset(struct fusb302_chip *chip) +{ + return fusb302_i2c_set_bits(chip, FUSB_REG_RESET, + FUSB_REG_RESET_PD_RESET); +} + +static int fusb302_pd_read_message(struct fusb302_chip *chip, + struct pd_message *msg) +{ + int ret = 0; + u8 token; + u8 crc[4]; + int len; + + /* first SOP token */ + ret = fusb302_i2c_read(chip, FUSB_REG_FIFOS, &token); + if (ret < 0) + return ret; + ret = fusb302_i2c_block_read(chip, FUSB_REG_FIFOS, 2, + (u8 *)&msg->header); + if (ret < 0) + return ret; + len = pd_header_cnt_le(msg->header) * 4; + /* add 4 to length to include the CRC */ + if (len > PD_MAX_PAYLOAD * 4) { + fusb302_log(chip, "PD message too long %d", len); + return -EINVAL; + } + if (len > 0) { + ret = fusb302_i2c_block_read(chip, FUSB_REG_FIFOS, len, + (u8 *)msg->payload); + if (ret < 0) + return ret; + } + /* another 4 bytes to read CRC out */ + ret = fusb302_i2c_block_read(chip, FUSB_REG_FIFOS, 4, crc); + if (ret < 0) + return ret; + fusb302_log(chip, "PD message header: %x", msg->header); + fusb302_log(chip, "PD message len: %d", len); + + /* + * Check if we've read off a GoodCRC message. If so then indicate to + * TCPM that the previous transmission has completed. Otherwise we pass + * the received message over to TCPM for processing. + * + * We make this check here instead of basing the reporting decision on + * the IRQ event type, as it's possible for the chip to report the + * TX_SUCCESS and GCRCSENT events out of order on occasion, so we need + * to check the message type to ensure correct reporting to TCPM. + */ + if ((!len) && (pd_header_type_le(msg->header) == PD_CTRL_GOOD_CRC)) + tcpm_pd_transmit_complete(chip->tcpm_port, TCPC_TX_SUCCESS); + else + tcpm_pd_receive(chip->tcpm_port, msg); + + return ret; +} + +static irqreturn_t fusb302_irq_intn(int irq, void *dev_id) +{ + struct fusb302_chip *chip = dev_id; + unsigned long flags; + + /* Disable our level triggered IRQ until our irq_work has cleared it */ + disable_irq_nosync(chip->gpio_int_n_irq); + + spin_lock_irqsave(&chip->irq_lock, flags); + if (chip->irq_suspended) + chip->irq_while_suspended = true; + else + schedule_work(&chip->irq_work); + spin_unlock_irqrestore(&chip->irq_lock, flags); + + return IRQ_HANDLED; +} + +static void fusb302_irq_work(struct work_struct *work) +{ + struct fusb302_chip *chip = container_of(work, struct fusb302_chip, + irq_work); + int ret = 0; + u8 interrupt; + u8 interrupta; + u8 interruptb; + u8 status0; + bool vbus_present; + bool comp_result; + bool intr_togdone; + bool intr_bc_lvl; + bool intr_comp_chng; + struct pd_message pd_msg; + + mutex_lock(&chip->lock); + /* grab a snapshot of intr flags */ + intr_togdone = chip->intr_togdone; + intr_bc_lvl = chip->intr_bc_lvl; + intr_comp_chng = chip->intr_comp_chng; + + ret = fusb302_i2c_read(chip, FUSB_REG_INTERRUPT, &interrupt); + if (ret < 0) + goto done; + ret = fusb302_i2c_read(chip, FUSB_REG_INTERRUPTA, &interrupta); + if (ret < 0) + goto done; + ret = fusb302_i2c_read(chip, FUSB_REG_INTERRUPTB, &interruptb); + if (ret < 0) + goto done; + ret = fusb302_i2c_read(chip, FUSB_REG_STATUS0, &status0); + if (ret < 0) + goto done; + fusb302_log(chip, + "IRQ: 0x%02x, a: 0x%02x, b: 0x%02x, status0: 0x%02x", + interrupt, interrupta, interruptb, status0); + + if (interrupt & FUSB_REG_INTERRUPT_VBUSOK) { + vbus_present = !!(status0 & FUSB_REG_STATUS0_VBUSOK); + fusb302_log(chip, "IRQ: VBUS_OK, vbus=%s", + vbus_present ? "On" : "Off"); + if (vbus_present != chip->vbus_present) { + chip->vbus_present = vbus_present; + tcpm_vbus_change(chip->tcpm_port); + } + } + + if ((interrupta & FUSB_REG_INTERRUPTA_TOGDONE) && intr_togdone) { + fusb302_log(chip, "IRQ: TOGDONE"); + ret = fusb302_handle_togdone(chip); + if (ret < 0) { + fusb302_log(chip, + "handle togdone error, ret=%d", ret); + goto done; + } + } + + if ((interrupt & FUSB_REG_INTERRUPT_BC_LVL) && intr_bc_lvl) { + fusb302_log(chip, "IRQ: BC_LVL, handler pending"); + /* + * as BC_LVL interrupt can be affected by PD activity, + * apply delay to for the handler to wait for the PD + * signaling to finish. + */ + mod_delayed_work(chip->wq, &chip->bc_lvl_handler, + msecs_to_jiffies(T_BC_LVL_DEBOUNCE_DELAY_MS)); + } + + if ((interrupt & FUSB_REG_INTERRUPT_COMP_CHNG) && intr_comp_chng) { + comp_result = !!(status0 & FUSB_REG_STATUS0_COMP); + fusb302_log(chip, "IRQ: COMP_CHNG, comp=%s", + comp_result ? "true" : "false"); + if (comp_result) { + /* cc level > Rd_threshold, detach */ + chip->cc1 = TYPEC_CC_OPEN; + chip->cc2 = TYPEC_CC_OPEN; + tcpm_cc_change(chip->tcpm_port); + } + } + + if (interrupt & FUSB_REG_INTERRUPT_COLLISION) { + fusb302_log(chip, "IRQ: PD collision"); + tcpm_pd_transmit_complete(chip->tcpm_port, TCPC_TX_FAILED); + } + + if (interrupta & FUSB_REG_INTERRUPTA_RETRYFAIL) { + fusb302_log(chip, "IRQ: PD retry failed"); + tcpm_pd_transmit_complete(chip->tcpm_port, TCPC_TX_FAILED); + } + + if (interrupta & FUSB_REG_INTERRUPTA_HARDSENT) { + fusb302_log(chip, "IRQ: PD hardreset sent"); + ret = fusb302_pd_reset(chip); + if (ret < 0) { + fusb302_log(chip, "cannot PD reset, ret=%d", ret); + goto done; + } + tcpm_pd_transmit_complete(chip->tcpm_port, TCPC_TX_SUCCESS); + } + + if (interrupta & FUSB_REG_INTERRUPTA_TX_SUCCESS) { + fusb302_log(chip, "IRQ: PD tx success"); + ret = fusb302_pd_read_message(chip, &pd_msg); + if (ret < 0) { + fusb302_log(chip, + "cannot read in PD message, ret=%d", ret); + goto done; + } + } + + if (interrupta & FUSB_REG_INTERRUPTA_HARDRESET) { + fusb302_log(chip, "IRQ: PD received hardreset"); + ret = fusb302_pd_reset(chip); + if (ret < 0) { + fusb302_log(chip, "cannot PD reset, ret=%d", ret); + goto done; + } + tcpm_pd_hard_reset(chip->tcpm_port); + } + + if (interruptb & FUSB_REG_INTERRUPTB_GCRCSENT) { + fusb302_log(chip, "IRQ: PD sent good CRC"); + ret = fusb302_pd_read_message(chip, &pd_msg); + if (ret < 0) { + fusb302_log(chip, + "cannot read in PD message, ret=%d", ret); + goto done; + } + } +done: + mutex_unlock(&chip->lock); + enable_irq(chip->gpio_int_n_irq); +} + +static int init_gpio(struct fusb302_chip *chip) +{ + struct device *dev = chip->dev; + int ret = 0; + + chip->gpio_int_n = devm_gpiod_get(dev, "fcs,int_n", GPIOD_IN); + if (IS_ERR(chip->gpio_int_n)) { + dev_err(dev, "failed to request gpio_int_n\n"); + return PTR_ERR(chip->gpio_int_n); + } + ret = gpiod_to_irq(chip->gpio_int_n); + if (ret < 0) { + dev_err(dev, + "cannot request IRQ for GPIO Int_N, ret=%d", ret); + return ret; + } + chip->gpio_int_n_irq = ret; + return 0; +} + +#define PDO_FIXED_FLAGS \ + (PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_USB_COMM) + +static const u32 src_pdo[] = { + PDO_FIXED(5000, 400, PDO_FIXED_FLAGS) +}; + +static const u32 snk_pdo[] = { + PDO_FIXED(5000, 400, PDO_FIXED_FLAGS) +}; + +static const struct property_entry port_props[] = { + PROPERTY_ENTRY_STRING("data-role", "dual"), + PROPERTY_ENTRY_STRING("power-role", "dual"), + PROPERTY_ENTRY_STRING("try-power-role", "sink"), + PROPERTY_ENTRY_U32_ARRAY("source-pdos", src_pdo), + PROPERTY_ENTRY_U32_ARRAY("sink-pdos", snk_pdo), + PROPERTY_ENTRY_U32("op-sink-microwatt", 2500000), + { } +}; + +static struct fwnode_handle *fusb302_fwnode_get(struct device *dev) +{ + struct fwnode_handle *fwnode; + + fwnode = device_get_named_child_node(dev, "connector"); + if (!fwnode) + fwnode = fwnode_create_software_node(port_props, NULL); + + return fwnode; +} + +static int fusb302_probe(struct i2c_client *client) +{ + struct fusb302_chip *chip; + struct i2c_adapter *adapter = client->adapter; + struct device *dev = &client->dev; + const char *name; + int ret = 0; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_I2C_BLOCK)) { + dev_err(&client->dev, + "I2C/SMBus block functionality not supported!\n"); + return -ENODEV; + } + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->i2c_client = client; + chip->dev = &client->dev; + mutex_init(&chip->lock); + + /* + * Devicetree platforms should get extcon via phandle (not yet + * supported). On ACPI platforms, we get the name from a device prop. + * This device prop is for kernel internal use only and is expected + * to be set by the platform code which also registers the i2c client + * for the fusb302. + */ + if (device_property_read_string(dev, "linux,extcon-name", &name) == 0) { + chip->extcon = extcon_get_extcon_dev(name); + if (IS_ERR(chip->extcon)) + return PTR_ERR(chip->extcon); + } + + chip->vbus = devm_regulator_get(chip->dev, "vbus"); + if (IS_ERR(chip->vbus)) + return PTR_ERR(chip->vbus); + + chip->wq = create_singlethread_workqueue(dev_name(chip->dev)); + if (!chip->wq) + return -ENOMEM; + + spin_lock_init(&chip->irq_lock); + INIT_WORK(&chip->irq_work, fusb302_irq_work); + INIT_DELAYED_WORK(&chip->bc_lvl_handler, fusb302_bc_lvl_handler_work); + init_tcpc_dev(&chip->tcpc_dev); + fusb302_debugfs_init(chip); + + if (client->irq) { + chip->gpio_int_n_irq = client->irq; + } else { + ret = init_gpio(chip); + if (ret < 0) + goto destroy_workqueue; + } + + chip->tcpc_dev.fwnode = fusb302_fwnode_get(dev); + if (IS_ERR(chip->tcpc_dev.fwnode)) { + ret = PTR_ERR(chip->tcpc_dev.fwnode); + goto destroy_workqueue; + } + + chip->tcpm_port = tcpm_register_port(&client->dev, &chip->tcpc_dev); + if (IS_ERR(chip->tcpm_port)) { + fwnode_handle_put(chip->tcpc_dev.fwnode); + ret = dev_err_probe(dev, PTR_ERR(chip->tcpm_port), + "cannot register tcpm port\n"); + goto destroy_workqueue; + } + + ret = request_irq(chip->gpio_int_n_irq, fusb302_irq_intn, + IRQF_ONESHOT | IRQF_TRIGGER_LOW, + "fsc_interrupt_int_n", chip); + if (ret < 0) { + dev_err(dev, "cannot request IRQ for GPIO Int_N, ret=%d", ret); + goto tcpm_unregister_port; + } + enable_irq_wake(chip->gpio_int_n_irq); + i2c_set_clientdata(client, chip); + + return ret; + +tcpm_unregister_port: + tcpm_unregister_port(chip->tcpm_port); + fwnode_handle_put(chip->tcpc_dev.fwnode); +destroy_workqueue: + fusb302_debugfs_exit(chip); + destroy_workqueue(chip->wq); + + return ret; +} + +static void fusb302_remove(struct i2c_client *client) +{ + struct fusb302_chip *chip = i2c_get_clientdata(client); + + disable_irq_wake(chip->gpio_int_n_irq); + free_irq(chip->gpio_int_n_irq, chip); + cancel_work_sync(&chip->irq_work); + cancel_delayed_work_sync(&chip->bc_lvl_handler); + tcpm_unregister_port(chip->tcpm_port); + fwnode_handle_put(chip->tcpc_dev.fwnode); + destroy_workqueue(chip->wq); + fusb302_debugfs_exit(chip); +} + +static int fusb302_pm_suspend(struct device *dev) +{ + struct fusb302_chip *chip = dev->driver_data; + unsigned long flags; + + spin_lock_irqsave(&chip->irq_lock, flags); + chip->irq_suspended = true; + spin_unlock_irqrestore(&chip->irq_lock, flags); + + /* Make sure any pending irq_work is finished before the bus suspends */ + flush_work(&chip->irq_work); + return 0; +} + +static int fusb302_pm_resume(struct device *dev) +{ + struct fusb302_chip *chip = dev->driver_data; + unsigned long flags; + + spin_lock_irqsave(&chip->irq_lock, flags); + if (chip->irq_while_suspended) { + schedule_work(&chip->irq_work); + chip->irq_while_suspended = false; + } + chip->irq_suspended = false; + spin_unlock_irqrestore(&chip->irq_lock, flags); + + return 0; +} + +static const struct of_device_id fusb302_dt_match[] __maybe_unused = { + {.compatible = "fcs,fusb302"}, + {}, +}; +MODULE_DEVICE_TABLE(of, fusb302_dt_match); + +static const struct i2c_device_id fusb302_i2c_device_id[] = { + {"typec_fusb302", 0}, + {}, +}; +MODULE_DEVICE_TABLE(i2c, fusb302_i2c_device_id); + +static const struct dev_pm_ops fusb302_pm_ops = { + .suspend = fusb302_pm_suspend, + .resume = fusb302_pm_resume, +}; + +static struct i2c_driver fusb302_driver = { + .driver = { + .name = "typec_fusb302", + .pm = &fusb302_pm_ops, + .of_match_table = of_match_ptr(fusb302_dt_match), + }, + .probe = fusb302_probe, + .remove = fusb302_remove, + .id_table = fusb302_i2c_device_id, +}; +module_i2c_driver(fusb302_driver); + +MODULE_AUTHOR("Yueyao Zhu "); +MODULE_DESCRIPTION("Fairchild FUSB302 Type-C Chip Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/fusb302_reg.h b/drivers/usb/typec/tcpm/fusb302_reg.h new file mode 100644 index 0000000000..edc0e4b0f1 --- /dev/null +++ b/drivers/usb/typec/tcpm/fusb302_reg.h @@ -0,0 +1,177 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Copyright 2016-2017 Google, Inc + * + * Fairchild FUSB302 Type-C Chip Driver + */ + +#ifndef FUSB302_REG_H +#define FUSB302_REG_H + +#define FUSB_REG_DEVICE_ID 0x01 +#define FUSB_REG_SWITCHES0 0x02 +#define FUSB_REG_SWITCHES0_CC2_PU_EN BIT(7) +#define FUSB_REG_SWITCHES0_CC1_PU_EN BIT(6) +#define FUSB_REG_SWITCHES0_VCONN_CC2 BIT(5) +#define FUSB_REG_SWITCHES0_VCONN_CC1 BIT(4) +#define FUSB_REG_SWITCHES0_MEAS_CC2 BIT(3) +#define FUSB_REG_SWITCHES0_MEAS_CC1 BIT(2) +#define FUSB_REG_SWITCHES0_CC2_PD_EN BIT(1) +#define FUSB_REG_SWITCHES0_CC1_PD_EN BIT(0) +#define FUSB_REG_SWITCHES1 0x03 +#define FUSB_REG_SWITCHES1_POWERROLE BIT(7) +#define FUSB_REG_SWITCHES1_SPECREV1 BIT(6) +#define FUSB_REG_SWITCHES1_SPECREV0 BIT(5) +#define FUSB_REG_SWITCHES1_DATAROLE BIT(4) +#define FUSB_REG_SWITCHES1_AUTO_GCRC BIT(2) +#define FUSB_REG_SWITCHES1_TXCC2_EN BIT(1) +#define FUSB_REG_SWITCHES1_TXCC1_EN BIT(0) +#define FUSB_REG_MEASURE 0x04 +#define FUSB_REG_MEASURE_MDAC5 BIT(7) +#define FUSB_REG_MEASURE_MDAC4 BIT(6) +#define FUSB_REG_MEASURE_MDAC3 BIT(5) +#define FUSB_REG_MEASURE_MDAC2 BIT(4) +#define FUSB_REG_MEASURE_MDAC1 BIT(3) +#define FUSB_REG_MEASURE_MDAC0 BIT(2) +#define FUSB_REG_MEASURE_VBUS BIT(1) +#define FUSB_REG_MEASURE_XXXX5 BIT(0) +#define FUSB_REG_CONTROL0 0x06 +#define FUSB_REG_CONTROL0_TX_FLUSH BIT(6) +#define FUSB_REG_CONTROL0_INT_MASK BIT(5) +#define FUSB_REG_CONTROL0_HOST_CUR_MASK (0xC) +#define FUSB_REG_CONTROL0_HOST_CUR_HIGH (0xC) +#define FUSB_REG_CONTROL0_HOST_CUR_MED (0x8) +#define FUSB_REG_CONTROL0_HOST_CUR_DEF (0x4) +#define FUSB_REG_CONTROL0_TX_START BIT(0) +#define FUSB_REG_CONTROL1 0x07 +#define FUSB_REG_CONTROL1_ENSOP2DB BIT(6) +#define FUSB_REG_CONTROL1_ENSOP1DB BIT(5) +#define FUSB_REG_CONTROL1_BIST_MODE2 BIT(4) +#define FUSB_REG_CONTROL1_RX_FLUSH BIT(2) +#define FUSB_REG_CONTROL1_ENSOP2 BIT(1) +#define FUSB_REG_CONTROL1_ENSOP1 BIT(0) +#define FUSB_REG_CONTROL2 0x08 +#define FUSB_REG_CONTROL2_MODE BIT(1) +#define FUSB_REG_CONTROL2_MODE_MASK (0x6) +#define FUSB_REG_CONTROL2_MODE_DFP (0x6) +#define FUSB_REG_CONTROL2_MODE_UFP (0x4) +#define FUSB_REG_CONTROL2_MODE_DRP (0x2) +#define FUSB_REG_CONTROL2_MODE_NONE (0x0) +#define FUSB_REG_CONTROL2_TOGGLE BIT(0) +#define FUSB_REG_CONTROL3 0x09 +#define FUSB_REG_CONTROL3_SEND_HARDRESET BIT(6) +#define FUSB_REG_CONTROL3_BIST_TMODE BIT(5) /* 302B Only */ +#define FUSB_REG_CONTROL3_AUTO_HARDRESET BIT(4) +#define FUSB_REG_CONTROL3_AUTO_SOFTRESET BIT(3) +#define FUSB_REG_CONTROL3_N_RETRIES BIT(1) +#define FUSB_REG_CONTROL3_N_RETRIES_MASK (0x6) +#define FUSB_REG_CONTROL3_N_RETRIES_3 (0x6) +#define FUSB_REG_CONTROL3_N_RETRIES_2 (0x4) +#define FUSB_REG_CONTROL3_N_RETRIES_1 (0x2) +#define FUSB_REG_CONTROL3_AUTO_RETRY BIT(0) +#define FUSB_REG_MASK 0x0A +#define FUSB_REG_MASK_VBUSOK BIT(7) +#define FUSB_REG_MASK_ACTIVITY BIT(6) +#define FUSB_REG_MASK_COMP_CHNG BIT(5) +#define FUSB_REG_MASK_CRC_CHK BIT(4) +#define FUSB_REG_MASK_ALERT BIT(3) +#define FUSB_REG_MASK_WAKE BIT(2) +#define FUSB_REG_MASK_COLLISION BIT(1) +#define FUSB_REG_MASK_BC_LVL BIT(0) +#define FUSB_REG_POWER 0x0B +#define FUSB_REG_POWER_PWR BIT(0) +#define FUSB_REG_POWER_PWR_LOW 0x1 +#define FUSB_REG_POWER_PWR_MEDIUM 0x3 +#define FUSB_REG_POWER_PWR_HIGH 0x7 +#define FUSB_REG_POWER_PWR_ALL 0xF +#define FUSB_REG_RESET 0x0C +#define FUSB_REG_RESET_PD_RESET BIT(1) +#define FUSB_REG_RESET_SW_RESET BIT(0) +#define FUSB_REG_MASKA 0x0E +#define FUSB_REG_MASKA_OCP_TEMP BIT(7) +#define FUSB_REG_MASKA_TOGDONE BIT(6) +#define FUSB_REG_MASKA_SOFTFAIL BIT(5) +#define FUSB_REG_MASKA_RETRYFAIL BIT(4) +#define FUSB_REG_MASKA_HARDSENT BIT(3) +#define FUSB_REG_MASKA_TX_SUCCESS BIT(2) +#define FUSB_REG_MASKA_SOFTRESET BIT(1) +#define FUSB_REG_MASKA_HARDRESET BIT(0) +#define FUSB_REG_MASKB 0x0F +#define FUSB_REG_MASKB_GCRCSENT BIT(0) +#define FUSB_REG_STATUS0A 0x3C +#define FUSB_REG_STATUS0A_SOFTFAIL BIT(5) +#define FUSB_REG_STATUS0A_RETRYFAIL BIT(4) +#define FUSB_REG_STATUS0A_POWER BIT(2) +#define FUSB_REG_STATUS0A_RX_SOFT_RESET BIT(1) +#define FUSB_REG_STATUS0A_RX_HARD_RESET BIT(0) +#define FUSB_REG_STATUS1A 0x3D +#define FUSB_REG_STATUS1A_TOGSS BIT(3) +#define FUSB_REG_STATUS1A_TOGSS_RUNNING 0x0 +#define FUSB_REG_STATUS1A_TOGSS_SRC1 0x1 +#define FUSB_REG_STATUS1A_TOGSS_SRC2 0x2 +#define FUSB_REG_STATUS1A_TOGSS_SNK1 0x5 +#define FUSB_REG_STATUS1A_TOGSS_SNK2 0x6 +#define FUSB_REG_STATUS1A_TOGSS_AA 0x7 +#define FUSB_REG_STATUS1A_TOGSS_POS (3) +#define FUSB_REG_STATUS1A_TOGSS_MASK (0x7) +#define FUSB_REG_STATUS1A_RXSOP2DB BIT(2) +#define FUSB_REG_STATUS1A_RXSOP1DB BIT(1) +#define FUSB_REG_STATUS1A_RXSOP BIT(0) +#define FUSB_REG_INTERRUPTA 0x3E +#define FUSB_REG_INTERRUPTA_OCP_TEMP BIT(7) +#define FUSB_REG_INTERRUPTA_TOGDONE BIT(6) +#define FUSB_REG_INTERRUPTA_SOFTFAIL BIT(5) +#define FUSB_REG_INTERRUPTA_RETRYFAIL BIT(4) +#define FUSB_REG_INTERRUPTA_HARDSENT BIT(3) +#define FUSB_REG_INTERRUPTA_TX_SUCCESS BIT(2) +#define FUSB_REG_INTERRUPTA_SOFTRESET BIT(1) +#define FUSB_REG_INTERRUPTA_HARDRESET BIT(0) +#define FUSB_REG_INTERRUPTB 0x3F +#define FUSB_REG_INTERRUPTB_GCRCSENT BIT(0) +#define FUSB_REG_STATUS0 0x40 +#define FUSB_REG_STATUS0_VBUSOK BIT(7) +#define FUSB_REG_STATUS0_ACTIVITY BIT(6) +#define FUSB_REG_STATUS0_COMP BIT(5) +#define FUSB_REG_STATUS0_CRC_CHK BIT(4) +#define FUSB_REG_STATUS0_ALERT BIT(3) +#define FUSB_REG_STATUS0_WAKE BIT(2) +#define FUSB_REG_STATUS0_BC_LVL_MASK 0x03 +#define FUSB_REG_STATUS0_BC_LVL_0_200 0x0 +#define FUSB_REG_STATUS0_BC_LVL_200_600 0x1 +#define FUSB_REG_STATUS0_BC_LVL_600_1230 0x2 +#define FUSB_REG_STATUS0_BC_LVL_1230_MAX 0x3 +#define FUSB_REG_STATUS0_BC_LVL1 BIT(1) +#define FUSB_REG_STATUS0_BC_LVL0 BIT(0) +#define FUSB_REG_STATUS1 0x41 +#define FUSB_REG_STATUS1_RXSOP2 BIT(7) +#define FUSB_REG_STATUS1_RXSOP1 BIT(6) +#define FUSB_REG_STATUS1_RX_EMPTY BIT(5) +#define FUSB_REG_STATUS1_RX_FULL BIT(4) +#define FUSB_REG_STATUS1_TX_EMPTY BIT(3) +#define FUSB_REG_STATUS1_TX_FULL BIT(2) +#define FUSB_REG_INTERRUPT 0x42 +#define FUSB_REG_INTERRUPT_VBUSOK BIT(7) +#define FUSB_REG_INTERRUPT_ACTIVITY BIT(6) +#define FUSB_REG_INTERRUPT_COMP_CHNG BIT(5) +#define FUSB_REG_INTERRUPT_CRC_CHK BIT(4) +#define FUSB_REG_INTERRUPT_ALERT BIT(3) +#define FUSB_REG_INTERRUPT_WAKE BIT(2) +#define FUSB_REG_INTERRUPT_COLLISION BIT(1) +#define FUSB_REG_INTERRUPT_BC_LVL BIT(0) +#define FUSB_REG_FIFOS 0x43 + +/* Tokens defined for the FUSB302 TX FIFO */ +enum fusb302_txfifo_tokens { + FUSB302_TKN_TXON = 0xA1, + FUSB302_TKN_SYNC1 = 0x12, + FUSB302_TKN_SYNC2 = 0x13, + FUSB302_TKN_SYNC3 = 0x1B, + FUSB302_TKN_RST1 = 0x15, + FUSB302_TKN_RST2 = 0x16, + FUSB302_TKN_PACKSYM = 0x80, + FUSB302_TKN_JAMCRC = 0xFF, + FUSB302_TKN_EOP = 0x14, + FUSB302_TKN_TXOFF = 0xFE, +}; + +#endif diff --git a/drivers/usb/typec/tcpm/maxim_contaminant.c b/drivers/usb/typec/tcpm/maxim_contaminant.c new file mode 100644 index 0000000000..f8504a90da --- /dev/null +++ b/drivers/usb/typec/tcpm/maxim_contaminant.c @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2022 Google, Inc + * + * USB-C module to reduce wakeups due to contaminants. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "tcpci_maxim.h" + +enum fladc_select { + CC1_SCALE1 = 1, + CC1_SCALE2, + CC2_SCALE1, + CC2_SCALE2, + SBU1, + SBU2, +}; + +#define FLADC_1uA_LSB_MV 25 +/* High range CC */ +#define FLADC_CC_HIGH_RANGE_LSB_MV 208 +/* Low range CC */ +#define FLADC_CC_LOW_RANGE_LSB_MV 126 + +/* 1uA current source */ +#define FLADC_CC_SCALE1 1 +/* 5 uA current source */ +#define FLADC_CC_SCALE2 5 + +#define FLADC_1uA_CC_OFFSET_MV 300 +#define FLADC_CC_HIGH_RANGE_OFFSET_MV 624 +#define FLADC_CC_LOW_RANGE_OFFSET_MV 378 + +#define CONTAMINANT_THRESHOLD_SBU_K 1000 +#define CONTAMINANT_THRESHOLD_CC_K 1000 + +#define READ1_SLEEP_MS 10 +#define READ2_SLEEP_MS 5 + +#define STATUS_CHECK(reg, mask, val) (((reg) & (mask)) == (val)) + +#define IS_CC_OPEN(cc_status) \ + (STATUS_CHECK((cc_status), TCPC_CC_STATUS_CC1_MASK << TCPC_CC_STATUS_CC1_SHIFT, \ + TCPC_CC_STATE_SRC_OPEN) && STATUS_CHECK((cc_status), \ + TCPC_CC_STATUS_CC2_MASK << \ + TCPC_CC_STATUS_CC2_SHIFT, \ + TCPC_CC_STATE_SRC_OPEN)) + +static int max_contaminant_adc_to_mv(struct max_tcpci_chip *chip, enum fladc_select channel, + bool ua_src, u8 fladc) +{ + /* SBU channels only have 1 scale with 1uA. */ + if ((ua_src && (channel == CC1_SCALE2 || channel == CC2_SCALE2 || channel == SBU1 || + channel == SBU2))) + /* Mean of range */ + return FLADC_1uA_CC_OFFSET_MV + (fladc * FLADC_1uA_LSB_MV); + else if (!ua_src && (channel == CC1_SCALE1 || channel == CC2_SCALE1)) + return FLADC_CC_HIGH_RANGE_OFFSET_MV + (fladc * FLADC_CC_HIGH_RANGE_LSB_MV); + else if (!ua_src && (channel == CC1_SCALE2 || channel == CC2_SCALE2)) + return FLADC_CC_LOW_RANGE_OFFSET_MV + (fladc * FLADC_CC_LOW_RANGE_LSB_MV); + + dev_err_once(chip->dev, "ADC ERROR: SCALE UNKNOWN"); + + return -EINVAL; +} + +static int max_contaminant_read_adc_mv(struct max_tcpci_chip *chip, enum fladc_select channel, + int sleep_msec, bool raw, bool ua_src) +{ + struct regmap *regmap = chip->data.regmap; + u8 fladc; + int ret; + + /* Channel & scale select */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCINSEL_MASK, + channel << ADC_CHANNEL_OFFSET); + if (ret < 0) + return ret; + + /* Enable ADC */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCEN, ADCEN); + if (ret < 0) + return ret; + + usleep_range(sleep_msec * 1000, (sleep_msec + 1) * 1000); + ret = max_tcpci_read8(chip, TCPC_VENDOR_FLADC_STATUS, &fladc); + if (ret < 0) + return ret; + + /* Disable ADC */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCEN, 0); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCINSEL_MASK, 0); + if (ret < 0) + return ret; + + if (!raw) + return max_contaminant_adc_to_mv(chip, channel, ua_src, fladc); + else + return fladc; +} + +static int max_contaminant_read_resistance_kohm(struct max_tcpci_chip *chip, + enum fladc_select channel, int sleep_msec, bool raw) +{ + struct regmap *regmap = chip->data.regmap; + int mv; + int ret; + + if (channel == CC1_SCALE1 || channel == CC2_SCALE1 || channel == CC1_SCALE2 || + channel == CC2_SCALE2) { + /* Enable 1uA current source */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCLPMODESEL_MASK, + ULTRA_LOW_POWER_MODE); + if (ret < 0) + return ret; + + /* Enable 1uA current source */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, UA_1_SRC); + if (ret < 0) + return ret; + + /* OVP disable */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCOVPDIS, CCOVPDIS); + if (ret < 0) + return ret; + + mv = max_contaminant_read_adc_mv(chip, channel, sleep_msec, raw, true); + if (mv < 0) + return ret; + + /* OVP enable */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCOVPDIS, 0); + if (ret < 0) + return ret; + /* returns KOhm as 1uA source is used. */ + return mv; + } + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBUOVPDIS, SBUOVPDIS); + if (ret < 0) + return ret; + + /* SBU switches auto configure when channel is selected. */ + /* Enable 1ua current source */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBURPCTRL, SBURPCTRL); + if (ret < 0) + return ret; + + mv = max_contaminant_read_adc_mv(chip, channel, sleep_msec, raw, true); + if (mv < 0) + return ret; + /* Disable current source */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBURPCTRL, 0); + if (ret < 0) + return ret; + + /* OVP disable */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBUOVPDIS, 0); + if (ret < 0) + return ret; + + return mv; +} + +static int max_contaminant_read_comparators(struct max_tcpci_chip *chip, u8 *vendor_cc_status2_cc1, + u8 *vendor_cc_status2_cc2) +{ + struct regmap *regmap = chip->data.regmap; + int ret; + + /* Enable 80uA source */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, UA_80_SRC); + if (ret < 0) + return ret; + + /* Enable comparators */ + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCOMPEN, CCCOMPEN); + if (ret < 0) + return ret; + + /* Sleep to allow comparators settle */ + usleep_range(5000, 6000); + ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_ORIENTATION, PLUG_ORNT_CC1); + if (ret < 0) + return ret; + + usleep_range(5000, 6000); + ret = max_tcpci_read8(chip, VENDOR_CC_STATUS2, vendor_cc_status2_cc1); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_ORIENTATION, PLUG_ORNT_CC2); + if (ret < 0) + return ret; + + usleep_range(5000, 6000); + ret = max_tcpci_read8(chip, VENDOR_CC_STATUS2, vendor_cc_status2_cc2); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCOMPEN, 0); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, 0); + if (ret < 0) + return ret; + + return 0; +} + +static int max_contaminant_detect_contaminant(struct max_tcpci_chip *chip) +{ + int cc1_k, cc2_k, sbu1_k, sbu2_k, ret; + u8 vendor_cc_status2_cc1 = 0xff, vendor_cc_status2_cc2 = 0xff; + u8 role_ctrl = 0, role_ctrl_backup = 0; + int inferred_state = NOT_DETECTED; + + ret = max_tcpci_read8(chip, TCPC_ROLE_CTRL, &role_ctrl); + if (ret < 0) + return NOT_DETECTED; + + role_ctrl_backup = role_ctrl; + role_ctrl = 0x0F; + ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl); + if (ret < 0) + return NOT_DETECTED; + + cc1_k = max_contaminant_read_resistance_kohm(chip, CC1_SCALE2, READ1_SLEEP_MS, false); + if (cc1_k < 0) + goto exit; + + cc2_k = max_contaminant_read_resistance_kohm(chip, CC2_SCALE2, READ2_SLEEP_MS, false); + if (cc2_k < 0) + goto exit; + + sbu1_k = max_contaminant_read_resistance_kohm(chip, SBU1, READ1_SLEEP_MS, false); + if (sbu1_k < 0) + goto exit; + + sbu2_k = max_contaminant_read_resistance_kohm(chip, SBU2, READ2_SLEEP_MS, false); + if (sbu2_k < 0) + goto exit; + + ret = max_contaminant_read_comparators(chip, &vendor_cc_status2_cc1, + &vendor_cc_status2_cc2); + + if (ret < 0) + goto exit; + + if ((!(CC1_VUFP_RD0P5 & vendor_cc_status2_cc1) || + !(CC2_VUFP_RD0P5 & vendor_cc_status2_cc2)) && + !(CC1_VUFP_RD0P5 & vendor_cc_status2_cc1 && CC2_VUFP_RD0P5 & vendor_cc_status2_cc2)) + inferred_state = SINK; + else if ((cc1_k < CONTAMINANT_THRESHOLD_CC_K || cc2_k < CONTAMINANT_THRESHOLD_CC_K) && + (sbu1_k < CONTAMINANT_THRESHOLD_SBU_K || sbu2_k < CONTAMINANT_THRESHOLD_SBU_K)) + inferred_state = DETECTED; + + if (inferred_state == NOT_DETECTED) + max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); + else + max_tcpci_write8(chip, TCPC_ROLE_CTRL, (TCPC_ROLE_CTRL_DRP | 0xA)); + + return inferred_state; +exit: + max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); + return NOT_DETECTED; +} + +static int max_contaminant_enable_dry_detection(struct max_tcpci_chip *chip) +{ + struct regmap *regmap = chip->data.regmap; + u8 temp; + int ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL3, CCWTRDEB_MASK | CCWTRSEL_MASK + | WTRCYCLE_MASK, CCWTRDEB_1MS << CCWTRDEB_SHIFT | + CCWTRSEL_1V << CCWTRSEL_SHIFT | WTRCYCLE_4_8_S << + WTRCYCLE_SHIFT); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP, TCPC_ROLE_CTRL_DRP); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCONNDRY, CCCONNDRY); + if (ret < 0) + return ret; + ret = max_tcpci_read8(chip, TCPC_VENDOR_CC_CTRL1, &temp); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCLPMODESEL_MASK, + ULTRA_LOW_POWER_MODE); + if (ret < 0) + return ret; + ret = max_tcpci_read8(chip, TCPC_VENDOR_CC_CTRL2, &temp); + if (ret < 0) + return ret; + + /* Enable Look4Connection before sending the command */ + ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_EN_LK4CONN_ALRT, + TCPC_TCPC_CTRL_EN_LK4CONN_ALRT); + if (ret < 0) + return ret; + + ret = max_tcpci_write8(chip, TCPC_COMMAND, TCPC_CMD_LOOK4CONNECTION); + if (ret < 0) + return ret; + return 0; +} + +bool max_contaminant_is_contaminant(struct max_tcpci_chip *chip, bool disconnect_while_debounce) +{ + u8 cc_status, pwr_cntl; + int ret; + + ret = max_tcpci_read8(chip, TCPC_CC_STATUS, &cc_status); + if (ret < 0) + return false; + + ret = max_tcpci_read8(chip, TCPC_POWER_CTRL, &pwr_cntl); + if (ret < 0) + return false; + + if (chip->contaminant_state == NOT_DETECTED || chip->contaminant_state == SINK) { + if (!disconnect_while_debounce) + msleep(100); + + ret = max_tcpci_read8(chip, TCPC_CC_STATUS, &cc_status); + if (ret < 0) + return false; + + if (IS_CC_OPEN(cc_status)) { + u8 role_ctrl, role_ctrl_backup; + + ret = max_tcpci_read8(chip, TCPC_ROLE_CTRL, &role_ctrl); + if (ret < 0) + return false; + + role_ctrl_backup = role_ctrl; + role_ctrl |= 0x0F; + role_ctrl &= ~(TCPC_ROLE_CTRL_DRP); + ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl); + if (ret < 0) + return false; + + chip->contaminant_state = max_contaminant_detect_contaminant(chip); + + ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); + if (ret < 0) + return false; + + if (chip->contaminant_state == DETECTED) { + max_contaminant_enable_dry_detection(chip); + return true; + } + } + return false; + } else if (chip->contaminant_state == DETECTED) { + if (STATUS_CHECK(cc_status, TCPC_CC_STATUS_TOGGLING, 0)) { + chip->contaminant_state = max_contaminant_detect_contaminant(chip); + if (chip->contaminant_state == DETECTED) { + max_contaminant_enable_dry_detection(chip); + return true; + } + } + } + + return false; +} + +MODULE_DESCRIPTION("MAXIM TCPC CONTAMINANT Module"); +MODULE_AUTHOR("Badhri Jagan Sridharan "); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/qcom/Makefile b/drivers/usb/typec/tcpm/qcom/Makefile new file mode 100644 index 0000000000..dc1e8832e1 --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 +# +obj-$(CONFIG_TYPEC_QCOM_PMIC) += qcom_pmic_tcpm.o +qcom_pmic_tcpm-y += qcom_pmic_typec.o \ + qcom_pmic_typec_port.o \ + qcom_pmic_typec_pdphy.o diff --git a/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec.c b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec.c new file mode 100644 index 0000000000..581199d37b --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec.c @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2023, Linaro Ltd. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "qcom_pmic_typec_pdphy.h" +#include "qcom_pmic_typec_port.h" + +struct pmic_typec_resources { + struct pmic_typec_pdphy_resources *pdphy_res; + struct pmic_typec_port_resources *port_res; +}; + +struct pmic_typec { + struct device *dev; + struct tcpm_port *tcpm_port; + struct tcpc_dev tcpc; + struct pmic_typec_pdphy *pmic_typec_pdphy; + struct pmic_typec_port *pmic_typec_port; + bool vbus_enabled; + struct mutex lock; /* VBUS state serialization */ + struct drm_bridge bridge; +}; + +#define tcpc_to_tcpm(_tcpc_) container_of(_tcpc_, struct pmic_typec, tcpc) + +static int qcom_pmic_typec_get_vbus(struct tcpc_dev *tcpc) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + int ret; + + mutex_lock(&tcpm->lock); + ret = tcpm->vbus_enabled || qcom_pmic_typec_port_get_vbus(tcpm->pmic_typec_port); + mutex_unlock(&tcpm->lock); + + return ret; +} + +static int qcom_pmic_typec_set_vbus(struct tcpc_dev *tcpc, bool on, bool sink) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + int ret = 0; + + mutex_lock(&tcpm->lock); + if (tcpm->vbus_enabled == on) + goto done; + + ret = qcom_pmic_typec_port_set_vbus(tcpm->pmic_typec_port, on); + if (ret) + goto done; + + tcpm->vbus_enabled = on; + tcpm_vbus_change(tcpm->tcpm_port); + +done: + dev_dbg(tcpm->dev, "set_vbus set: %d result %d\n", on, ret); + mutex_unlock(&tcpm->lock); + + return ret; +} + +static int qcom_pmic_typec_set_vconn(struct tcpc_dev *tcpc, bool on) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_port_set_vconn(tcpm->pmic_typec_port, on); +} + +static int qcom_pmic_typec_get_cc(struct tcpc_dev *tcpc, + enum typec_cc_status *cc1, + enum typec_cc_status *cc2) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_port_get_cc(tcpm->pmic_typec_port, cc1, cc2); +} + +static int qcom_pmic_typec_set_cc(struct tcpc_dev *tcpc, + enum typec_cc_status cc) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_port_set_cc(tcpm->pmic_typec_port, cc); +} + +static int qcom_pmic_typec_set_polarity(struct tcpc_dev *tcpc, + enum typec_cc_polarity pol) +{ + /* Polarity is set separately by phy-qcom-qmp.c */ + return 0; +} + +static int qcom_pmic_typec_start_toggling(struct tcpc_dev *tcpc, + enum typec_port_type port_type, + enum typec_cc_status cc) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_port_start_toggling(tcpm->pmic_typec_port, + port_type, cc); +} + +static int qcom_pmic_typec_set_roles(struct tcpc_dev *tcpc, bool attached, + enum typec_role power_role, + enum typec_data_role data_role) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_pdphy_set_roles(tcpm->pmic_typec_pdphy, + data_role, power_role); +} + +static int qcom_pmic_typec_set_pd_rx(struct tcpc_dev *tcpc, bool on) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_pdphy_set_pd_rx(tcpm->pmic_typec_pdphy, on); +} + +static int qcom_pmic_typec_pd_transmit(struct tcpc_dev *tcpc, + enum tcpm_transmit_type type, + const struct pd_message *msg, + unsigned int negotiated_rev) +{ + struct pmic_typec *tcpm = tcpc_to_tcpm(tcpc); + + return qcom_pmic_typec_pdphy_pd_transmit(tcpm->pmic_typec_pdphy, type, + msg, negotiated_rev); +} + +static int qcom_pmic_typec_init(struct tcpc_dev *tcpc) +{ + return 0; +} + +#if IS_ENABLED(CONFIG_DRM) +static int qcom_pmic_typec_attach(struct drm_bridge *bridge, + enum drm_bridge_attach_flags flags) +{ + return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; +} + +static const struct drm_bridge_funcs qcom_pmic_typec_bridge_funcs = { + .attach = qcom_pmic_typec_attach, +}; + +static int qcom_pmic_typec_init_drm(struct pmic_typec *tcpm) +{ + tcpm->bridge.funcs = &qcom_pmic_typec_bridge_funcs; +#ifdef CONFIG_OF + tcpm->bridge.of_node = of_get_child_by_name(tcpm->dev->of_node, "connector"); +#endif + tcpm->bridge.ops = DRM_BRIDGE_OP_HPD; + tcpm->bridge.type = DRM_MODE_CONNECTOR_DisplayPort; + + return devm_drm_bridge_add(tcpm->dev, &tcpm->bridge); +} +#else +static int qcom_pmic_typec_init_drm(struct pmic_typec *tcpm) +{ + return 0; +} +#endif + +static int qcom_pmic_typec_probe(struct platform_device *pdev) +{ + struct pmic_typec *tcpm; + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + const struct pmic_typec_resources *res; + struct regmap *regmap; + u32 base[2]; + int ret; + + res = of_device_get_match_data(dev); + if (!res) + return -ENODEV; + + tcpm = devm_kzalloc(dev, sizeof(*tcpm), GFP_KERNEL); + if (!tcpm) + return -ENOMEM; + + tcpm->dev = dev; + tcpm->tcpc.init = qcom_pmic_typec_init; + tcpm->tcpc.get_vbus = qcom_pmic_typec_get_vbus; + tcpm->tcpc.set_vbus = qcom_pmic_typec_set_vbus; + tcpm->tcpc.set_cc = qcom_pmic_typec_set_cc; + tcpm->tcpc.get_cc = qcom_pmic_typec_get_cc; + tcpm->tcpc.set_polarity = qcom_pmic_typec_set_polarity; + tcpm->tcpc.set_vconn = qcom_pmic_typec_set_vconn; + tcpm->tcpc.start_toggling = qcom_pmic_typec_start_toggling; + tcpm->tcpc.set_pd_rx = qcom_pmic_typec_set_pd_rx; + tcpm->tcpc.set_roles = qcom_pmic_typec_set_roles; + tcpm->tcpc.pd_transmit = qcom_pmic_typec_pd_transmit; + + regmap = dev_get_regmap(dev->parent, NULL); + if (!regmap) { + dev_err(dev, "Failed to get regmap\n"); + return -ENODEV; + } + + ret = of_property_read_u32_array(np, "reg", base, 2); + if (ret) + return ret; + + tcpm->pmic_typec_port = qcom_pmic_typec_port_alloc(dev); + if (IS_ERR(tcpm->pmic_typec_port)) + return PTR_ERR(tcpm->pmic_typec_port); + + tcpm->pmic_typec_pdphy = qcom_pmic_typec_pdphy_alloc(dev); + if (IS_ERR(tcpm->pmic_typec_pdphy)) + return PTR_ERR(tcpm->pmic_typec_pdphy); + + ret = qcom_pmic_typec_port_probe(pdev, tcpm->pmic_typec_port, + res->port_res, regmap, base[0]); + if (ret) + return ret; + + ret = qcom_pmic_typec_pdphy_probe(pdev, tcpm->pmic_typec_pdphy, + res->pdphy_res, regmap, base[1]); + if (ret) + return ret; + + mutex_init(&tcpm->lock); + platform_set_drvdata(pdev, tcpm); + + ret = qcom_pmic_typec_init_drm(tcpm); + if (ret) + return ret; + + tcpm->tcpc.fwnode = device_get_named_child_node(tcpm->dev, "connector"); + if (!tcpm->tcpc.fwnode) + return -EINVAL; + + tcpm->tcpm_port = tcpm_register_port(tcpm->dev, &tcpm->tcpc); + if (IS_ERR(tcpm->tcpm_port)) { + ret = PTR_ERR(tcpm->tcpm_port); + goto fwnode_remove; + } + + ret = qcom_pmic_typec_port_start(tcpm->pmic_typec_port, + tcpm->tcpm_port); + if (ret) + goto fwnode_remove; + + ret = qcom_pmic_typec_pdphy_start(tcpm->pmic_typec_pdphy, + tcpm->tcpm_port); + if (ret) + goto fwnode_remove; + + return 0; + +fwnode_remove: + fwnode_remove_software_node(tcpm->tcpc.fwnode); + + return ret; +} + +static void qcom_pmic_typec_remove(struct platform_device *pdev) +{ + struct pmic_typec *tcpm = platform_get_drvdata(pdev); + + qcom_pmic_typec_pdphy_stop(tcpm->pmic_typec_pdphy); + qcom_pmic_typec_port_stop(tcpm->pmic_typec_port); + tcpm_unregister_port(tcpm->tcpm_port); + fwnode_remove_software_node(tcpm->tcpc.fwnode); +} + +static struct pmic_typec_pdphy_resources pm8150b_pdphy_res = { + .irq_params = { + { + .virq = PMIC_PDPHY_SIG_TX_IRQ, + .irq_name = "sig-tx", + }, + { + .virq = PMIC_PDPHY_SIG_RX_IRQ, + .irq_name = "sig-rx", + }, + { + .virq = PMIC_PDPHY_MSG_TX_IRQ, + .irq_name = "msg-tx", + }, + { + .virq = PMIC_PDPHY_MSG_RX_IRQ, + .irq_name = "msg-rx", + }, + { + .virq = PMIC_PDPHY_MSG_TX_FAIL_IRQ, + .irq_name = "msg-tx-failed", + }, + { + .virq = PMIC_PDPHY_MSG_TX_DISCARD_IRQ, + .irq_name = "msg-tx-discarded", + }, + { + .virq = PMIC_PDPHY_MSG_RX_DISCARD_IRQ, + .irq_name = "msg-rx-discarded", + }, + }, + .nr_irqs = 7, +}; + +static struct pmic_typec_port_resources pm8150b_port_res = { + .irq_params = { + { + .irq_name = "vpd-detect", + .virq = PMIC_TYPEC_VPD_IRQ, + }, + + { + .irq_name = "cc-state-change", + .virq = PMIC_TYPEC_CC_STATE_IRQ, + }, + { + .irq_name = "vconn-oc", + .virq = PMIC_TYPEC_VCONN_OC_IRQ, + }, + + { + .irq_name = "vbus-change", + .virq = PMIC_TYPEC_VBUS_IRQ, + }, + + { + .irq_name = "attach-detach", + .virq = PMIC_TYPEC_ATTACH_DETACH_IRQ, + }, + { + .irq_name = "legacy-cable-detect", + .virq = PMIC_TYPEC_LEGACY_CABLE_IRQ, + }, + + { + .irq_name = "try-snk-src-detect", + .virq = PMIC_TYPEC_TRY_SNK_SRC_IRQ, + }, + }, + .nr_irqs = 7, +}; + +static struct pmic_typec_resources pm8150b_typec_res = { + .pdphy_res = &pm8150b_pdphy_res, + .port_res = &pm8150b_port_res, +}; + +static const struct of_device_id qcom_pmic_typec_table[] = { + { .compatible = "qcom,pm8150b-typec", .data = &pm8150b_typec_res }, + { } +}; +MODULE_DEVICE_TABLE(of, qcom_pmic_typec_table); + +static struct platform_driver qcom_pmic_typec_driver = { + .driver = { + .name = "qcom,pmic-typec", + .of_match_table = qcom_pmic_typec_table, + }, + .probe = qcom_pmic_typec_probe, + .remove_new = qcom_pmic_typec_remove, +}; + +module_platform_driver(qcom_pmic_typec_driver); + +MODULE_DESCRIPTION("QCOM PMIC USB Type-C Port Manager Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.c b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.c new file mode 100644 index 0000000000..52c81378e3 --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.c @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2023, Linaro Ltd. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "qcom_pmic_typec_pdphy.h" + +struct pmic_typec_pdphy_irq_data { + int virq; + int irq; + struct pmic_typec_pdphy *pmic_typec_pdphy; +}; + +struct pmic_typec_pdphy { + struct device *dev; + struct tcpm_port *tcpm_port; + struct regmap *regmap; + u32 base; + + unsigned int nr_irqs; + struct pmic_typec_pdphy_irq_data *irq_data; + + struct work_struct reset_work; + struct work_struct receive_work; + struct regulator *vdd_pdphy; + spinlock_t lock; /* Register atomicity */ +}; + +static void qcom_pmic_typec_pdphy_reset_on(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + struct device *dev = pmic_typec_pdphy->dev; + int ret; + + /* Terminate TX */ + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_CONTROL_REG, 0); + if (ret) + goto err; + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_FRAME_FILTER_REG, 0); + if (ret) + goto err; + + return; +err: + dev_err(dev, "pd_reset_on error\n"); +} + +static void qcom_pmic_typec_pdphy_reset_off(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + struct device *dev = pmic_typec_pdphy->dev; + int ret; + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_FRAME_FILTER_REG, + FRAME_FILTER_EN_SOP | FRAME_FILTER_EN_HARD_RESET); + if (ret) + dev_err(dev, "pd_reset_off error\n"); +} + +static void qcom_pmic_typec_pdphy_sig_reset_work(struct work_struct *work) +{ + struct pmic_typec_pdphy *pmic_typec_pdphy = container_of(work, struct pmic_typec_pdphy, + reset_work); + unsigned long flags; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + qcom_pmic_typec_pdphy_reset_on(pmic_typec_pdphy); + qcom_pmic_typec_pdphy_reset_off(pmic_typec_pdphy); + + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + tcpm_pd_hard_reset(pmic_typec_pdphy->tcpm_port); +} + +static int +qcom_pmic_typec_pdphy_clear_tx_control_reg(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + struct device *dev = pmic_typec_pdphy->dev; + unsigned int val; + int ret; + + /* Clear TX control register */ + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_CONTROL_REG, 0); + if (ret) + goto done; + + /* Perform readback to ensure sufficient delay for command to latch */ + ret = regmap_read(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_CONTROL_REG, &val); + +done: + if (ret) + dev_err(dev, "pd_clear_tx_control_reg: clear tx flag\n"); + + return ret; +} + +static int +qcom_pmic_typec_pdphy_pd_transmit_signal(struct pmic_typec_pdphy *pmic_typec_pdphy, + enum tcpm_transmit_type type, + unsigned int negotiated_rev) +{ + struct device *dev = pmic_typec_pdphy->dev; + unsigned int val; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + /* Clear TX control register */ + ret = qcom_pmic_typec_pdphy_clear_tx_control_reg(pmic_typec_pdphy); + if (ret) + goto done; + + val = TX_CONTROL_SEND_SIGNAL; + if (negotiated_rev == PD_REV30) + val |= TX_CONTROL_RETRY_COUNT(2); + else + val |= TX_CONTROL_RETRY_COUNT(3); + + if (type == TCPC_TX_CABLE_RESET || type == TCPC_TX_HARD_RESET) + val |= TX_CONTROL_FRAME_TYPE(1); + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_CONTROL_REG, val); + +done: + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + dev_vdbg(dev, "pd_transmit_signal: type %d negotiate_rev %d send %d\n", + type, negotiated_rev, ret); + + return ret; +} + +static int +qcom_pmic_typec_pdphy_pd_transmit_payload(struct pmic_typec_pdphy *pmic_typec_pdphy, + enum tcpm_transmit_type type, + const struct pd_message *msg, + unsigned int negotiated_rev) +{ + struct device *dev = pmic_typec_pdphy->dev; + unsigned int val, hdr_len, txbuf_len, txsize_len; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + ret = regmap_read(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_ACKNOWLEDGE_REG, + &val); + if (ret) + goto done; + + if (val) { + dev_err(dev, "pd_transmit_payload: RX message pending\n"); + ret = -EBUSY; + goto done; + } + + /* Clear TX control register */ + ret = qcom_pmic_typec_pdphy_clear_tx_control_reg(pmic_typec_pdphy); + if (ret) + goto done; + + hdr_len = sizeof(msg->header); + txbuf_len = pd_header_cnt_le(msg->header) * 4; + txsize_len = hdr_len + txbuf_len - 1; + + /* Write message header sizeof(u16) to USB_PDPHY_TX_BUFFER_HDR_REG */ + ret = regmap_bulk_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_BUFFER_HDR_REG, + &msg->header, hdr_len); + if (ret) + goto done; + + /* Write payload to USB_PDPHY_TX_BUFFER_DATA_REG for txbuf_len */ + if (txbuf_len) { + ret = regmap_bulk_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_BUFFER_DATA_REG, + &msg->payload, txbuf_len); + if (ret) + goto done; + } + + /* Write total length ((header + data) - 1) to USB_PDPHY_TX_SIZE_REG */ + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_SIZE_REG, + txsize_len); + if (ret) + goto done; + + /* Clear TX control register */ + ret = qcom_pmic_typec_pdphy_clear_tx_control_reg(pmic_typec_pdphy); + if (ret) + goto done; + + /* Initiate transmit with retry count as indicated by PD revision */ + val = TX_CONTROL_FRAME_TYPE(type) | TX_CONTROL_SEND_MSG; + if (pd_header_rev(msg->header) == PD_REV30) + val |= TX_CONTROL_RETRY_COUNT(2); + else + val |= TX_CONTROL_RETRY_COUNT(3); + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_TX_CONTROL_REG, val); + +done: + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + if (ret) { + dev_err(dev, "pd_transmit_payload: hdr %*ph data %*ph ret %d\n", + hdr_len, &msg->header, txbuf_len, &msg->payload, ret); + } + + return ret; +} + +int qcom_pmic_typec_pdphy_pd_transmit(struct pmic_typec_pdphy *pmic_typec_pdphy, + enum tcpm_transmit_type type, + const struct pd_message *msg, + unsigned int negotiated_rev) +{ + struct device *dev = pmic_typec_pdphy->dev; + int ret; + + if (msg) { + ret = qcom_pmic_typec_pdphy_pd_transmit_payload(pmic_typec_pdphy, + type, msg, + negotiated_rev); + } else { + ret = qcom_pmic_typec_pdphy_pd_transmit_signal(pmic_typec_pdphy, + type, + negotiated_rev); + } + + if (ret) + dev_dbg(dev, "pd_transmit: type %x result %d\n", type, ret); + + return ret; +} + +static void qcom_pmic_typec_pdphy_pd_receive(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + struct device *dev = pmic_typec_pdphy->dev; + struct pd_message msg; + unsigned int size, rx_status; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + ret = regmap_read(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_SIZE_REG, &size); + if (ret) + goto done; + + /* Hardware requires +1 of the real read value to be passed */ + if (size < 1 || size > sizeof(msg.payload) + 1) { + dev_dbg(dev, "pd_receive: invalid size %d\n", size); + goto done; + } + + size += 1; + ret = regmap_read(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_STATUS_REG, + &rx_status); + + if (ret) + goto done; + + ret = regmap_bulk_read(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_BUFFER_REG, + (u8 *)&msg, size); + if (ret) + goto done; + + /* Return ownership of RX buffer to hardware */ + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_ACKNOWLEDGE_REG, 0); + +done: + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + if (!ret) { + dev_vdbg(dev, "pd_receive: handing %d bytes to tcpm\n", size); + tcpm_pd_receive(pmic_typec_pdphy->tcpm_port, &msg); + } +} + +static irqreturn_t qcom_pmic_typec_pdphy_isr(int irq, void *dev_id) +{ + struct pmic_typec_pdphy_irq_data *irq_data = dev_id; + struct pmic_typec_pdphy *pmic_typec_pdphy = irq_data->pmic_typec_pdphy; + struct device *dev = pmic_typec_pdphy->dev; + + switch (irq_data->virq) { + case PMIC_PDPHY_SIG_TX_IRQ: + dev_err(dev, "isr: tx_sig\n"); + break; + case PMIC_PDPHY_SIG_RX_IRQ: + schedule_work(&pmic_typec_pdphy->reset_work); + break; + case PMIC_PDPHY_MSG_TX_IRQ: + tcpm_pd_transmit_complete(pmic_typec_pdphy->tcpm_port, + TCPC_TX_SUCCESS); + break; + case PMIC_PDPHY_MSG_RX_IRQ: + qcom_pmic_typec_pdphy_pd_receive(pmic_typec_pdphy); + break; + case PMIC_PDPHY_MSG_TX_FAIL_IRQ: + tcpm_pd_transmit_complete(pmic_typec_pdphy->tcpm_port, + TCPC_TX_FAILED); + break; + case PMIC_PDPHY_MSG_TX_DISCARD_IRQ: + tcpm_pd_transmit_complete(pmic_typec_pdphy->tcpm_port, + TCPC_TX_DISCARDED); + break; + } + + return IRQ_HANDLED; +} + +int qcom_pmic_typec_pdphy_set_pd_rx(struct pmic_typec_pdphy *pmic_typec_pdphy, bool on) +{ + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_RX_ACKNOWLEDGE_REG, !on); + + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + dev_dbg(pmic_typec_pdphy->dev, "set_pd_rx: %s\n", on ? "on" : "off"); + + return ret; +} + +int qcom_pmic_typec_pdphy_set_roles(struct pmic_typec_pdphy *pmic_typec_pdphy, + bool data_role_host, bool power_role_src) +{ + struct device *dev = pmic_typec_pdphy->dev; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_pdphy->lock, flags); + + ret = regmap_update_bits(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_MSG_CONFIG_REG, + MSG_CONFIG_PORT_DATA_ROLE | + MSG_CONFIG_PORT_POWER_ROLE, + data_role_host << 3 | power_role_src << 2); + + spin_unlock_irqrestore(&pmic_typec_pdphy->lock, flags); + + dev_dbg(dev, "pdphy_set_roles: data_role_host=%d power_role_src=%d\n", + data_role_host, power_role_src); + + return ret; +} + +static int qcom_pmic_typec_pdphy_enable(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + struct device *dev = pmic_typec_pdphy->dev; + int ret; + + /* PD 2.0, DR=TYPEC_DEVICE, PR=TYPEC_SINK */ + ret = regmap_update_bits(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_MSG_CONFIG_REG, + MSG_CONFIG_SPEC_REV_MASK, PD_REV20); + if (ret) + goto done; + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_EN_CONTROL_REG, 0); + if (ret) + goto done; + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_EN_CONTROL_REG, + CONTROL_ENABLE); + if (ret) + goto done; + + qcom_pmic_typec_pdphy_reset_off(pmic_typec_pdphy); +done: + if (ret) { + regulator_disable(pmic_typec_pdphy->vdd_pdphy); + dev_err(dev, "pdphy_enable fail %d\n", ret); + } + + return ret; +} + +static int qcom_pmic_typec_pdphy_disable(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + int ret; + + qcom_pmic_typec_pdphy_reset_on(pmic_typec_pdphy); + + ret = regmap_write(pmic_typec_pdphy->regmap, + pmic_typec_pdphy->base + USB_PDPHY_EN_CONTROL_REG, 0); + + return ret; +} + +static int pmic_typec_pdphy_reset(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + int ret; + + ret = qcom_pmic_typec_pdphy_disable(pmic_typec_pdphy); + if (ret) + goto done; + + usleep_range(400, 500); + ret = qcom_pmic_typec_pdphy_enable(pmic_typec_pdphy); +done: + return ret; +} + +int qcom_pmic_typec_pdphy_start(struct pmic_typec_pdphy *pmic_typec_pdphy, + struct tcpm_port *tcpm_port) +{ + int i; + int ret; + + ret = regulator_enable(pmic_typec_pdphy->vdd_pdphy); + if (ret) + return ret; + + pmic_typec_pdphy->tcpm_port = tcpm_port; + + ret = pmic_typec_pdphy_reset(pmic_typec_pdphy); + if (ret) + return ret; + + for (i = 0; i < pmic_typec_pdphy->nr_irqs; i++) + enable_irq(pmic_typec_pdphy->irq_data[i].irq); + + return 0; +} + +void qcom_pmic_typec_pdphy_stop(struct pmic_typec_pdphy *pmic_typec_pdphy) +{ + int i; + + for (i = 0; i < pmic_typec_pdphy->nr_irqs; i++) + disable_irq(pmic_typec_pdphy->irq_data[i].irq); + + qcom_pmic_typec_pdphy_reset_on(pmic_typec_pdphy); + + regulator_disable(pmic_typec_pdphy->vdd_pdphy); +} + +struct pmic_typec_pdphy *qcom_pmic_typec_pdphy_alloc(struct device *dev) +{ + return devm_kzalloc(dev, sizeof(struct pmic_typec_pdphy), GFP_KERNEL); +} + +int qcom_pmic_typec_pdphy_probe(struct platform_device *pdev, + struct pmic_typec_pdphy *pmic_typec_pdphy, + struct pmic_typec_pdphy_resources *res, + struct regmap *regmap, + u32 base) +{ + struct device *dev = &pdev->dev; + struct pmic_typec_pdphy_irq_data *irq_data; + int i, ret, irq; + + if (!res->nr_irqs || res->nr_irqs > PMIC_PDPHY_MAX_IRQS) + return -EINVAL; + + irq_data = devm_kzalloc(dev, sizeof(*irq_data) * res->nr_irqs, + GFP_KERNEL); + if (!irq_data) + return -ENOMEM; + + pmic_typec_pdphy->vdd_pdphy = devm_regulator_get(dev, "vdd-pdphy"); + if (IS_ERR(pmic_typec_pdphy->vdd_pdphy)) + return PTR_ERR(pmic_typec_pdphy->vdd_pdphy); + + pmic_typec_pdphy->dev = dev; + pmic_typec_pdphy->base = base; + pmic_typec_pdphy->regmap = regmap; + pmic_typec_pdphy->nr_irqs = res->nr_irqs; + pmic_typec_pdphy->irq_data = irq_data; + spin_lock_init(&pmic_typec_pdphy->lock); + INIT_WORK(&pmic_typec_pdphy->reset_work, qcom_pmic_typec_pdphy_sig_reset_work); + + for (i = 0; i < res->nr_irqs; i++, irq_data++) { + irq = platform_get_irq_byname(pdev, res->irq_params[i].irq_name); + if (irq < 0) + return irq; + + irq_data->pmic_typec_pdphy = pmic_typec_pdphy; + irq_data->irq = irq; + irq_data->virq = res->irq_params[i].virq; + + ret = devm_request_threaded_irq(dev, irq, NULL, + qcom_pmic_typec_pdphy_isr, + IRQF_ONESHOT | IRQF_NO_AUTOEN, + res->irq_params[i].irq_name, + irq_data); + if (ret) + return ret; + } + + return 0; +} diff --git a/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.h b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.h new file mode 100644 index 0000000000..e67954e31b --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_pdphy.h @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2018-2019 The Linux Foundation. All rights reserved. + * Copyright (c) 2023, Linaro Ltd. All rights reserved. + */ +#ifndef __QCOM_PMIC_PDPHY_H__ +#define __QCOM_PMIC_PDPHY_H__ + +#include +#include +#include + +#define USB_PDPHY_MAX_DATA_OBJ_LEN 28 +#define USB_PDPHY_MSG_HDR_LEN 2 + +/* PD PHY register offsets and bit fields */ +#define USB_PDPHY_MSG_CONFIG_REG 0x40 +#define MSG_CONFIG_PORT_DATA_ROLE BIT(3) +#define MSG_CONFIG_PORT_POWER_ROLE BIT(2) +#define MSG_CONFIG_SPEC_REV_MASK (BIT(1) | BIT(0)) + +#define USB_PDPHY_EN_CONTROL_REG 0x46 +#define CONTROL_ENABLE BIT(0) + +#define USB_PDPHY_RX_STATUS_REG 0x4A +#define RX_FRAME_TYPE (BIT(0) | BIT(1) | BIT(2)) + +#define USB_PDPHY_FRAME_FILTER_REG 0x4C +#define FRAME_FILTER_EN_HARD_RESET BIT(5) +#define FRAME_FILTER_EN_SOP BIT(0) + +#define USB_PDPHY_TX_SIZE_REG 0x42 +#define TX_SIZE_MASK 0xF + +#define USB_PDPHY_TX_CONTROL_REG 0x44 +#define TX_CONTROL_RETRY_COUNT(n) (((n) & 0x3) << 5) +#define TX_CONTROL_FRAME_TYPE(n) (((n) & 0x7) << 2) +#define TX_CONTROL_FRAME_TYPE_CABLE_RESET (0x1 << 2) +#define TX_CONTROL_SEND_SIGNAL BIT(1) +#define TX_CONTROL_SEND_MSG BIT(0) + +#define USB_PDPHY_RX_SIZE_REG 0x48 + +#define USB_PDPHY_RX_ACKNOWLEDGE_REG 0x4B +#define RX_BUFFER_TOKEN BIT(0) + +#define USB_PDPHY_BIST_MODE_REG 0x4E +#define BIST_MODE_MASK 0xF +#define BIST_ENABLE BIT(7) +#define PD_MSG_BIST 0x3 +#define PD_BIST_TEST_DATA_MODE 0x8 + +#define USB_PDPHY_TX_BUFFER_HDR_REG 0x60 +#define USB_PDPHY_TX_BUFFER_DATA_REG 0x62 + +#define USB_PDPHY_RX_BUFFER_REG 0x80 + +/* VDD regulator */ +#define VDD_PDPHY_VOL_MIN 2800000 /* uV */ +#define VDD_PDPHY_VOL_MAX 3300000 /* uV */ +#define VDD_PDPHY_HPM_LOAD 3000 /* uA */ + +/* Message Spec Rev field */ +#define PD_MSG_HDR_REV(hdr) (((hdr) >> 6) & 3) + +/* timers */ +#define RECEIVER_RESPONSE_TIME 15 /* tReceiverResponse */ +#define HARD_RESET_COMPLETE_TIME 5 /* tHardResetComplete */ + +/* Interrupt numbers */ +#define PMIC_PDPHY_SIG_TX_IRQ 0x0 +#define PMIC_PDPHY_SIG_RX_IRQ 0x1 +#define PMIC_PDPHY_MSG_TX_IRQ 0x2 +#define PMIC_PDPHY_MSG_RX_IRQ 0x3 +#define PMIC_PDPHY_MSG_TX_FAIL_IRQ 0x4 +#define PMIC_PDPHY_MSG_TX_DISCARD_IRQ 0x5 +#define PMIC_PDPHY_MSG_RX_DISCARD_IRQ 0x6 +#define PMIC_PDPHY_FR_SWAP_IRQ 0x7 + +/* Resources */ +#define PMIC_PDPHY_MAX_IRQS 0x08 + +struct pmic_typec_pdphy_irq_params { + int virq; + char *irq_name; +}; + +struct pmic_typec_pdphy_resources { + unsigned int nr_irqs; + struct pmic_typec_pdphy_irq_params irq_params[PMIC_PDPHY_MAX_IRQS]; +}; + +/* API */ +struct pmic_typec_pdphy; + +struct pmic_typec_pdphy *qcom_pmic_typec_pdphy_alloc(struct device *dev); + +int qcom_pmic_typec_pdphy_probe(struct platform_device *pdev, + struct pmic_typec_pdphy *pmic_typec_pdphy, + struct pmic_typec_pdphy_resources *res, + struct regmap *regmap, + u32 base); + +int qcom_pmic_typec_pdphy_start(struct pmic_typec_pdphy *pmic_typec_pdphy, + struct tcpm_port *tcpm_port); + +void qcom_pmic_typec_pdphy_stop(struct pmic_typec_pdphy *pmic_typec_pdphy); + +int qcom_pmic_typec_pdphy_set_roles(struct pmic_typec_pdphy *pmic_typec_pdphy, + bool power_role_src, bool data_role_host); + +int qcom_pmic_typec_pdphy_set_pd_rx(struct pmic_typec_pdphy *pmic_typec_pdphy, bool on); + +int qcom_pmic_typec_pdphy_pd_transmit(struct pmic_typec_pdphy *pmic_typec_pdphy, + enum tcpm_transmit_type type, + const struct pd_message *msg, + unsigned int negotiated_rev); + +#endif /* __QCOM_PMIC_TYPEC_PDPHY_H__ */ diff --git a/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.c b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.c new file mode 100644 index 0000000000..a8f3f4d3a4 --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.c @@ -0,0 +1,560 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2023, Linaro Ltd. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "qcom_pmic_typec_port.h" + +struct pmic_typec_port_irq_data { + int virq; + int irq; + struct pmic_typec_port *pmic_typec_port; +}; + +struct pmic_typec_port { + struct device *dev; + struct tcpm_port *tcpm_port; + struct regmap *regmap; + u32 base; + unsigned int nr_irqs; + struct pmic_typec_port_irq_data *irq_data; + + struct regulator *vdd_vbus; + + int cc; + bool debouncing_cc; + struct delayed_work cc_debounce_dwork; + + spinlock_t lock; /* Register atomicity */ +}; + +static const char * const typec_cc_status_name[] = { + [TYPEC_CC_OPEN] = "Open", + [TYPEC_CC_RA] = "Ra", + [TYPEC_CC_RD] = "Rd", + [TYPEC_CC_RP_DEF] = "Rp-def", + [TYPEC_CC_RP_1_5] = "Rp-1.5", + [TYPEC_CC_RP_3_0] = "Rp-3.0", +}; + +static const char *rp_unknown = "unknown"; + +static const char *cc_to_name(enum typec_cc_status cc) +{ + if (cc > TYPEC_CC_RP_3_0) + return rp_unknown; + + return typec_cc_status_name[cc]; +} + +static const char * const rp_sel_name[] = { + [TYPEC_SRC_RP_SEL_80UA] = "Rp-def-80uA", + [TYPEC_SRC_RP_SEL_180UA] = "Rp-1.5-180uA", + [TYPEC_SRC_RP_SEL_330UA] = "Rp-3.0-330uA", +}; + +static const char *rp_sel_to_name(int rp_sel) +{ + if (rp_sel > TYPEC_SRC_RP_SEL_330UA) + return rp_unknown; + + return rp_sel_name[rp_sel]; +} + +#define misc_to_cc(msic) !!(misc & CC_ORIENTATION) ? "cc1" : "cc2" +#define misc_to_vconn(msic) !!(misc & CC_ORIENTATION) ? "cc2" : "cc1" + +static void qcom_pmic_typec_port_cc_debounce(struct work_struct *work) +{ + struct pmic_typec_port *pmic_typec_port = + container_of(work, struct pmic_typec_port, cc_debounce_dwork.work); + unsigned long flags; + + spin_lock_irqsave(&pmic_typec_port->lock, flags); + pmic_typec_port->debouncing_cc = false; + spin_unlock_irqrestore(&pmic_typec_port->lock, flags); + + dev_dbg(pmic_typec_port->dev, "Debounce cc complete\n"); +} + +static irqreturn_t pmic_typec_port_isr(int irq, void *dev_id) +{ + struct pmic_typec_port_irq_data *irq_data = dev_id; + struct pmic_typec_port *pmic_typec_port = irq_data->pmic_typec_port; + u32 misc_stat; + bool vbus_change = false; + bool cc_change = false; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_port->lock, flags); + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, + &misc_stat); + if (ret) + goto done; + + switch (irq_data->virq) { + case PMIC_TYPEC_VBUS_IRQ: + vbus_change = true; + break; + case PMIC_TYPEC_CC_STATE_IRQ: + case PMIC_TYPEC_ATTACH_DETACH_IRQ: + if (!pmic_typec_port->debouncing_cc) + cc_change = true; + break; + } + +done: + spin_unlock_irqrestore(&pmic_typec_port->lock, flags); + + if (vbus_change) + tcpm_vbus_change(pmic_typec_port->tcpm_port); + + if (cc_change) + tcpm_cc_change(pmic_typec_port->tcpm_port); + + return IRQ_HANDLED; +} + +int qcom_pmic_typec_port_get_vbus(struct pmic_typec_port *pmic_typec_port) +{ + struct device *dev = pmic_typec_port->dev; + unsigned int misc; + int ret; + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, + &misc); + if (ret) + misc = 0; + + dev_dbg(dev, "get_vbus: 0x%08x detect %d\n", misc, !!(misc & TYPEC_VBUS_DETECT)); + + return !!(misc & TYPEC_VBUS_DETECT); +} + +int qcom_pmic_typec_port_set_vbus(struct pmic_typec_port *pmic_typec_port, bool on) +{ + u32 sm_stat; + u32 val; + int ret; + + if (on) { + ret = regulator_enable(pmic_typec_port->vdd_vbus); + if (ret) + return ret; + + val = TYPEC_SM_VBUS_VSAFE5V; + } else { + ret = regulator_disable(pmic_typec_port->vdd_vbus); + if (ret) + return ret; + + val = TYPEC_SM_VBUS_VSAFE0V; + } + + /* Poll waiting for transition to required vSafe5V or vSafe0V */ + ret = regmap_read_poll_timeout(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_SM_STATUS_REG, + sm_stat, sm_stat & val, + 100, 250000); + if (ret) + dev_warn(pmic_typec_port->dev, "vbus vsafe%dv fail\n", on ? 5 : 0); + + return 0; +} + +int qcom_pmic_typec_port_get_cc(struct pmic_typec_port *pmic_typec_port, + enum typec_cc_status *cc1, + enum typec_cc_status *cc2) +{ + struct device *dev = pmic_typec_port->dev; + unsigned int misc, val; + bool attached; + int ret = 0; + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, &misc); + if (ret) + goto done; + + attached = !!(misc & CC_ATTACHED); + + if (pmic_typec_port->debouncing_cc) { + ret = -EBUSY; + goto done; + } + + *cc1 = TYPEC_CC_OPEN; + *cc2 = TYPEC_CC_OPEN; + + if (!attached) + goto done; + + if (misc & SNK_SRC_MODE) { + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_SRC_STATUS_REG, + &val); + if (ret) + goto done; + switch (val & DETECTED_SRC_TYPE_MASK) { + case AUDIO_ACCESS_RA_RA: + val = TYPEC_CC_RA; + *cc1 = TYPEC_CC_RA; + *cc2 = TYPEC_CC_RA; + break; + case SRC_RD_OPEN: + val = TYPEC_CC_RD; + break; + case SRC_RD_RA_VCONN: + val = TYPEC_CC_RD; + *cc1 = TYPEC_CC_RA; + *cc2 = TYPEC_CC_RA; + break; + default: + dev_warn(dev, "unexpected src status %.2x\n", val); + val = TYPEC_CC_RD; + break; + } + } else { + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_SNK_STATUS_REG, + &val); + if (ret) + goto done; + switch (val & DETECTED_SNK_TYPE_MASK) { + case SNK_RP_STD: + val = TYPEC_CC_RP_DEF; + break; + case SNK_RP_1P5: + val = TYPEC_CC_RP_1_5; + break; + case SNK_RP_3P0: + val = TYPEC_CC_RP_3_0; + break; + default: + dev_warn(dev, "unexpected snk status %.2x\n", val); + val = TYPEC_CC_RP_DEF; + break; + } + val = TYPEC_CC_RP_DEF; + } + + if (misc & CC_ORIENTATION) + *cc2 = val; + else + *cc1 = val; + +done: + dev_dbg(dev, "get_cc: misc 0x%08x cc1 0x%08x %s cc2 0x%08x %s attached %d cc=%s\n", + misc, *cc1, cc_to_name(*cc1), *cc2, cc_to_name(*cc2), attached, + misc_to_cc(misc)); + + return ret; +} + +static void qcom_pmic_set_cc_debounce(struct pmic_typec_port *pmic_typec_port) +{ + pmic_typec_port->debouncing_cc = true; + schedule_delayed_work(&pmic_typec_port->cc_debounce_dwork, + msecs_to_jiffies(2)); +} + +int qcom_pmic_typec_port_set_cc(struct pmic_typec_port *pmic_typec_port, + enum typec_cc_status cc) +{ + struct device *dev = pmic_typec_port->dev; + unsigned int mode, currsrc; + unsigned int misc; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_port->lock, flags); + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, + &misc); + if (ret) + goto done; + + mode = EN_SRC_ONLY; + + switch (cc) { + case TYPEC_CC_OPEN: + currsrc = TYPEC_SRC_RP_SEL_80UA; + break; + case TYPEC_CC_RP_DEF: + currsrc = TYPEC_SRC_RP_SEL_80UA; + break; + case TYPEC_CC_RP_1_5: + currsrc = TYPEC_SRC_RP_SEL_180UA; + break; + case TYPEC_CC_RP_3_0: + currsrc = TYPEC_SRC_RP_SEL_330UA; + break; + case TYPEC_CC_RD: + currsrc = TYPEC_SRC_RP_SEL_80UA; + mode = EN_SNK_ONLY; + break; + default: + dev_warn(dev, "unexpected set_cc %d\n", cc); + ret = -EINVAL; + goto done; + } + + if (mode == EN_SRC_ONLY) { + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_CURRSRC_CFG_REG, + currsrc); + if (ret) + goto done; + } + + pmic_typec_port->cc = cc; + qcom_pmic_set_cc_debounce(pmic_typec_port); + ret = 0; + +done: + spin_unlock_irqrestore(&pmic_typec_port->lock, flags); + + dev_dbg(dev, "set_cc: currsrc=%x %s mode %s debounce %d attached %d cc=%s\n", + currsrc, rp_sel_to_name(currsrc), + mode == EN_SRC_ONLY ? "EN_SRC_ONLY" : "EN_SNK_ONLY", + pmic_typec_port->debouncing_cc, !!(misc & CC_ATTACHED), + misc_to_cc(misc)); + + return ret; +} + +int qcom_pmic_typec_port_set_vconn(struct pmic_typec_port *pmic_typec_port, bool on) +{ + struct device *dev = pmic_typec_port->dev; + unsigned int orientation, misc, mask, value; + unsigned long flags; + int ret; + + spin_lock_irqsave(&pmic_typec_port->lock, flags); + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, &misc); + if (ret) + goto done; + + /* Set VCONN on the inversion of the active CC channel */ + orientation = (misc & CC_ORIENTATION) ? 0 : VCONN_EN_ORIENTATION; + if (on) { + mask = VCONN_EN_ORIENTATION | VCONN_EN_VALUE; + value = orientation | VCONN_EN_VALUE | VCONN_EN_SRC; + } else { + mask = VCONN_EN_VALUE; + value = 0; + } + + ret = regmap_update_bits(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_VCONN_CONTROL_REG, + mask, value); +done: + spin_unlock_irqrestore(&pmic_typec_port->lock, flags); + + dev_dbg(dev, "set_vconn: orientation %d control 0x%08x state %s cc %s vconn %s\n", + orientation, value, on ? "on" : "off", misc_to_vconn(misc), misc_to_cc(misc)); + + return ret; +} + +int qcom_pmic_typec_port_start_toggling(struct pmic_typec_port *pmic_typec_port, + enum typec_port_type port_type, + enum typec_cc_status cc) +{ + struct device *dev = pmic_typec_port->dev; + unsigned int misc; + u8 mode = 0; + unsigned long flags; + int ret; + + switch (port_type) { + case TYPEC_PORT_SRC: + mode = EN_SRC_ONLY; + break; + case TYPEC_PORT_SNK: + mode = EN_SNK_ONLY; + break; + case TYPEC_PORT_DRP: + mode = EN_TRY_SNK; + break; + } + + spin_lock_irqsave(&pmic_typec_port->lock, flags); + + ret = regmap_read(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MISC_STATUS_REG, &misc); + if (ret) + goto done; + + dev_dbg(dev, "start_toggling: misc 0x%08x attached %d port_type %d current cc %d new %d\n", + misc, !!(misc & CC_ATTACHED), port_type, pmic_typec_port->cc, cc); + + qcom_pmic_set_cc_debounce(pmic_typec_port); + + /* force it to toggle at least once */ + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MODE_CFG_REG, + TYPEC_DISABLE_CMD); + if (ret) + goto done; + + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MODE_CFG_REG, + mode); +done: + spin_unlock_irqrestore(&pmic_typec_port->lock, flags); + + return ret; +} + +#define TYPEC_INTR_EN_CFG_1_MASK \ + (TYPEC_LEGACY_CABLE_INT_EN | \ + TYPEC_NONCOMPLIANT_LEGACY_CABLE_INT_EN | \ + TYPEC_TRYSOURCE_DETECT_INT_EN | \ + TYPEC_TRYSINK_DETECT_INT_EN | \ + TYPEC_CCOUT_DETACH_INT_EN | \ + TYPEC_CCOUT_ATTACH_INT_EN | \ + TYPEC_VBUS_DEASSERT_INT_EN | \ + TYPEC_VBUS_ASSERT_INT_EN) + +#define TYPEC_INTR_EN_CFG_2_MASK \ + (TYPEC_STATE_MACHINE_CHANGE_INT_EN | TYPEC_VBUS_ERROR_INT_EN | \ + TYPEC_DEBOUNCE_DONE_INT_EN) + +int qcom_pmic_typec_port_start(struct pmic_typec_port *pmic_typec_port, + struct tcpm_port *tcpm_port) +{ + int i; + int mask; + int ret; + + /* Configure interrupt sources */ + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_INTERRUPT_EN_CFG_1_REG, + TYPEC_INTR_EN_CFG_1_MASK); + if (ret) + goto done; + + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_INTERRUPT_EN_CFG_2_REG, + TYPEC_INTR_EN_CFG_2_MASK); + if (ret) + goto done; + + /* start in TRY_SNK mode */ + ret = regmap_write(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_MODE_CFG_REG, EN_TRY_SNK); + if (ret) + goto done; + + /* Configure VCONN for software control */ + ret = regmap_update_bits(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_VCONN_CONTROL_REG, + VCONN_EN_SRC | VCONN_EN_VALUE, VCONN_EN_SRC); + if (ret) + goto done; + + /* Set CC threshold to 1.6 Volts | tPDdebounce = 10-20ms */ + mask = SEL_SRC_UPPER_REF | USE_TPD_FOR_EXITING_ATTACHSRC; + ret = regmap_update_bits(pmic_typec_port->regmap, + pmic_typec_port->base + TYPEC_EXIT_STATE_CFG_REG, + mask, mask); + if (ret) + goto done; + + pmic_typec_port->tcpm_port = tcpm_port; + + for (i = 0; i < pmic_typec_port->nr_irqs; i++) + enable_irq(pmic_typec_port->irq_data[i].irq); + +done: + return ret; +} + +void qcom_pmic_typec_port_stop(struct pmic_typec_port *pmic_typec_port) +{ + int i; + + for (i = 0; i < pmic_typec_port->nr_irqs; i++) + disable_irq(pmic_typec_port->irq_data[i].irq); +} + +struct pmic_typec_port *qcom_pmic_typec_port_alloc(struct device *dev) +{ + return devm_kzalloc(dev, sizeof(struct pmic_typec_port), GFP_KERNEL); +} + +int qcom_pmic_typec_port_probe(struct platform_device *pdev, + struct pmic_typec_port *pmic_typec_port, + struct pmic_typec_port_resources *res, + struct regmap *regmap, + u32 base) +{ + struct device *dev = &pdev->dev; + struct pmic_typec_port_irq_data *irq_data; + int i, ret, irq; + + if (!res->nr_irqs || res->nr_irqs > PMIC_TYPEC_MAX_IRQS) + return -EINVAL; + + irq_data = devm_kzalloc(dev, sizeof(*irq_data) * res->nr_irqs, + GFP_KERNEL); + if (!irq_data) + return -ENOMEM; + + pmic_typec_port->vdd_vbus = devm_regulator_get(dev, "vdd-vbus"); + if (IS_ERR(pmic_typec_port->vdd_vbus)) + return PTR_ERR(pmic_typec_port->vdd_vbus); + + pmic_typec_port->dev = dev; + pmic_typec_port->base = base; + pmic_typec_port->regmap = regmap; + pmic_typec_port->nr_irqs = res->nr_irqs; + pmic_typec_port->irq_data = irq_data; + spin_lock_init(&pmic_typec_port->lock); + INIT_DELAYED_WORK(&pmic_typec_port->cc_debounce_dwork, + qcom_pmic_typec_port_cc_debounce); + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + for (i = 0; i < res->nr_irqs; i++, irq_data++) { + irq = platform_get_irq_byname(pdev, + res->irq_params[i].irq_name); + if (irq < 0) + return irq; + + irq_data->pmic_typec_port = pmic_typec_port; + irq_data->irq = irq; + irq_data->virq = res->irq_params[i].virq; + ret = devm_request_threaded_irq(dev, irq, NULL, pmic_typec_port_isr, + IRQF_ONESHOT | IRQF_NO_AUTOEN, + res->irq_params[i].irq_name, + irq_data); + if (ret) + return ret; + } + + return 0; +} diff --git a/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.h b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.h new file mode 100644 index 0000000000..d4d358c680 --- /dev/null +++ b/drivers/usb/typec/tcpm/qcom/qcom_pmic_typec_port.h @@ -0,0 +1,195 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2018-2019 The Linux Foundation. All rights reserved. + * Copyright (c) 2023, Linaro Ltd. All rights reserved. + */ +#ifndef __QCOM_PMIC_TYPEC_H__ +#define __QCOM_PMIC_TYPEC_H__ + +#include +#include + +#define TYPEC_SNK_STATUS_REG 0x06 +#define DETECTED_SNK_TYPE_MASK GENMASK(6, 0) +#define SNK_DAM_MASK GENMASK(6, 4) +#define SNK_DAM_500MA BIT(6) +#define SNK_DAM_1500MA BIT(5) +#define SNK_DAM_3000MA BIT(4) +#define SNK_RP_STD BIT(3) +#define SNK_RP_1P5 BIT(2) +#define SNK_RP_3P0 BIT(1) +#define SNK_RP_SHORT BIT(0) + +#define TYPEC_SRC_STATUS_REG 0x08 +#define DETECTED_SRC_TYPE_MASK GENMASK(4, 0) +#define SRC_HIGH_BATT BIT(5) +#define SRC_DEBUG_ACCESS BIT(4) +#define SRC_RD_OPEN BIT(3) +#define SRC_RD_RA_VCONN BIT(2) +#define SRC_RA_OPEN BIT(1) +#define AUDIO_ACCESS_RA_RA BIT(0) + +#define TYPEC_STATE_MACHINE_STATUS_REG 0x09 +#define TYPEC_ATTACH_DETACH_STATE BIT(5) + +#define TYPEC_SM_STATUS_REG 0x0A +#define TYPEC_SM_VBUS_VSAFE5V BIT(5) +#define TYPEC_SM_VBUS_VSAFE0V BIT(6) +#define TYPEC_SM_USBIN_LT_LV BIT(7) + +#define TYPEC_MISC_STATUS_REG 0x0B +#define TYPEC_WATER_DETECTION_STATUS BIT(7) +#define SNK_SRC_MODE BIT(6) +#define TYPEC_VBUS_DETECT BIT(5) +#define TYPEC_VBUS_ERROR_STATUS BIT(4) +#define TYPEC_DEBOUNCE_DONE BIT(3) +#define CC_ORIENTATION BIT(1) +#define CC_ATTACHED BIT(0) + +#define LEGACY_CABLE_STATUS_REG 0x0D +#define TYPEC_LEGACY_CABLE_STATUS BIT(1) +#define TYPEC_NONCOMP_LEGACY_CABLE_STATUS BIT(0) + +#define TYPEC_U_USB_STATUS_REG 0x0F +#define U_USB_GROUND_NOVBUS BIT(6) +#define U_USB_GROUND BIT(4) +#define U_USB_FMB1 BIT(3) +#define U_USB_FLOAT1 BIT(2) +#define U_USB_FMB2 BIT(1) +#define U_USB_FLOAT2 BIT(0) + +#define TYPEC_MODE_CFG_REG 0x44 +#define TYPEC_TRY_MODE_MASK GENMASK(4, 3) +#define EN_TRY_SNK BIT(4) +#define EN_TRY_SRC BIT(3) +#define TYPEC_POWER_ROLE_CMD_MASK GENMASK(2, 0) +#define EN_SRC_ONLY BIT(2) +#define EN_SNK_ONLY BIT(1) +#define TYPEC_DISABLE_CMD BIT(0) + +#define TYPEC_VCONN_CONTROL_REG 0x46 +#define VCONN_EN_ORIENTATION BIT(2) +#define VCONN_EN_VALUE BIT(1) +#define VCONN_EN_SRC BIT(0) + +#define TYPEC_CCOUT_CONTROL_REG 0x48 +#define TYPEC_CCOUT_BUFFER_EN BIT(2) +#define TYPEC_CCOUT_VALUE BIT(1) +#define TYPEC_CCOUT_SRC BIT(0) + +#define DEBUG_ACCESS_SRC_CFG_REG 0x4C +#define EN_UNORIENTED_DEBUG_ACCESS_SRC BIT(0) + +#define TYPE_C_CRUDE_SENSOR_CFG_REG 0x4e +#define EN_SRC_CRUDE_SENSOR BIT(1) +#define EN_SNK_CRUDE_SENSOR BIT(0) + +#define TYPEC_EXIT_STATE_CFG_REG 0x50 +#define BYPASS_VSAFE0V_DURING_ROLE_SWAP BIT(3) +#define SEL_SRC_UPPER_REF BIT(2) +#define USE_TPD_FOR_EXITING_ATTACHSRC BIT(1) +#define EXIT_SNK_BASED_ON_CC BIT(0) + +#define TYPEC_CURRSRC_CFG_REG 0x52 +#define TYPEC_SRC_RP_SEL_330UA BIT(1) +#define TYPEC_SRC_RP_SEL_180UA BIT(0) +#define TYPEC_SRC_RP_SEL_80UA 0 +#define TYPEC_SRC_RP_SEL_MASK GENMASK(1, 0) + +#define TYPEC_INTERRUPT_EN_CFG_1_REG 0x5E +#define TYPEC_LEGACY_CABLE_INT_EN BIT(7) +#define TYPEC_NONCOMPLIANT_LEGACY_CABLE_INT_EN BIT(6) +#define TYPEC_TRYSOURCE_DETECT_INT_EN BIT(5) +#define TYPEC_TRYSINK_DETECT_INT_EN BIT(4) +#define TYPEC_CCOUT_DETACH_INT_EN BIT(3) +#define TYPEC_CCOUT_ATTACH_INT_EN BIT(2) +#define TYPEC_VBUS_DEASSERT_INT_EN BIT(1) +#define TYPEC_VBUS_ASSERT_INT_EN BIT(0) + +#define TYPEC_INTERRUPT_EN_CFG_2_REG 0x60 +#define TYPEC_SRC_BATT_HPWR_INT_EN BIT(6) +#define MICRO_USB_STATE_CHANGE_INT_EN BIT(5) +#define TYPEC_STATE_MACHINE_CHANGE_INT_EN BIT(4) +#define TYPEC_DEBUG_ACCESS_DETECT_INT_EN BIT(3) +#define TYPEC_WATER_DETECTION_INT_EN BIT(2) +#define TYPEC_VBUS_ERROR_INT_EN BIT(1) +#define TYPEC_DEBOUNCE_DONE_INT_EN BIT(0) + +#define TYPEC_DEBOUNCE_OPTION_REG 0x62 +#define REDUCE_TCCDEBOUNCE_TO_2MS BIT(2) + +#define TYPE_C_SBU_CFG_REG 0x6A +#define SEL_SBU1_ISRC_VAL 0x04 +#define SEL_SBU2_ISRC_VAL 0x01 + +#define TYPEC_U_USB_CFG_REG 0x70 +#define EN_MICRO_USB_FACTORY_MODE BIT(1) +#define EN_MICRO_USB_MODE BIT(0) + +#define TYPEC_PMI632_U_USB_WATER_PROTECTION_CFG_REG 0x72 + +#define TYPEC_U_USB_WATER_PROTECTION_CFG_REG 0x73 +#define EN_MICRO_USB_WATER_PROTECTION BIT(4) +#define MICRO_USB_DETECTION_ON_TIME_CFG_MASK GENMASK(3, 2) +#define MICRO_USB_DETECTION_PERIOD_CFG_MASK GENMASK(1, 0) + +#define TYPEC_PMI632_MICRO_USB_MODE_REG 0x73 +#define MICRO_USB_MODE_ONLY BIT(0) + +/* Interrupt numbers */ +#define PMIC_TYPEC_OR_RID_IRQ 0x0 +#define PMIC_TYPEC_VPD_IRQ 0x1 +#define PMIC_TYPEC_CC_STATE_IRQ 0x2 +#define PMIC_TYPEC_VCONN_OC_IRQ 0x3 +#define PMIC_TYPEC_VBUS_IRQ 0x4 +#define PMIC_TYPEC_ATTACH_DETACH_IRQ 0x5 +#define PMIC_TYPEC_LEGACY_CABLE_IRQ 0x6 +#define PMIC_TYPEC_TRY_SNK_SRC_IRQ 0x7 + +/* Resources */ +#define PMIC_TYPEC_MAX_IRQS 0x08 + +struct pmic_typec_port_irq_params { + int virq; + char *irq_name; +}; + +struct pmic_typec_port_resources { + unsigned int nr_irqs; + struct pmic_typec_port_irq_params irq_params[PMIC_TYPEC_MAX_IRQS]; +}; + +/* API */ +struct pmic_typec; + +struct pmic_typec_port *qcom_pmic_typec_port_alloc(struct device *dev); + +int qcom_pmic_typec_port_probe(struct platform_device *pdev, + struct pmic_typec_port *pmic_typec_port, + struct pmic_typec_port_resources *res, + struct regmap *regmap, + u32 base); + +int qcom_pmic_typec_port_start(struct pmic_typec_port *pmic_typec_port, + struct tcpm_port *tcpm_port); + +void qcom_pmic_typec_port_stop(struct pmic_typec_port *pmic_typec_port); + +int qcom_pmic_typec_port_get_cc(struct pmic_typec_port *pmic_typec_port, + enum typec_cc_status *cc1, + enum typec_cc_status *cc2); + +int qcom_pmic_typec_port_set_cc(struct pmic_typec_port *pmic_typec_port, + enum typec_cc_status cc); + +int qcom_pmic_typec_port_get_vbus(struct pmic_typec_port *pmic_typec_port); + +int qcom_pmic_typec_port_set_vconn(struct pmic_typec_port *pmic_typec_port, bool on); + +int qcom_pmic_typec_port_start_toggling(struct pmic_typec_port *pmic_typec_port, + enum typec_port_type port_type, + enum typec_cc_status cc); + +int qcom_pmic_typec_port_set_vbus(struct pmic_typec_port *pmic_typec_port, bool on); + +#endif /* __QCOM_PMIC_TYPE_C_PORT_H__ */ diff --git a/drivers/usb/typec/tcpm/tcpci.c b/drivers/usb/typec/tcpm/tcpci.c new file mode 100644 index 0000000000..0ee3e6e29b --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci.c @@ -0,0 +1,909 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2015-2017 Google, Inc + * + * USB Type-C Port Controller Interface. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PD_RETRY_COUNT_DEFAULT 3 +#define PD_RETRY_COUNT_3_0_OR_HIGHER 2 +#define AUTO_DISCHARGE_DEFAULT_THRESHOLD_MV 3500 +#define VSINKPD_MIN_IR_DROP_MV 750 +#define VSRC_NEW_MIN_PERCENT 95 +#define VSRC_VALID_MIN_MV 500 +#define VPPS_NEW_MIN_PERCENT 95 +#define VPPS_VALID_MIN_MV 100 +#define VSINKDISCONNECT_PD_MIN_PERCENT 90 + +struct tcpci { + struct device *dev; + + struct tcpm_port *port; + + struct regmap *regmap; + unsigned int alert_mask; + + bool controls_vbus; + + struct tcpc_dev tcpc; + struct tcpci_data *data; +}; + +struct tcpci_chip { + struct tcpci *tcpci; + struct tcpci_data data; +}; + +struct tcpm_port *tcpci_get_tcpm_port(struct tcpci *tcpci) +{ + return tcpci->port; +} +EXPORT_SYMBOL_GPL(tcpci_get_tcpm_port); + +static inline struct tcpci *tcpc_to_tcpci(struct tcpc_dev *tcpc) +{ + return container_of(tcpc, struct tcpci, tcpc); +} + +static int tcpci_read16(struct tcpci *tcpci, unsigned int reg, u16 *val) +{ + return regmap_raw_read(tcpci->regmap, reg, val, sizeof(u16)); +} + +static int tcpci_write16(struct tcpci *tcpci, unsigned int reg, u16 val) +{ + return regmap_raw_write(tcpci->regmap, reg, &val, sizeof(u16)); +} + +static int tcpci_set_cc(struct tcpc_dev *tcpc, enum typec_cc_status cc) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + bool vconn_pres; + enum typec_cc_polarity polarity = TYPEC_POLARITY_CC1; + unsigned int reg; + int ret; + + ret = regmap_read(tcpci->regmap, TCPC_POWER_STATUS, ®); + if (ret < 0) + return ret; + + vconn_pres = !!(reg & TCPC_POWER_STATUS_VCONN_PRES); + if (vconn_pres) { + ret = regmap_read(tcpci->regmap, TCPC_TCPC_CTRL, ®); + if (ret < 0) + return ret; + + if (reg & TCPC_TCPC_CTRL_ORIENTATION) + polarity = TYPEC_POLARITY_CC2; + } + + switch (cc) { + case TYPEC_CC_RA: + reg = (TCPC_ROLE_CTRL_CC_RA << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RA << TCPC_ROLE_CTRL_CC2_SHIFT); + break; + case TYPEC_CC_RD: + reg = (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT); + break; + case TYPEC_CC_RP_DEF: + reg = (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT) | + (TCPC_ROLE_CTRL_RP_VAL_DEF << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_1_5: + reg = (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT) | + (TCPC_ROLE_CTRL_RP_VAL_1_5 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_3_0: + reg = (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT) | + (TCPC_ROLE_CTRL_RP_VAL_3_0 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_OPEN: + default: + reg = (TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC2_SHIFT); + break; + } + + if (vconn_pres) { + if (polarity == TYPEC_POLARITY_CC2) { + reg &= ~(TCPC_ROLE_CTRL_CC1_MASK << TCPC_ROLE_CTRL_CC1_SHIFT); + reg |= (TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC1_SHIFT); + } else { + reg &= ~(TCPC_ROLE_CTRL_CC2_MASK << TCPC_ROLE_CTRL_CC2_SHIFT); + reg |= (TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC2_SHIFT); + } + } + + ret = regmap_write(tcpci->regmap, TCPC_ROLE_CTRL, reg); + if (ret < 0) + return ret; + + return 0; +} + +static int tcpci_apply_rc(struct tcpc_dev *tcpc, enum typec_cc_status cc, + enum typec_cc_polarity polarity) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg; + int ret; + + ret = regmap_read(tcpci->regmap, TCPC_ROLE_CTRL, ®); + if (ret < 0) + return ret; + + /* + * APPLY_RC state is when ROLE_CONTROL.CC1 != ROLE_CONTROL.CC2 and vbus autodischarge on + * disconnect is disabled. Bail out when ROLE_CONTROL.CC1 != ROLE_CONTROL.CC2. + */ + if (((reg & (TCPC_ROLE_CTRL_CC2_MASK << TCPC_ROLE_CTRL_CC2_SHIFT)) >> + TCPC_ROLE_CTRL_CC2_SHIFT) != + ((reg & (TCPC_ROLE_CTRL_CC1_MASK << TCPC_ROLE_CTRL_CC1_SHIFT)) >> + TCPC_ROLE_CTRL_CC1_SHIFT)) + return 0; + + return regmap_update_bits(tcpci->regmap, TCPC_ROLE_CTRL, polarity == TYPEC_POLARITY_CC1 ? + TCPC_ROLE_CTRL_CC2_MASK << TCPC_ROLE_CTRL_CC2_SHIFT : + TCPC_ROLE_CTRL_CC1_MASK << TCPC_ROLE_CTRL_CC1_SHIFT, + TCPC_ROLE_CTRL_CC_OPEN); +} + +static int tcpci_start_toggling(struct tcpc_dev *tcpc, + enum typec_port_type port_type, + enum typec_cc_status cc) +{ + int ret; + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg = TCPC_ROLE_CTRL_DRP; + + if (port_type != TYPEC_PORT_DRP) + return -EOPNOTSUPP; + + /* Handle vendor drp toggling */ + if (tcpci->data->start_drp_toggling) { + ret = tcpci->data->start_drp_toggling(tcpci, tcpci->data, cc); + if (ret < 0) + return ret; + } + + switch (cc) { + default: + case TYPEC_CC_RP_DEF: + reg |= (TCPC_ROLE_CTRL_RP_VAL_DEF << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_1_5: + reg |= (TCPC_ROLE_CTRL_RP_VAL_1_5 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_3_0: + reg |= (TCPC_ROLE_CTRL_RP_VAL_3_0 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + } + + if (cc == TYPEC_CC_RD) + reg |= (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT); + else + reg |= (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT); + ret = regmap_write(tcpci->regmap, TCPC_ROLE_CTRL, reg); + if (ret < 0) + return ret; + return regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_LOOK4CONNECTION); +} + +static int tcpci_get_cc(struct tcpc_dev *tcpc, + enum typec_cc_status *cc1, enum typec_cc_status *cc2) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg, role_control; + int ret; + + ret = regmap_read(tcpci->regmap, TCPC_ROLE_CTRL, &role_control); + if (ret < 0) + return ret; + + ret = regmap_read(tcpci->regmap, TCPC_CC_STATUS, ®); + if (ret < 0) + return ret; + + *cc1 = tcpci_to_typec_cc((reg >> TCPC_CC_STATUS_CC1_SHIFT) & + TCPC_CC_STATUS_CC1_MASK, + reg & TCPC_CC_STATUS_TERM || + tcpc_presenting_rd(role_control, CC1)); + *cc2 = tcpci_to_typec_cc((reg >> TCPC_CC_STATUS_CC2_SHIFT) & + TCPC_CC_STATUS_CC2_MASK, + reg & TCPC_CC_STATUS_TERM || + tcpc_presenting_rd(role_control, CC2)); + + return 0; +} + +static int tcpci_set_polarity(struct tcpc_dev *tcpc, + enum typec_cc_polarity polarity) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg; + int ret; + enum typec_cc_status cc1, cc2; + + /* Obtain Rp setting from role control */ + ret = regmap_read(tcpci->regmap, TCPC_ROLE_CTRL, ®); + if (ret < 0) + return ret; + + ret = tcpci_get_cc(tcpc, &cc1, &cc2); + if (ret < 0) + return ret; + + /* + * When port has drp toggling enabled, ROLE_CONTROL would only have the initial + * terminations for the toggling and does not indicate the final cc + * terminations when ConnectionResult is 0 i.e. drp toggling stops and + * the connection is resolved. Infer port role from TCPC_CC_STATUS based on the + * terminations seen. The port role is then used to set the cc terminations. + */ + if (reg & TCPC_ROLE_CTRL_DRP) { + /* Disable DRP for the OPEN setting to take effect */ + reg = reg & ~TCPC_ROLE_CTRL_DRP; + + if (polarity == TYPEC_POLARITY_CC2) { + reg &= ~(TCPC_ROLE_CTRL_CC2_MASK << TCPC_ROLE_CTRL_CC2_SHIFT); + /* Local port is source */ + if (cc2 == TYPEC_CC_RD) + /* Role control would have the Rp setting when DRP was enabled */ + reg |= TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT; + else + reg |= TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT; + } else { + reg &= ~(TCPC_ROLE_CTRL_CC1_MASK << TCPC_ROLE_CTRL_CC1_SHIFT); + /* Local port is source */ + if (cc1 == TYPEC_CC_RD) + /* Role control would have the Rp setting when DRP was enabled */ + reg |= TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT; + else + reg |= TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT; + } + } + + if (polarity == TYPEC_POLARITY_CC2) + reg |= TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC1_SHIFT; + else + reg |= TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC2_SHIFT; + ret = regmap_write(tcpci->regmap, TCPC_ROLE_CTRL, reg); + if (ret < 0) + return ret; + + return regmap_write(tcpci->regmap, TCPC_TCPC_CTRL, + (polarity == TYPEC_POLARITY_CC2) ? + TCPC_TCPC_CTRL_ORIENTATION : 0); +} + +static void tcpci_set_partner_usb_comm_capable(struct tcpc_dev *tcpc, bool capable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + + if (tcpci->data->set_partner_usb_comm_capable) + tcpci->data->set_partner_usb_comm_capable(tcpci, tcpci->data, capable); +} + +static int tcpci_set_vconn(struct tcpc_dev *tcpc, bool enable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + int ret; + + /* Handle vendor set vconn */ + if (tcpci->data->set_vconn) { + ret = tcpci->data->set_vconn(tcpci, tcpci->data, enable); + if (ret < 0) + return ret; + } + + return regmap_update_bits(tcpci->regmap, TCPC_POWER_CTRL, + TCPC_POWER_CTRL_VCONN_ENABLE, + enable ? TCPC_POWER_CTRL_VCONN_ENABLE : 0); +} + +static int tcpci_enable_auto_vbus_discharge(struct tcpc_dev *dev, bool enable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(dev); + int ret; + + ret = regmap_update_bits(tcpci->regmap, TCPC_POWER_CTRL, TCPC_POWER_CTRL_AUTO_DISCHARGE, + enable ? TCPC_POWER_CTRL_AUTO_DISCHARGE : 0); + return ret; +} + +static int tcpci_set_auto_vbus_discharge_threshold(struct tcpc_dev *dev, enum typec_pwr_opmode mode, + bool pps_active, u32 requested_vbus_voltage_mv) +{ + struct tcpci *tcpci = tcpc_to_tcpci(dev); + unsigned int pwr_ctrl, threshold = 0; + int ret; + + /* + * Indicates that vbus is going to go away due PR_SWAP, hard reset etc. + * Do not discharge vbus here. + */ + if (requested_vbus_voltage_mv == 0) + goto write_thresh; + + ret = regmap_read(tcpci->regmap, TCPC_POWER_CTRL, &pwr_ctrl); + if (ret < 0) + return ret; + + if (pwr_ctrl & TCPC_FAST_ROLE_SWAP_EN) { + /* To prevent disconnect when the source is fast role swap is capable. */ + threshold = AUTO_DISCHARGE_DEFAULT_THRESHOLD_MV; + } else if (mode == TYPEC_PWR_MODE_PD) { + if (pps_active) + threshold = ((VPPS_NEW_MIN_PERCENT * requested_vbus_voltage_mv / 100) - + VSINKPD_MIN_IR_DROP_MV - VPPS_VALID_MIN_MV) * + VSINKDISCONNECT_PD_MIN_PERCENT / 100; + else + threshold = ((VSRC_NEW_MIN_PERCENT * requested_vbus_voltage_mv / 100) - + VSINKPD_MIN_IR_DROP_MV - VSRC_VALID_MIN_MV) * + VSINKDISCONNECT_PD_MIN_PERCENT / 100; + } else { + /* 3.5V for non-pd sink */ + threshold = AUTO_DISCHARGE_DEFAULT_THRESHOLD_MV; + } + + threshold = threshold / TCPC_VBUS_SINK_DISCONNECT_THRESH_LSB_MV; + + if (threshold > TCPC_VBUS_SINK_DISCONNECT_THRESH_MAX) + return -EINVAL; + +write_thresh: + return tcpci_write16(tcpci, TCPC_VBUS_SINK_DISCONNECT_THRESH, threshold); +} + +static int tcpci_enable_frs(struct tcpc_dev *dev, bool enable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(dev); + int ret; + + /* To prevent disconnect during FRS, set disconnect threshold to 3.5V */ + ret = tcpci_write16(tcpci, TCPC_VBUS_SINK_DISCONNECT_THRESH, enable ? 0 : 0x8c); + if (ret < 0) + return ret; + + ret = regmap_update_bits(tcpci->regmap, TCPC_POWER_CTRL, TCPC_FAST_ROLE_SWAP_EN, enable ? + TCPC_FAST_ROLE_SWAP_EN : 0); + + return ret; +} + +static void tcpci_frs_sourcing_vbus(struct tcpc_dev *dev) +{ + struct tcpci *tcpci = tcpc_to_tcpci(dev); + + if (tcpci->data->frs_sourcing_vbus) + tcpci->data->frs_sourcing_vbus(tcpci, tcpci->data); +} + +static void tcpci_check_contaminant(struct tcpc_dev *dev) +{ + struct tcpci *tcpci = tcpc_to_tcpci(dev); + + if (tcpci->data->check_contaminant) + tcpci->data->check_contaminant(tcpci, tcpci->data); +} + +static int tcpci_set_bist_data(struct tcpc_dev *tcpc, bool enable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + + return regmap_update_bits(tcpci->regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_BIST_TM, + enable ? TCPC_TCPC_CTRL_BIST_TM : 0); +} + +static int tcpci_set_roles(struct tcpc_dev *tcpc, bool attached, + enum typec_role role, enum typec_data_role data) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg; + int ret; + + reg = PD_REV20 << TCPC_MSG_HDR_INFO_REV_SHIFT; + if (role == TYPEC_SOURCE) + reg |= TCPC_MSG_HDR_INFO_PWR_ROLE; + if (data == TYPEC_HOST) + reg |= TCPC_MSG_HDR_INFO_DATA_ROLE; + ret = regmap_write(tcpci->regmap, TCPC_MSG_HDR_INFO, reg); + if (ret < 0) + return ret; + + return 0; +} + +static int tcpci_set_pd_rx(struct tcpc_dev *tcpc, bool enable) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg = 0; + int ret; + + if (enable) + reg = TCPC_RX_DETECT_SOP | TCPC_RX_DETECT_HARD_RESET; + ret = regmap_write(tcpci->regmap, TCPC_RX_DETECT, reg); + if (ret < 0) + return ret; + + return 0; +} + +static int tcpci_get_vbus(struct tcpc_dev *tcpc) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg; + int ret; + + ret = regmap_read(tcpci->regmap, TCPC_POWER_STATUS, ®); + if (ret < 0) + return ret; + + return !!(reg & TCPC_POWER_STATUS_VBUS_PRES); +} + +static bool tcpci_is_vbus_vsafe0v(struct tcpc_dev *tcpc) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned int reg; + int ret; + + ret = regmap_read(tcpci->regmap, TCPC_EXTENDED_STATUS, ®); + if (ret < 0) + return false; + + return !!(reg & TCPC_EXTENDED_STATUS_VSAFE0V); +} + +static int tcpci_set_vbus(struct tcpc_dev *tcpc, bool source, bool sink) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + int ret; + + if (tcpci->data->set_vbus) { + ret = tcpci->data->set_vbus(tcpci, tcpci->data, source, sink); + /* Bypass when ret > 0 */ + if (ret != 0) + return ret < 0 ? ret : 0; + } + + /* Disable both source and sink first before enabling anything */ + + if (!source) { + ret = regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_DISABLE_SRC_VBUS); + if (ret < 0) + return ret; + } + + if (!sink) { + ret = regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_DISABLE_SINK_VBUS); + if (ret < 0) + return ret; + } + + if (source) { + ret = regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_SRC_VBUS_DEFAULT); + if (ret < 0) + return ret; + } + + if (sink) { + ret = regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_SINK_VBUS); + if (ret < 0) + return ret; + } + + return 0; +} + +static int tcpci_pd_transmit(struct tcpc_dev *tcpc, enum tcpm_transmit_type type, + const struct pd_message *msg, unsigned int negotiated_rev) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + u16 header = msg ? le16_to_cpu(msg->header) : 0; + unsigned int reg, cnt; + int ret; + + cnt = msg ? pd_header_cnt(header) * 4 : 0; + /** + * TCPCI spec forbids direct access of TCPC_TX_DATA. + * But, since some of the chipsets offer this capability, + * it's fair to support both. + */ + if (tcpci->data->TX_BUF_BYTE_x_hidden) { + u8 buf[TCPC_TRANSMIT_BUFFER_MAX_LEN] = {0,}; + u8 pos = 0; + + /* Payload + header + TCPC_TX_BYTE_CNT */ + buf[pos++] = cnt + 2; + + if (msg) + memcpy(&buf[pos], &msg->header, sizeof(msg->header)); + + pos += sizeof(header); + + if (cnt > 0) + memcpy(&buf[pos], msg->payload, cnt); + + pos += cnt; + ret = regmap_raw_write(tcpci->regmap, TCPC_TX_BYTE_CNT, buf, pos); + if (ret < 0) + return ret; + } else { + ret = regmap_write(tcpci->regmap, TCPC_TX_BYTE_CNT, cnt + 2); + if (ret < 0) + return ret; + + ret = tcpci_write16(tcpci, TCPC_TX_HDR, header); + if (ret < 0) + return ret; + + if (cnt > 0) { + ret = regmap_raw_write(tcpci->regmap, TCPC_TX_DATA, &msg->payload, cnt); + if (ret < 0) + return ret; + } + } + + /* nRetryCount is 3 in PD2.0 spec where 2 in PD3.0 spec */ + reg = ((negotiated_rev > PD_REV20 ? PD_RETRY_COUNT_3_0_OR_HIGHER : PD_RETRY_COUNT_DEFAULT) + << TCPC_TRANSMIT_RETRY_SHIFT) | (type << TCPC_TRANSMIT_TYPE_SHIFT); + ret = regmap_write(tcpci->regmap, TCPC_TRANSMIT, reg); + if (ret < 0) + return ret; + + return 0; +} + +static int tcpci_init(struct tcpc_dev *tcpc) +{ + struct tcpci *tcpci = tcpc_to_tcpci(tcpc); + unsigned long timeout = jiffies + msecs_to_jiffies(2000); /* XXX */ + unsigned int reg; + int ret; + + while (time_before_eq(jiffies, timeout)) { + ret = regmap_read(tcpci->regmap, TCPC_POWER_STATUS, ®); + if (ret < 0) + return ret; + if (!(reg & TCPC_POWER_STATUS_UNINIT)) + break; + usleep_range(10000, 20000); + } + if (time_after(jiffies, timeout)) + return -ETIMEDOUT; + + ret = tcpci_write16(tcpci, TCPC_FAULT_STATUS, TCPC_FAULT_STATUS_ALL_REG_RST_TO_DEFAULT); + if (ret < 0) + return ret; + + /* Handle vendor init */ + if (tcpci->data->init) { + ret = tcpci->data->init(tcpci, tcpci->data); + if (ret < 0) + return ret; + } + + /* Clear all events */ + ret = tcpci_write16(tcpci, TCPC_ALERT, 0xffff); + if (ret < 0) + return ret; + + if (tcpci->controls_vbus) + reg = TCPC_POWER_STATUS_VBUS_PRES; + else + reg = 0; + ret = regmap_write(tcpci->regmap, TCPC_POWER_STATUS_MASK, reg); + if (ret < 0) + return ret; + + /* Enable Vbus detection */ + ret = regmap_write(tcpci->regmap, TCPC_COMMAND, + TCPC_CMD_ENABLE_VBUS_DETECT); + if (ret < 0) + return ret; + + reg = TCPC_ALERT_TX_SUCCESS | TCPC_ALERT_TX_FAILED | + TCPC_ALERT_TX_DISCARDED | TCPC_ALERT_RX_STATUS | + TCPC_ALERT_RX_HARD_RST | TCPC_ALERT_CC_STATUS; + if (tcpci->controls_vbus) + reg |= TCPC_ALERT_POWER_STATUS; + /* Enable VSAFE0V status interrupt when detecting VSAFE0V is supported */ + if (tcpci->data->vbus_vsafe0v) { + reg |= TCPC_ALERT_EXTENDED_STATUS; + ret = regmap_write(tcpci->regmap, TCPC_EXTENDED_STATUS_MASK, + TCPC_EXTENDED_STATUS_VSAFE0V); + if (ret < 0) + return ret; + } + + tcpci->alert_mask = reg; + + return tcpci_write16(tcpci, TCPC_ALERT_MASK, reg); +} + +irqreturn_t tcpci_irq(struct tcpci *tcpci) +{ + u16 status; + int ret; + unsigned int raw; + + tcpci_read16(tcpci, TCPC_ALERT, &status); + + /* + * Clear alert status for everything except RX_STATUS, which shouldn't + * be cleared until we have successfully retrieved message. + */ + if (status & ~TCPC_ALERT_RX_STATUS) + tcpci_write16(tcpci, TCPC_ALERT, + status & ~TCPC_ALERT_RX_STATUS); + + if (status & TCPC_ALERT_CC_STATUS) + tcpm_cc_change(tcpci->port); + + if (status & TCPC_ALERT_POWER_STATUS) { + regmap_read(tcpci->regmap, TCPC_POWER_STATUS_MASK, &raw); + /* + * If power status mask has been reset, then the TCPC + * has reset. + */ + if (raw == 0xff) + tcpm_tcpc_reset(tcpci->port); + else + tcpm_vbus_change(tcpci->port); + } + + if (status & TCPC_ALERT_RX_STATUS) { + struct pd_message msg; + unsigned int cnt, payload_cnt; + u16 header; + + regmap_read(tcpci->regmap, TCPC_RX_BYTE_CNT, &cnt); + /* + * 'cnt' corresponds to READABLE_BYTE_COUNT in section 4.4.14 + * of the TCPCI spec [Rev 2.0 Ver 1.0 October 2017] and is + * defined in table 4-36 as one greater than the number of + * bytes received. And that number includes the header. So: + */ + if (cnt > 3) + payload_cnt = cnt - (1 + sizeof(msg.header)); + else + payload_cnt = 0; + + tcpci_read16(tcpci, TCPC_RX_HDR, &header); + msg.header = cpu_to_le16(header); + + if (WARN_ON(payload_cnt > sizeof(msg.payload))) + payload_cnt = sizeof(msg.payload); + + if (payload_cnt > 0) + regmap_raw_read(tcpci->regmap, TCPC_RX_DATA, + &msg.payload, payload_cnt); + + /* Read complete, clear RX status alert bit */ + tcpci_write16(tcpci, TCPC_ALERT, TCPC_ALERT_RX_STATUS); + + tcpm_pd_receive(tcpci->port, &msg); + } + + if (tcpci->data->vbus_vsafe0v && (status & TCPC_ALERT_EXTENDED_STATUS)) { + ret = regmap_read(tcpci->regmap, TCPC_EXTENDED_STATUS, &raw); + if (!ret && (raw & TCPC_EXTENDED_STATUS_VSAFE0V)) + tcpm_vbus_change(tcpci->port); + } + + if (status & TCPC_ALERT_RX_HARD_RST) + tcpm_pd_hard_reset(tcpci->port); + + if (status & TCPC_ALERT_TX_SUCCESS) + tcpm_pd_transmit_complete(tcpci->port, TCPC_TX_SUCCESS); + else if (status & TCPC_ALERT_TX_DISCARDED) + tcpm_pd_transmit_complete(tcpci->port, TCPC_TX_DISCARDED); + else if (status & TCPC_ALERT_TX_FAILED) + tcpm_pd_transmit_complete(tcpci->port, TCPC_TX_FAILED); + + return IRQ_RETVAL(status & tcpci->alert_mask); +} +EXPORT_SYMBOL_GPL(tcpci_irq); + +static irqreturn_t _tcpci_irq(int irq, void *dev_id) +{ + struct tcpci_chip *chip = dev_id; + + return tcpci_irq(chip->tcpci); +} + +static const struct regmap_config tcpci_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = 0x7F, /* 0x80 .. 0xFF are vendor defined */ +}; + +static int tcpci_parse_config(struct tcpci *tcpci) +{ + tcpci->controls_vbus = true; /* XXX */ + + tcpci->tcpc.fwnode = device_get_named_child_node(tcpci->dev, + "connector"); + if (!tcpci->tcpc.fwnode) { + dev_err(tcpci->dev, "Can't find connector node.\n"); + return -EINVAL; + } + + return 0; +} + +struct tcpci *tcpci_register_port(struct device *dev, struct tcpci_data *data) +{ + struct tcpci *tcpci; + int err; + + tcpci = devm_kzalloc(dev, sizeof(*tcpci), GFP_KERNEL); + if (!tcpci) + return ERR_PTR(-ENOMEM); + + tcpci->dev = dev; + tcpci->data = data; + tcpci->regmap = data->regmap; + + tcpci->tcpc.init = tcpci_init; + tcpci->tcpc.get_vbus = tcpci_get_vbus; + tcpci->tcpc.set_vbus = tcpci_set_vbus; + tcpci->tcpc.set_cc = tcpci_set_cc; + tcpci->tcpc.apply_rc = tcpci_apply_rc; + tcpci->tcpc.get_cc = tcpci_get_cc; + tcpci->tcpc.set_polarity = tcpci_set_polarity; + tcpci->tcpc.set_vconn = tcpci_set_vconn; + tcpci->tcpc.start_toggling = tcpci_start_toggling; + + tcpci->tcpc.set_pd_rx = tcpci_set_pd_rx; + tcpci->tcpc.set_roles = tcpci_set_roles; + tcpci->tcpc.pd_transmit = tcpci_pd_transmit; + tcpci->tcpc.set_bist_data = tcpci_set_bist_data; + tcpci->tcpc.enable_frs = tcpci_enable_frs; + tcpci->tcpc.frs_sourcing_vbus = tcpci_frs_sourcing_vbus; + tcpci->tcpc.set_partner_usb_comm_capable = tcpci_set_partner_usb_comm_capable; + + if (tcpci->data->check_contaminant) + tcpci->tcpc.check_contaminant = tcpci_check_contaminant; + + if (tcpci->data->auto_discharge_disconnect) { + tcpci->tcpc.enable_auto_vbus_discharge = tcpci_enable_auto_vbus_discharge; + tcpci->tcpc.set_auto_vbus_discharge_threshold = + tcpci_set_auto_vbus_discharge_threshold; + regmap_update_bits(tcpci->regmap, TCPC_POWER_CTRL, TCPC_POWER_CTRL_BLEED_DISCHARGE, + TCPC_POWER_CTRL_BLEED_DISCHARGE); + } + + if (tcpci->data->vbus_vsafe0v) + tcpci->tcpc.is_vbus_vsafe0v = tcpci_is_vbus_vsafe0v; + + err = tcpci_parse_config(tcpci); + if (err < 0) + return ERR_PTR(err); + + tcpci->port = tcpm_register_port(tcpci->dev, &tcpci->tcpc); + if (IS_ERR(tcpci->port)) { + fwnode_handle_put(tcpci->tcpc.fwnode); + return ERR_CAST(tcpci->port); + } + + return tcpci; +} +EXPORT_SYMBOL_GPL(tcpci_register_port); + +void tcpci_unregister_port(struct tcpci *tcpci) +{ + tcpm_unregister_port(tcpci->port); + fwnode_handle_put(tcpci->tcpc.fwnode); +} +EXPORT_SYMBOL_GPL(tcpci_unregister_port); + +static int tcpci_probe(struct i2c_client *client) +{ + struct tcpci_chip *chip; + int err; + u16 val = 0; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->data.regmap = devm_regmap_init_i2c(client, &tcpci_regmap_config); + if (IS_ERR(chip->data.regmap)) + return PTR_ERR(chip->data.regmap); + + i2c_set_clientdata(client, chip); + + /* Disable chip interrupts before requesting irq */ + err = regmap_raw_write(chip->data.regmap, TCPC_ALERT_MASK, &val, + sizeof(u16)); + if (err < 0) + return err; + + chip->tcpci = tcpci_register_port(&client->dev, &chip->data); + if (IS_ERR(chip->tcpci)) + return PTR_ERR(chip->tcpci); + + err = devm_request_threaded_irq(&client->dev, client->irq, NULL, + _tcpci_irq, + IRQF_SHARED | IRQF_ONESHOT | IRQF_TRIGGER_LOW, + dev_name(&client->dev), chip); + if (err < 0) { + tcpci_unregister_port(chip->tcpci); + return err; + } + + return 0; +} + +static void tcpci_remove(struct i2c_client *client) +{ + struct tcpci_chip *chip = i2c_get_clientdata(client); + int err; + + /* Disable chip interrupts before unregistering port */ + err = tcpci_write16(chip->tcpci, TCPC_ALERT_MASK, 0); + if (err < 0) + dev_warn(&client->dev, "Failed to disable irqs (%pe)\n", ERR_PTR(err)); + + tcpci_unregister_port(chip->tcpci); +} + +static const struct i2c_device_id tcpci_id[] = { + { "tcpci", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tcpci_id); + +#ifdef CONFIG_OF +static const struct of_device_id tcpci_of_match[] = { + { .compatible = "nxp,ptn5110", }, + {}, +}; +MODULE_DEVICE_TABLE(of, tcpci_of_match); +#endif + +static struct i2c_driver tcpci_i2c_driver = { + .driver = { + .name = "tcpci", + .of_match_table = of_match_ptr(tcpci_of_match), + }, + .probe = tcpci_probe, + .remove = tcpci_remove, + .id_table = tcpci_id, +}; +module_i2c_driver(tcpci_i2c_driver); + +MODULE_DESCRIPTION("USB Type-C Port Controller Interface driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/tcpci_maxim.h b/drivers/usb/typec/tcpm/tcpci_maxim.h new file mode 100644 index 0000000000..2c1c4d161b --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci_maxim.h @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Copyright 2022 Google, Inc + * + * MAXIM TCPC header file. + */ +#ifndef TCPCI_MAXIM_H_ +#define TCPCI_MAXIM_H_ + +#define VENDOR_CC_STATUS2 0x85 +#define CC1_VUFP_RD0P5 BIT(1) +#define CC2_VUFP_RD0P5 BIT(5) +#define TCPC_VENDOR_FLADC_STATUS 0x89 + +#define TCPC_VENDOR_CC_CTRL1 0x8c +#define CCCONNDRY BIT(7) +#define CCCOMPEN BIT(5) + +#define TCPC_VENDOR_CC_CTRL2 0x8d +#define SBUOVPDIS BIT(7) +#define CCOVPDIS BIT(6) +#define SBURPCTRL BIT(5) +#define CCLPMODESEL_MASK GENMASK(4, 3) +#define ULTRA_LOW_POWER_MODE BIT(3) +#define CCRPCTRL_MASK GENMASK(2, 0) +#define UA_1_SRC 1 +#define UA_80_SRC 3 + +#define TCPC_VENDOR_CC_CTRL3 0x8e +#define CCWTRDEB_MASK GENMASK(7, 6) +#define CCWTRDEB_SHIFT 6 +#define CCWTRDEB_1MS 1 +#define CCWTRSEL_MASK GENMASK(5, 3) +#define CCWTRSEL_SHIFT 3 +#define CCWTRSEL_1V 0x4 +#define CCLADDERDIS BIT(2) +#define WTRCYCLE_MASK BIT(0) +#define WTRCYCLE_SHIFT 0 +#define WTRCYCLE_2_4_S 0 +#define WTRCYCLE_4_8_S 1 + +#define TCPC_VENDOR_ADC_CTRL1 0x91 +#define ADCINSEL_MASK GENMASK(7, 5) +#define ADC_CHANNEL_OFFSET 5 +#define ADCEN BIT(0) + +enum contamiant_state { + NOT_DETECTED, + DETECTED, + SINK, +}; + +/* + * @potential_contaminant: + * Last returned result to tcpm indicating whether the TCPM port + * has potential contaminant. + */ +struct max_tcpci_chip { + struct tcpci_data data; + struct tcpci *tcpci; + struct device *dev; + struct i2c_client *client; + struct tcpm_port *port; + enum contamiant_state contaminant_state; +}; + +static inline int max_tcpci_read16(struct max_tcpci_chip *chip, unsigned int reg, u16 *val) +{ + return regmap_raw_read(chip->data.regmap, reg, val, sizeof(u16)); +} + +static inline int max_tcpci_write16(struct max_tcpci_chip *chip, unsigned int reg, u16 val) +{ + return regmap_raw_write(chip->data.regmap, reg, &val, sizeof(u16)); +} + +static inline int max_tcpci_read8(struct max_tcpci_chip *chip, unsigned int reg, u8 *val) +{ + return regmap_raw_read(chip->data.regmap, reg, val, sizeof(u8)); +} + +static inline int max_tcpci_write8(struct max_tcpci_chip *chip, unsigned int reg, u8 val) +{ + return regmap_raw_write(chip->data.regmap, reg, &val, sizeof(u8)); +} + +bool max_contaminant_is_contaminant(struct max_tcpci_chip *chip, bool disconnect_while_debounce); + +#endif // TCPCI_MAXIM_H_ diff --git a/drivers/usb/typec/tcpm/tcpci_maxim_core.c b/drivers/usb/typec/tcpm/tcpci_maxim_core.c new file mode 100644 index 0000000000..9454b12a07 --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci_maxim_core.c @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2020 - 2022, Google LLC + * + * MAXIM TCPCI based TCPC driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tcpci_maxim.h" + +#define PD_ACTIVITY_TIMEOUT_MS 10000 + +#define TCPC_VENDOR_ALERT 0x80 +#define TCPC_VENDOR_USBSW_CTRL 0x93 +#define TCPC_VENDOR_USBSW_CTRL_ENABLE_USB_DATA 0x9 +#define TCPC_VENDOR_USBSW_CTRL_DISABLE_USB_DATA 0 + +#define TCPC_RECEIVE_BUFFER_COUNT_OFFSET 0 +#define TCPC_RECEIVE_BUFFER_FRAME_TYPE_OFFSET 1 +#define TCPC_RECEIVE_BUFFER_RX_BYTE_BUF_OFFSET 2 + +/* + * LongMessage not supported, hence 32 bytes for buf to be read from RECEIVE_BUFFER. + * DEVICE_CAPABILITIES_2.LongMessage = 0, the value in READABLE_BYTE_COUNT reg shall be + * less than or equal to 31. Since, RECEIVE_BUFFER len = 31 + 1(READABLE_BYTE_COUNT). + */ +#define TCPC_RECEIVE_BUFFER_LEN 32 + +#define MAX_BUCK_BOOST_SID 0x69 +#define MAX_BUCK_BOOST_OP 0xb9 +#define MAX_BUCK_BOOST_OFF 0 +#define MAX_BUCK_BOOST_SOURCE 0xa +#define MAX_BUCK_BOOST_SINK 0x5 + +static const struct regmap_range max_tcpci_tcpci_range[] = { + regmap_reg_range(0x00, 0x95) +}; + +static const struct regmap_access_table max_tcpci_tcpci_write_table = { + .yes_ranges = max_tcpci_tcpci_range, + .n_yes_ranges = ARRAY_SIZE(max_tcpci_tcpci_range), +}; + +static const struct regmap_config max_tcpci_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0x95, + .wr_table = &max_tcpci_tcpci_write_table, +}; + +static struct max_tcpci_chip *tdata_to_max_tcpci(struct tcpci_data *tdata) +{ + return container_of(tdata, struct max_tcpci_chip, data); +} + +static void max_tcpci_init_regs(struct max_tcpci_chip *chip) +{ + u16 alert_mask = 0; + int ret; + + ret = max_tcpci_write16(chip, TCPC_ALERT, 0xffff); + if (ret < 0) { + dev_err(chip->dev, "Error writing to TCPC_ALERT ret:%d\n", ret); + return; + } + + ret = max_tcpci_write16(chip, TCPC_VENDOR_ALERT, 0xffff); + if (ret < 0) { + dev_err(chip->dev, "Error writing to TCPC_VENDOR_ALERT ret:%d\n", ret); + return; + } + + ret = max_tcpci_write8(chip, TCPC_ALERT_EXTENDED, 0xff); + if (ret < 0) { + dev_err(chip->dev, "Unable to clear TCPC_ALERT_EXTENDED ret:%d\n", ret); + return; + } + + /* Enable VSAFE0V detection */ + ret = max_tcpci_write8(chip, TCPC_EXTENDED_STATUS_MASK, TCPC_EXTENDED_STATUS_VSAFE0V); + if (ret < 0) { + dev_err(chip->dev, "Unable to unmask TCPC_EXTENDED_STATUS_VSAFE0V ret:%d\n", ret); + return; + } + + alert_mask = TCPC_ALERT_TX_SUCCESS | TCPC_ALERT_TX_DISCARDED | TCPC_ALERT_TX_FAILED | + TCPC_ALERT_RX_HARD_RST | TCPC_ALERT_RX_STATUS | TCPC_ALERT_CC_STATUS | + TCPC_ALERT_VBUS_DISCNCT | TCPC_ALERT_RX_BUF_OVF | TCPC_ALERT_POWER_STATUS | + /* Enable Extended alert for detecting Fast Role Swap Signal */ + TCPC_ALERT_EXTND | TCPC_ALERT_EXTENDED_STATUS; + + ret = max_tcpci_write16(chip, TCPC_ALERT_MASK, alert_mask); + if (ret < 0) { + dev_err(chip->dev, + "Error enabling TCPC_ALERT: TCPC_ALERT_MASK write failed ret:%d\n", ret); + return; + } + + /* Enable vbus voltage monitoring and voltage alerts */ + ret = max_tcpci_write8(chip, TCPC_POWER_CTRL, 0); + if (ret < 0) { + dev_err(chip->dev, "Error writing to TCPC_POWER_CTRL ret:%d\n", ret); + return; + } + + ret = max_tcpci_write8(chip, TCPC_ALERT_EXTENDED_MASK, TCPC_SINK_FAST_ROLE_SWAP); + if (ret < 0) + return; +} + +static void process_rx(struct max_tcpci_chip *chip, u16 status) +{ + struct pd_message msg; + u8 count, frame_type, rx_buf[TCPC_RECEIVE_BUFFER_LEN]; + int ret, payload_index; + u8 *rx_buf_ptr; + + /* + * READABLE_BYTE_COUNT: Indicates the number of bytes in the RX_BUF_BYTE_x registers + * plus one (for the RX_BUF_FRAME_TYPE) Table 4-36. + * Read the count and frame type. + */ + ret = regmap_raw_read(chip->data.regmap, TCPC_RX_BYTE_CNT, rx_buf, 2); + if (ret < 0) { + dev_err(chip->dev, "TCPC_RX_BYTE_CNT read failed ret:%d\n", ret); + return; + } + + count = rx_buf[TCPC_RECEIVE_BUFFER_COUNT_OFFSET]; + frame_type = rx_buf[TCPC_RECEIVE_BUFFER_FRAME_TYPE_OFFSET]; + + if (count == 0 || frame_type != TCPC_RX_BUF_FRAME_TYPE_SOP) { + max_tcpci_write16(chip, TCPC_ALERT, TCPC_ALERT_RX_STATUS); + dev_err(chip->dev, "%s\n", count == 0 ? "error: count is 0" : + "error frame_type is not SOP"); + return; + } + + if (count > sizeof(struct pd_message) || count + 1 > TCPC_RECEIVE_BUFFER_LEN) { + dev_err(chip->dev, "Invalid TCPC_RX_BYTE_CNT %d\n", count); + return; + } + + /* + * Read count + 1 as RX_BUF_BYTE_x is hidden and can only be read through + * TCPC_RX_BYTE_CNT + */ + count += 1; + ret = regmap_raw_read(chip->data.regmap, TCPC_RX_BYTE_CNT, rx_buf, count); + if (ret < 0) { + dev_err(chip->dev, "Error: TCPC_RX_BYTE_CNT read failed: %d\n", ret); + return; + } + + rx_buf_ptr = rx_buf + TCPC_RECEIVE_BUFFER_RX_BYTE_BUF_OFFSET; + msg.header = cpu_to_le16(*(u16 *)rx_buf_ptr); + rx_buf_ptr = rx_buf_ptr + sizeof(msg.header); + for (payload_index = 0; payload_index < pd_header_cnt_le(msg.header); payload_index++, + rx_buf_ptr += sizeof(msg.payload[0])) + msg.payload[payload_index] = cpu_to_le32(*(u32 *)rx_buf_ptr); + + /* + * Read complete, clear RX status alert bit. + * Clear overflow as well if set. + */ + ret = max_tcpci_write16(chip, TCPC_ALERT, status & TCPC_ALERT_RX_BUF_OVF ? + TCPC_ALERT_RX_STATUS | TCPC_ALERT_RX_BUF_OVF : + TCPC_ALERT_RX_STATUS); + if (ret < 0) + return; + + tcpm_pd_receive(chip->port, &msg); +} + +static int max_tcpci_set_vbus(struct tcpci *tcpci, struct tcpci_data *tdata, bool source, bool sink) +{ + struct max_tcpci_chip *chip = tdata_to_max_tcpci(tdata); + u8 buffer_source[2] = {MAX_BUCK_BOOST_OP, MAX_BUCK_BOOST_SOURCE}; + u8 buffer_sink[2] = {MAX_BUCK_BOOST_OP, MAX_BUCK_BOOST_SINK}; + u8 buffer_none[2] = {MAX_BUCK_BOOST_OP, MAX_BUCK_BOOST_OFF}; + struct i2c_client *i2c = chip->client; + int ret; + + struct i2c_msg msgs[] = { + { + .addr = MAX_BUCK_BOOST_SID, + .flags = i2c->flags & I2C_M_TEN, + .len = 2, + .buf = source ? buffer_source : sink ? buffer_sink : buffer_none, + }, + }; + + if (source && sink) { + dev_err(chip->dev, "Both source and sink set\n"); + return -EINVAL; + } + + ret = i2c_transfer(i2c->adapter, msgs, 1); + + return ret < 0 ? ret : 1; +} + +static void process_power_status(struct max_tcpci_chip *chip) +{ + u8 pwr_status; + int ret; + + ret = max_tcpci_read8(chip, TCPC_POWER_STATUS, &pwr_status); + if (ret < 0) + return; + + if (pwr_status == 0xff) + max_tcpci_init_regs(chip); + else if (pwr_status & TCPC_POWER_STATUS_SOURCING_VBUS) + tcpm_sourcing_vbus(chip->port); + else + tcpm_vbus_change(chip->port); +} + +static void max_tcpci_frs_sourcing_vbus(struct tcpci *tcpci, struct tcpci_data *tdata) +{ + /* + * For Fast Role Swap case, Boost turns on autonomously without + * AP intervention, but, needs AP to enable source mode explicitly + * for AP to regain control. + */ + max_tcpci_set_vbus(tcpci, tdata, true, false); +} + +static void process_tx(struct max_tcpci_chip *chip, u16 status) +{ + if (status & TCPC_ALERT_TX_SUCCESS) + tcpm_pd_transmit_complete(chip->port, TCPC_TX_SUCCESS); + else if (status & TCPC_ALERT_TX_DISCARDED) + tcpm_pd_transmit_complete(chip->port, TCPC_TX_DISCARDED); + else if (status & TCPC_ALERT_TX_FAILED) + tcpm_pd_transmit_complete(chip->port, TCPC_TX_FAILED); + + /* Reinit regs as Hard reset sets them to default value */ + if ((status & TCPC_ALERT_TX_SUCCESS) && (status & TCPC_ALERT_TX_FAILED)) + max_tcpci_init_regs(chip); +} + +/* Enable USB switches when partner is USB communications capable */ +static void max_tcpci_set_partner_usb_comm_capable(struct tcpci *tcpci, struct tcpci_data *data, + bool capable) +{ + struct max_tcpci_chip *chip = tdata_to_max_tcpci(data); + int ret; + + ret = max_tcpci_write8(chip, TCPC_VENDOR_USBSW_CTRL, capable ? + TCPC_VENDOR_USBSW_CTRL_ENABLE_USB_DATA : + TCPC_VENDOR_USBSW_CTRL_DISABLE_USB_DATA); + + if (ret < 0) + dev_err(chip->dev, "Failed to enable USB switches"); +} + +static irqreturn_t _max_tcpci_irq(struct max_tcpci_chip *chip, u16 status) +{ + u16 mask; + int ret; + u8 reg_status; + + /* + * Clear alert status for everything except RX_STATUS, which shouldn't + * be cleared until we have successfully retrieved message. + */ + if (status & ~TCPC_ALERT_RX_STATUS) { + mask = status & TCPC_ALERT_RX_BUF_OVF ? + status & ~(TCPC_ALERT_RX_STATUS | TCPC_ALERT_RX_BUF_OVF) : + status & ~TCPC_ALERT_RX_STATUS; + ret = max_tcpci_write16(chip, TCPC_ALERT, mask); + if (ret < 0) { + dev_err(chip->dev, "ALERT clear failed\n"); + return ret; + } + } + + if (status & TCPC_ALERT_RX_BUF_OVF && !(status & TCPC_ALERT_RX_STATUS)) { + ret = max_tcpci_write16(chip, TCPC_ALERT, (TCPC_ALERT_RX_STATUS | + TCPC_ALERT_RX_BUF_OVF)); + if (ret < 0) { + dev_err(chip->dev, "ALERT clear failed\n"); + return ret; + } + } + + if (status & TCPC_ALERT_EXTND) { + ret = max_tcpci_read8(chip, TCPC_ALERT_EXTENDED, ®_status); + if (ret < 0) + return ret; + + ret = max_tcpci_write8(chip, TCPC_ALERT_EXTENDED, reg_status); + if (ret < 0) + return ret; + + if (reg_status & TCPC_SINK_FAST_ROLE_SWAP) { + dev_info(chip->dev, "FRS Signal\n"); + tcpm_sink_frs(chip->port); + } + } + + if (status & TCPC_ALERT_EXTENDED_STATUS) { + ret = max_tcpci_read8(chip, TCPC_EXTENDED_STATUS, (u8 *)®_status); + if (ret >= 0 && (reg_status & TCPC_EXTENDED_STATUS_VSAFE0V)) + tcpm_vbus_change(chip->port); + } + + if (status & TCPC_ALERT_RX_STATUS) + process_rx(chip, status); + + if (status & TCPC_ALERT_VBUS_DISCNCT) + tcpm_vbus_change(chip->port); + + if (status & TCPC_ALERT_CC_STATUS) { + if (chip->contaminant_state == DETECTED || tcpm_port_is_toggling(chip->port)) { + if (!max_contaminant_is_contaminant(chip, false)) + tcpm_port_clean(chip->port); + } else { + tcpm_cc_change(chip->port); + } + } + + if (status & TCPC_ALERT_POWER_STATUS) + process_power_status(chip); + + if (status & TCPC_ALERT_RX_HARD_RST) { + tcpm_pd_hard_reset(chip->port); + max_tcpci_init_regs(chip); + } + + if (status & TCPC_ALERT_TX_SUCCESS || status & TCPC_ALERT_TX_DISCARDED || status & + TCPC_ALERT_TX_FAILED) + process_tx(chip, status); + + return IRQ_HANDLED; +} + +static irqreturn_t max_tcpci_irq(int irq, void *dev_id) +{ + struct max_tcpci_chip *chip = dev_id; + u16 status; + irqreturn_t irq_return = IRQ_HANDLED; + int ret; + + if (!chip->port) + return IRQ_HANDLED; + + ret = max_tcpci_read16(chip, TCPC_ALERT, &status); + if (ret < 0) { + dev_err(chip->dev, "ALERT read failed\n"); + return ret; + } + while (status) { + irq_return = _max_tcpci_irq(chip, status); + /* Do not return if the ALERT is already set. */ + ret = max_tcpci_read16(chip, TCPC_ALERT, &status); + if (ret < 0) + break; + } + + return irq_return; +} + +static irqreturn_t max_tcpci_isr(int irq, void *dev_id) +{ + struct max_tcpci_chip *chip = dev_id; + + pm_wakeup_event(chip->dev, PD_ACTIVITY_TIMEOUT_MS); + + if (!chip->port) + return IRQ_HANDLED; + + return IRQ_WAKE_THREAD; +} + +static int max_tcpci_init_alert(struct max_tcpci_chip *chip, struct i2c_client *client) +{ + int ret; + + ret = devm_request_threaded_irq(chip->dev, client->irq, max_tcpci_isr, max_tcpci_irq, + (IRQF_TRIGGER_LOW | IRQF_ONESHOT), dev_name(chip->dev), + chip); + + if (ret < 0) + return ret; + + enable_irq_wake(client->irq); + return 0; +} + +static int max_tcpci_start_toggling(struct tcpci *tcpci, struct tcpci_data *tdata, + enum typec_cc_status cc) +{ + struct max_tcpci_chip *chip = tdata_to_max_tcpci(tdata); + + max_tcpci_init_regs(chip); + + return 0; +} + +static int tcpci_init(struct tcpci *tcpci, struct tcpci_data *data) +{ + /* + * Generic TCPCI overwrites the regs once this driver initializes + * them. Prevent this by returning -1. + */ + return -1; +} + +static void max_tcpci_check_contaminant(struct tcpci *tcpci, struct tcpci_data *tdata) +{ + struct max_tcpci_chip *chip = tdata_to_max_tcpci(tdata); + + if (!max_contaminant_is_contaminant(chip, true)) + tcpm_port_clean(chip->port); +} + +static int max_tcpci_probe(struct i2c_client *client) +{ + int ret; + struct max_tcpci_chip *chip; + u8 power_status; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->client = client; + chip->data.regmap = devm_regmap_init_i2c(client, &max_tcpci_regmap_config); + if (IS_ERR(chip->data.regmap)) { + dev_err(&client->dev, "Regmap init failed\n"); + return PTR_ERR(chip->data.regmap); + } + + chip->dev = &client->dev; + i2c_set_clientdata(client, chip); + + ret = max_tcpci_read8(chip, TCPC_POWER_STATUS, &power_status); + if (ret < 0) + return ret; + + /* Chip level tcpci callbacks */ + chip->data.set_vbus = max_tcpci_set_vbus; + chip->data.start_drp_toggling = max_tcpci_start_toggling; + chip->data.TX_BUF_BYTE_x_hidden = true; + chip->data.init = tcpci_init; + chip->data.frs_sourcing_vbus = max_tcpci_frs_sourcing_vbus; + chip->data.auto_discharge_disconnect = true; + chip->data.vbus_vsafe0v = true; + chip->data.set_partner_usb_comm_capable = max_tcpci_set_partner_usb_comm_capable; + chip->data.check_contaminant = max_tcpci_check_contaminant; + + max_tcpci_init_regs(chip); + chip->tcpci = tcpci_register_port(chip->dev, &chip->data); + if (IS_ERR(chip->tcpci)) { + dev_err(&client->dev, "TCPCI port registration failed\n"); + return PTR_ERR(chip->tcpci); + } + chip->port = tcpci_get_tcpm_port(chip->tcpci); + ret = max_tcpci_init_alert(chip, client); + if (ret < 0) + goto unreg_port; + + device_init_wakeup(chip->dev, true); + return 0; + +unreg_port: + tcpci_unregister_port(chip->tcpci); + + return ret; +} + +static void max_tcpci_remove(struct i2c_client *client) +{ + struct max_tcpci_chip *chip = i2c_get_clientdata(client); + + if (!IS_ERR_OR_NULL(chip->tcpci)) + tcpci_unregister_port(chip->tcpci); +} + +static const struct i2c_device_id max_tcpci_id[] = { + { "maxtcpc", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max_tcpci_id); + +#ifdef CONFIG_OF +static const struct of_device_id max_tcpci_of_match[] = { + { .compatible = "maxim,max33359", }, + {}, +}; +MODULE_DEVICE_TABLE(of, max_tcpci_of_match); +#endif + +static struct i2c_driver max_tcpci_i2c_driver = { + .driver = { + .name = "maxtcpc", + .of_match_table = of_match_ptr(max_tcpci_of_match), + }, + .probe = max_tcpci_probe, + .remove = max_tcpci_remove, + .id_table = max_tcpci_id, +}; +module_i2c_driver(max_tcpci_i2c_driver); + +MODULE_AUTHOR("Badhri Jagan Sridharan "); +MODULE_DESCRIPTION("Maxim TCPCI based USB Type-C Port Controller Interface Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/typec/tcpm/tcpci_mt6360.c b/drivers/usb/typec/tcpm/tcpci_mt6360.c new file mode 100644 index 0000000000..02b7fd3022 --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci_mt6360.c @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2020 MediaTek Inc. + * + * Author: ChiYuan Huang + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define MT6360_REG_PHYCTRL1 0x80 +#define MT6360_REG_PHYCTRL3 0x82 +#define MT6360_REG_PHYCTRL7 0x86 +#define MT6360_REG_VCONNCTRL1 0x8C +#define MT6360_REG_MODECTRL2 0x8F +#define MT6360_REG_SWRESET 0xA0 +#define MT6360_REG_DEBCTRL1 0xA1 +#define MT6360_REG_DRPCTRL1 0xA2 +#define MT6360_REG_DRPCTRL2 0xA3 +#define MT6360_REG_I2CTORST 0xBF +#define MT6360_REG_PHYCTRL11 0xCA +#define MT6360_REG_RXCTRL1 0xCE +#define MT6360_REG_RXCTRL2 0xCF +#define MT6360_REG_CTDCTRL2 0xEC + +/* MT6360_REG_VCONNCTRL1 */ +#define MT6360_VCONNCL_ENABLE BIT(0) +/* MT6360_REG_RXCTRL2 */ +#define MT6360_OPEN40M_ENABLE BIT(7) +/* MT6360_REG_CTDCTRL2 */ +#define MT6360_RPONESHOT_ENABLE BIT(6) + +struct mt6360_tcpc_info { + struct tcpci_data tdata; + struct tcpci *tcpci; + struct device *dev; + int irq; +}; + +static inline int mt6360_tcpc_write16(struct regmap *regmap, + unsigned int reg, u16 val) +{ + return regmap_raw_write(regmap, reg, &val, sizeof(u16)); +} + +static int mt6360_tcpc_init(struct tcpci *tcpci, struct tcpci_data *tdata) +{ + struct regmap *regmap = tdata->regmap; + int ret; + + ret = regmap_write(regmap, MT6360_REG_SWRESET, 0x01); + if (ret) + return ret; + + /* after reset command, wait 1~2ms to wait IC action */ + usleep_range(1000, 2000); + + /* write all alert to masked */ + ret = mt6360_tcpc_write16(regmap, TCPC_ALERT_MASK, 0); + if (ret) + return ret; + + /* config I2C timeout reset enable , and timeout to 200ms */ + ret = regmap_write(regmap, MT6360_REG_I2CTORST, 0x8F); + if (ret) + return ret; + + /* config CC Detect Debounce : 26.7*val us */ + ret = regmap_write(regmap, MT6360_REG_DEBCTRL1, 0x10); + if (ret) + return ret; + + /* DRP Toggle Cycle : 51.2 + 6.4*val ms */ + ret = regmap_write(regmap, MT6360_REG_DRPCTRL1, 4); + if (ret) + return ret; + + /* DRP Duyt Ctrl : dcSRC: /1024 */ + ret = mt6360_tcpc_write16(regmap, MT6360_REG_DRPCTRL2, 330); + if (ret) + return ret; + + /* Enable VCONN Current Limit function */ + ret = regmap_update_bits(regmap, MT6360_REG_VCONNCTRL1, MT6360_VCONNCL_ENABLE, + MT6360_VCONNCL_ENABLE); + if (ret) + return ret; + + /* Enable cc open 40ms when pmic send vsysuv signal */ + ret = regmap_update_bits(regmap, MT6360_REG_RXCTRL2, MT6360_OPEN40M_ENABLE, + MT6360_OPEN40M_ENABLE); + if (ret) + return ret; + + /* Enable Rpdet oneshot detection */ + ret = regmap_update_bits(regmap, MT6360_REG_CTDCTRL2, MT6360_RPONESHOT_ENABLE, + MT6360_RPONESHOT_ENABLE); + if (ret) + return ret; + + /* BMC PHY */ + ret = mt6360_tcpc_write16(regmap, MT6360_REG_PHYCTRL1, 0x3A70); + if (ret) + return ret; + + ret = regmap_write(regmap, MT6360_REG_PHYCTRL3, 0x82); + if (ret) + return ret; + + ret = regmap_write(regmap, MT6360_REG_PHYCTRL7, 0x36); + if (ret) + return ret; + + ret = mt6360_tcpc_write16(regmap, MT6360_REG_PHYCTRL11, 0x3C60); + if (ret) + return ret; + + ret = regmap_write(regmap, MT6360_REG_RXCTRL1, 0xE8); + if (ret) + return ret; + + /* Set shipping mode off, AUTOIDLE on */ + return regmap_write(regmap, MT6360_REG_MODECTRL2, 0x7A); +} + +static irqreturn_t mt6360_irq(int irq, void *dev_id) +{ + struct mt6360_tcpc_info *mti = dev_id; + + return tcpci_irq(mti->tcpci); +} + +static int mt6360_tcpc_probe(struct platform_device *pdev) +{ + struct mt6360_tcpc_info *mti; + int ret; + + mti = devm_kzalloc(&pdev->dev, sizeof(*mti), GFP_KERNEL); + if (!mti) + return -ENOMEM; + + mti->dev = &pdev->dev; + + mti->tdata.regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!mti->tdata.regmap) { + dev_err(&pdev->dev, "Failed to get parent regmap\n"); + return -ENODEV; + } + + mti->irq = platform_get_irq_byname(pdev, "PD_IRQB"); + if (mti->irq < 0) + return mti->irq; + + mti->tdata.init = mt6360_tcpc_init; + mti->tcpci = tcpci_register_port(&pdev->dev, &mti->tdata); + if (IS_ERR(mti->tcpci)) { + dev_err(&pdev->dev, "Failed to register tcpci port\n"); + return PTR_ERR(mti->tcpci); + } + + ret = devm_request_threaded_irq(mti->dev, mti->irq, NULL, mt6360_irq, IRQF_ONESHOT, + dev_name(&pdev->dev), mti); + if (ret) { + dev_err(mti->dev, "Failed to register irq\n"); + tcpci_unregister_port(mti->tcpci); + return ret; + } + + device_init_wakeup(&pdev->dev, true); + platform_set_drvdata(pdev, mti); + + return 0; +} + +static void mt6360_tcpc_remove(struct platform_device *pdev) +{ + struct mt6360_tcpc_info *mti = platform_get_drvdata(pdev); + + disable_irq(mti->irq); + tcpci_unregister_port(mti->tcpci); +} + +static int __maybe_unused mt6360_tcpc_suspend(struct device *dev) +{ + struct mt6360_tcpc_info *mti = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) + enable_irq_wake(mti->irq); + + return 0; +} + +static int __maybe_unused mt6360_tcpc_resume(struct device *dev) +{ + struct mt6360_tcpc_info *mti = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) + disable_irq_wake(mti->irq); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(mt6360_tcpc_pm_ops, mt6360_tcpc_suspend, mt6360_tcpc_resume); + +static const struct of_device_id __maybe_unused mt6360_tcpc_of_id[] = { + { .compatible = "mediatek,mt6360-tcpc", }, + {}, +}; +MODULE_DEVICE_TABLE(of, mt6360_tcpc_of_id); + +static struct platform_driver mt6360_tcpc_driver = { + .driver = { + .name = "mt6360-tcpc", + .pm = &mt6360_tcpc_pm_ops, + .of_match_table = mt6360_tcpc_of_id, + }, + .probe = mt6360_tcpc_probe, + .remove_new = mt6360_tcpc_remove, +}; +module_platform_driver(mt6360_tcpc_driver); + +MODULE_AUTHOR("ChiYuan Huang "); +MODULE_DESCRIPTION("MT6360 USB Type-C Port Controller Interface Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/typec/tcpm/tcpci_mt6370.c b/drivers/usb/typec/tcpm/tcpci_mt6370.c new file mode 100644 index 0000000000..9cda1005ef --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci_mt6370.c @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2022 Richtek Technology Corp. + * + * Author: ChiYuan Huang + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MT6370_REG_SYSCTRL8 0x9B + +#define MT6370_AUTOIDLE_MASK BIT(3) + +#define MT6370_VENDOR_ID 0x29CF +#define MT6370_TCPC_DID_A 0x2170 + +struct mt6370_priv { + struct device *dev; + struct regulator *vbus; + struct tcpci *tcpci; + struct tcpci_data tcpci_data; +}; + +static const struct reg_sequence mt6370_reg_init[] = { + REG_SEQ(0xA0, 0x1, 1000), + REG_SEQ(0x81, 0x38, 0), + REG_SEQ(0x82, 0x82, 0), + REG_SEQ(0xBA, 0xFC, 0), + REG_SEQ(0xBB, 0x50, 0), + REG_SEQ(0x9E, 0x8F, 0), + REG_SEQ(0xA1, 0x5, 0), + REG_SEQ(0xA2, 0x4, 0), + REG_SEQ(0xA3, 0x4A, 0), + REG_SEQ(0xA4, 0x01, 0), + REG_SEQ(0x95, 0x01, 0), + REG_SEQ(0x80, 0x71, 0), + REG_SEQ(0x9B, 0x3A, 1000), +}; + +static int mt6370_tcpc_init(struct tcpci *tcpci, struct tcpci_data *data) +{ + u16 did; + int ret; + + ret = regmap_register_patch(data->regmap, mt6370_reg_init, + ARRAY_SIZE(mt6370_reg_init)); + if (ret) + return ret; + + ret = regmap_raw_read(data->regmap, TCPC_BCD_DEV, &did, sizeof(u16)); + if (ret) + return ret; + + if (did == MT6370_TCPC_DID_A) + return regmap_write(data->regmap, TCPC_FAULT_CTRL, 0x80); + + return 0; +} + +static int mt6370_tcpc_set_vconn(struct tcpci *tcpci, struct tcpci_data *data, + bool enable) +{ + return regmap_update_bits(data->regmap, MT6370_REG_SYSCTRL8, + MT6370_AUTOIDLE_MASK, + enable ? 0 : MT6370_AUTOIDLE_MASK); +} + +static int mt6370_tcpc_set_vbus(struct tcpci *tcpci, struct tcpci_data *data, + bool source, bool sink) +{ + struct mt6370_priv *priv = container_of(data, struct mt6370_priv, + tcpci_data); + int ret; + + ret = regulator_is_enabled(priv->vbus); + if (ret < 0) + return ret; + + if (ret && !source) + return regulator_disable(priv->vbus); + + if (!ret && source) + return regulator_enable(priv->vbus); + + return 0; +} + +static irqreturn_t mt6370_irq_handler(int irq, void *dev_id) +{ + struct mt6370_priv *priv = dev_id; + + return tcpci_irq(priv->tcpci); +} + +static int mt6370_check_vendor_info(struct mt6370_priv *priv) +{ + struct regmap *regmap = priv->tcpci_data.regmap; + u16 vid; + int ret; + + ret = regmap_raw_read(regmap, TCPC_VENDOR_ID, &vid, sizeof(u16)); + if (ret) + return ret; + + if (vid != MT6370_VENDOR_ID) + return dev_err_probe(priv->dev, -ENODEV, + "Vendor ID not correct 0x%02x\n", vid); + + return 0; +} + +static void mt6370_unregister_tcpci_port(void *tcpci) +{ + tcpci_unregister_port(tcpci); +} + +static int mt6370_tcpc_probe(struct platform_device *pdev) +{ + struct mt6370_priv *priv; + struct device *dev = &pdev->dev; + int irq, ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + + priv->tcpci_data.regmap = dev_get_regmap(dev->parent, NULL); + if (!priv->tcpci_data.regmap) + return dev_err_probe(dev, -ENODEV, "Failed to init regmap\n"); + + ret = mt6370_check_vendor_info(priv); + if (ret) + return ret; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + /* Assign TCPCI feature and ops */ + priv->tcpci_data.auto_discharge_disconnect = 1; + priv->tcpci_data.init = mt6370_tcpc_init; + priv->tcpci_data.set_vconn = mt6370_tcpc_set_vconn; + + priv->vbus = devm_regulator_get_optional(dev, "vbus"); + if (!IS_ERR(priv->vbus)) + priv->tcpci_data.set_vbus = mt6370_tcpc_set_vbus; + + priv->tcpci = tcpci_register_port(dev, &priv->tcpci_data); + if (IS_ERR(priv->tcpci)) + return dev_err_probe(dev, PTR_ERR(priv->tcpci), + "Failed to register tcpci port\n"); + + ret = devm_add_action_or_reset(dev, mt6370_unregister_tcpci_port, priv->tcpci); + if (ret) + return ret; + + ret = devm_request_threaded_irq(dev, irq, NULL, mt6370_irq_handler, + IRQF_ONESHOT, dev_name(dev), priv); + if (ret) + return dev_err_probe(dev, ret, "Failed to allocate irq\n"); + + device_init_wakeup(dev, true); + dev_pm_set_wake_irq(dev, irq); + + return 0; +} + +static void mt6370_tcpc_remove(struct platform_device *pdev) +{ + dev_pm_clear_wake_irq(&pdev->dev); + device_init_wakeup(&pdev->dev, false); +} + +static const struct of_device_id mt6370_tcpc_devid_table[] = { + { .compatible = "mediatek,mt6370-tcpc" }, + {} +}; +MODULE_DEVICE_TABLE(of, mt6370_tcpc_devid_table); + +static struct platform_driver mt6370_tcpc_driver = { + .driver = { + .name = "mt6370-tcpc", + .of_match_table = mt6370_tcpc_devid_table, + }, + .probe = mt6370_tcpc_probe, + .remove_new = mt6370_tcpc_remove, +}; +module_platform_driver(mt6370_tcpc_driver); + +MODULE_AUTHOR("ChiYuan Huang "); +MODULE_DESCRIPTION("MT6370 USB Type-C Port Controller Interface Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/typec/tcpm/tcpci_rt1711h.c b/drivers/usb/typec/tcpm/tcpci_rt1711h.c new file mode 100644 index 0000000000..17ebc5fb68 --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpci_rt1711h.c @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2018, Richtek Technology Corporation + * + * Richtek RT1711H Type-C Chip Driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RT1711H_VID 0x29CF +#define RT1711H_PID 0x1711 +#define RT1711H_DID 0x2171 +#define RT1715_DID 0x2173 + +#define RT1711H_PHYCTRL1 0x80 +#define RT1711H_PHYCTRL2 0x81 + +#define RT1711H_RTCTRL4 0x93 +/* rx threshold of rd/rp: 1b0 for level 0.4V/0.7V, 1b1 for 0.35V/0.75V */ +#define RT1711H_BMCIO_RXDZSEL BIT(0) + +#define RT1711H_RTCTRL8 0x9B +/* Autoidle timeout = (tout * 2 + 1) * 6.4ms */ +#define RT1711H_RTCTRL8_SET(ck300, ship_off, auto_idle, tout) \ + (((ck300) << 7) | ((ship_off) << 5) | \ + ((auto_idle) << 3) | ((tout) & 0x07)) +#define RT1711H_AUTOIDLEEN BIT(3) +#define RT1711H_ENEXTMSG BIT(4) + +#define RT1711H_RTCTRL11 0x9E + +/* I2C timeout = (tout + 1) * 12.5ms */ +#define RT1711H_RTCTRL11_SET(en, tout) \ + (((en) << 7) | ((tout) & 0x0F)) + +#define RT1711H_RTCTRL13 0xA0 +#define RT1711H_RTCTRL14 0xA1 +#define RT1711H_RTCTRL15 0xA2 +#define RT1711H_RTCTRL16 0xA3 + +#define RT1711H_RTCTRL18 0xAF +/* 1b0 as fixed rx threshold of rd/rp 0.55V, 1b1 depends on RTCRTL4[0] */ +#define BMCIO_RXDZEN BIT(0) + +struct rt1711h_chip { + struct tcpci_data data; + struct tcpci *tcpci; + struct device *dev; + struct regulator *vbus; + bool src_en; + u16 did; +}; + +static int rt1711h_read16(struct rt1711h_chip *chip, unsigned int reg, u16 *val) +{ + return regmap_raw_read(chip->data.regmap, reg, val, sizeof(u16)); +} + +static int rt1711h_write16(struct rt1711h_chip *chip, unsigned int reg, u16 val) +{ + return regmap_raw_write(chip->data.regmap, reg, &val, sizeof(u16)); +} + +static int rt1711h_read8(struct rt1711h_chip *chip, unsigned int reg, u8 *val) +{ + return regmap_raw_read(chip->data.regmap, reg, val, sizeof(u8)); +} + +static int rt1711h_write8(struct rt1711h_chip *chip, unsigned int reg, u8 val) +{ + return regmap_raw_write(chip->data.regmap, reg, &val, sizeof(u8)); +} + +static const struct regmap_config rt1711h_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = 0xFF, /* 0x80 .. 0xFF are vendor defined */ +}; + +static struct rt1711h_chip *tdata_to_rt1711h(struct tcpci_data *tdata) +{ + return container_of(tdata, struct rt1711h_chip, data); +} + +static int rt1711h_init(struct tcpci *tcpci, struct tcpci_data *tdata) +{ + struct rt1711h_chip *chip = tdata_to_rt1711h(tdata); + struct regmap *regmap = chip->data.regmap; + int ret; + + /* CK 300K from 320K, shipping off, auto_idle enable, tout = 32ms */ + ret = rt1711h_write8(chip, RT1711H_RTCTRL8, + RT1711H_RTCTRL8_SET(0, 1, 1, 2)); + if (ret < 0) + return ret; + + /* Enable PD30 extended message for RT1715 */ + if (chip->did == RT1715_DID) { + ret = regmap_update_bits(regmap, RT1711H_RTCTRL8, + RT1711H_ENEXTMSG, RT1711H_ENEXTMSG); + if (ret < 0) + return ret; + } + + /* I2C reset : (val + 1) * 12.5ms */ + ret = rt1711h_write8(chip, RT1711H_RTCTRL11, + RT1711H_RTCTRL11_SET(1, 0x0F)); + if (ret < 0) + return ret; + + /* tTCPCfilter : (26.7 * val) us */ + ret = rt1711h_write8(chip, RT1711H_RTCTRL14, 0x0F); + if (ret < 0) + return ret; + + /* tDRP : (51.2 + 6.4 * val) ms */ + ret = rt1711h_write8(chip, RT1711H_RTCTRL15, 0x04); + if (ret < 0) + return ret; + + /* dcSRC.DRP : 33% */ + ret = rt1711h_write16(chip, RT1711H_RTCTRL16, 330); + if (ret < 0) + return ret; + + /* Enable phy discard retry, retry count 7, rx filter deglitch 100 us */ + ret = rt1711h_write8(chip, RT1711H_PHYCTRL1, 0xF1); + if (ret < 0) + return ret; + + /* Decrease wait time of BMC-encoded 1 bit from 2.67us to 2.55us */ + /* wait time : (val * .4167) us */ + return rt1711h_write8(chip, RT1711H_PHYCTRL2, 62); +} + +static int rt1711h_set_vbus(struct tcpci *tcpci, struct tcpci_data *tdata, + bool src, bool snk) +{ + struct rt1711h_chip *chip = tdata_to_rt1711h(tdata); + int ret; + + if (chip->src_en == src) + return 0; + + if (src) + ret = regulator_enable(chip->vbus); + else + ret = regulator_disable(chip->vbus); + + if (!ret) + chip->src_en = src; + return ret; +} + +static int rt1711h_set_vconn(struct tcpci *tcpci, struct tcpci_data *tdata, + bool enable) +{ + struct rt1711h_chip *chip = tdata_to_rt1711h(tdata); + + return regmap_update_bits(chip->data.regmap, RT1711H_RTCTRL8, + RT1711H_AUTOIDLEEN, enable ? 0 : RT1711H_AUTOIDLEEN); +} + +/* + * Selects the CC PHY noise filter voltage level according to the remote current + * CC voltage level. + * + * @status: The port's current cc status read from IC + * Return 0 if writes succeed; failure code otherwise + */ +static inline int rt1711h_init_cc_params(struct rt1711h_chip *chip, u8 status) +{ + int ret, cc1, cc2; + u8 role = 0; + u32 rxdz_en, rxdz_sel; + + ret = rt1711h_read8(chip, TCPC_ROLE_CTRL, &role); + if (ret < 0) + return ret; + + cc1 = tcpci_to_typec_cc((status >> TCPC_CC_STATUS_CC1_SHIFT) & + TCPC_CC_STATUS_CC1_MASK, + status & TCPC_CC_STATUS_TERM || + tcpc_presenting_rd(role, CC1)); + cc2 = tcpci_to_typec_cc((status >> TCPC_CC_STATUS_CC2_SHIFT) & + TCPC_CC_STATUS_CC2_MASK, + status & TCPC_CC_STATUS_TERM || + tcpc_presenting_rd(role, CC2)); + + if ((cc1 >= TYPEC_CC_RP_1_5 && cc2 < TYPEC_CC_RP_DEF) || + (cc2 >= TYPEC_CC_RP_1_5 && cc1 < TYPEC_CC_RP_DEF)) { + rxdz_en = BMCIO_RXDZEN; + if (chip->did == RT1715_DID) + rxdz_sel = RT1711H_BMCIO_RXDZSEL; + else + rxdz_sel = 0; + } else { + rxdz_en = 0; + rxdz_sel = RT1711H_BMCIO_RXDZSEL; + } + + ret = regmap_update_bits(chip->data.regmap, RT1711H_RTCTRL18, + BMCIO_RXDZEN, rxdz_en); + if (ret < 0) + return ret; + + return regmap_update_bits(chip->data.regmap, RT1711H_RTCTRL4, + RT1711H_BMCIO_RXDZSEL, rxdz_sel); +} + +static int rt1711h_start_drp_toggling(struct tcpci *tcpci, + struct tcpci_data *tdata, + enum typec_cc_status cc) +{ + struct rt1711h_chip *chip = tdata_to_rt1711h(tdata); + int ret; + unsigned int reg = 0; + + switch (cc) { + default: + case TYPEC_CC_RP_DEF: + reg |= (TCPC_ROLE_CTRL_RP_VAL_DEF << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_1_5: + reg |= (TCPC_ROLE_CTRL_RP_VAL_1_5 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + case TYPEC_CC_RP_3_0: + reg |= (TCPC_ROLE_CTRL_RP_VAL_3_0 << + TCPC_ROLE_CTRL_RP_VAL_SHIFT); + break; + } + + if (cc == TYPEC_CC_RD) + reg |= (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT); + else + reg |= (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC1_SHIFT) | + (TCPC_ROLE_CTRL_CC_RP << TCPC_ROLE_CTRL_CC2_SHIFT); + + ret = rt1711h_write8(chip, TCPC_ROLE_CTRL, reg); + if (ret < 0) + return ret; + usleep_range(500, 1000); + + return 0; +} + +static irqreturn_t rt1711h_irq(int irq, void *dev_id) +{ + int ret; + u16 alert; + u8 status; + struct rt1711h_chip *chip = dev_id; + + if (!chip->tcpci) + return IRQ_HANDLED; + + ret = rt1711h_read16(chip, TCPC_ALERT, &alert); + if (ret < 0) + goto out; + + if (alert & TCPC_ALERT_CC_STATUS) { + ret = rt1711h_read8(chip, TCPC_CC_STATUS, &status); + if (ret < 0) + goto out; + /* Clear cc change event triggered by starting toggling */ + if (status & TCPC_CC_STATUS_TOGGLING) + rt1711h_write8(chip, TCPC_ALERT, TCPC_ALERT_CC_STATUS); + else + rt1711h_init_cc_params(chip, status); + } + +out: + return tcpci_irq(chip->tcpci); +} + +static int rt1711h_sw_reset(struct rt1711h_chip *chip) +{ + int ret; + + ret = rt1711h_write8(chip, RT1711H_RTCTRL13, 0x01); + if (ret < 0) + return ret; + + usleep_range(1000, 2000); + return 0; +} + +static int rt1711h_check_revision(struct i2c_client *i2c, struct rt1711h_chip *chip) +{ + int ret; + + ret = i2c_smbus_read_word_data(i2c, TCPC_VENDOR_ID); + if (ret < 0) + return ret; + if (ret != RT1711H_VID) { + dev_err(&i2c->dev, "vid is not correct, 0x%04x\n", ret); + return -ENODEV; + } + ret = i2c_smbus_read_word_data(i2c, TCPC_PRODUCT_ID); + if (ret < 0) + return ret; + if (ret != RT1711H_PID) { + dev_err(&i2c->dev, "pid is not correct, 0x%04x\n", ret); + return -ENODEV; + } + ret = i2c_smbus_read_word_data(i2c, TCPC_BCD_DEV); + if (ret < 0) + return ret; + if (ret != chip->did) { + dev_err(&i2c->dev, "did is not correct, 0x%04x\n", ret); + return -ENODEV; + } + dev_dbg(&i2c->dev, "did is 0x%04x\n", ret); + return ret; +} + +static int rt1711h_probe(struct i2c_client *client) +{ + int ret; + struct rt1711h_chip *chip; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->did = (size_t)device_get_match_data(&client->dev); + + ret = rt1711h_check_revision(client, chip); + if (ret < 0) { + dev_err(&client->dev, "check vid/pid fail\n"); + return ret; + } + + chip->data.regmap = devm_regmap_init_i2c(client, + &rt1711h_regmap_config); + if (IS_ERR(chip->data.regmap)) + return PTR_ERR(chip->data.regmap); + + chip->dev = &client->dev; + i2c_set_clientdata(client, chip); + + ret = rt1711h_sw_reset(chip); + if (ret < 0) + return ret; + + /* Disable chip interrupts before requesting irq */ + ret = rt1711h_write16(chip, TCPC_ALERT_MASK, 0); + if (ret < 0) + return ret; + + chip->vbus = devm_regulator_get(&client->dev, "vbus"); + if (IS_ERR(chip->vbus)) + return PTR_ERR(chip->vbus); + + chip->data.init = rt1711h_init; + chip->data.set_vbus = rt1711h_set_vbus; + chip->data.set_vconn = rt1711h_set_vconn; + chip->data.start_drp_toggling = rt1711h_start_drp_toggling; + chip->tcpci = tcpci_register_port(chip->dev, &chip->data); + if (IS_ERR_OR_NULL(chip->tcpci)) + return PTR_ERR(chip->tcpci); + + ret = devm_request_threaded_irq(chip->dev, client->irq, NULL, + rt1711h_irq, + IRQF_ONESHOT | IRQF_TRIGGER_LOW, + dev_name(chip->dev), chip); + if (ret < 0) + return ret; + enable_irq_wake(client->irq); + + return 0; +} + +static void rt1711h_remove(struct i2c_client *client) +{ + struct rt1711h_chip *chip = i2c_get_clientdata(client); + + tcpci_unregister_port(chip->tcpci); +} + +static const struct i2c_device_id rt1711h_id[] = { + { "rt1711h", 0 }, + { "rt1715", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, rt1711h_id); + +#ifdef CONFIG_OF +static const struct of_device_id rt1711h_of_match[] = { + { .compatible = "richtek,rt1711h", .data = (void *)RT1711H_DID }, + { .compatible = "richtek,rt1715", .data = (void *)RT1715_DID }, + {}, +}; +MODULE_DEVICE_TABLE(of, rt1711h_of_match); +#endif + +static struct i2c_driver rt1711h_i2c_driver = { + .driver = { + .name = "rt1711h", + .of_match_table = of_match_ptr(rt1711h_of_match), + }, + .probe = rt1711h_probe, + .remove = rt1711h_remove, + .id_table = rt1711h_id, +}; +module_i2c_driver(rt1711h_i2c_driver); + +MODULE_AUTHOR("ShuFan Lee "); +MODULE_DESCRIPTION("RT1711H USB Type-C Port Controller Interface Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c new file mode 100644 index 0000000000..6d455ca761 --- /dev/null +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -0,0 +1,6675 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2015-2017 Google, Inc + * + * USB Power Delivery protocol stack. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define FOREACH_STATE(S) \ + S(INVALID_STATE), \ + S(TOGGLING), \ + S(CHECK_CONTAMINANT), \ + S(SRC_UNATTACHED), \ + S(SRC_ATTACH_WAIT), \ + S(SRC_ATTACHED), \ + S(SRC_STARTUP), \ + S(SRC_SEND_CAPABILITIES), \ + S(SRC_SEND_CAPABILITIES_TIMEOUT), \ + S(SRC_NEGOTIATE_CAPABILITIES), \ + S(SRC_TRANSITION_SUPPLY), \ + S(SRC_READY), \ + S(SRC_WAIT_NEW_CAPABILITIES), \ + \ + S(SNK_UNATTACHED), \ + S(SNK_ATTACH_WAIT), \ + S(SNK_DEBOUNCED), \ + S(SNK_ATTACHED), \ + S(SNK_STARTUP), \ + S(SNK_DISCOVERY), \ + S(SNK_DISCOVERY_DEBOUNCE), \ + S(SNK_DISCOVERY_DEBOUNCE_DONE), \ + S(SNK_WAIT_CAPABILITIES), \ + S(SNK_NEGOTIATE_CAPABILITIES), \ + S(SNK_NEGOTIATE_PPS_CAPABILITIES), \ + S(SNK_TRANSITION_SINK), \ + S(SNK_TRANSITION_SINK_VBUS), \ + S(SNK_READY), \ + \ + S(ACC_UNATTACHED), \ + S(DEBUG_ACC_ATTACHED), \ + S(AUDIO_ACC_ATTACHED), \ + S(AUDIO_ACC_DEBOUNCE), \ + \ + S(HARD_RESET_SEND), \ + S(HARD_RESET_START), \ + S(SRC_HARD_RESET_VBUS_OFF), \ + S(SRC_HARD_RESET_VBUS_ON), \ + S(SNK_HARD_RESET_SINK_OFF), \ + S(SNK_HARD_RESET_WAIT_VBUS), \ + S(SNK_HARD_RESET_SINK_ON), \ + \ + S(SOFT_RESET), \ + S(SRC_SOFT_RESET_WAIT_SNK_TX), \ + S(SNK_SOFT_RESET), \ + S(SOFT_RESET_SEND), \ + \ + S(DR_SWAP_ACCEPT), \ + S(DR_SWAP_SEND), \ + S(DR_SWAP_SEND_TIMEOUT), \ + S(DR_SWAP_CANCEL), \ + S(DR_SWAP_CHANGE_DR), \ + \ + S(PR_SWAP_ACCEPT), \ + S(PR_SWAP_SEND), \ + S(PR_SWAP_SEND_TIMEOUT), \ + S(PR_SWAP_CANCEL), \ + S(PR_SWAP_START), \ + S(PR_SWAP_SRC_SNK_TRANSITION_OFF), \ + S(PR_SWAP_SRC_SNK_SOURCE_OFF), \ + S(PR_SWAP_SRC_SNK_SOURCE_OFF_CC_DEBOUNCED), \ + S(PR_SWAP_SRC_SNK_SINK_ON), \ + S(PR_SWAP_SNK_SRC_SINK_OFF), \ + S(PR_SWAP_SNK_SRC_SOURCE_ON), \ + S(PR_SWAP_SNK_SRC_SOURCE_ON_VBUS_RAMPED_UP), \ + \ + S(VCONN_SWAP_ACCEPT), \ + S(VCONN_SWAP_SEND), \ + S(VCONN_SWAP_SEND_TIMEOUT), \ + S(VCONN_SWAP_CANCEL), \ + S(VCONN_SWAP_START), \ + S(VCONN_SWAP_WAIT_FOR_VCONN), \ + S(VCONN_SWAP_TURN_ON_VCONN), \ + S(VCONN_SWAP_TURN_OFF_VCONN), \ + \ + S(FR_SWAP_SEND), \ + S(FR_SWAP_SEND_TIMEOUT), \ + S(FR_SWAP_SNK_SRC_TRANSITION_TO_OFF), \ + S(FR_SWAP_SNK_SRC_NEW_SINK_READY), \ + S(FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED), \ + S(FR_SWAP_CANCEL), \ + \ + S(SNK_TRY), \ + S(SNK_TRY_WAIT), \ + S(SNK_TRY_WAIT_DEBOUNCE), \ + S(SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS), \ + S(SRC_TRYWAIT), \ + S(SRC_TRYWAIT_DEBOUNCE), \ + S(SRC_TRYWAIT_UNATTACHED), \ + \ + S(SRC_TRY), \ + S(SRC_TRY_WAIT), \ + S(SRC_TRY_DEBOUNCE), \ + S(SNK_TRYWAIT), \ + S(SNK_TRYWAIT_DEBOUNCE), \ + S(SNK_TRYWAIT_VBUS), \ + S(BIST_RX), \ + \ + S(GET_STATUS_SEND), \ + S(GET_STATUS_SEND_TIMEOUT), \ + S(GET_PPS_STATUS_SEND), \ + S(GET_PPS_STATUS_SEND_TIMEOUT), \ + \ + S(GET_SINK_CAP), \ + S(GET_SINK_CAP_TIMEOUT), \ + \ + S(ERROR_RECOVERY), \ + S(PORT_RESET), \ + S(PORT_RESET_WAIT_OFF), \ + \ + S(AMS_START), \ + S(CHUNK_NOT_SUPP) + +#define FOREACH_AMS(S) \ + S(NONE_AMS), \ + S(POWER_NEGOTIATION), \ + S(GOTOMIN), \ + S(SOFT_RESET_AMS), \ + S(HARD_RESET), \ + S(CABLE_RESET), \ + S(GET_SOURCE_CAPABILITIES), \ + S(GET_SINK_CAPABILITIES), \ + S(POWER_ROLE_SWAP), \ + S(FAST_ROLE_SWAP), \ + S(DATA_ROLE_SWAP), \ + S(VCONN_SWAP), \ + S(SOURCE_ALERT), \ + S(GETTING_SOURCE_EXTENDED_CAPABILITIES),\ + S(GETTING_SOURCE_SINK_STATUS), \ + S(GETTING_BATTERY_CAPABILITIES), \ + S(GETTING_BATTERY_STATUS), \ + S(GETTING_MANUFACTURER_INFORMATION), \ + S(SECURITY), \ + S(FIRMWARE_UPDATE), \ + S(DISCOVER_IDENTITY), \ + S(SOURCE_STARTUP_CABLE_PLUG_DISCOVER_IDENTITY), \ + S(DISCOVER_SVIDS), \ + S(DISCOVER_MODES), \ + S(DFP_TO_UFP_ENTER_MODE), \ + S(DFP_TO_UFP_EXIT_MODE), \ + S(DFP_TO_CABLE_PLUG_ENTER_MODE), \ + S(DFP_TO_CABLE_PLUG_EXIT_MODE), \ + S(ATTENTION), \ + S(BIST), \ + S(UNSTRUCTURED_VDMS), \ + S(STRUCTURED_VDMS), \ + S(COUNTRY_INFO), \ + S(COUNTRY_CODES) + +#define GENERATE_ENUM(e) e +#define GENERATE_STRING(s) #s + +enum tcpm_state { + FOREACH_STATE(GENERATE_ENUM) +}; + +static const char * const tcpm_states[] = { + FOREACH_STATE(GENERATE_STRING) +}; + +enum tcpm_ams { + FOREACH_AMS(GENERATE_ENUM) +}; + +static const char * const tcpm_ams_str[] = { + FOREACH_AMS(GENERATE_STRING) +}; + +enum vdm_states { + VDM_STATE_ERR_BUSY = -3, + VDM_STATE_ERR_SEND = -2, + VDM_STATE_ERR_TMOUT = -1, + VDM_STATE_DONE = 0, + /* Anything >0 represents an active state */ + VDM_STATE_READY = 1, + VDM_STATE_BUSY = 2, + VDM_STATE_WAIT_RSP_BUSY = 3, + VDM_STATE_SEND_MESSAGE = 4, +}; + +enum pd_msg_request { + PD_MSG_NONE = 0, + PD_MSG_CTRL_REJECT, + PD_MSG_CTRL_WAIT, + PD_MSG_CTRL_NOT_SUPP, + PD_MSG_DATA_SINK_CAP, + PD_MSG_DATA_SOURCE_CAP, +}; + +enum adev_actions { + ADEV_NONE = 0, + ADEV_NOTIFY_USB_AND_QUEUE_VDM, + ADEV_QUEUE_VDM, + ADEV_QUEUE_VDM_SEND_EXIT_MODE_ON_FAIL, + ADEV_ATTENTION, +}; + +/* + * Initial current capability of the new source when vSafe5V is applied during PD3.0 Fast Role Swap. + * Based on "Table 6-14 Fixed Supply PDO - Sink" of "USB Power Delivery Specification Revision 3.0, + * Version 1.2" + */ +enum frs_typec_current { + FRS_NOT_SUPPORTED, + FRS_DEFAULT_POWER, + FRS_5V_1P5A, + FRS_5V_3A, +}; + +/* Events from low level driver */ + +#define TCPM_CC_EVENT BIT(0) +#define TCPM_VBUS_EVENT BIT(1) +#define TCPM_RESET_EVENT BIT(2) +#define TCPM_FRS_EVENT BIT(3) +#define TCPM_SOURCING_VBUS BIT(4) +#define TCPM_PORT_CLEAN BIT(5) + +#define LOG_BUFFER_ENTRIES 1024 +#define LOG_BUFFER_ENTRY_SIZE 128 + +/* Alternate mode support */ + +#define SVID_DISCOVERY_MAX 16 +#define ALTMODE_DISCOVERY_MAX (SVID_DISCOVERY_MAX * MODE_DISCOVERY_MAX) + +#define GET_SINK_CAP_RETRY_MS 100 +#define SEND_DISCOVER_RETRY_MS 100 + +struct pd_mode_data { + int svid_index; /* current SVID index */ + int nsvids; + u16 svids[SVID_DISCOVERY_MAX]; + int altmodes; /* number of alternate modes */ + struct typec_altmode_desc altmode_desc[ALTMODE_DISCOVERY_MAX]; +}; + +/* + * @min_volt: Actual min voltage at the local port + * @req_min_volt: Requested min voltage to the port partner + * @max_volt: Actual max voltage at the local port + * @req_max_volt: Requested max voltage to the port partner + * @max_curr: Actual max current at the local port + * @req_max_curr: Requested max current of the port partner + * @req_out_volt: Requested output voltage to the port partner + * @req_op_curr: Requested operating current to the port partner + * @supported: Parter has at least one APDO hence supports PPS + * @active: PPS mode is active + */ +struct pd_pps_data { + u32 min_volt; + u32 req_min_volt; + u32 max_volt; + u32 req_max_volt; + u32 max_curr; + u32 req_max_curr; + u32 req_out_volt; + u32 req_op_curr; + bool supported; + bool active; +}; + +struct tcpm_port { + struct device *dev; + + struct mutex lock; /* tcpm state machine lock */ + struct kthread_worker *wq; + + struct typec_capability typec_caps; + struct typec_port *typec_port; + + struct tcpc_dev *tcpc; + struct usb_role_switch *role_sw; + + enum typec_role vconn_role; + enum typec_role pwr_role; + enum typec_data_role data_role; + enum typec_pwr_opmode pwr_opmode; + + struct usb_pd_identity partner_ident; + struct typec_partner_desc partner_desc; + struct typec_partner *partner; + + enum typec_cc_status cc_req; + enum typec_cc_status src_rp; /* work only if pd_supported == false */ + + enum typec_cc_status cc1; + enum typec_cc_status cc2; + enum typec_cc_polarity polarity; + + bool attached; + bool connected; + bool registered; + bool pd_supported; + enum typec_port_type port_type; + + /* + * Set to true when vbus is greater than VSAFE5V min. + * Set to false when vbus falls below vSinkDisconnect max threshold. + */ + bool vbus_present; + + /* + * Set to true when vbus is less than VSAFE0V max. + * Set to false when vbus is greater than VSAFE0V max. + */ + bool vbus_vsafe0v; + + bool vbus_never_low; + bool vbus_source; + bool vbus_charge; + + /* Set to true when Discover_Identity Command is expected to be sent in Ready states. */ + bool send_discover; + bool op_vsafe5v; + + int try_role; + int try_snk_count; + int try_src_count; + + enum pd_msg_request queued_message; + + enum tcpm_state enter_state; + enum tcpm_state prev_state; + enum tcpm_state state; + enum tcpm_state delayed_state; + ktime_t delayed_runtime; + unsigned long delay_ms; + + spinlock_t pd_event_lock; + u32 pd_events; + + struct kthread_work event_work; + struct hrtimer state_machine_timer; + struct kthread_work state_machine; + struct hrtimer vdm_state_machine_timer; + struct kthread_work vdm_state_machine; + struct hrtimer enable_frs_timer; + struct kthread_work enable_frs; + struct hrtimer send_discover_timer; + struct kthread_work send_discover_work; + bool state_machine_running; + /* Set to true when VDM State Machine has following actions. */ + bool vdm_sm_running; + + struct completion tx_complete; + enum tcpm_transmit_status tx_status; + + struct mutex swap_lock; /* swap command lock */ + bool swap_pending; + bool non_pd_role_swap; + struct completion swap_complete; + int swap_status; + + unsigned int negotiated_rev; + unsigned int message_id; + unsigned int caps_count; + unsigned int hard_reset_count; + bool pd_capable; + bool explicit_contract; + unsigned int rx_msgid; + + /* USB PD objects */ + struct usb_power_delivery *pd; + struct usb_power_delivery_capabilities *port_source_caps; + struct usb_power_delivery_capabilities *port_sink_caps; + struct usb_power_delivery *partner_pd; + struct usb_power_delivery_capabilities *partner_source_caps; + struct usb_power_delivery_capabilities *partner_sink_caps; + + /* Partner capabilities/requests */ + u32 sink_request; + u32 source_caps[PDO_MAX_OBJECTS]; + unsigned int nr_source_caps; + u32 sink_caps[PDO_MAX_OBJECTS]; + unsigned int nr_sink_caps; + + /* Local capabilities */ + u32 src_pdo[PDO_MAX_OBJECTS]; + unsigned int nr_src_pdo; + u32 snk_pdo[PDO_MAX_OBJECTS]; + unsigned int nr_snk_pdo; + u32 snk_vdo_v1[VDO_MAX_OBJECTS]; + unsigned int nr_snk_vdo_v1; + u32 snk_vdo[VDO_MAX_OBJECTS]; + unsigned int nr_snk_vdo; + + unsigned int operating_snk_mw; + bool update_sink_caps; + + /* Requested current / voltage to the port partner */ + u32 req_current_limit; + u32 req_supply_voltage; + /* Actual current / voltage limit of the local port */ + u32 current_limit; + u32 supply_voltage; + + /* Used to export TA voltage and current */ + struct power_supply *psy; + struct power_supply_desc psy_desc; + enum power_supply_usb_type usb_type; + + u32 bist_request; + + /* PD state for Vendor Defined Messages */ + enum vdm_states vdm_state; + u32 vdm_retries; + /* next Vendor Defined Message to send */ + u32 vdo_data[VDO_MAX_SIZE]; + u8 vdo_count; + /* VDO to retry if UFP responder replied busy */ + u32 vdo_retry; + + /* PPS */ + struct pd_pps_data pps_data; + struct completion pps_complete; + bool pps_pending; + int pps_status; + + /* Alternate mode data */ + struct pd_mode_data mode_data; + struct typec_altmode *partner_altmode[ALTMODE_DISCOVERY_MAX]; + struct typec_altmode *port_altmode[ALTMODE_DISCOVERY_MAX]; + + /* Deadline in jiffies to exit src_try_wait state */ + unsigned long max_wait; + + /* port belongs to a self powered device */ + bool self_powered; + + /* Sink FRS */ + enum frs_typec_current new_source_frs_current; + + /* Sink caps have been queried */ + bool sink_cap_done; + + /* Collision Avoidance and Atomic Message Sequence */ + enum tcpm_state upcoming_state; + enum tcpm_ams ams; + enum tcpm_ams next_ams; + bool in_ams; + + /* Auto vbus discharge status */ + bool auto_vbus_discharge_enabled; + + /* + * When set, port requests PD_P_SNK_STDBY_MW upon entering SNK_DISCOVERY and + * the actual current limit after RX of PD_CTRL_PSRDY for PD link, + * SNK_READY for non-pd link. + */ + bool slow_charger_loop; + + /* + * When true indicates that the lower level drivers indicate potential presence + * of contaminant in the connector pins based on the tcpm state machine + * transitions. + */ + bool potential_contaminant; +#ifdef CONFIG_DEBUG_FS + struct dentry *dentry; + struct mutex logbuffer_lock; /* log buffer access lock */ + int logbuffer_head; + int logbuffer_tail; + u8 *logbuffer[LOG_BUFFER_ENTRIES]; +#endif +}; + +struct pd_rx_event { + struct kthread_work work; + struct tcpm_port *port; + struct pd_message msg; +}; + +static const char * const pd_rev[] = { + [PD_REV10] = "rev1", + [PD_REV20] = "rev2", + [PD_REV30] = "rev3", +}; + +#define tcpm_cc_is_sink(cc) \ + ((cc) == TYPEC_CC_RP_DEF || (cc) == TYPEC_CC_RP_1_5 || \ + (cc) == TYPEC_CC_RP_3_0) + +#define tcpm_port_is_sink(port) \ + ((tcpm_cc_is_sink((port)->cc1) && !tcpm_cc_is_sink((port)->cc2)) || \ + (tcpm_cc_is_sink((port)->cc2) && !tcpm_cc_is_sink((port)->cc1))) + +#define tcpm_cc_is_source(cc) ((cc) == TYPEC_CC_RD) +#define tcpm_cc_is_audio(cc) ((cc) == TYPEC_CC_RA) +#define tcpm_cc_is_open(cc) ((cc) == TYPEC_CC_OPEN) + +#define tcpm_port_is_source(port) \ + ((tcpm_cc_is_source((port)->cc1) && \ + !tcpm_cc_is_source((port)->cc2)) || \ + (tcpm_cc_is_source((port)->cc2) && \ + !tcpm_cc_is_source((port)->cc1))) + +#define tcpm_port_is_debug(port) \ + (tcpm_cc_is_source((port)->cc1) && tcpm_cc_is_source((port)->cc2)) + +#define tcpm_port_is_audio(port) \ + (tcpm_cc_is_audio((port)->cc1) && tcpm_cc_is_audio((port)->cc2)) + +#define tcpm_port_is_audio_detached(port) \ + ((tcpm_cc_is_audio((port)->cc1) && tcpm_cc_is_open((port)->cc2)) || \ + (tcpm_cc_is_audio((port)->cc2) && tcpm_cc_is_open((port)->cc1))) + +#define tcpm_try_snk(port) \ + ((port)->try_snk_count == 0 && (port)->try_role == TYPEC_SINK && \ + (port)->port_type == TYPEC_PORT_DRP) + +#define tcpm_try_src(port) \ + ((port)->try_src_count == 0 && (port)->try_role == TYPEC_SOURCE && \ + (port)->port_type == TYPEC_PORT_DRP) + +#define tcpm_data_role_for_source(port) \ + ((port)->typec_caps.data == TYPEC_PORT_UFP ? \ + TYPEC_DEVICE : TYPEC_HOST) + +#define tcpm_data_role_for_sink(port) \ + ((port)->typec_caps.data == TYPEC_PORT_DFP ? \ + TYPEC_HOST : TYPEC_DEVICE) + +#define tcpm_sink_tx_ok(port) \ + (tcpm_port_is_sink(port) && \ + ((port)->cc1 == TYPEC_CC_RP_3_0 || (port)->cc2 == TYPEC_CC_RP_3_0)) + +#define tcpm_wait_for_discharge(port) \ + (((port)->auto_vbus_discharge_enabled && !(port)->vbus_vsafe0v) ? PD_T_SAFE_0V : 0) + +static enum tcpm_state tcpm_default_state(struct tcpm_port *port) +{ + if (port->port_type == TYPEC_PORT_DRP) { + if (port->try_role == TYPEC_SINK) + return SNK_UNATTACHED; + else if (port->try_role == TYPEC_SOURCE) + return SRC_UNATTACHED; + /* Fall through to return SRC_UNATTACHED */ + } else if (port->port_type == TYPEC_PORT_SNK) { + return SNK_UNATTACHED; + } + return SRC_UNATTACHED; +} + +static bool tcpm_port_is_disconnected(struct tcpm_port *port) +{ + return (!port->attached && port->cc1 == TYPEC_CC_OPEN && + port->cc2 == TYPEC_CC_OPEN) || + (port->attached && ((port->polarity == TYPEC_POLARITY_CC1 && + port->cc1 == TYPEC_CC_OPEN) || + (port->polarity == TYPEC_POLARITY_CC2 && + port->cc2 == TYPEC_CC_OPEN))); +} + +/* + * Logging + */ + +#ifdef CONFIG_DEBUG_FS + +static bool tcpm_log_full(struct tcpm_port *port) +{ + return port->logbuffer_tail == + (port->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; +} + +__printf(2, 0) +static void _tcpm_log(struct tcpm_port *port, const char *fmt, va_list args) +{ + char tmpbuffer[LOG_BUFFER_ENTRY_SIZE]; + u64 ts_nsec = local_clock(); + unsigned long rem_nsec; + + mutex_lock(&port->logbuffer_lock); + if (!port->logbuffer[port->logbuffer_head]) { + port->logbuffer[port->logbuffer_head] = + kzalloc(LOG_BUFFER_ENTRY_SIZE, GFP_KERNEL); + if (!port->logbuffer[port->logbuffer_head]) { + mutex_unlock(&port->logbuffer_lock); + return; + } + } + + vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args); + + if (tcpm_log_full(port)) { + port->logbuffer_head = max(port->logbuffer_head - 1, 0); + strcpy(tmpbuffer, "overflow"); + } + + if (port->logbuffer_head < 0 || + port->logbuffer_head >= LOG_BUFFER_ENTRIES) { + dev_warn(port->dev, + "Bad log buffer index %d\n", port->logbuffer_head); + goto abort; + } + + if (!port->logbuffer[port->logbuffer_head]) { + dev_warn(port->dev, + "Log buffer index %d is NULL\n", port->logbuffer_head); + goto abort; + } + + rem_nsec = do_div(ts_nsec, 1000000000); + scnprintf(port->logbuffer[port->logbuffer_head], + LOG_BUFFER_ENTRY_SIZE, "[%5lu.%06lu] %s", + (unsigned long)ts_nsec, rem_nsec / 1000, + tmpbuffer); + port->logbuffer_head = (port->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; + +abort: + mutex_unlock(&port->logbuffer_lock); +} + +__printf(2, 3) +static void tcpm_log(struct tcpm_port *port, const char *fmt, ...) +{ + va_list args; + + /* Do not log while disconnected and unattached */ + if (tcpm_port_is_disconnected(port) && + (port->state == SRC_UNATTACHED || port->state == SNK_UNATTACHED || + port->state == TOGGLING || port->state == CHECK_CONTAMINANT)) + return; + + va_start(args, fmt); + _tcpm_log(port, fmt, args); + va_end(args); +} + +__printf(2, 3) +static void tcpm_log_force(struct tcpm_port *port, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + _tcpm_log(port, fmt, args); + va_end(args); +} + +static void tcpm_log_source_caps(struct tcpm_port *port) +{ + int i; + + for (i = 0; i < port->nr_source_caps; i++) { + u32 pdo = port->source_caps[i]; + enum pd_pdo_type type = pdo_type(pdo); + char msg[64]; + + switch (type) { + case PDO_TYPE_FIXED: + scnprintf(msg, sizeof(msg), + "%u mV, %u mA [%s%s%s%s%s%s]", + pdo_fixed_voltage(pdo), + pdo_max_current(pdo), + (pdo & PDO_FIXED_DUAL_ROLE) ? + "R" : "", + (pdo & PDO_FIXED_SUSPEND) ? + "S" : "", + (pdo & PDO_FIXED_HIGHER_CAP) ? + "H" : "", + (pdo & PDO_FIXED_USB_COMM) ? + "U" : "", + (pdo & PDO_FIXED_DATA_SWAP) ? + "D" : "", + (pdo & PDO_FIXED_EXTPOWER) ? + "E" : ""); + break; + case PDO_TYPE_VAR: + scnprintf(msg, sizeof(msg), + "%u-%u mV, %u mA", + pdo_min_voltage(pdo), + pdo_max_voltage(pdo), + pdo_max_current(pdo)); + break; + case PDO_TYPE_BATT: + scnprintf(msg, sizeof(msg), + "%u-%u mV, %u mW", + pdo_min_voltage(pdo), + pdo_max_voltage(pdo), + pdo_max_power(pdo)); + break; + case PDO_TYPE_APDO: + if (pdo_apdo_type(pdo) == APDO_TYPE_PPS) + scnprintf(msg, sizeof(msg), + "%u-%u mV, %u mA", + pdo_pps_apdo_min_voltage(pdo), + pdo_pps_apdo_max_voltage(pdo), + pdo_pps_apdo_max_current(pdo)); + else + strcpy(msg, "undefined APDO"); + break; + default: + strcpy(msg, "undefined"); + break; + } + tcpm_log(port, " PDO %d: type %d, %s", + i, type, msg); + } +} + +static int tcpm_debug_show(struct seq_file *s, void *v) +{ + struct tcpm_port *port = s->private; + int tail; + + mutex_lock(&port->logbuffer_lock); + tail = port->logbuffer_tail; + while (tail != port->logbuffer_head) { + seq_printf(s, "%s\n", port->logbuffer[tail]); + tail = (tail + 1) % LOG_BUFFER_ENTRIES; + } + if (!seq_has_overflowed(s)) + port->logbuffer_tail = tail; + mutex_unlock(&port->logbuffer_lock); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tcpm_debug); + +static void tcpm_debugfs_init(struct tcpm_port *port) +{ + char name[NAME_MAX]; + + mutex_init(&port->logbuffer_lock); + snprintf(name, NAME_MAX, "tcpm-%s", dev_name(port->dev)); + port->dentry = debugfs_create_dir(name, usb_debug_root); + debugfs_create_file("log", S_IFREG | 0444, port->dentry, port, + &tcpm_debug_fops); +} + +static void tcpm_debugfs_exit(struct tcpm_port *port) +{ + int i; + + mutex_lock(&port->logbuffer_lock); + for (i = 0; i < LOG_BUFFER_ENTRIES; i++) { + kfree(port->logbuffer[i]); + port->logbuffer[i] = NULL; + } + mutex_unlock(&port->logbuffer_lock); + + debugfs_remove(port->dentry); +} + +#else + +__printf(2, 3) +static void tcpm_log(const struct tcpm_port *port, const char *fmt, ...) { } +__printf(2, 3) +static void tcpm_log_force(struct tcpm_port *port, const char *fmt, ...) { } +static void tcpm_log_source_caps(struct tcpm_port *port) { } +static void tcpm_debugfs_init(const struct tcpm_port *port) { } +static void tcpm_debugfs_exit(const struct tcpm_port *port) { } + +#endif + +static void tcpm_set_cc(struct tcpm_port *port, enum typec_cc_status cc) +{ + tcpm_log(port, "cc:=%d", cc); + port->cc_req = cc; + port->tcpc->set_cc(port->tcpc, cc); +} + +static int tcpm_enable_auto_vbus_discharge(struct tcpm_port *port, bool enable) +{ + int ret = 0; + + if (port->tcpc->enable_auto_vbus_discharge) { + ret = port->tcpc->enable_auto_vbus_discharge(port->tcpc, enable); + tcpm_log_force(port, "%s vbus discharge ret:%d", enable ? "enable" : "disable", + ret); + if (!ret) + port->auto_vbus_discharge_enabled = enable; + } + + return ret; +} + +static void tcpm_apply_rc(struct tcpm_port *port) +{ + /* + * TCPCI: Move to APPLY_RC state to prevent disconnect during PR_SWAP + * when Vbus auto discharge on disconnect is enabled. + */ + if (port->tcpc->enable_auto_vbus_discharge && port->tcpc->apply_rc) { + tcpm_log(port, "Apply_RC"); + port->tcpc->apply_rc(port->tcpc, port->cc_req, port->polarity); + tcpm_enable_auto_vbus_discharge(port, false); + } +} + +/* + * Determine RP value to set based on maximum current supported + * by a port if configured as source. + * Returns CC value to report to link partner. + */ +static enum typec_cc_status tcpm_rp_cc(struct tcpm_port *port) +{ + const u32 *src_pdo = port->src_pdo; + int nr_pdo = port->nr_src_pdo; + int i; + + if (!port->pd_supported) + return port->src_rp; + + /* + * Search for first entry with matching voltage. + * It should report the maximum supported current. + */ + for (i = 0; i < nr_pdo; i++) { + const u32 pdo = src_pdo[i]; + + if (pdo_type(pdo) == PDO_TYPE_FIXED && + pdo_fixed_voltage(pdo) == 5000) { + unsigned int curr = pdo_max_current(pdo); + + if (curr >= 3000) + return TYPEC_CC_RP_3_0; + else if (curr >= 1500) + return TYPEC_CC_RP_1_5; + return TYPEC_CC_RP_DEF; + } + } + + return TYPEC_CC_RP_DEF; +} + +static void tcpm_ams_finish(struct tcpm_port *port) +{ + tcpm_log(port, "AMS %s finished", tcpm_ams_str[port->ams]); + + if (port->pd_capable && port->pwr_role == TYPEC_SOURCE) { + if (port->negotiated_rev >= PD_REV30) + tcpm_set_cc(port, SINK_TX_OK); + else + tcpm_set_cc(port, SINK_TX_NG); + } else if (port->pwr_role == TYPEC_SOURCE) { + tcpm_set_cc(port, tcpm_rp_cc(port)); + } + + port->in_ams = false; + port->ams = NONE_AMS; +} + +static int tcpm_pd_transmit(struct tcpm_port *port, + enum tcpm_transmit_type type, + const struct pd_message *msg) +{ + unsigned long timeout; + int ret; + + if (msg) + tcpm_log(port, "PD TX, header: %#x", le16_to_cpu(msg->header)); + else + tcpm_log(port, "PD TX, type: %#x", type); + + reinit_completion(&port->tx_complete); + ret = port->tcpc->pd_transmit(port->tcpc, type, msg, port->negotiated_rev); + if (ret < 0) + return ret; + + mutex_unlock(&port->lock); + timeout = wait_for_completion_timeout(&port->tx_complete, + msecs_to_jiffies(PD_T_TCPC_TX_TIMEOUT)); + mutex_lock(&port->lock); + if (!timeout) + return -ETIMEDOUT; + + switch (port->tx_status) { + case TCPC_TX_SUCCESS: + port->message_id = (port->message_id + 1) & PD_HEADER_ID_MASK; + /* + * USB PD rev 2.0, 8.3.2.2.1: + * USB PD rev 3.0, 8.3.2.1.3: + * "... Note that every AMS is Interruptible until the first + * Message in the sequence has been successfully sent (GoodCRC + * Message received)." + */ + if (port->ams != NONE_AMS) + port->in_ams = true; + break; + case TCPC_TX_DISCARDED: + ret = -EAGAIN; + break; + case TCPC_TX_FAILED: + default: + ret = -EIO; + break; + } + + /* Some AMS don't expect responses. Finish them here. */ + if (port->ams == ATTENTION || port->ams == SOURCE_ALERT) + tcpm_ams_finish(port); + + return ret; +} + +void tcpm_pd_transmit_complete(struct tcpm_port *port, + enum tcpm_transmit_status status) +{ + tcpm_log(port, "PD TX complete, status: %u", status); + port->tx_status = status; + complete(&port->tx_complete); +} +EXPORT_SYMBOL_GPL(tcpm_pd_transmit_complete); + +static int tcpm_mux_set(struct tcpm_port *port, int state, + enum usb_role usb_role, + enum typec_orientation orientation) +{ + int ret; + + tcpm_log(port, "Requesting mux state %d, usb-role %d, orientation %d", + state, usb_role, orientation); + + ret = typec_set_orientation(port->typec_port, orientation); + if (ret) + return ret; + + if (port->role_sw) { + ret = usb_role_switch_set_role(port->role_sw, usb_role); + if (ret) + return ret; + } + + return typec_set_mode(port->typec_port, state); +} + +static int tcpm_set_polarity(struct tcpm_port *port, + enum typec_cc_polarity polarity) +{ + int ret; + + tcpm_log(port, "polarity %d", polarity); + + ret = port->tcpc->set_polarity(port->tcpc, polarity); + if (ret < 0) + return ret; + + port->polarity = polarity; + + return 0; +} + +static int tcpm_set_vconn(struct tcpm_port *port, bool enable) +{ + int ret; + + tcpm_log(port, "vconn:=%d", enable); + + ret = port->tcpc->set_vconn(port->tcpc, enable); + if (!ret) { + port->vconn_role = enable ? TYPEC_SOURCE : TYPEC_SINK; + typec_set_vconn_role(port->typec_port, port->vconn_role); + } + + return ret; +} + +static u32 tcpm_get_current_limit(struct tcpm_port *port) +{ + enum typec_cc_status cc; + u32 limit; + + cc = port->polarity ? port->cc2 : port->cc1; + switch (cc) { + case TYPEC_CC_RP_1_5: + limit = 1500; + break; + case TYPEC_CC_RP_3_0: + limit = 3000; + break; + case TYPEC_CC_RP_DEF: + default: + if (port->tcpc->get_current_limit) + limit = port->tcpc->get_current_limit(port->tcpc); + else + limit = 0; + break; + } + + return limit; +} + +static int tcpm_set_current_limit(struct tcpm_port *port, u32 max_ma, u32 mv) +{ + int ret = -EOPNOTSUPP; + + tcpm_log(port, "Setting voltage/current limit %u mV %u mA", mv, max_ma); + + port->supply_voltage = mv; + port->current_limit = max_ma; + power_supply_changed(port->psy); + + if (port->tcpc->set_current_limit) + ret = port->tcpc->set_current_limit(port->tcpc, max_ma, mv); + + return ret; +} + +static int tcpm_set_attached_state(struct tcpm_port *port, bool attached) +{ + return port->tcpc->set_roles(port->tcpc, attached, port->pwr_role, + port->data_role); +} + +static int tcpm_set_roles(struct tcpm_port *port, bool attached, + enum typec_role role, enum typec_data_role data) +{ + enum typec_orientation orientation; + enum usb_role usb_role; + int ret; + + if (port->polarity == TYPEC_POLARITY_CC1) + orientation = TYPEC_ORIENTATION_NORMAL; + else + orientation = TYPEC_ORIENTATION_REVERSE; + + if (port->typec_caps.data == TYPEC_PORT_DRD) { + if (data == TYPEC_HOST) + usb_role = USB_ROLE_HOST; + else + usb_role = USB_ROLE_DEVICE; + } else if (port->typec_caps.data == TYPEC_PORT_DFP) { + if (data == TYPEC_HOST) { + if (role == TYPEC_SOURCE) + usb_role = USB_ROLE_HOST; + else + usb_role = USB_ROLE_NONE; + } else { + return -ENOTSUPP; + } + } else { + if (data == TYPEC_DEVICE) { + if (role == TYPEC_SINK) + usb_role = USB_ROLE_DEVICE; + else + usb_role = USB_ROLE_NONE; + } else { + return -ENOTSUPP; + } + } + + ret = tcpm_mux_set(port, TYPEC_STATE_USB, usb_role, orientation); + if (ret < 0) + return ret; + + ret = port->tcpc->set_roles(port->tcpc, attached, role, data); + if (ret < 0) + return ret; + + port->pwr_role = role; + port->data_role = data; + typec_set_data_role(port->typec_port, data); + typec_set_pwr_role(port->typec_port, role); + + return 0; +} + +static int tcpm_set_pwr_role(struct tcpm_port *port, enum typec_role role) +{ + int ret; + + ret = port->tcpc->set_roles(port->tcpc, true, role, + port->data_role); + if (ret < 0) + return ret; + + port->pwr_role = role; + typec_set_pwr_role(port->typec_port, role); + + return 0; +} + +/* + * Transform the PDO to be compliant to PD rev2.0. + * Return 0 if the PDO type is not defined in PD rev2.0. + * Otherwise, return the converted PDO. + */ +static u32 tcpm_forge_legacy_pdo(struct tcpm_port *port, u32 pdo, enum typec_role role) +{ + switch (pdo_type(pdo)) { + case PDO_TYPE_FIXED: + if (role == TYPEC_SINK) + return pdo & ~PDO_FIXED_FRS_CURR_MASK; + else + return pdo & ~PDO_FIXED_UNCHUNK_EXT; + case PDO_TYPE_VAR: + case PDO_TYPE_BATT: + return pdo; + case PDO_TYPE_APDO: + default: + return 0; + } +} + +static int tcpm_pd_send_source_caps(struct tcpm_port *port) +{ + struct pd_message msg; + u32 pdo; + unsigned int i, nr_pdo = 0; + + memset(&msg, 0, sizeof(msg)); + + for (i = 0; i < port->nr_src_pdo; i++) { + if (port->negotiated_rev >= PD_REV30) { + msg.payload[nr_pdo++] = cpu_to_le32(port->src_pdo[i]); + } else { + pdo = tcpm_forge_legacy_pdo(port, port->src_pdo[i], TYPEC_SOURCE); + if (pdo) + msg.payload[nr_pdo++] = cpu_to_le32(pdo); + } + } + + if (!nr_pdo) { + /* No source capabilities defined, sink only */ + msg.header = PD_HEADER_LE(PD_CTRL_REJECT, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, 0); + } else { + msg.header = PD_HEADER_LE(PD_DATA_SOURCE_CAP, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, + nr_pdo); + } + + return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); +} + +static int tcpm_pd_send_sink_caps(struct tcpm_port *port) +{ + struct pd_message msg; + u32 pdo; + unsigned int i, nr_pdo = 0; + + memset(&msg, 0, sizeof(msg)); + + for (i = 0; i < port->nr_snk_pdo; i++) { + if (port->negotiated_rev >= PD_REV30) { + msg.payload[nr_pdo++] = cpu_to_le32(port->snk_pdo[i]); + } else { + pdo = tcpm_forge_legacy_pdo(port, port->snk_pdo[i], TYPEC_SINK); + if (pdo) + msg.payload[nr_pdo++] = cpu_to_le32(pdo); + } + } + + if (!nr_pdo) { + /* No sink capabilities defined, source only */ + msg.header = PD_HEADER_LE(PD_CTRL_REJECT, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, 0); + } else { + msg.header = PD_HEADER_LE(PD_DATA_SINK_CAP, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, + nr_pdo); + } + + return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); +} + +static void mod_tcpm_delayed_work(struct tcpm_port *port, unsigned int delay_ms) +{ + if (delay_ms) { + hrtimer_start(&port->state_machine_timer, ms_to_ktime(delay_ms), HRTIMER_MODE_REL); + } else { + hrtimer_cancel(&port->state_machine_timer); + kthread_queue_work(port->wq, &port->state_machine); + } +} + +static void mod_vdm_delayed_work(struct tcpm_port *port, unsigned int delay_ms) +{ + if (delay_ms) { + hrtimer_start(&port->vdm_state_machine_timer, ms_to_ktime(delay_ms), + HRTIMER_MODE_REL); + } else { + hrtimer_cancel(&port->vdm_state_machine_timer); + kthread_queue_work(port->wq, &port->vdm_state_machine); + } +} + +static void mod_enable_frs_delayed_work(struct tcpm_port *port, unsigned int delay_ms) +{ + if (delay_ms) { + hrtimer_start(&port->enable_frs_timer, ms_to_ktime(delay_ms), HRTIMER_MODE_REL); + } else { + hrtimer_cancel(&port->enable_frs_timer); + kthread_queue_work(port->wq, &port->enable_frs); + } +} + +static void mod_send_discover_delayed_work(struct tcpm_port *port, unsigned int delay_ms) +{ + if (delay_ms) { + hrtimer_start(&port->send_discover_timer, ms_to_ktime(delay_ms), HRTIMER_MODE_REL); + } else { + hrtimer_cancel(&port->send_discover_timer); + kthread_queue_work(port->wq, &port->send_discover_work); + } +} + +static void tcpm_set_state(struct tcpm_port *port, enum tcpm_state state, + unsigned int delay_ms) +{ + if (delay_ms) { + tcpm_log(port, "pending state change %s -> %s @ %u ms [%s %s]", + tcpm_states[port->state], tcpm_states[state], delay_ms, + pd_rev[port->negotiated_rev], tcpm_ams_str[port->ams]); + port->delayed_state = state; + mod_tcpm_delayed_work(port, delay_ms); + port->delayed_runtime = ktime_add(ktime_get(), ms_to_ktime(delay_ms)); + port->delay_ms = delay_ms; + } else { + tcpm_log(port, "state change %s -> %s [%s %s]", + tcpm_states[port->state], tcpm_states[state], + pd_rev[port->negotiated_rev], tcpm_ams_str[port->ams]); + port->delayed_state = INVALID_STATE; + port->prev_state = port->state; + port->state = state; + /* + * Don't re-queue the state machine work item if we're currently + * in the state machine and we're immediately changing states. + * tcpm_state_machine_work() will continue running the state + * machine. + */ + if (!port->state_machine_running) + mod_tcpm_delayed_work(port, 0); + } +} + +static void tcpm_set_state_cond(struct tcpm_port *port, enum tcpm_state state, + unsigned int delay_ms) +{ + if (port->enter_state == port->state) + tcpm_set_state(port, state, delay_ms); + else + tcpm_log(port, + "skipped %sstate change %s -> %s [%u ms], context state %s [%s %s]", + delay_ms ? "delayed " : "", + tcpm_states[port->state], tcpm_states[state], + delay_ms, tcpm_states[port->enter_state], + pd_rev[port->negotiated_rev], tcpm_ams_str[port->ams]); +} + +static void tcpm_queue_message(struct tcpm_port *port, + enum pd_msg_request message) +{ + port->queued_message = message; + mod_tcpm_delayed_work(port, 0); +} + +static bool tcpm_vdm_ams(struct tcpm_port *port) +{ + switch (port->ams) { + case DISCOVER_IDENTITY: + case SOURCE_STARTUP_CABLE_PLUG_DISCOVER_IDENTITY: + case DISCOVER_SVIDS: + case DISCOVER_MODES: + case DFP_TO_UFP_ENTER_MODE: + case DFP_TO_UFP_EXIT_MODE: + case DFP_TO_CABLE_PLUG_ENTER_MODE: + case DFP_TO_CABLE_PLUG_EXIT_MODE: + case ATTENTION: + case UNSTRUCTURED_VDMS: + case STRUCTURED_VDMS: + break; + default: + return false; + } + + return true; +} + +static bool tcpm_ams_interruptible(struct tcpm_port *port) +{ + switch (port->ams) { + /* Interruptible AMS */ + case NONE_AMS: + case SECURITY: + case FIRMWARE_UPDATE: + case DISCOVER_IDENTITY: + case SOURCE_STARTUP_CABLE_PLUG_DISCOVER_IDENTITY: + case DISCOVER_SVIDS: + case DISCOVER_MODES: + case DFP_TO_UFP_ENTER_MODE: + case DFP_TO_UFP_EXIT_MODE: + case DFP_TO_CABLE_PLUG_ENTER_MODE: + case DFP_TO_CABLE_PLUG_EXIT_MODE: + case UNSTRUCTURED_VDMS: + case STRUCTURED_VDMS: + case COUNTRY_INFO: + case COUNTRY_CODES: + break; + /* Non-Interruptible AMS */ + default: + if (port->in_ams) + return false; + break; + } + + return true; +} + +static int tcpm_ams_start(struct tcpm_port *port, enum tcpm_ams ams) +{ + int ret = 0; + + tcpm_log(port, "AMS %s start", tcpm_ams_str[ams]); + + if (!tcpm_ams_interruptible(port) && + !(ams == HARD_RESET || ams == SOFT_RESET_AMS)) { + port->upcoming_state = INVALID_STATE; + tcpm_log(port, "AMS %s not interruptible, aborting", + tcpm_ams_str[port->ams]); + return -EAGAIN; + } + + if (port->pwr_role == TYPEC_SOURCE) { + enum typec_cc_status cc_req = port->cc_req; + + port->ams = ams; + + if (ams == HARD_RESET) { + tcpm_set_cc(port, tcpm_rp_cc(port)); + tcpm_pd_transmit(port, TCPC_TX_HARD_RESET, NULL); + tcpm_set_state(port, HARD_RESET_START, 0); + return ret; + } else if (ams == SOFT_RESET_AMS) { + if (!port->explicit_contract) + tcpm_set_cc(port, tcpm_rp_cc(port)); + tcpm_set_state(port, SOFT_RESET_SEND, 0); + return ret; + } else if (tcpm_vdm_ams(port)) { + /* tSinkTx is enforced in vdm_run_state_machine */ + if (port->negotiated_rev >= PD_REV30) + tcpm_set_cc(port, SINK_TX_NG); + return ret; + } + + if (port->negotiated_rev >= PD_REV30) + tcpm_set_cc(port, SINK_TX_NG); + + switch (port->state) { + case SRC_READY: + case SRC_STARTUP: + case SRC_SOFT_RESET_WAIT_SNK_TX: + case SOFT_RESET: + case SOFT_RESET_SEND: + if (port->negotiated_rev >= PD_REV30) + tcpm_set_state(port, AMS_START, + cc_req == SINK_TX_OK ? + PD_T_SINK_TX : 0); + else + tcpm_set_state(port, AMS_START, 0); + break; + default: + if (port->negotiated_rev >= PD_REV30) + tcpm_set_state(port, SRC_READY, + cc_req == SINK_TX_OK ? + PD_T_SINK_TX : 0); + else + tcpm_set_state(port, SRC_READY, 0); + break; + } + } else { + if (port->negotiated_rev >= PD_REV30 && + !tcpm_sink_tx_ok(port) && + ams != SOFT_RESET_AMS && + ams != HARD_RESET) { + port->upcoming_state = INVALID_STATE; + tcpm_log(port, "Sink TX No Go"); + return -EAGAIN; + } + + port->ams = ams; + + if (ams == HARD_RESET) { + tcpm_pd_transmit(port, TCPC_TX_HARD_RESET, NULL); + tcpm_set_state(port, HARD_RESET_START, 0); + return ret; + } else if (tcpm_vdm_ams(port)) { + return ret; + } + + if (port->state == SNK_READY || + port->state == SNK_SOFT_RESET) + tcpm_set_state(port, AMS_START, 0); + else + tcpm_set_state(port, SNK_READY, 0); + } + + return ret; +} + +/* + * VDM/VDO handling functions + */ +static void tcpm_queue_vdm(struct tcpm_port *port, const u32 header, + const u32 *data, int cnt) +{ + u32 vdo_hdr = port->vdo_data[0]; + + WARN_ON(!mutex_is_locked(&port->lock)); + + /* If is sending discover_identity, handle received message first */ + if (PD_VDO_SVDM(vdo_hdr) && PD_VDO_CMD(vdo_hdr) == CMD_DISCOVER_IDENT) { + port->send_discover = true; + mod_send_discover_delayed_work(port, SEND_DISCOVER_RETRY_MS); + } else { + /* Make sure we are not still processing a previous VDM packet */ + WARN_ON(port->vdm_state > VDM_STATE_DONE); + } + + port->vdo_count = cnt + 1; + port->vdo_data[0] = header; + memcpy(&port->vdo_data[1], data, sizeof(u32) * cnt); + /* Set ready, vdm state machine will actually send */ + port->vdm_retries = 0; + port->vdm_state = VDM_STATE_READY; + port->vdm_sm_running = true; + + mod_vdm_delayed_work(port, 0); +} + +static void tcpm_queue_vdm_unlocked(struct tcpm_port *port, const u32 header, + const u32 *data, int cnt) +{ + mutex_lock(&port->lock); + tcpm_queue_vdm(port, header, data, cnt); + mutex_unlock(&port->lock); +} + +static void svdm_consume_identity(struct tcpm_port *port, const u32 *p, int cnt) +{ + u32 vdo = p[VDO_INDEX_IDH]; + u32 product = p[VDO_INDEX_PRODUCT]; + + memset(&port->mode_data, 0, sizeof(port->mode_data)); + + port->partner_ident.id_header = vdo; + port->partner_ident.cert_stat = p[VDO_INDEX_CSTAT]; + port->partner_ident.product = product; + + typec_partner_set_identity(port->partner); + + tcpm_log(port, "Identity: %04x:%04x.%04x", + PD_IDH_VID(vdo), + PD_PRODUCT_PID(product), product & 0xffff); +} + +static bool svdm_consume_svids(struct tcpm_port *port, const u32 *p, int cnt) +{ + struct pd_mode_data *pmdata = &port->mode_data; + int i; + + for (i = 1; i < cnt; i++) { + u16 svid; + + svid = (p[i] >> 16) & 0xffff; + if (!svid) + return false; + + if (pmdata->nsvids >= SVID_DISCOVERY_MAX) + goto abort; + + pmdata->svids[pmdata->nsvids++] = svid; + tcpm_log(port, "SVID %d: 0x%x", pmdata->nsvids, svid); + + svid = p[i] & 0xffff; + if (!svid) + return false; + + if (pmdata->nsvids >= SVID_DISCOVERY_MAX) + goto abort; + + pmdata->svids[pmdata->nsvids++] = svid; + tcpm_log(port, "SVID %d: 0x%x", pmdata->nsvids, svid); + } + + /* + * PD3.0 Spec 6.4.4.3.2: The SVIDs are returned 2 per VDO (see Table + * 6-43), and can be returned maximum 6 VDOs per response (see Figure + * 6-19). If the Respondersupports 12 or more SVID then the Discover + * SVIDs Command Shall be executed multiple times until a Discover + * SVIDs VDO is returned ending either with a SVID value of 0x0000 in + * the last part of the last VDO or with a VDO containing two SVIDs + * with values of 0x0000. + * + * However, some odd dockers support SVIDs less than 12 but without + * 0x0000 in the last VDO, so we need to break the Discover SVIDs + * request and return false here. + */ + return cnt == 7; +abort: + tcpm_log(port, "SVID_DISCOVERY_MAX(%d) too low!", SVID_DISCOVERY_MAX); + return false; +} + +static void svdm_consume_modes(struct tcpm_port *port, const u32 *p, int cnt) +{ + struct pd_mode_data *pmdata = &port->mode_data; + struct typec_altmode_desc *paltmode; + int i; + + if (pmdata->altmodes >= ARRAY_SIZE(port->partner_altmode)) { + /* Already logged in svdm_consume_svids() */ + return; + } + + for (i = 1; i < cnt; i++) { + paltmode = &pmdata->altmode_desc[pmdata->altmodes]; + memset(paltmode, 0, sizeof(*paltmode)); + + paltmode->svid = pmdata->svids[pmdata->svid_index]; + paltmode->mode = i; + paltmode->vdo = p[i]; + + tcpm_log(port, " Alternate mode %d: SVID 0x%04x, VDO %d: 0x%08x", + pmdata->altmodes, paltmode->svid, + paltmode->mode, paltmode->vdo); + + pmdata->altmodes++; + } +} + +static void tcpm_register_partner_altmodes(struct tcpm_port *port) +{ + struct pd_mode_data *modep = &port->mode_data; + struct typec_altmode *altmode; + int i; + + for (i = 0; i < modep->altmodes; i++) { + altmode = typec_partner_register_altmode(port->partner, + &modep->altmode_desc[i]); + if (IS_ERR(altmode)) { + tcpm_log(port, "Failed to register partner SVID 0x%04x", + modep->altmode_desc[i].svid); + altmode = NULL; + } + port->partner_altmode[i] = altmode; + } +} + +#define supports_modal(port) PD_IDH_MODAL_SUPP((port)->partner_ident.id_header) + +static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, + const u32 *p, int cnt, u32 *response, + enum adev_actions *adev_action) +{ + struct typec_port *typec = port->typec_port; + struct typec_altmode *pdev; + struct pd_mode_data *modep; + int svdm_version; + int rlen = 0; + int cmd_type; + int cmd; + int i; + + cmd_type = PD_VDO_CMDT(p[0]); + cmd = PD_VDO_CMD(p[0]); + + tcpm_log(port, "Rx VDM cmd 0x%x type %d cmd %d len %d", + p[0], cmd_type, cmd, cnt); + + modep = &port->mode_data; + + pdev = typec_match_altmode(port->partner_altmode, ALTMODE_DISCOVERY_MAX, + PD_VDO_VID(p[0]), PD_VDO_OPOS(p[0])); + + svdm_version = typec_get_negotiated_svdm_version(typec); + if (svdm_version < 0) + return 0; + + switch (cmd_type) { + case CMDT_INIT: + switch (cmd) { + case CMD_DISCOVER_IDENT: + if (PD_VDO_VID(p[0]) != USB_SID_PD) + break; + + if (IS_ERR_OR_NULL(port->partner)) + break; + + if (PD_VDO_SVDM_VER(p[0]) < svdm_version) { + typec_partner_set_svdm_version(port->partner, + PD_VDO_SVDM_VER(p[0])); + svdm_version = PD_VDO_SVDM_VER(p[0]); + } + + port->ams = DISCOVER_IDENTITY; + /* + * PD2.0 Spec 6.10.3: respond with NAK as DFP (data host) + * PD3.1 Spec 6.4.4.2.5.1: respond with NAK if "invalid field" or + * "wrong configuation" or "Unrecognized" + */ + if ((port->data_role == TYPEC_DEVICE || svdm_version >= SVDM_VER_2_0) && + port->nr_snk_vdo) { + if (svdm_version < SVDM_VER_2_0) { + for (i = 0; i < port->nr_snk_vdo_v1; i++) + response[i + 1] = port->snk_vdo_v1[i]; + rlen = port->nr_snk_vdo_v1 + 1; + + } else { + for (i = 0; i < port->nr_snk_vdo; i++) + response[i + 1] = port->snk_vdo[i]; + rlen = port->nr_snk_vdo + 1; + } + } + break; + case CMD_DISCOVER_SVID: + port->ams = DISCOVER_SVIDS; + break; + case CMD_DISCOVER_MODES: + port->ams = DISCOVER_MODES; + break; + case CMD_ENTER_MODE: + port->ams = DFP_TO_UFP_ENTER_MODE; + break; + case CMD_EXIT_MODE: + port->ams = DFP_TO_UFP_EXIT_MODE; + break; + case CMD_ATTENTION: + /* Attention command does not have response */ + *adev_action = ADEV_ATTENTION; + return 0; + default: + break; + } + if (rlen >= 1) { + response[0] = p[0] | VDO_CMDT(CMDT_RSP_ACK); + } else if (rlen == 0) { + response[0] = p[0] | VDO_CMDT(CMDT_RSP_NAK); + rlen = 1; + } else { + response[0] = p[0] | VDO_CMDT(CMDT_RSP_BUSY); + rlen = 1; + } + response[0] = (response[0] & ~VDO_SVDM_VERS_MASK) | + (VDO_SVDM_VERS(typec_get_negotiated_svdm_version(typec))); + break; + case CMDT_RSP_ACK: + /* silently drop message if we are not connected */ + if (IS_ERR_OR_NULL(port->partner)) + break; + + tcpm_ams_finish(port); + + switch (cmd) { + case CMD_DISCOVER_IDENT: + if (PD_VDO_SVDM_VER(p[0]) < svdm_version) + typec_partner_set_svdm_version(port->partner, + PD_VDO_SVDM_VER(p[0])); + /* 6.4.4.3.1 */ + svdm_consume_identity(port, p, cnt); + response[0] = VDO(USB_SID_PD, 1, typec_get_negotiated_svdm_version(typec), + CMD_DISCOVER_SVID); + rlen = 1; + break; + case CMD_DISCOVER_SVID: + /* 6.4.4.3.2 */ + if (svdm_consume_svids(port, p, cnt)) { + response[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_SVID); + rlen = 1; + } else if (modep->nsvids && supports_modal(port)) { + response[0] = VDO(modep->svids[0], 1, svdm_version, + CMD_DISCOVER_MODES); + rlen = 1; + } + break; + case CMD_DISCOVER_MODES: + /* 6.4.4.3.3 */ + svdm_consume_modes(port, p, cnt); + modep->svid_index++; + if (modep->svid_index < modep->nsvids) { + u16 svid = modep->svids[modep->svid_index]; + response[0] = VDO(svid, 1, svdm_version, CMD_DISCOVER_MODES); + rlen = 1; + } else { + tcpm_register_partner_altmodes(port); + } + break; + case CMD_ENTER_MODE: + if (adev && pdev) + *adev_action = ADEV_QUEUE_VDM_SEND_EXIT_MODE_ON_FAIL; + return 0; + case CMD_EXIT_MODE: + if (adev && pdev) { + /* Back to USB Operation */ + *adev_action = ADEV_NOTIFY_USB_AND_QUEUE_VDM; + return 0; + } + break; + case VDO_CMD_VENDOR(0) ... VDO_CMD_VENDOR(15): + break; + default: + /* Unrecognized SVDM */ + response[0] = p[0] | VDO_CMDT(CMDT_RSP_NAK); + rlen = 1; + response[0] = (response[0] & ~VDO_SVDM_VERS_MASK) | + (VDO_SVDM_VERS(svdm_version)); + break; + } + break; + case CMDT_RSP_NAK: + tcpm_ams_finish(port); + switch (cmd) { + case CMD_DISCOVER_IDENT: + case CMD_DISCOVER_SVID: + case CMD_DISCOVER_MODES: + case VDO_CMD_VENDOR(0) ... VDO_CMD_VENDOR(15): + break; + case CMD_ENTER_MODE: + /* Back to USB Operation */ + *adev_action = ADEV_NOTIFY_USB_AND_QUEUE_VDM; + return 0; + default: + /* Unrecognized SVDM */ + response[0] = p[0] | VDO_CMDT(CMDT_RSP_NAK); + rlen = 1; + response[0] = (response[0] & ~VDO_SVDM_VERS_MASK) | + (VDO_SVDM_VERS(svdm_version)); + break; + } + break; + default: + response[0] = p[0] | VDO_CMDT(CMDT_RSP_NAK); + rlen = 1; + response[0] = (response[0] & ~VDO_SVDM_VERS_MASK) | + (VDO_SVDM_VERS(svdm_version)); + break; + } + + /* Informing the alternate mode drivers about everything */ + *adev_action = ADEV_QUEUE_VDM; + return rlen; +} + +static void tcpm_pd_handle_msg(struct tcpm_port *port, + enum pd_msg_request message, + enum tcpm_ams ams); + +static void tcpm_handle_vdm_request(struct tcpm_port *port, + const __le32 *payload, int cnt) +{ + enum adev_actions adev_action = ADEV_NONE; + struct typec_altmode *adev; + u32 p[PD_MAX_PAYLOAD]; + u32 response[8] = { }; + int i, rlen = 0; + + for (i = 0; i < cnt; i++) + p[i] = le32_to_cpu(payload[i]); + + adev = typec_match_altmode(port->port_altmode, ALTMODE_DISCOVERY_MAX, + PD_VDO_VID(p[0]), PD_VDO_OPOS(p[0])); + + if (port->vdm_state == VDM_STATE_BUSY) { + /* If UFP responded busy retry after timeout */ + if (PD_VDO_CMDT(p[0]) == CMDT_RSP_BUSY) { + port->vdm_state = VDM_STATE_WAIT_RSP_BUSY; + port->vdo_retry = (p[0] & ~VDO_CMDT_MASK) | + CMDT_INIT; + mod_vdm_delayed_work(port, PD_T_VDM_BUSY); + return; + } + port->vdm_state = VDM_STATE_DONE; + } + + if (PD_VDO_SVDM(p[0]) && (adev || tcpm_vdm_ams(port) || port->nr_snk_vdo)) { + /* + * Here a SVDM is received (INIT or RSP or unknown). Set the vdm_sm_running in + * advance because we are dropping the lock but may send VDMs soon. + * For the cases of INIT received: + * - If no response to send, it will be cleared later in this function. + * - If there are responses to send, it will be cleared in the state machine. + * For the cases of RSP received: + * - If no further INIT to send, it will be cleared later in this function. + * - Otherwise, it will be cleared in the state machine if timeout or it will go + * back here until no further INIT to send. + * For the cases of unknown type received: + * - We will send NAK and the flag will be cleared in the state machine. + */ + port->vdm_sm_running = true; + rlen = tcpm_pd_svdm(port, adev, p, cnt, response, &adev_action); + } else { + if (port->negotiated_rev >= PD_REV30) + tcpm_pd_handle_msg(port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); + } + + /* + * We are done with any state stored in the port struct now, except + * for any port struct changes done by the tcpm_queue_vdm() call + * below, which is a separate operation. + * + * So we can safely release the lock here; and we MUST release the + * lock here to avoid an AB BA lock inversion: + * + * If we keep the lock here then the lock ordering in this path is: + * 1. tcpm_pd_rx_handler take the tcpm port lock + * 2. One of the typec_altmode_* calls below takes the alt-mode's lock + * + * And we also have this ordering: + * 1. alt-mode driver takes the alt-mode's lock + * 2. alt-mode driver calls tcpm_altmode_enter which takes the + * tcpm port lock + * + * Dropping our lock here avoids this. + */ + mutex_unlock(&port->lock); + + if (adev) { + switch (adev_action) { + case ADEV_NONE: + break; + case ADEV_NOTIFY_USB_AND_QUEUE_VDM: + WARN_ON(typec_altmode_notify(adev, TYPEC_STATE_USB, NULL)); + typec_altmode_vdm(adev, p[0], &p[1], cnt); + break; + case ADEV_QUEUE_VDM: + typec_altmode_vdm(adev, p[0], &p[1], cnt); + break; + case ADEV_QUEUE_VDM_SEND_EXIT_MODE_ON_FAIL: + if (typec_altmode_vdm(adev, p[0], &p[1], cnt)) { + int svdm_version = typec_get_negotiated_svdm_version( + port->typec_port); + if (svdm_version < 0) + break; + + response[0] = VDO(adev->svid, 1, svdm_version, + CMD_EXIT_MODE); + response[0] |= VDO_OPOS(adev->mode); + rlen = 1; + } + break; + case ADEV_ATTENTION: + if (typec_altmode_attention(adev, p[1])) + tcpm_log(port, "typec_altmode_attention no port partner altmode"); + break; + } + } + + /* + * We must re-take the lock here to balance the unlock in + * tcpm_pd_rx_handler, note that no changes, other then the + * tcpm_queue_vdm call, are made while the lock is held again. + * All that is done after the call is unwinding the call stack until + * we return to tcpm_pd_rx_handler and do the unlock there. + */ + mutex_lock(&port->lock); + + if (rlen > 0) + tcpm_queue_vdm(port, response[0], &response[1], rlen - 1); + else + port->vdm_sm_running = false; +} + +static void tcpm_send_vdm(struct tcpm_port *port, u32 vid, int cmd, + const u32 *data, int count) +{ + int svdm_version = typec_get_negotiated_svdm_version(port->typec_port); + u32 header; + + if (svdm_version < 0) + return; + + if (WARN_ON(count > VDO_MAX_SIZE - 1)) + count = VDO_MAX_SIZE - 1; + + /* set VDM header with VID & CMD */ + header = VDO(vid, ((vid & USB_SID_PD) == USB_SID_PD) ? + 1 : (PD_VDO_CMD(cmd) <= CMD_ATTENTION), + svdm_version, cmd); + tcpm_queue_vdm(port, header, data, count); +} + +static unsigned int vdm_ready_timeout(u32 vdm_hdr) +{ + unsigned int timeout; + int cmd = PD_VDO_CMD(vdm_hdr); + + /* its not a structured VDM command */ + if (!PD_VDO_SVDM(vdm_hdr)) + return PD_T_VDM_UNSTRUCTURED; + + switch (PD_VDO_CMDT(vdm_hdr)) { + case CMDT_INIT: + if (cmd == CMD_ENTER_MODE || cmd == CMD_EXIT_MODE) + timeout = PD_T_VDM_WAIT_MODE_E; + else + timeout = PD_T_VDM_SNDR_RSP; + break; + default: + if (cmd == CMD_ENTER_MODE || cmd == CMD_EXIT_MODE) + timeout = PD_T_VDM_E_MODE; + else + timeout = PD_T_VDM_RCVR_RSP; + break; + } + return timeout; +} + +static void vdm_run_state_machine(struct tcpm_port *port) +{ + struct pd_message msg; + int i, res = 0; + u32 vdo_hdr = port->vdo_data[0]; + + switch (port->vdm_state) { + case VDM_STATE_READY: + /* Only transmit VDM if attached */ + if (!port->attached) { + port->vdm_state = VDM_STATE_ERR_BUSY; + break; + } + + /* + * if there's traffic or we're not in PDO ready state don't send + * a VDM. + */ + if (port->state != SRC_READY && port->state != SNK_READY) { + port->vdm_sm_running = false; + break; + } + + /* TODO: AMS operation for Unstructured VDM */ + if (PD_VDO_SVDM(vdo_hdr) && PD_VDO_CMDT(vdo_hdr) == CMDT_INIT) { + switch (PD_VDO_CMD(vdo_hdr)) { + case CMD_DISCOVER_IDENT: + res = tcpm_ams_start(port, DISCOVER_IDENTITY); + if (res == 0) { + port->send_discover = false; + } else if (res == -EAGAIN) { + port->vdo_data[0] = 0; + mod_send_discover_delayed_work(port, + SEND_DISCOVER_RETRY_MS); + } + break; + case CMD_DISCOVER_SVID: + res = tcpm_ams_start(port, DISCOVER_SVIDS); + break; + case CMD_DISCOVER_MODES: + res = tcpm_ams_start(port, DISCOVER_MODES); + break; + case CMD_ENTER_MODE: + res = tcpm_ams_start(port, DFP_TO_UFP_ENTER_MODE); + break; + case CMD_EXIT_MODE: + res = tcpm_ams_start(port, DFP_TO_UFP_EXIT_MODE); + break; + case CMD_ATTENTION: + res = tcpm_ams_start(port, ATTENTION); + break; + case VDO_CMD_VENDOR(0) ... VDO_CMD_VENDOR(15): + res = tcpm_ams_start(port, STRUCTURED_VDMS); + break; + default: + res = -EOPNOTSUPP; + break; + } + + if (res < 0) { + port->vdm_state = VDM_STATE_ERR_BUSY; + return; + } + } + + port->vdm_state = VDM_STATE_SEND_MESSAGE; + mod_vdm_delayed_work(port, (port->negotiated_rev >= PD_REV30 && + port->pwr_role == TYPEC_SOURCE && + PD_VDO_SVDM(vdo_hdr) && + PD_VDO_CMDT(vdo_hdr) == CMDT_INIT) ? + PD_T_SINK_TX : 0); + break; + case VDM_STATE_WAIT_RSP_BUSY: + port->vdo_data[0] = port->vdo_retry; + port->vdo_count = 1; + port->vdm_state = VDM_STATE_READY; + tcpm_ams_finish(port); + break; + case VDM_STATE_BUSY: + port->vdm_state = VDM_STATE_ERR_TMOUT; + if (port->ams != NONE_AMS) + tcpm_ams_finish(port); + break; + case VDM_STATE_ERR_SEND: + /* + * A partner which does not support USB PD will not reply, + * so this is not a fatal error. At the same time, some + * devices may not return GoodCRC under some circumstances, + * so we need to retry. + */ + if (port->vdm_retries < 3) { + tcpm_log(port, "VDM Tx error, retry"); + port->vdm_retries++; + port->vdm_state = VDM_STATE_READY; + if (PD_VDO_SVDM(vdo_hdr) && PD_VDO_CMDT(vdo_hdr) == CMDT_INIT) + tcpm_ams_finish(port); + } else { + tcpm_ams_finish(port); + } + break; + case VDM_STATE_SEND_MESSAGE: + /* Prepare and send VDM */ + memset(&msg, 0, sizeof(msg)); + msg.header = PD_HEADER_LE(PD_DATA_VENDOR_DEF, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, port->vdo_count); + for (i = 0; i < port->vdo_count; i++) + msg.payload[i] = cpu_to_le32(port->vdo_data[i]); + res = tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); + if (res < 0) { + port->vdm_state = VDM_STATE_ERR_SEND; + } else { + unsigned long timeout; + + port->vdm_retries = 0; + port->vdo_data[0] = 0; + port->vdm_state = VDM_STATE_BUSY; + timeout = vdm_ready_timeout(vdo_hdr); + mod_vdm_delayed_work(port, timeout); + } + break; + default: + break; + } +} + +static void vdm_state_machine_work(struct kthread_work *work) +{ + struct tcpm_port *port = container_of(work, struct tcpm_port, vdm_state_machine); + enum vdm_states prev_state; + + mutex_lock(&port->lock); + + /* + * Continue running as long as the port is not busy and there was + * a state change. + */ + do { + prev_state = port->vdm_state; + vdm_run_state_machine(port); + } while (port->vdm_state != prev_state && + port->vdm_state != VDM_STATE_BUSY && + port->vdm_state != VDM_STATE_SEND_MESSAGE); + + if (port->vdm_state < VDM_STATE_READY) + port->vdm_sm_running = false; + + mutex_unlock(&port->lock); +} + +enum pdo_err { + PDO_NO_ERR, + PDO_ERR_NO_VSAFE5V, + PDO_ERR_VSAFE5V_NOT_FIRST, + PDO_ERR_PDO_TYPE_NOT_IN_ORDER, + PDO_ERR_FIXED_NOT_SORTED, + PDO_ERR_VARIABLE_BATT_NOT_SORTED, + PDO_ERR_DUPE_PDO, + PDO_ERR_PPS_APDO_NOT_SORTED, + PDO_ERR_DUPE_PPS_APDO, +}; + +static const char * const pdo_err_msg[] = { + [PDO_ERR_NO_VSAFE5V] = + " err: source/sink caps should at least have vSafe5V", + [PDO_ERR_VSAFE5V_NOT_FIRST] = + " err: vSafe5V Fixed Supply Object Shall always be the first object", + [PDO_ERR_PDO_TYPE_NOT_IN_ORDER] = + " err: PDOs should be in the following order: Fixed; Battery; Variable", + [PDO_ERR_FIXED_NOT_SORTED] = + " err: Fixed supply pdos should be in increasing order of their fixed voltage", + [PDO_ERR_VARIABLE_BATT_NOT_SORTED] = + " err: Variable/Battery supply pdos should be in increasing order of their minimum voltage", + [PDO_ERR_DUPE_PDO] = + " err: Variable/Batt supply pdos cannot have same min/max voltage", + [PDO_ERR_PPS_APDO_NOT_SORTED] = + " err: Programmable power supply apdos should be in increasing order of their maximum voltage", + [PDO_ERR_DUPE_PPS_APDO] = + " err: Programmable power supply apdos cannot have same min/max voltage and max current", +}; + +static enum pdo_err tcpm_caps_err(struct tcpm_port *port, const u32 *pdo, + unsigned int nr_pdo) +{ + unsigned int i; + + /* Should at least contain vSafe5v */ + if (nr_pdo < 1) + return PDO_ERR_NO_VSAFE5V; + + /* The vSafe5V Fixed Supply Object Shall always be the first object */ + if (pdo_type(pdo[0]) != PDO_TYPE_FIXED || + pdo_fixed_voltage(pdo[0]) != VSAFE5V) + return PDO_ERR_VSAFE5V_NOT_FIRST; + + for (i = 1; i < nr_pdo; i++) { + if (pdo_type(pdo[i]) < pdo_type(pdo[i - 1])) { + return PDO_ERR_PDO_TYPE_NOT_IN_ORDER; + } else if (pdo_type(pdo[i]) == pdo_type(pdo[i - 1])) { + enum pd_pdo_type type = pdo_type(pdo[i]); + + switch (type) { + /* + * The remaining Fixed Supply Objects, if + * present, shall be sent in voltage order; + * lowest to highest. + */ + case PDO_TYPE_FIXED: + if (pdo_fixed_voltage(pdo[i]) <= + pdo_fixed_voltage(pdo[i - 1])) + return PDO_ERR_FIXED_NOT_SORTED; + break; + /* + * The Battery Supply Objects and Variable + * supply, if present shall be sent in Minimum + * Voltage order; lowest to highest. + */ + case PDO_TYPE_VAR: + case PDO_TYPE_BATT: + if (pdo_min_voltage(pdo[i]) < + pdo_min_voltage(pdo[i - 1])) + return PDO_ERR_VARIABLE_BATT_NOT_SORTED; + else if ((pdo_min_voltage(pdo[i]) == + pdo_min_voltage(pdo[i - 1])) && + (pdo_max_voltage(pdo[i]) == + pdo_max_voltage(pdo[i - 1]))) + return PDO_ERR_DUPE_PDO; + break; + /* + * The Programmable Power Supply APDOs, if present, + * shall be sent in Maximum Voltage order; + * lowest to highest. + */ + case PDO_TYPE_APDO: + if (pdo_apdo_type(pdo[i]) != APDO_TYPE_PPS) + break; + + if (pdo_pps_apdo_max_voltage(pdo[i]) < + pdo_pps_apdo_max_voltage(pdo[i - 1])) + return PDO_ERR_PPS_APDO_NOT_SORTED; + else if (pdo_pps_apdo_min_voltage(pdo[i]) == + pdo_pps_apdo_min_voltage(pdo[i - 1]) && + pdo_pps_apdo_max_voltage(pdo[i]) == + pdo_pps_apdo_max_voltage(pdo[i - 1]) && + pdo_pps_apdo_max_current(pdo[i]) == + pdo_pps_apdo_max_current(pdo[i - 1])) + return PDO_ERR_DUPE_PPS_APDO; + break; + default: + tcpm_log_force(port, " Unknown pdo type"); + } + } + } + + return PDO_NO_ERR; +} + +static int tcpm_validate_caps(struct tcpm_port *port, const u32 *pdo, + unsigned int nr_pdo) +{ + enum pdo_err err_index = tcpm_caps_err(port, pdo, nr_pdo); + + if (err_index != PDO_NO_ERR) { + tcpm_log_force(port, " %s", pdo_err_msg[err_index]); + return -EINVAL; + } + + return 0; +} + +static int tcpm_altmode_enter(struct typec_altmode *altmode, u32 *vdo) +{ + struct tcpm_port *port = typec_altmode_get_drvdata(altmode); + int svdm_version; + u32 header; + + svdm_version = typec_get_negotiated_svdm_version(port->typec_port); + if (svdm_version < 0) + return svdm_version; + + header = VDO(altmode->svid, vdo ? 2 : 1, svdm_version, CMD_ENTER_MODE); + header |= VDO_OPOS(altmode->mode); + + tcpm_queue_vdm_unlocked(port, header, vdo, vdo ? 1 : 0); + return 0; +} + +static int tcpm_altmode_exit(struct typec_altmode *altmode) +{ + struct tcpm_port *port = typec_altmode_get_drvdata(altmode); + int svdm_version; + u32 header; + + svdm_version = typec_get_negotiated_svdm_version(port->typec_port); + if (svdm_version < 0) + return svdm_version; + + header = VDO(altmode->svid, 1, svdm_version, CMD_EXIT_MODE); + header |= VDO_OPOS(altmode->mode); + + tcpm_queue_vdm_unlocked(port, header, NULL, 0); + return 0; +} + +static int tcpm_altmode_vdm(struct typec_altmode *altmode, + u32 header, const u32 *data, int count) +{ + struct tcpm_port *port = typec_altmode_get_drvdata(altmode); + + tcpm_queue_vdm_unlocked(port, header, data, count - 1); + + return 0; +} + +static const struct typec_altmode_ops tcpm_altmode_ops = { + .enter = tcpm_altmode_enter, + .exit = tcpm_altmode_exit, + .vdm = tcpm_altmode_vdm, +}; + +/* + * PD (data, control) command handling functions + */ +static inline enum tcpm_state ready_state(struct tcpm_port *port) +{ + if (port->pwr_role == TYPEC_SOURCE) + return SRC_READY; + else + return SNK_READY; +} + +static int tcpm_pd_send_control(struct tcpm_port *port, + enum pd_ctrl_msg_type type); + +static void tcpm_handle_alert(struct tcpm_port *port, const __le32 *payload, + int cnt) +{ + u32 p0 = le32_to_cpu(payload[0]); + unsigned int type = usb_pd_ado_type(p0); + + if (!type) { + tcpm_log(port, "Alert message received with no type"); + tcpm_queue_message(port, PD_MSG_CTRL_NOT_SUPP); + return; + } + + /* Just handling non-battery alerts for now */ + if (!(type & USB_PD_ADO_TYPE_BATT_STATUS_CHANGE)) { + if (port->pwr_role == TYPEC_SOURCE) { + port->upcoming_state = GET_STATUS_SEND; + tcpm_ams_start(port, GETTING_SOURCE_SINK_STATUS); + } else { + /* + * Do not check SinkTxOk here in case the Source doesn't set its Rp to + * SinkTxOk in time. + */ + port->ams = GETTING_SOURCE_SINK_STATUS; + tcpm_set_state(port, GET_STATUS_SEND, 0); + } + } else { + tcpm_queue_message(port, PD_MSG_CTRL_NOT_SUPP); + } +} + +static int tcpm_set_auto_vbus_discharge_threshold(struct tcpm_port *port, + enum typec_pwr_opmode mode, bool pps_active, + u32 requested_vbus_voltage) +{ + int ret; + + if (!port->tcpc->set_auto_vbus_discharge_threshold) + return 0; + + ret = port->tcpc->set_auto_vbus_discharge_threshold(port->tcpc, mode, pps_active, + requested_vbus_voltage); + tcpm_log_force(port, + "set_auto_vbus_discharge_threshold mode:%d pps_active:%c vbus:%u ret:%d", + mode, pps_active ? 'y' : 'n', requested_vbus_voltage, ret); + + return ret; +} + +static void tcpm_pd_handle_state(struct tcpm_port *port, + enum tcpm_state state, + enum tcpm_ams ams, + unsigned int delay_ms) +{ + switch (port->state) { + case SRC_READY: + case SNK_READY: + port->ams = ams; + tcpm_set_state(port, state, delay_ms); + break; + /* 8.3.3.4.1.1 and 6.8.1 power transitioning */ + case SNK_TRANSITION_SINK: + case SNK_TRANSITION_SINK_VBUS: + case SRC_TRANSITION_SUPPLY: + tcpm_set_state(port, HARD_RESET_SEND, 0); + break; + default: + if (!tcpm_ams_interruptible(port)) { + tcpm_set_state(port, port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : + SNK_SOFT_RESET, + 0); + } else { + /* process the Message 6.8.1 */ + port->upcoming_state = state; + port->next_ams = ams; + tcpm_set_state(port, ready_state(port), delay_ms); + } + break; + } +} + +static void tcpm_pd_handle_msg(struct tcpm_port *port, + enum pd_msg_request message, + enum tcpm_ams ams) +{ + switch (port->state) { + case SRC_READY: + case SNK_READY: + port->ams = ams; + tcpm_queue_message(port, message); + break; + /* PD 3.0 Spec 8.3.3.4.1.1 and 6.8.1 */ + case SNK_TRANSITION_SINK: + case SNK_TRANSITION_SINK_VBUS: + case SRC_TRANSITION_SUPPLY: + tcpm_set_state(port, HARD_RESET_SEND, 0); + break; + default: + if (!tcpm_ams_interruptible(port)) { + tcpm_set_state(port, port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : + SNK_SOFT_RESET, + 0); + } else { + port->next_ams = ams; + tcpm_set_state(port, ready_state(port), 0); + /* 6.8.1 process the Message */ + tcpm_queue_message(port, message); + } + break; + } +} + +static int tcpm_register_source_caps(struct tcpm_port *port) +{ + struct usb_power_delivery_desc desc = { port->negotiated_rev }; + struct usb_power_delivery_capabilities_desc caps = { }; + struct usb_power_delivery_capabilities *cap; + + if (!port->partner_pd) + port->partner_pd = usb_power_delivery_register(NULL, &desc); + if (IS_ERR(port->partner_pd)) + return PTR_ERR(port->partner_pd); + + memcpy(caps.pdo, port->source_caps, sizeof(u32) * port->nr_source_caps); + caps.role = TYPEC_SOURCE; + + cap = usb_power_delivery_register_capabilities(port->partner_pd, &caps); + if (IS_ERR(cap)) + return PTR_ERR(cap); + + port->partner_source_caps = cap; + + return 0; +} + +static int tcpm_register_sink_caps(struct tcpm_port *port) +{ + struct usb_power_delivery_desc desc = { port->negotiated_rev }; + struct usb_power_delivery_capabilities_desc caps = { }; + struct usb_power_delivery_capabilities *cap; + + if (!port->partner_pd) + port->partner_pd = usb_power_delivery_register(NULL, &desc); + if (IS_ERR(port->partner_pd)) + return PTR_ERR(port->partner_pd); + + memcpy(caps.pdo, port->sink_caps, sizeof(u32) * port->nr_sink_caps); + caps.role = TYPEC_SINK; + + cap = usb_power_delivery_register_capabilities(port->partner_pd, &caps); + if (IS_ERR(cap)) + return PTR_ERR(cap); + + port->partner_sink_caps = cap; + + return 0; +} + +static void tcpm_pd_data_request(struct tcpm_port *port, + const struct pd_message *msg) +{ + enum pd_data_msg_type type = pd_header_type_le(msg->header); + unsigned int cnt = pd_header_cnt_le(msg->header); + unsigned int rev = pd_header_rev_le(msg->header); + unsigned int i; + enum frs_typec_current partner_frs_current; + bool frs_enable; + int ret; + + if (tcpm_vdm_ams(port) && type != PD_DATA_VENDOR_DEF) { + port->vdm_state = VDM_STATE_ERR_BUSY; + tcpm_ams_finish(port); + mod_vdm_delayed_work(port, 0); + } + + switch (type) { + case PD_DATA_SOURCE_CAP: + for (i = 0; i < cnt; i++) + port->source_caps[i] = le32_to_cpu(msg->payload[i]); + + port->nr_source_caps = cnt; + + tcpm_log_source_caps(port); + + tcpm_validate_caps(port, port->source_caps, + port->nr_source_caps); + + tcpm_register_source_caps(port); + + /* + * Adjust revision in subsequent message headers, as required, + * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't + * support Rev 1.0 so just do nothing in that scenario. + */ + if (rev == PD_REV10) { + if (port->ams == GET_SOURCE_CAPABILITIES) + tcpm_ams_finish(port); + break; + } + + if (rev < PD_MAX_REV) + port->negotiated_rev = rev; + + if (port->pwr_role == TYPEC_SOURCE) { + if (port->ams == GET_SOURCE_CAPABILITIES) + tcpm_pd_handle_state(port, SRC_READY, NONE_AMS, 0); + /* Unexpected Source Capabilities */ + else + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + } else if (port->state == SNK_WAIT_CAPABILITIES) { + /* + * This message may be received even if VBUS is not + * present. This is quite unexpected; see USB PD + * specification, sections 8.3.3.6.3.1 and 8.3.3.6.3.2. + * However, at the same time, we must be ready to + * receive this message and respond to it 15ms after + * receiving PS_RDY during power swap operations, no matter + * if VBUS is available or not (USB PD specification, + * section 6.5.9.2). + * So we need to accept the message either way, + * but be prepared to keep waiting for VBUS after it was + * handled. + */ + port->ams = POWER_NEGOTIATION; + port->in_ams = true; + tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0); + } else { + if (port->ams == GET_SOURCE_CAPABILITIES) + tcpm_ams_finish(port); + tcpm_pd_handle_state(port, SNK_NEGOTIATE_CAPABILITIES, + POWER_NEGOTIATION, 0); + } + break; + case PD_DATA_REQUEST: + /* + * Adjust revision in subsequent message headers, as required, + * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't + * support Rev 1.0 so just reject in that scenario. + */ + if (rev == PD_REV10) { + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + break; + } + + if (rev < PD_MAX_REV) + port->negotiated_rev = rev; + + if (port->pwr_role != TYPEC_SOURCE || cnt != 1) { + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + break; + } + + port->sink_request = le32_to_cpu(msg->payload[0]); + + if (port->vdm_sm_running && port->explicit_contract) { + tcpm_pd_handle_msg(port, PD_MSG_CTRL_WAIT, port->ams); + break; + } + + if (port->state == SRC_SEND_CAPABILITIES) + tcpm_set_state(port, SRC_NEGOTIATE_CAPABILITIES, 0); + else + tcpm_pd_handle_state(port, SRC_NEGOTIATE_CAPABILITIES, + POWER_NEGOTIATION, 0); + break; + case PD_DATA_SINK_CAP: + /* We don't do anything with this at the moment... */ + for (i = 0; i < cnt; i++) + port->sink_caps[i] = le32_to_cpu(msg->payload[i]); + + partner_frs_current = (port->sink_caps[0] & PDO_FIXED_FRS_CURR_MASK) >> + PDO_FIXED_FRS_CURR_SHIFT; + frs_enable = partner_frs_current && (partner_frs_current <= + port->new_source_frs_current); + tcpm_log(port, + "Port partner FRS capable partner_frs_current:%u port_frs_current:%u enable:%c", + partner_frs_current, port->new_source_frs_current, frs_enable ? 'y' : 'n'); + if (frs_enable) { + ret = port->tcpc->enable_frs(port->tcpc, true); + tcpm_log(port, "Enable FRS %s, ret:%d\n", ret ? "fail" : "success", ret); + } + + port->nr_sink_caps = cnt; + port->sink_cap_done = true; + tcpm_register_sink_caps(port); + + if (port->ams == GET_SINK_CAPABILITIES) + tcpm_set_state(port, ready_state(port), 0); + /* Unexpected Sink Capabilities */ + else + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + break; + case PD_DATA_VENDOR_DEF: + tcpm_handle_vdm_request(port, msg->payload, cnt); + break; + case PD_DATA_BIST: + port->bist_request = le32_to_cpu(msg->payload[0]); + tcpm_pd_handle_state(port, BIST_RX, BIST, 0); + break; + case PD_DATA_ALERT: + if (port->state != SRC_READY && port->state != SNK_READY) + tcpm_pd_handle_state(port, port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : SNK_SOFT_RESET, + NONE_AMS, 0); + else + tcpm_handle_alert(port, msg->payload, cnt); + break; + case PD_DATA_BATT_STATUS: + case PD_DATA_GET_COUNTRY_INFO: + /* Currently unsupported */ + tcpm_pd_handle_msg(port, port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + break; + default: + tcpm_pd_handle_msg(port, port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + tcpm_log(port, "Unrecognized data message type %#x", type); + break; + } +} + +static void tcpm_pps_complete(struct tcpm_port *port, int result) +{ + if (port->pps_pending) { + port->pps_status = result; + port->pps_pending = false; + complete(&port->pps_complete); + } +} + +static void tcpm_pd_ctrl_request(struct tcpm_port *port, + const struct pd_message *msg) +{ + enum pd_ctrl_msg_type type = pd_header_type_le(msg->header); + enum tcpm_state next_state; + + /* + * Stop VDM state machine if interrupted by other Messages while NOT_SUPP is allowed in + * VDM AMS if waiting for VDM responses and will be handled later. + */ + if (tcpm_vdm_ams(port) && type != PD_CTRL_NOT_SUPP && type != PD_CTRL_GOOD_CRC) { + port->vdm_state = VDM_STATE_ERR_BUSY; + tcpm_ams_finish(port); + mod_vdm_delayed_work(port, 0); + } + + switch (type) { + case PD_CTRL_GOOD_CRC: + case PD_CTRL_PING: + break; + case PD_CTRL_GET_SOURCE_CAP: + tcpm_pd_handle_msg(port, PD_MSG_DATA_SOURCE_CAP, GET_SOURCE_CAPABILITIES); + break; + case PD_CTRL_GET_SINK_CAP: + tcpm_pd_handle_msg(port, PD_MSG_DATA_SINK_CAP, GET_SINK_CAPABILITIES); + break; + case PD_CTRL_GOTO_MIN: + break; + case PD_CTRL_PS_RDY: + switch (port->state) { + case SNK_TRANSITION_SINK: + if (port->vbus_present) { + tcpm_set_current_limit(port, + port->req_current_limit, + port->req_supply_voltage); + port->explicit_contract = true; + tcpm_set_auto_vbus_discharge_threshold(port, + TYPEC_PWR_MODE_PD, + port->pps_data.active, + port->supply_voltage); + tcpm_set_state(port, SNK_READY, 0); + } else { + /* + * Seen after power swap. Keep waiting for VBUS + * in a transitional state. + */ + tcpm_set_state(port, + SNK_TRANSITION_SINK_VBUS, 0); + } + break; + case PR_SWAP_SRC_SNK_SOURCE_OFF_CC_DEBOUNCED: + tcpm_set_state(port, PR_SWAP_SRC_SNK_SINK_ON, 0); + break; + case PR_SWAP_SNK_SRC_SINK_OFF: + tcpm_set_state(port, PR_SWAP_SNK_SRC_SOURCE_ON, 0); + break; + case VCONN_SWAP_WAIT_FOR_VCONN: + tcpm_set_state(port, VCONN_SWAP_TURN_OFF_VCONN, 0); + break; + case FR_SWAP_SNK_SRC_TRANSITION_TO_OFF: + tcpm_set_state(port, FR_SWAP_SNK_SRC_NEW_SINK_READY, 0); + break; + default: + tcpm_pd_handle_state(port, + port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : + SNK_SOFT_RESET, + NONE_AMS, 0); + break; + } + break; + case PD_CTRL_REJECT: + case PD_CTRL_WAIT: + case PD_CTRL_NOT_SUPP: + switch (port->state) { + case SNK_NEGOTIATE_CAPABILITIES: + /* USB PD specification, Figure 8-43 */ + if (port->explicit_contract) + next_state = SNK_READY; + else + next_state = SNK_WAIT_CAPABILITIES; + + /* Threshold was relaxed before sending Request. Restore it back. */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_PD, + port->pps_data.active, + port->supply_voltage); + tcpm_set_state(port, next_state, 0); + break; + case SNK_NEGOTIATE_PPS_CAPABILITIES: + /* Revert data back from any requested PPS updates */ + port->pps_data.req_out_volt = port->supply_voltage; + port->pps_data.req_op_curr = port->current_limit; + port->pps_status = (type == PD_CTRL_WAIT ? + -EAGAIN : -EOPNOTSUPP); + + /* Threshold was relaxed before sending Request. Restore it back. */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_PD, + port->pps_data.active, + port->supply_voltage); + + tcpm_set_state(port, SNK_READY, 0); + break; + case DR_SWAP_SEND: + port->swap_status = (type == PD_CTRL_WAIT ? + -EAGAIN : -EOPNOTSUPP); + tcpm_set_state(port, DR_SWAP_CANCEL, 0); + break; + case PR_SWAP_SEND: + port->swap_status = (type == PD_CTRL_WAIT ? + -EAGAIN : -EOPNOTSUPP); + tcpm_set_state(port, PR_SWAP_CANCEL, 0); + break; + case VCONN_SWAP_SEND: + port->swap_status = (type == PD_CTRL_WAIT ? + -EAGAIN : -EOPNOTSUPP); + tcpm_set_state(port, VCONN_SWAP_CANCEL, 0); + break; + case FR_SWAP_SEND: + tcpm_set_state(port, FR_SWAP_CANCEL, 0); + break; + case GET_SINK_CAP: + port->sink_cap_done = true; + tcpm_set_state(port, ready_state(port), 0); + break; + /* + * Some port partners do not support GET_STATUS, avoid soft reset the link to + * prevent redundant power re-negotiation + */ + case GET_STATUS_SEND: + tcpm_set_state(port, ready_state(port), 0); + break; + case SRC_READY: + case SNK_READY: + if (port->vdm_state > VDM_STATE_READY) { + port->vdm_state = VDM_STATE_DONE; + if (tcpm_vdm_ams(port)) + tcpm_ams_finish(port); + mod_vdm_delayed_work(port, 0); + break; + } + fallthrough; + default: + tcpm_pd_handle_state(port, + port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : + SNK_SOFT_RESET, + NONE_AMS, 0); + break; + } + break; + case PD_CTRL_ACCEPT: + switch (port->state) { + case SNK_NEGOTIATE_CAPABILITIES: + port->pps_data.active = false; + tcpm_set_state(port, SNK_TRANSITION_SINK, 0); + break; + case SNK_NEGOTIATE_PPS_CAPABILITIES: + port->pps_data.active = true; + port->pps_data.min_volt = port->pps_data.req_min_volt; + port->pps_data.max_volt = port->pps_data.req_max_volt; + port->pps_data.max_curr = port->pps_data.req_max_curr; + port->req_supply_voltage = port->pps_data.req_out_volt; + port->req_current_limit = port->pps_data.req_op_curr; + power_supply_changed(port->psy); + tcpm_set_state(port, SNK_TRANSITION_SINK, 0); + break; + case SOFT_RESET_SEND: + if (port->ams == SOFT_RESET_AMS) + tcpm_ams_finish(port); + if (port->pwr_role == TYPEC_SOURCE) { + port->upcoming_state = SRC_SEND_CAPABILITIES; + tcpm_ams_start(port, POWER_NEGOTIATION); + } else { + tcpm_set_state(port, SNK_WAIT_CAPABILITIES, 0); + } + break; + case DR_SWAP_SEND: + tcpm_set_state(port, DR_SWAP_CHANGE_DR, 0); + break; + case PR_SWAP_SEND: + tcpm_set_state(port, PR_SWAP_START, 0); + break; + case VCONN_SWAP_SEND: + tcpm_set_state(port, VCONN_SWAP_START, 0); + break; + case FR_SWAP_SEND: + tcpm_set_state(port, FR_SWAP_SNK_SRC_TRANSITION_TO_OFF, 0); + break; + default: + tcpm_pd_handle_state(port, + port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : + SNK_SOFT_RESET, + NONE_AMS, 0); + break; + } + break; + case PD_CTRL_SOFT_RESET: + port->ams = SOFT_RESET_AMS; + tcpm_set_state(port, SOFT_RESET, 0); + break; + case PD_CTRL_DR_SWAP: + /* + * XXX + * 6.3.9: If an alternate mode is active, a request to swap + * alternate modes shall trigger a port reset. + */ + if (port->typec_caps.data != TYPEC_PORT_DRD) { + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + } else { + if (port->send_discover) { + tcpm_queue_message(port, PD_MSG_CTRL_WAIT); + break; + } + + tcpm_pd_handle_state(port, DR_SWAP_ACCEPT, DATA_ROLE_SWAP, 0); + } + break; + case PD_CTRL_PR_SWAP: + if (port->port_type != TYPEC_PORT_DRP) { + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + } else { + if (port->send_discover) { + tcpm_queue_message(port, PD_MSG_CTRL_WAIT); + break; + } + + tcpm_pd_handle_state(port, PR_SWAP_ACCEPT, POWER_ROLE_SWAP, 0); + } + break; + case PD_CTRL_VCONN_SWAP: + if (port->send_discover) { + tcpm_queue_message(port, PD_MSG_CTRL_WAIT); + break; + } + + tcpm_pd_handle_state(port, VCONN_SWAP_ACCEPT, VCONN_SWAP, 0); + break; + case PD_CTRL_GET_SOURCE_CAP_EXT: + case PD_CTRL_GET_STATUS: + case PD_CTRL_FR_SWAP: + case PD_CTRL_GET_PPS_STATUS: + case PD_CTRL_GET_COUNTRY_CODES: + /* Currently not supported */ + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + break; + default: + tcpm_pd_handle_msg(port, + port->negotiated_rev < PD_REV30 ? + PD_MSG_CTRL_REJECT : + PD_MSG_CTRL_NOT_SUPP, + NONE_AMS); + tcpm_log(port, "Unrecognized ctrl message type %#x", type); + break; + } +} + +static void tcpm_pd_ext_msg_request(struct tcpm_port *port, + const struct pd_message *msg) +{ + enum pd_ext_msg_type type = pd_header_type_le(msg->header); + unsigned int data_size = pd_ext_header_data_size_le(msg->ext_msg.header); + + /* stopping VDM state machine if interrupted by other Messages */ + if (tcpm_vdm_ams(port)) { + port->vdm_state = VDM_STATE_ERR_BUSY; + tcpm_ams_finish(port); + mod_vdm_delayed_work(port, 0); + } + + if (!(le16_to_cpu(msg->ext_msg.header) & PD_EXT_HDR_CHUNKED)) { + tcpm_pd_handle_msg(port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); + tcpm_log(port, "Unchunked extended messages unsupported"); + return; + } + + if (data_size > PD_EXT_MAX_CHUNK_DATA) { + tcpm_pd_handle_state(port, CHUNK_NOT_SUPP, NONE_AMS, PD_T_CHUNK_NOT_SUPP); + tcpm_log(port, "Chunk handling not yet supported"); + return; + } + + switch (type) { + case PD_EXT_STATUS: + case PD_EXT_PPS_STATUS: + if (port->ams == GETTING_SOURCE_SINK_STATUS) { + tcpm_ams_finish(port); + tcpm_set_state(port, ready_state(port), 0); + } else { + /* unexpected Status or PPS_Status Message */ + tcpm_pd_handle_state(port, port->pwr_role == TYPEC_SOURCE ? + SRC_SOFT_RESET_WAIT_SNK_TX : SNK_SOFT_RESET, + NONE_AMS, 0); + } + break; + case PD_EXT_SOURCE_CAP_EXT: + case PD_EXT_GET_BATT_CAP: + case PD_EXT_GET_BATT_STATUS: + case PD_EXT_BATT_CAP: + case PD_EXT_GET_MANUFACTURER_INFO: + case PD_EXT_MANUFACTURER_INFO: + case PD_EXT_SECURITY_REQUEST: + case PD_EXT_SECURITY_RESPONSE: + case PD_EXT_FW_UPDATE_REQUEST: + case PD_EXT_FW_UPDATE_RESPONSE: + case PD_EXT_COUNTRY_INFO: + case PD_EXT_COUNTRY_CODES: + tcpm_pd_handle_msg(port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); + break; + default: + tcpm_pd_handle_msg(port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); + tcpm_log(port, "Unrecognized extended message type %#x", type); + break; + } +} + +static void tcpm_pd_rx_handler(struct kthread_work *work) +{ + struct pd_rx_event *event = container_of(work, + struct pd_rx_event, work); + const struct pd_message *msg = &event->msg; + unsigned int cnt = pd_header_cnt_le(msg->header); + struct tcpm_port *port = event->port; + + mutex_lock(&port->lock); + + tcpm_log(port, "PD RX, header: %#x [%d]", le16_to_cpu(msg->header), + port->attached); + + if (port->attached) { + enum pd_ctrl_msg_type type = pd_header_type_le(msg->header); + unsigned int msgid = pd_header_msgid_le(msg->header); + + /* + * USB PD standard, 6.6.1.2: + * "... if MessageID value in a received Message is the + * same as the stored value, the receiver shall return a + * GoodCRC Message with that MessageID value and drop + * the Message (this is a retry of an already received + * Message). Note: this shall not apply to the Soft_Reset + * Message which always has a MessageID value of zero." + */ + if (msgid == port->rx_msgid && type != PD_CTRL_SOFT_RESET) + goto done; + port->rx_msgid = msgid; + + /* + * If both ends believe to be DFP/host, we have a data role + * mismatch. + */ + if (!!(le16_to_cpu(msg->header) & PD_HEADER_DATA_ROLE) == + (port->data_role == TYPEC_HOST)) { + tcpm_log(port, + "Data role mismatch, initiating error recovery"); + tcpm_set_state(port, ERROR_RECOVERY, 0); + } else { + if (le16_to_cpu(msg->header) & PD_HEADER_EXT_HDR) + tcpm_pd_ext_msg_request(port, msg); + else if (cnt) + tcpm_pd_data_request(port, msg); + else + tcpm_pd_ctrl_request(port, msg); + } + } + +done: + mutex_unlock(&port->lock); + kfree(event); +} + +void tcpm_pd_receive(struct tcpm_port *port, const struct pd_message *msg) +{ + struct pd_rx_event *event; + + event = kzalloc(sizeof(*event), GFP_ATOMIC); + if (!event) + return; + + kthread_init_work(&event->work, tcpm_pd_rx_handler); + event->port = port; + memcpy(&event->msg, msg, sizeof(*msg)); + kthread_queue_work(port->wq, &event->work); +} +EXPORT_SYMBOL_GPL(tcpm_pd_receive); + +static int tcpm_pd_send_control(struct tcpm_port *port, + enum pd_ctrl_msg_type type) +{ + struct pd_message msg; + + memset(&msg, 0, sizeof(msg)); + msg.header = PD_HEADER_LE(type, port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, 0); + + return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); +} + +/* + * Send queued message without affecting state. + * Return true if state machine should go back to sleep, + * false otherwise. + */ +static bool tcpm_send_queued_message(struct tcpm_port *port) +{ + enum pd_msg_request queued_message; + int ret; + + do { + queued_message = port->queued_message; + port->queued_message = PD_MSG_NONE; + + switch (queued_message) { + case PD_MSG_CTRL_WAIT: + tcpm_pd_send_control(port, PD_CTRL_WAIT); + break; + case PD_MSG_CTRL_REJECT: + tcpm_pd_send_control(port, PD_CTRL_REJECT); + break; + case PD_MSG_CTRL_NOT_SUPP: + tcpm_pd_send_control(port, PD_CTRL_NOT_SUPP); + break; + case PD_MSG_DATA_SINK_CAP: + ret = tcpm_pd_send_sink_caps(port); + if (ret < 0) { + tcpm_log(port, "Unable to send snk caps, ret=%d", ret); + tcpm_set_state(port, SNK_SOFT_RESET, 0); + } + tcpm_ams_finish(port); + break; + case PD_MSG_DATA_SOURCE_CAP: + ret = tcpm_pd_send_source_caps(port); + if (ret < 0) { + tcpm_log(port, + "Unable to send src caps, ret=%d", + ret); + tcpm_set_state(port, SOFT_RESET_SEND, 0); + } else if (port->pwr_role == TYPEC_SOURCE) { + tcpm_ams_finish(port); + tcpm_set_state(port, HARD_RESET_SEND, + PD_T_SENDER_RESPONSE); + } else { + tcpm_ams_finish(port); + } + break; + default: + break; + } + } while (port->queued_message != PD_MSG_NONE); + + if (port->delayed_state != INVALID_STATE) { + if (ktime_after(port->delayed_runtime, ktime_get())) { + mod_tcpm_delayed_work(port, ktime_to_ms(ktime_sub(port->delayed_runtime, + ktime_get()))); + return true; + } + port->delayed_state = INVALID_STATE; + } + return false; +} + +static int tcpm_pd_check_request(struct tcpm_port *port) +{ + u32 pdo, rdo = port->sink_request; + unsigned int max, op, pdo_max, index; + enum pd_pdo_type type; + + index = rdo_index(rdo); + if (!index || index > port->nr_src_pdo) + return -EINVAL; + + pdo = port->src_pdo[index - 1]; + type = pdo_type(pdo); + switch (type) { + case PDO_TYPE_FIXED: + case PDO_TYPE_VAR: + max = rdo_max_current(rdo); + op = rdo_op_current(rdo); + pdo_max = pdo_max_current(pdo); + + if (op > pdo_max) + return -EINVAL; + if (max > pdo_max && !(rdo & RDO_CAP_MISMATCH)) + return -EINVAL; + + if (type == PDO_TYPE_FIXED) + tcpm_log(port, + "Requested %u mV, %u mA for %u / %u mA", + pdo_fixed_voltage(pdo), pdo_max, op, max); + else + tcpm_log(port, + "Requested %u -> %u mV, %u mA for %u / %u mA", + pdo_min_voltage(pdo), pdo_max_voltage(pdo), + pdo_max, op, max); + break; + case PDO_TYPE_BATT: + max = rdo_max_power(rdo); + op = rdo_op_power(rdo); + pdo_max = pdo_max_power(pdo); + + if (op > pdo_max) + return -EINVAL; + if (max > pdo_max && !(rdo & RDO_CAP_MISMATCH)) + return -EINVAL; + tcpm_log(port, + "Requested %u -> %u mV, %u mW for %u / %u mW", + pdo_min_voltage(pdo), pdo_max_voltage(pdo), + pdo_max, op, max); + break; + default: + return -EINVAL; + } + + port->op_vsafe5v = index == 1; + + return 0; +} + +#define min_power(x, y) min(pdo_max_power(x), pdo_max_power(y)) +#define min_current(x, y) min(pdo_max_current(x), pdo_max_current(y)) + +static int tcpm_pd_select_pdo(struct tcpm_port *port, int *sink_pdo, + int *src_pdo) +{ + unsigned int i, j, max_src_mv = 0, min_src_mv = 0, max_mw = 0, + max_mv = 0, src_mw = 0, src_ma = 0, max_snk_mv = 0, + min_snk_mv = 0; + int ret = -EINVAL; + + port->pps_data.supported = false; + port->usb_type = POWER_SUPPLY_USB_TYPE_PD; + power_supply_changed(port->psy); + + /* + * Select the source PDO providing the most power which has a + * matchig sink cap. + */ + for (i = 0; i < port->nr_source_caps; i++) { + u32 pdo = port->source_caps[i]; + enum pd_pdo_type type = pdo_type(pdo); + + switch (type) { + case PDO_TYPE_FIXED: + max_src_mv = pdo_fixed_voltage(pdo); + min_src_mv = max_src_mv; + break; + case PDO_TYPE_BATT: + case PDO_TYPE_VAR: + max_src_mv = pdo_max_voltage(pdo); + min_src_mv = pdo_min_voltage(pdo); + break; + case PDO_TYPE_APDO: + if (pdo_apdo_type(pdo) == APDO_TYPE_PPS) { + port->pps_data.supported = true; + port->usb_type = + POWER_SUPPLY_USB_TYPE_PD_PPS; + power_supply_changed(port->psy); + } + continue; + default: + tcpm_log(port, "Invalid source PDO type, ignoring"); + continue; + } + + switch (type) { + case PDO_TYPE_FIXED: + case PDO_TYPE_VAR: + src_ma = pdo_max_current(pdo); + src_mw = src_ma * min_src_mv / 1000; + break; + case PDO_TYPE_BATT: + src_mw = pdo_max_power(pdo); + break; + case PDO_TYPE_APDO: + continue; + default: + tcpm_log(port, "Invalid source PDO type, ignoring"); + continue; + } + + for (j = 0; j < port->nr_snk_pdo; j++) { + pdo = port->snk_pdo[j]; + + switch (pdo_type(pdo)) { + case PDO_TYPE_FIXED: + max_snk_mv = pdo_fixed_voltage(pdo); + min_snk_mv = max_snk_mv; + break; + case PDO_TYPE_BATT: + case PDO_TYPE_VAR: + max_snk_mv = pdo_max_voltage(pdo); + min_snk_mv = pdo_min_voltage(pdo); + break; + case PDO_TYPE_APDO: + continue; + default: + tcpm_log(port, "Invalid sink PDO type, ignoring"); + continue; + } + + if (max_src_mv <= max_snk_mv && + min_src_mv >= min_snk_mv) { + /* Prefer higher voltages if available */ + if ((src_mw == max_mw && min_src_mv > max_mv) || + src_mw > max_mw) { + *src_pdo = i; + *sink_pdo = j; + max_mw = src_mw; + max_mv = min_src_mv; + ret = 0; + } + } + } + } + + return ret; +} + +static unsigned int tcpm_pd_select_pps_apdo(struct tcpm_port *port) +{ + unsigned int i, src_ma, max_temp_mw = 0, max_op_ma, op_mw; + unsigned int src_pdo = 0; + u32 pdo, src; + + for (i = 1; i < port->nr_source_caps; ++i) { + pdo = port->source_caps[i]; + + switch (pdo_type(pdo)) { + case PDO_TYPE_APDO: + if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) { + tcpm_log(port, "Not PPS APDO (source), ignoring"); + continue; + } + + if (port->pps_data.req_out_volt > pdo_pps_apdo_max_voltage(pdo) || + port->pps_data.req_out_volt < pdo_pps_apdo_min_voltage(pdo)) + continue; + + src_ma = pdo_pps_apdo_max_current(pdo); + max_op_ma = min(src_ma, port->pps_data.req_op_curr); + op_mw = max_op_ma * port->pps_data.req_out_volt / 1000; + if (op_mw > max_temp_mw) { + src_pdo = i; + max_temp_mw = op_mw; + } + break; + default: + tcpm_log(port, "Not APDO type (source), ignoring"); + continue; + } + } + + if (src_pdo) { + src = port->source_caps[src_pdo]; + + port->pps_data.req_min_volt = pdo_pps_apdo_min_voltage(src); + port->pps_data.req_max_volt = pdo_pps_apdo_max_voltage(src); + port->pps_data.req_max_curr = pdo_pps_apdo_max_current(src); + port->pps_data.req_op_curr = min(port->pps_data.req_max_curr, + port->pps_data.req_op_curr); + } + + return src_pdo; +} + +static int tcpm_pd_build_request(struct tcpm_port *port, u32 *rdo) +{ + unsigned int mv, ma, mw, flags; + unsigned int max_ma, max_mw; + enum pd_pdo_type type; + u32 pdo, matching_snk_pdo; + int src_pdo_index = 0; + int snk_pdo_index = 0; + int ret; + + ret = tcpm_pd_select_pdo(port, &snk_pdo_index, &src_pdo_index); + if (ret < 0) + return ret; + + pdo = port->source_caps[src_pdo_index]; + matching_snk_pdo = port->snk_pdo[snk_pdo_index]; + type = pdo_type(pdo); + + switch (type) { + case PDO_TYPE_FIXED: + mv = pdo_fixed_voltage(pdo); + break; + case PDO_TYPE_BATT: + case PDO_TYPE_VAR: + mv = pdo_min_voltage(pdo); + break; + default: + tcpm_log(port, "Invalid PDO selected!"); + return -EINVAL; + } + + /* Select maximum available current within the sink pdo's limit */ + if (type == PDO_TYPE_BATT) { + mw = min_power(pdo, matching_snk_pdo); + ma = 1000 * mw / mv; + } else { + ma = min_current(pdo, matching_snk_pdo); + mw = ma * mv / 1000; + } + + flags = RDO_USB_COMM | RDO_NO_SUSPEND; + + /* Set mismatch bit if offered power is less than operating power */ + max_ma = ma; + max_mw = mw; + if (mw < port->operating_snk_mw) { + flags |= RDO_CAP_MISMATCH; + if (type == PDO_TYPE_BATT && + (pdo_max_power(matching_snk_pdo) > pdo_max_power(pdo))) + max_mw = pdo_max_power(matching_snk_pdo); + else if (pdo_max_current(matching_snk_pdo) > + pdo_max_current(pdo)) + max_ma = pdo_max_current(matching_snk_pdo); + } + + tcpm_log(port, "cc=%d cc1=%d cc2=%d vbus=%d vconn=%s polarity=%d", + port->cc_req, port->cc1, port->cc2, port->vbus_source, + port->vconn_role == TYPEC_SOURCE ? "source" : "sink", + port->polarity); + + if (type == PDO_TYPE_BATT) { + *rdo = RDO_BATT(src_pdo_index + 1, mw, max_mw, flags); + + tcpm_log(port, "Requesting PDO %d: %u mV, %u mW%s", + src_pdo_index, mv, mw, + flags & RDO_CAP_MISMATCH ? " [mismatch]" : ""); + } else { + *rdo = RDO_FIXED(src_pdo_index + 1, ma, max_ma, flags); + + tcpm_log(port, "Requesting PDO %d: %u mV, %u mA%s", + src_pdo_index, mv, ma, + flags & RDO_CAP_MISMATCH ? " [mismatch]" : ""); + } + + port->req_current_limit = ma; + port->req_supply_voltage = mv; + + return 0; +} + +static int tcpm_pd_send_request(struct tcpm_port *port) +{ + struct pd_message msg; + int ret; + u32 rdo; + + ret = tcpm_pd_build_request(port, &rdo); + if (ret < 0) + return ret; + + /* + * Relax the threshold as voltage will be adjusted after Accept Message plus tSrcTransition. + * It is safer to modify the threshold here. + */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, false, 0); + + memset(&msg, 0, sizeof(msg)); + msg.header = PD_HEADER_LE(PD_DATA_REQUEST, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, 1); + msg.payload[0] = cpu_to_le32(rdo); + + return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); +} + +static int tcpm_pd_build_pps_request(struct tcpm_port *port, u32 *rdo) +{ + unsigned int out_mv, op_ma, op_mw, max_mv, max_ma, flags; + unsigned int src_pdo_index; + + src_pdo_index = tcpm_pd_select_pps_apdo(port); + if (!src_pdo_index) + return -EOPNOTSUPP; + + max_mv = port->pps_data.req_max_volt; + max_ma = port->pps_data.req_max_curr; + out_mv = port->pps_data.req_out_volt; + op_ma = port->pps_data.req_op_curr; + + flags = RDO_USB_COMM | RDO_NO_SUSPEND; + + op_mw = (op_ma * out_mv) / 1000; + if (op_mw < port->operating_snk_mw) { + /* + * Try raising current to meet power needs. If that's not enough + * then try upping the voltage. If that's still not enough + * then we've obviously chosen a PPS APDO which really isn't + * suitable so abandon ship. + */ + op_ma = (port->operating_snk_mw * 1000) / out_mv; + if ((port->operating_snk_mw * 1000) % out_mv) + ++op_ma; + op_ma += RDO_PROG_CURR_MA_STEP - (op_ma % RDO_PROG_CURR_MA_STEP); + + if (op_ma > max_ma) { + op_ma = max_ma; + out_mv = (port->operating_snk_mw * 1000) / op_ma; + if ((port->operating_snk_mw * 1000) % op_ma) + ++out_mv; + out_mv += RDO_PROG_VOLT_MV_STEP - + (out_mv % RDO_PROG_VOLT_MV_STEP); + + if (out_mv > max_mv) { + tcpm_log(port, "Invalid PPS APDO selected!"); + return -EINVAL; + } + } + } + + tcpm_log(port, "cc=%d cc1=%d cc2=%d vbus=%d vconn=%s polarity=%d", + port->cc_req, port->cc1, port->cc2, port->vbus_source, + port->vconn_role == TYPEC_SOURCE ? "source" : "sink", + port->polarity); + + *rdo = RDO_PROG(src_pdo_index + 1, out_mv, op_ma, flags); + + tcpm_log(port, "Requesting APDO %d: %u mV, %u mA", + src_pdo_index, out_mv, op_ma); + + port->pps_data.req_op_curr = op_ma; + port->pps_data.req_out_volt = out_mv; + + return 0; +} + +static int tcpm_pd_send_pps_request(struct tcpm_port *port) +{ + struct pd_message msg; + int ret; + u32 rdo; + + ret = tcpm_pd_build_pps_request(port, &rdo); + if (ret < 0) + return ret; + + /* Relax the threshold as voltage will be adjusted right after Accept Message. */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, false, 0); + + memset(&msg, 0, sizeof(msg)); + msg.header = PD_HEADER_LE(PD_DATA_REQUEST, + port->pwr_role, + port->data_role, + port->negotiated_rev, + port->message_id, 1); + msg.payload[0] = cpu_to_le32(rdo); + + return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg); +} + +static int tcpm_set_vbus(struct tcpm_port *port, bool enable) +{ + int ret; + + if (enable && port->vbus_charge) + return -EINVAL; + + tcpm_log(port, "vbus:=%d charge=%d", enable, port->vbus_charge); + + ret = port->tcpc->set_vbus(port->tcpc, enable, port->vbus_charge); + if (ret < 0) + return ret; + + port->vbus_source = enable; + return 0; +} + +static int tcpm_set_charge(struct tcpm_port *port, bool charge) +{ + int ret; + + if (charge && port->vbus_source) + return -EINVAL; + + if (charge != port->vbus_charge) { + tcpm_log(port, "vbus=%d charge:=%d", port->vbus_source, charge); + ret = port->tcpc->set_vbus(port->tcpc, port->vbus_source, + charge); + if (ret < 0) + return ret; + } + port->vbus_charge = charge; + power_supply_changed(port->psy); + return 0; +} + +static bool tcpm_start_toggling(struct tcpm_port *port, enum typec_cc_status cc) +{ + int ret; + + if (!port->tcpc->start_toggling) + return false; + + tcpm_log_force(port, "Start toggling"); + ret = port->tcpc->start_toggling(port->tcpc, port->port_type, cc); + return ret == 0; +} + +static int tcpm_init_vbus(struct tcpm_port *port) +{ + int ret; + + ret = port->tcpc->set_vbus(port->tcpc, false, false); + port->vbus_source = false; + port->vbus_charge = false; + return ret; +} + +static int tcpm_init_vconn(struct tcpm_port *port) +{ + int ret; + + ret = port->tcpc->set_vconn(port->tcpc, false); + port->vconn_role = TYPEC_SINK; + return ret; +} + +static void tcpm_typec_connect(struct tcpm_port *port) +{ + if (!port->connected) { + /* Make sure we don't report stale identity information */ + memset(&port->partner_ident, 0, sizeof(port->partner_ident)); + port->partner_desc.usb_pd = port->pd_capable; + if (tcpm_port_is_debug(port)) + port->partner_desc.accessory = TYPEC_ACCESSORY_DEBUG; + else if (tcpm_port_is_audio(port)) + port->partner_desc.accessory = TYPEC_ACCESSORY_AUDIO; + else + port->partner_desc.accessory = TYPEC_ACCESSORY_NONE; + port->partner = typec_register_partner(port->typec_port, + &port->partner_desc); + port->connected = true; + typec_partner_set_usb_power_delivery(port->partner, port->partner_pd); + } +} + +static int tcpm_src_attach(struct tcpm_port *port) +{ + enum typec_cc_polarity polarity = + port->cc2 == TYPEC_CC_RD ? TYPEC_POLARITY_CC2 + : TYPEC_POLARITY_CC1; + int ret; + + if (port->attached) + return 0; + + ret = tcpm_set_polarity(port, polarity); + if (ret < 0) + return ret; + + tcpm_enable_auto_vbus_discharge(port, true); + + ret = tcpm_set_roles(port, true, TYPEC_SOURCE, tcpm_data_role_for_source(port)); + if (ret < 0) + return ret; + + if (port->pd_supported) { + ret = port->tcpc->set_pd_rx(port->tcpc, true); + if (ret < 0) + goto out_disable_mux; + } + + /* + * USB Type-C specification, version 1.2, + * chapter 4.5.2.2.8.1 (Attached.SRC Requirements) + * Enable VCONN only if the non-RD port is set to RA. + */ + if ((polarity == TYPEC_POLARITY_CC1 && port->cc2 == TYPEC_CC_RA) || + (polarity == TYPEC_POLARITY_CC2 && port->cc1 == TYPEC_CC_RA)) { + ret = tcpm_set_vconn(port, true); + if (ret < 0) + goto out_disable_pd; + } + + ret = tcpm_set_vbus(port, true); + if (ret < 0) + goto out_disable_vconn; + + port->pd_capable = false; + + port->partner = NULL; + + port->attached = true; + port->send_discover = true; + + return 0; + +out_disable_vconn: + tcpm_set_vconn(port, false); +out_disable_pd: + if (port->pd_supported) + port->tcpc->set_pd_rx(port->tcpc, false); +out_disable_mux: + tcpm_mux_set(port, TYPEC_STATE_SAFE, USB_ROLE_NONE, + TYPEC_ORIENTATION_NONE); + return ret; +} + +static void tcpm_typec_disconnect(struct tcpm_port *port) +{ + if (port->connected) { + typec_partner_set_usb_power_delivery(port->partner, NULL); + typec_unregister_partner(port->partner); + port->partner = NULL; + port->connected = false; + } +} + +static void tcpm_unregister_altmodes(struct tcpm_port *port) +{ + struct pd_mode_data *modep = &port->mode_data; + int i; + + for (i = 0; i < modep->altmodes; i++) { + typec_unregister_altmode(port->partner_altmode[i]); + port->partner_altmode[i] = NULL; + } + + memset(modep, 0, sizeof(*modep)); +} + +static void tcpm_set_partner_usb_comm_capable(struct tcpm_port *port, bool capable) +{ + tcpm_log(port, "Setting usb_comm capable %s", capable ? "true" : "false"); + + if (port->tcpc->set_partner_usb_comm_capable) + port->tcpc->set_partner_usb_comm_capable(port->tcpc, capable); +} + +static void tcpm_reset_port(struct tcpm_port *port) +{ + tcpm_enable_auto_vbus_discharge(port, false); + port->in_ams = false; + port->ams = NONE_AMS; + port->vdm_sm_running = false; + tcpm_unregister_altmodes(port); + tcpm_typec_disconnect(port); + port->attached = false; + port->pd_capable = false; + port->pps_data.supported = false; + tcpm_set_partner_usb_comm_capable(port, false); + + /* + * First Rx ID should be 0; set this to a sentinel of -1 so that + * we can check tcpm_pd_rx_handler() if we had seen it before. + */ + port->rx_msgid = -1; + + port->tcpc->set_pd_rx(port->tcpc, false); + tcpm_init_vbus(port); /* also disables charging */ + tcpm_init_vconn(port); + tcpm_set_current_limit(port, 0, 0); + tcpm_set_polarity(port, TYPEC_POLARITY_CC1); + tcpm_mux_set(port, TYPEC_STATE_SAFE, USB_ROLE_NONE, + TYPEC_ORIENTATION_NONE); + tcpm_set_attached_state(port, false); + port->try_src_count = 0; + port->try_snk_count = 0; + port->usb_type = POWER_SUPPLY_USB_TYPE_C; + power_supply_changed(port->psy); + port->nr_sink_caps = 0; + port->sink_cap_done = false; + if (port->tcpc->enable_frs) + port->tcpc->enable_frs(port->tcpc, false); + + usb_power_delivery_unregister_capabilities(port->partner_sink_caps); + port->partner_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(port->partner_source_caps); + port->partner_source_caps = NULL; + usb_power_delivery_unregister(port->partner_pd); + port->partner_pd = NULL; +} + +static void tcpm_detach(struct tcpm_port *port) +{ + if (tcpm_port_is_disconnected(port)) + port->hard_reset_count = 0; + + port->try_src_count = 0; + port->try_snk_count = 0; + + if (!port->attached) + return; + + if (port->tcpc->set_bist_data) { + tcpm_log(port, "disable BIST MODE TESTDATA"); + port->tcpc->set_bist_data(port->tcpc, false); + } + + tcpm_reset_port(port); +} + +static void tcpm_src_detach(struct tcpm_port *port) +{ + tcpm_detach(port); +} + +static int tcpm_snk_attach(struct tcpm_port *port) +{ + int ret; + + if (port->attached) + return 0; + + ret = tcpm_set_polarity(port, port->cc2 != TYPEC_CC_OPEN ? + TYPEC_POLARITY_CC2 : TYPEC_POLARITY_CC1); + if (ret < 0) + return ret; + + tcpm_enable_auto_vbus_discharge(port, true); + + ret = tcpm_set_roles(port, true, TYPEC_SINK, tcpm_data_role_for_sink(port)); + if (ret < 0) + return ret; + + port->pd_capable = false; + + port->partner = NULL; + + port->attached = true; + port->send_discover = true; + + return 0; +} + +static void tcpm_snk_detach(struct tcpm_port *port) +{ + tcpm_detach(port); +} + +static int tcpm_acc_attach(struct tcpm_port *port) +{ + int ret; + + if (port->attached) + return 0; + + ret = tcpm_set_roles(port, true, TYPEC_SOURCE, + tcpm_data_role_for_source(port)); + if (ret < 0) + return ret; + + port->partner = NULL; + + tcpm_typec_connect(port); + + port->attached = true; + + return 0; +} + +static void tcpm_acc_detach(struct tcpm_port *port) +{ + tcpm_detach(port); +} + +static inline enum tcpm_state hard_reset_state(struct tcpm_port *port) +{ + if (port->hard_reset_count < PD_N_HARD_RESET_COUNT) + return HARD_RESET_SEND; + if (port->pd_capable) + return ERROR_RECOVERY; + if (port->pwr_role == TYPEC_SOURCE) + return SRC_UNATTACHED; + if (port->state == SNK_WAIT_CAPABILITIES) + return SNK_READY; + return SNK_UNATTACHED; +} + +static inline enum tcpm_state unattached_state(struct tcpm_port *port) +{ + if (port->port_type == TYPEC_PORT_DRP) { + if (port->pwr_role == TYPEC_SOURCE) + return SRC_UNATTACHED; + else + return SNK_UNATTACHED; + } else if (port->port_type == TYPEC_PORT_SRC) { + return SRC_UNATTACHED; + } + + return SNK_UNATTACHED; +} + +static void tcpm_swap_complete(struct tcpm_port *port, int result) +{ + if (port->swap_pending) { + port->swap_status = result; + port->swap_pending = false; + port->non_pd_role_swap = false; + complete(&port->swap_complete); + } +} + +static enum typec_pwr_opmode tcpm_get_pwr_opmode(enum typec_cc_status cc) +{ + switch (cc) { + case TYPEC_CC_RP_1_5: + return TYPEC_PWR_MODE_1_5A; + case TYPEC_CC_RP_3_0: + return TYPEC_PWR_MODE_3_0A; + case TYPEC_CC_RP_DEF: + default: + return TYPEC_PWR_MODE_USB; + } +} + +static enum typec_cc_status tcpm_pwr_opmode_to_rp(enum typec_pwr_opmode opmode) +{ + switch (opmode) { + case TYPEC_PWR_MODE_USB: + return TYPEC_CC_RP_DEF; + case TYPEC_PWR_MODE_1_5A: + return TYPEC_CC_RP_1_5; + case TYPEC_PWR_MODE_3_0A: + case TYPEC_PWR_MODE_PD: + default: + return TYPEC_CC_RP_3_0; + } +} + +static void tcpm_set_initial_svdm_version(struct tcpm_port *port) +{ + switch (port->negotiated_rev) { + case PD_REV30: + break; + /* + * 6.4.4.2.3 Structured VDM Version + * 2.0 states "At this time, there is only one version (1.0) defined. + * This field Shall be set to zero to indicate Version 1.0." + * 3.0 states "This field Shall be set to 01b to indicate Version 2.0." + * To ensure that we follow the Power Delivery revision we are currently + * operating on, downgrade the SVDM version to the highest one supported + * by the Power Delivery revision. + */ + case PD_REV20: + typec_partner_set_svdm_version(port->partner, SVDM_VER_1_0); + break; + default: + typec_partner_set_svdm_version(port->partner, SVDM_VER_1_0); + break; + } +} + +static void run_state_machine(struct tcpm_port *port) +{ + int ret; + enum typec_pwr_opmode opmode; + unsigned int msecs; + enum tcpm_state upcoming_state; + + if (port->tcpc->check_contaminant && port->state != CHECK_CONTAMINANT) + port->potential_contaminant = ((port->enter_state == SRC_ATTACH_WAIT && + port->state == SRC_UNATTACHED) || + (port->enter_state == SNK_ATTACH_WAIT && + port->state == SNK_UNATTACHED) || + (port->enter_state == SNK_DEBOUNCED && + port->state == SNK_UNATTACHED)); + + port->enter_state = port->state; + switch (port->state) { + case TOGGLING: + break; + case CHECK_CONTAMINANT: + port->tcpc->check_contaminant(port->tcpc); + break; + /* SRC states */ + case SRC_UNATTACHED: + if (!port->non_pd_role_swap) + tcpm_swap_complete(port, -ENOTCONN); + tcpm_src_detach(port); + if (port->potential_contaminant) { + tcpm_set_state(port, CHECK_CONTAMINANT, 0); + break; + } + if (tcpm_start_toggling(port, tcpm_rp_cc(port))) { + tcpm_set_state(port, TOGGLING, 0); + break; + } + tcpm_set_cc(port, tcpm_rp_cc(port)); + if (port->port_type == TYPEC_PORT_DRP) + tcpm_set_state(port, SNK_UNATTACHED, PD_T_DRP_SNK); + break; + case SRC_ATTACH_WAIT: + if (tcpm_port_is_debug(port)) + tcpm_set_state(port, DEBUG_ACC_ATTACHED, + PD_T_CC_DEBOUNCE); + else if (tcpm_port_is_audio(port)) + tcpm_set_state(port, AUDIO_ACC_ATTACHED, + PD_T_CC_DEBOUNCE); + else if (tcpm_port_is_source(port) && port->vbus_vsafe0v) + tcpm_set_state(port, + tcpm_try_snk(port) ? SNK_TRY + : SRC_ATTACHED, + PD_T_CC_DEBOUNCE); + break; + + case SNK_TRY: + port->try_snk_count++; + /* + * Requirements: + * - Do not drive vconn or vbus + * - Terminate CC pins (both) to Rd + * Action: + * - Wait for tDRPTry (PD_T_DRP_TRY). + * Until then, ignore any state changes. + */ + tcpm_set_cc(port, TYPEC_CC_RD); + tcpm_set_state(port, SNK_TRY_WAIT, PD_T_DRP_TRY); + break; + case SNK_TRY_WAIT: + if (tcpm_port_is_sink(port)) { + tcpm_set_state(port, SNK_TRY_WAIT_DEBOUNCE, 0); + } else { + tcpm_set_state(port, SRC_TRYWAIT, 0); + port->max_wait = 0; + } + break; + case SNK_TRY_WAIT_DEBOUNCE: + tcpm_set_state(port, SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS, + PD_T_TRY_CC_DEBOUNCE); + break; + case SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS: + if (port->vbus_present && tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_ATTACHED, 0); + else + port->max_wait = 0; + break; + case SRC_TRYWAIT: + tcpm_set_cc(port, tcpm_rp_cc(port)); + if (port->max_wait == 0) { + port->max_wait = jiffies + + msecs_to_jiffies(PD_T_DRP_TRY); + tcpm_set_state(port, SRC_TRYWAIT_UNATTACHED, + PD_T_DRP_TRY); + } else { + if (time_is_after_jiffies(port->max_wait)) + tcpm_set_state(port, SRC_TRYWAIT_UNATTACHED, + jiffies_to_msecs(port->max_wait - + jiffies)); + else + tcpm_set_state(port, SNK_UNATTACHED, 0); + } + break; + case SRC_TRYWAIT_DEBOUNCE: + tcpm_set_state(port, SRC_ATTACHED, PD_T_CC_DEBOUNCE); + break; + case SRC_TRYWAIT_UNATTACHED: + tcpm_set_state(port, SNK_UNATTACHED, 0); + break; + + case SRC_ATTACHED: + ret = tcpm_src_attach(port); + tcpm_set_state(port, SRC_UNATTACHED, + ret < 0 ? 0 : PD_T_PS_SOURCE_ON); + break; + case SRC_STARTUP: + opmode = tcpm_get_pwr_opmode(tcpm_rp_cc(port)); + typec_set_pwr_opmode(port->typec_port, opmode); + port->pwr_opmode = TYPEC_PWR_MODE_USB; + port->caps_count = 0; + port->negotiated_rev = PD_MAX_REV; + port->message_id = 0; + port->rx_msgid = -1; + port->explicit_contract = false; + /* SNK -> SRC POWER/FAST_ROLE_SWAP finished */ + if (port->ams == POWER_ROLE_SWAP || + port->ams == FAST_ROLE_SWAP) + tcpm_ams_finish(port); + if (!port->pd_supported) { + tcpm_set_state(port, SRC_READY, 0); + break; + } + port->upcoming_state = SRC_SEND_CAPABILITIES; + tcpm_ams_start(port, POWER_NEGOTIATION); + break; + case SRC_SEND_CAPABILITIES: + port->caps_count++; + if (port->caps_count > PD_N_CAPS_COUNT) { + tcpm_set_state(port, SRC_READY, 0); + break; + } + ret = tcpm_pd_send_source_caps(port); + if (ret < 0) { + tcpm_set_state(port, SRC_SEND_CAPABILITIES, + PD_T_SEND_SOURCE_CAP); + } else { + /* + * Per standard, we should clear the reset counter here. + * However, that can result in state machine hang-ups. + * Reset it only in READY state to improve stability. + */ + /* port->hard_reset_count = 0; */ + port->caps_count = 0; + port->pd_capable = true; + tcpm_set_state_cond(port, SRC_SEND_CAPABILITIES_TIMEOUT, + PD_T_SEND_SOURCE_CAP); + } + break; + case SRC_SEND_CAPABILITIES_TIMEOUT: + /* + * Error recovery for a PD_DATA_SOURCE_CAP reply timeout. + * + * PD 2.0 sinks are supposed to accept src-capabilities with a + * 3.0 header and simply ignore any src PDOs which the sink does + * not understand such as PPS but some 2.0 sinks instead ignore + * the entire PD_DATA_SOURCE_CAP message, causing contract + * negotiation to fail. + * + * After PD_N_HARD_RESET_COUNT hard-reset attempts, we try + * sending src-capabilities with a lower PD revision to + * make these broken sinks work. + */ + if (port->hard_reset_count < PD_N_HARD_RESET_COUNT) { + tcpm_set_state(port, HARD_RESET_SEND, 0); + } else if (port->negotiated_rev > PD_REV20) { + port->negotiated_rev--; + port->hard_reset_count = 0; + tcpm_set_state(port, SRC_SEND_CAPABILITIES, 0); + } else { + tcpm_set_state(port, hard_reset_state(port), 0); + } + break; + case SRC_NEGOTIATE_CAPABILITIES: + ret = tcpm_pd_check_request(port); + if (ret < 0) { + tcpm_pd_send_control(port, PD_CTRL_REJECT); + if (!port->explicit_contract) { + tcpm_set_state(port, + SRC_WAIT_NEW_CAPABILITIES, 0); + } else { + tcpm_set_state(port, SRC_READY, 0); + } + } else { + tcpm_pd_send_control(port, PD_CTRL_ACCEPT); + tcpm_set_partner_usb_comm_capable(port, + !!(port->sink_request & RDO_USB_COMM)); + tcpm_set_state(port, SRC_TRANSITION_SUPPLY, + PD_T_SRC_TRANSITION); + } + break; + case SRC_TRANSITION_SUPPLY: + /* XXX: regulator_set_voltage(vbus, ...) */ + tcpm_pd_send_control(port, PD_CTRL_PS_RDY); + port->explicit_contract = true; + typec_set_pwr_opmode(port->typec_port, TYPEC_PWR_MODE_PD); + port->pwr_opmode = TYPEC_PWR_MODE_PD; + tcpm_set_state_cond(port, SRC_READY, 0); + break; + case SRC_READY: +#if 1 + port->hard_reset_count = 0; +#endif + port->try_src_count = 0; + + tcpm_swap_complete(port, 0); + tcpm_typec_connect(port); + + if (port->ams != NONE_AMS) + tcpm_ams_finish(port); + if (port->next_ams != NONE_AMS) { + port->ams = port->next_ams; + port->next_ams = NONE_AMS; + } + + /* + * If previous AMS is interrupted, switch to the upcoming + * state. + */ + if (port->upcoming_state != INVALID_STATE) { + upcoming_state = port->upcoming_state; + port->upcoming_state = INVALID_STATE; + tcpm_set_state(port, upcoming_state, 0); + break; + } + + /* + * 6.4.4.3.1 Discover Identity + * "The Discover Identity Command Shall only be sent to SOP when there is an + * Explicit Contract." + * For now, this driver only supports SOP for DISCOVER_IDENTITY, thus using + * port->explicit_contract to decide whether to send the command. + */ + if (port->explicit_contract) { + tcpm_set_initial_svdm_version(port); + mod_send_discover_delayed_work(port, 0); + } else { + port->send_discover = false; + } + + /* + * 6.3.5 + * Sending ping messages is not necessary if + * - the source operates at vSafe5V + * or + * - The system is not operating in PD mode + * or + * - Both partners are connected using a Type-C connector + * + * There is no actual need to send PD messages since the local + * port type-c and the spec does not clearly say whether PD is + * possible when type-c is connected to Type-A/B + */ + break; + case SRC_WAIT_NEW_CAPABILITIES: + /* Nothing to do... */ + break; + + /* SNK states */ + case SNK_UNATTACHED: + if (!port->non_pd_role_swap) + tcpm_swap_complete(port, -ENOTCONN); + tcpm_pps_complete(port, -ENOTCONN); + tcpm_snk_detach(port); + if (port->potential_contaminant) { + tcpm_set_state(port, CHECK_CONTAMINANT, 0); + break; + } + if (tcpm_start_toggling(port, TYPEC_CC_RD)) { + tcpm_set_state(port, TOGGLING, 0); + break; + } + tcpm_set_cc(port, TYPEC_CC_RD); + if (port->port_type == TYPEC_PORT_DRP) + tcpm_set_state(port, SRC_UNATTACHED, PD_T_DRP_SRC); + break; + case SNK_ATTACH_WAIT: + if ((port->cc1 == TYPEC_CC_OPEN && + port->cc2 != TYPEC_CC_OPEN) || + (port->cc1 != TYPEC_CC_OPEN && + port->cc2 == TYPEC_CC_OPEN)) + tcpm_set_state(port, SNK_DEBOUNCED, + PD_T_CC_DEBOUNCE); + else if (tcpm_port_is_disconnected(port)) + tcpm_set_state(port, SNK_UNATTACHED, + PD_T_PD_DEBOUNCE); + break; + case SNK_DEBOUNCED: + if (tcpm_port_is_disconnected(port)) + tcpm_set_state(port, SNK_UNATTACHED, + PD_T_PD_DEBOUNCE); + else if (port->vbus_present) + tcpm_set_state(port, + tcpm_try_src(port) ? SRC_TRY + : SNK_ATTACHED, + 0); + break; + case SRC_TRY: + port->try_src_count++; + tcpm_set_cc(port, tcpm_rp_cc(port)); + port->max_wait = 0; + tcpm_set_state(port, SRC_TRY_WAIT, 0); + break; + case SRC_TRY_WAIT: + if (port->max_wait == 0) { + port->max_wait = jiffies + + msecs_to_jiffies(PD_T_DRP_TRY); + msecs = PD_T_DRP_TRY; + } else { + if (time_is_after_jiffies(port->max_wait)) + msecs = jiffies_to_msecs(port->max_wait - + jiffies); + else + msecs = 0; + } + tcpm_set_state(port, SNK_TRYWAIT, msecs); + break; + case SRC_TRY_DEBOUNCE: + tcpm_set_state(port, SRC_ATTACHED, PD_T_PD_DEBOUNCE); + break; + case SNK_TRYWAIT: + tcpm_set_cc(port, TYPEC_CC_RD); + tcpm_set_state(port, SNK_TRYWAIT_VBUS, PD_T_CC_DEBOUNCE); + break; + case SNK_TRYWAIT_VBUS: + /* + * TCPM stays in this state indefinitely until VBUS + * is detected as long as Rp is not detected for + * more than a time period of tPDDebounce. + */ + if (port->vbus_present && tcpm_port_is_sink(port)) { + tcpm_set_state(port, SNK_ATTACHED, 0); + break; + } + if (!tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_TRYWAIT_DEBOUNCE, 0); + break; + case SNK_TRYWAIT_DEBOUNCE: + tcpm_set_state(port, SNK_UNATTACHED, PD_T_PD_DEBOUNCE); + break; + case SNK_ATTACHED: + ret = tcpm_snk_attach(port); + if (ret < 0) + tcpm_set_state(port, SNK_UNATTACHED, 0); + else + tcpm_set_state(port, SNK_STARTUP, 0); + break; + case SNK_STARTUP: + opmode = tcpm_get_pwr_opmode(port->polarity ? + port->cc2 : port->cc1); + typec_set_pwr_opmode(port->typec_port, opmode); + port->pwr_opmode = TYPEC_PWR_MODE_USB; + port->negotiated_rev = PD_MAX_REV; + port->message_id = 0; + port->rx_msgid = -1; + port->explicit_contract = false; + + if (port->ams == POWER_ROLE_SWAP || + port->ams == FAST_ROLE_SWAP) + /* SRC -> SNK POWER/FAST_ROLE_SWAP finished */ + tcpm_ams_finish(port); + + tcpm_set_state(port, SNK_DISCOVERY, 0); + break; + case SNK_DISCOVERY: + if (port->vbus_present) { + u32 current_lim = tcpm_get_current_limit(port); + + if (port->slow_charger_loop && (current_lim > PD_P_SNK_STDBY_MW / 5)) + current_lim = PD_P_SNK_STDBY_MW / 5; + tcpm_set_current_limit(port, current_lim, 5000); + /* Not sink vbus if operational current is 0mA */ + tcpm_set_charge(port, !port->pd_supported || + pdo_max_current(port->snk_pdo[0])); + + if (!port->pd_supported) + tcpm_set_state(port, SNK_READY, 0); + else + tcpm_set_state(port, SNK_WAIT_CAPABILITIES, 0); + break; + } + /* + * For DRP, timeouts differ. Also, handling is supposed to be + * different and much more complex (dead battery detection; + * see USB power delivery specification, section 8.3.3.6.1.5.1). + */ + tcpm_set_state(port, hard_reset_state(port), + port->port_type == TYPEC_PORT_DRP ? + PD_T_DB_DETECT : PD_T_NO_RESPONSE); + break; + case SNK_DISCOVERY_DEBOUNCE: + tcpm_set_state(port, SNK_DISCOVERY_DEBOUNCE_DONE, + PD_T_CC_DEBOUNCE); + break; + case SNK_DISCOVERY_DEBOUNCE_DONE: + if (!tcpm_port_is_disconnected(port) && + tcpm_port_is_sink(port) && + ktime_after(port->delayed_runtime, ktime_get())) { + tcpm_set_state(port, SNK_DISCOVERY, + ktime_to_ms(ktime_sub(port->delayed_runtime, ktime_get()))); + break; + } + tcpm_set_state(port, unattached_state(port), 0); + break; + case SNK_WAIT_CAPABILITIES: + ret = port->tcpc->set_pd_rx(port->tcpc, true); + if (ret < 0) { + tcpm_set_state(port, SNK_READY, 0); + break; + } + /* + * If VBUS has never been low, and we time out waiting + * for source cap, try a soft reset first, in case we + * were already in a stable contract before this boot. + * Do this only once. + */ + if (port->vbus_never_low) { + port->vbus_never_low = false; + tcpm_set_state(port, SNK_SOFT_RESET, + PD_T_SINK_WAIT_CAP); + } else { + tcpm_set_state(port, hard_reset_state(port), + PD_T_SINK_WAIT_CAP); + } + break; + case SNK_NEGOTIATE_CAPABILITIES: + port->pd_capable = true; + tcpm_set_partner_usb_comm_capable(port, + !!(port->source_caps[0] & PDO_FIXED_USB_COMM)); + port->hard_reset_count = 0; + ret = tcpm_pd_send_request(port); + if (ret < 0) { + /* Restore back to the original state */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_PD, + port->pps_data.active, + port->supply_voltage); + /* Let the Source send capabilities again. */ + tcpm_set_state(port, SNK_WAIT_CAPABILITIES, 0); + } else { + tcpm_set_state_cond(port, hard_reset_state(port), + PD_T_SENDER_RESPONSE); + } + break; + case SNK_NEGOTIATE_PPS_CAPABILITIES: + ret = tcpm_pd_send_pps_request(port); + if (ret < 0) { + /* Restore back to the original state */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_PD, + port->pps_data.active, + port->supply_voltage); + port->pps_status = ret; + /* + * If this was called due to updates to sink + * capabilities, and pps is no longer valid, we should + * safely fall back to a standard PDO. + */ + if (port->update_sink_caps) + tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0); + else + tcpm_set_state(port, SNK_READY, 0); + } else { + tcpm_set_state_cond(port, hard_reset_state(port), + PD_T_SENDER_RESPONSE); + } + break; + case SNK_TRANSITION_SINK: + /* From the USB PD spec: + * "The Sink Shall transition to Sink Standby before a positive or + * negative voltage transition of VBUS. During Sink Standby + * the Sink Shall reduce its power draw to pSnkStdby." + * + * This is not applicable to PPS though as the port can continue + * to draw negotiated power without switching to standby. + */ + if (port->supply_voltage != port->req_supply_voltage && !port->pps_data.active && + port->current_limit * port->supply_voltage / 1000 > PD_P_SNK_STDBY_MW) { + u32 stdby_ma = PD_P_SNK_STDBY_MW * 1000 / port->supply_voltage; + + tcpm_log(port, "Setting standby current %u mV @ %u mA", + port->supply_voltage, stdby_ma); + tcpm_set_current_limit(port, stdby_ma, port->supply_voltage); + } + fallthrough; + case SNK_TRANSITION_SINK_VBUS: + tcpm_set_state(port, hard_reset_state(port), + PD_T_PS_TRANSITION); + break; + case SNK_READY: + port->try_snk_count = 0; + port->update_sink_caps = false; + if (port->explicit_contract) { + typec_set_pwr_opmode(port->typec_port, + TYPEC_PWR_MODE_PD); + port->pwr_opmode = TYPEC_PWR_MODE_PD; + } + + if (!port->pd_capable && port->slow_charger_loop) + tcpm_set_current_limit(port, tcpm_get_current_limit(port), 5000); + tcpm_swap_complete(port, 0); + tcpm_typec_connect(port); + mod_enable_frs_delayed_work(port, 0); + tcpm_pps_complete(port, port->pps_status); + + if (port->ams != NONE_AMS) + tcpm_ams_finish(port); + if (port->next_ams != NONE_AMS) { + port->ams = port->next_ams; + port->next_ams = NONE_AMS; + } + + /* + * If previous AMS is interrupted, switch to the upcoming + * state. + */ + if (port->upcoming_state != INVALID_STATE) { + upcoming_state = port->upcoming_state; + port->upcoming_state = INVALID_STATE; + tcpm_set_state(port, upcoming_state, 0); + break; + } + + /* + * 6.4.4.3.1 Discover Identity + * "The Discover Identity Command Shall only be sent to SOP when there is an + * Explicit Contract." + * For now, this driver only supports SOP for DISCOVER_IDENTITY, thus using + * port->explicit_contract. + */ + if (port->explicit_contract) { + tcpm_set_initial_svdm_version(port); + mod_send_discover_delayed_work(port, 0); + } else { + port->send_discover = false; + } + + power_supply_changed(port->psy); + break; + + /* Accessory states */ + case ACC_UNATTACHED: + tcpm_acc_detach(port); + tcpm_set_state(port, SRC_UNATTACHED, 0); + break; + case DEBUG_ACC_ATTACHED: + case AUDIO_ACC_ATTACHED: + ret = tcpm_acc_attach(port); + if (ret < 0) + tcpm_set_state(port, ACC_UNATTACHED, 0); + break; + case AUDIO_ACC_DEBOUNCE: + tcpm_set_state(port, ACC_UNATTACHED, PD_T_CC_DEBOUNCE); + break; + + /* Hard_Reset states */ + case HARD_RESET_SEND: + if (port->ams != NONE_AMS) + tcpm_ams_finish(port); + /* + * State machine will be directed to HARD_RESET_START, + * thus set upcoming_state to INVALID_STATE. + */ + port->upcoming_state = INVALID_STATE; + tcpm_ams_start(port, HARD_RESET); + break; + case HARD_RESET_START: + port->sink_cap_done = false; + if (port->tcpc->enable_frs) + port->tcpc->enable_frs(port->tcpc, false); + port->hard_reset_count++; + port->tcpc->set_pd_rx(port->tcpc, false); + tcpm_unregister_altmodes(port); + port->nr_sink_caps = 0; + port->send_discover = true; + if (port->pwr_role == TYPEC_SOURCE) + tcpm_set_state(port, SRC_HARD_RESET_VBUS_OFF, + PD_T_PS_HARD_RESET); + else + tcpm_set_state(port, SNK_HARD_RESET_SINK_OFF, 0); + break; + case SRC_HARD_RESET_VBUS_OFF: + /* + * 7.1.5 Response to Hard Resets + * Hard Reset Signaling indicates a communication failure has occurred and the + * Source Shall stop driving VCONN, Shall remove Rp from the VCONN pin and Shall + * drive VBUS to vSafe0V as shown in Figure 7-9. + */ + tcpm_set_vconn(port, false); + tcpm_set_vbus(port, false); + tcpm_set_roles(port, port->self_powered, TYPEC_SOURCE, + tcpm_data_role_for_source(port)); + /* + * If tcpc fails to notify vbus off, TCPM will wait for PD_T_SAFE_0V + + * PD_T_SRC_RECOVER before turning vbus back on. + * From Table 7-12 Sequence Description for a Source Initiated Hard Reset: + * 4. Policy Engine waits tPSHardReset after sending Hard Reset Signaling and then + * tells the Device Policy Manager to instruct the power supply to perform a + * Hard Reset. The transition to vSafe0V Shall occur within tSafe0V (t2). + * 5. After tSrcRecover the Source applies power to VBUS in an attempt to + * re-establish communication with the Sink and resume USB Default Operation. + * The transition to vSafe5V Shall occur within tSrcTurnOn(t4). + */ + tcpm_set_state(port, SRC_HARD_RESET_VBUS_ON, PD_T_SAFE_0V + PD_T_SRC_RECOVER); + break; + case SRC_HARD_RESET_VBUS_ON: + tcpm_set_vconn(port, true); + tcpm_set_vbus(port, true); + if (port->ams == HARD_RESET) + tcpm_ams_finish(port); + if (port->pd_supported) + port->tcpc->set_pd_rx(port->tcpc, true); + tcpm_set_attached_state(port, true); + tcpm_set_state(port, SRC_UNATTACHED, PD_T_PS_SOURCE_ON); + break; + case SNK_HARD_RESET_SINK_OFF: + /* Do not discharge/disconnect during hard reseet */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, false, 0); + memset(&port->pps_data, 0, sizeof(port->pps_data)); + tcpm_set_vconn(port, false); + if (port->pd_capable) + tcpm_set_charge(port, false); + tcpm_set_roles(port, port->self_powered, TYPEC_SINK, + tcpm_data_role_for_sink(port)); + /* + * VBUS may or may not toggle, depending on the adapter. + * If it doesn't toggle, transition to SNK_HARD_RESET_SINK_ON + * directly after timeout. + */ + tcpm_set_state(port, SNK_HARD_RESET_SINK_ON, PD_T_SAFE_0V); + break; + case SNK_HARD_RESET_WAIT_VBUS: + if (port->ams == HARD_RESET) + tcpm_ams_finish(port); + /* Assume we're disconnected if VBUS doesn't come back. */ + tcpm_set_state(port, SNK_UNATTACHED, + PD_T_SRC_RECOVER_MAX + PD_T_SRC_TURN_ON); + break; + case SNK_HARD_RESET_SINK_ON: + /* Note: There is no guarantee that VBUS is on in this state */ + /* + * XXX: + * The specification suggests that dual mode ports in sink + * mode should transition to state PE_SRC_Transition_to_default. + * See USB power delivery specification chapter 8.3.3.6.1.3. + * This would mean to + * - turn off VCONN, reset power supply + * - request hardware reset + * - turn on VCONN + * - Transition to state PE_Src_Startup + * SNK only ports shall transition to state Snk_Startup + * (see chapter 8.3.3.3.8). + * Similar, dual-mode ports in source mode should transition + * to PE_SNK_Transition_to_default. + */ + if (port->pd_capable) { + tcpm_set_current_limit(port, + tcpm_get_current_limit(port), + 5000); + /* Not sink vbus if operational current is 0mA */ + tcpm_set_charge(port, !!pdo_max_current(port->snk_pdo[0])); + } + if (port->ams == HARD_RESET) + tcpm_ams_finish(port); + tcpm_set_attached_state(port, true); + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, false, VSAFE5V); + tcpm_set_state(port, SNK_STARTUP, 0); + break; + + /* Soft_Reset states */ + case SOFT_RESET: + port->message_id = 0; + port->rx_msgid = -1; + /* remove existing capabilities */ + usb_power_delivery_unregister_capabilities(port->partner_source_caps); + port->partner_source_caps = NULL; + tcpm_pd_send_control(port, PD_CTRL_ACCEPT); + tcpm_ams_finish(port); + if (port->pwr_role == TYPEC_SOURCE) { + port->upcoming_state = SRC_SEND_CAPABILITIES; + tcpm_ams_start(port, POWER_NEGOTIATION); + } else { + tcpm_set_state(port, SNK_WAIT_CAPABILITIES, 0); + } + break; + case SRC_SOFT_RESET_WAIT_SNK_TX: + case SNK_SOFT_RESET: + if (port->ams != NONE_AMS) + tcpm_ams_finish(port); + port->upcoming_state = SOFT_RESET_SEND; + tcpm_ams_start(port, SOFT_RESET_AMS); + break; + case SOFT_RESET_SEND: + port->message_id = 0; + port->rx_msgid = -1; + /* remove existing capabilities */ + usb_power_delivery_unregister_capabilities(port->partner_source_caps); + port->partner_source_caps = NULL; + if (tcpm_pd_send_control(port, PD_CTRL_SOFT_RESET)) + tcpm_set_state_cond(port, hard_reset_state(port), 0); + else + tcpm_set_state_cond(port, hard_reset_state(port), + PD_T_SENDER_RESPONSE); + break; + + /* DR_Swap states */ + case DR_SWAP_SEND: + tcpm_pd_send_control(port, PD_CTRL_DR_SWAP); + if (port->data_role == TYPEC_DEVICE || port->negotiated_rev > PD_REV20) + port->send_discover = true; + tcpm_set_state_cond(port, DR_SWAP_SEND_TIMEOUT, + PD_T_SENDER_RESPONSE); + break; + case DR_SWAP_ACCEPT: + tcpm_pd_send_control(port, PD_CTRL_ACCEPT); + if (port->data_role == TYPEC_DEVICE || port->negotiated_rev > PD_REV20) + port->send_discover = true; + tcpm_set_state_cond(port, DR_SWAP_CHANGE_DR, 0); + break; + case DR_SWAP_SEND_TIMEOUT: + tcpm_swap_complete(port, -ETIMEDOUT); + port->send_discover = false; + tcpm_ams_finish(port); + tcpm_set_state(port, ready_state(port), 0); + break; + case DR_SWAP_CHANGE_DR: + tcpm_unregister_altmodes(port); + if (port->data_role == TYPEC_HOST) + tcpm_set_roles(port, true, port->pwr_role, + TYPEC_DEVICE); + else + tcpm_set_roles(port, true, port->pwr_role, + TYPEC_HOST); + tcpm_ams_finish(port); + tcpm_set_state(port, ready_state(port), 0); + break; + + case FR_SWAP_SEND: + if (tcpm_pd_send_control(port, PD_CTRL_FR_SWAP)) { + tcpm_set_state(port, ERROR_RECOVERY, 0); + break; + } + tcpm_set_state_cond(port, FR_SWAP_SEND_TIMEOUT, PD_T_SENDER_RESPONSE); + break; + case FR_SWAP_SEND_TIMEOUT: + tcpm_set_state(port, ERROR_RECOVERY, 0); + break; + case FR_SWAP_SNK_SRC_TRANSITION_TO_OFF: + tcpm_set_state(port, ERROR_RECOVERY, PD_T_PS_SOURCE_OFF); + break; + case FR_SWAP_SNK_SRC_NEW_SINK_READY: + if (port->vbus_source) + tcpm_set_state(port, FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED, 0); + else + tcpm_set_state(port, ERROR_RECOVERY, PD_T_RECEIVER_RESPONSE); + break; + case FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED: + tcpm_set_pwr_role(port, TYPEC_SOURCE); + if (tcpm_pd_send_control(port, PD_CTRL_PS_RDY)) { + tcpm_set_state(port, ERROR_RECOVERY, 0); + break; + } + tcpm_set_cc(port, tcpm_rp_cc(port)); + tcpm_set_state(port, SRC_STARTUP, PD_T_SWAP_SRC_START); + break; + + /* PR_Swap states */ + case PR_SWAP_ACCEPT: + tcpm_pd_send_control(port, PD_CTRL_ACCEPT); + tcpm_set_state(port, PR_SWAP_START, 0); + break; + case PR_SWAP_SEND: + tcpm_pd_send_control(port, PD_CTRL_PR_SWAP); + tcpm_set_state_cond(port, PR_SWAP_SEND_TIMEOUT, + PD_T_SENDER_RESPONSE); + break; + case PR_SWAP_SEND_TIMEOUT: + tcpm_swap_complete(port, -ETIMEDOUT); + tcpm_set_state(port, ready_state(port), 0); + break; + case PR_SWAP_START: + tcpm_apply_rc(port); + if (port->pwr_role == TYPEC_SOURCE) + tcpm_set_state(port, PR_SWAP_SRC_SNK_TRANSITION_OFF, + PD_T_SRC_TRANSITION); + else + tcpm_set_state(port, PR_SWAP_SNK_SRC_SINK_OFF, 0); + break; + case PR_SWAP_SRC_SNK_TRANSITION_OFF: + /* + * Prevent vbus discharge circuit from turning on during PR_SWAP + * as this is not a disconnect. + */ + tcpm_set_vbus(port, false); + port->explicit_contract = false; + /* allow time for Vbus discharge, must be < tSrcSwapStdby */ + tcpm_set_state(port, PR_SWAP_SRC_SNK_SOURCE_OFF, + PD_T_SRCSWAPSTDBY); + break; + case PR_SWAP_SRC_SNK_SOURCE_OFF: + tcpm_set_cc(port, TYPEC_CC_RD); + /* allow CC debounce */ + tcpm_set_state(port, PR_SWAP_SRC_SNK_SOURCE_OFF_CC_DEBOUNCED, + PD_T_CC_DEBOUNCE); + break; + case PR_SWAP_SRC_SNK_SOURCE_OFF_CC_DEBOUNCED: + /* + * USB-PD standard, 6.2.1.4, Port Power Role: + * "During the Power Role Swap Sequence, for the initial Source + * Port, the Port Power Role field shall be set to Sink in the + * PS_RDY Message indicating that the initial Source’s power + * supply is turned off" + */ + tcpm_set_pwr_role(port, TYPEC_SINK); + if (tcpm_pd_send_control(port, PD_CTRL_PS_RDY)) { + tcpm_set_state(port, ERROR_RECOVERY, 0); + break; + } + tcpm_set_state(port, ERROR_RECOVERY, PD_T_PS_SOURCE_ON_PRS); + break; + case PR_SWAP_SRC_SNK_SINK_ON: + tcpm_enable_auto_vbus_discharge(port, true); + /* Set the vbus disconnect threshold for implicit contract */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, false, VSAFE5V); + tcpm_set_state(port, SNK_STARTUP, 0); + break; + case PR_SWAP_SNK_SRC_SINK_OFF: + /* will be source, remove existing capabilities */ + usb_power_delivery_unregister_capabilities(port->partner_source_caps); + port->partner_source_caps = NULL; + /* + * Prevent vbus discharge circuit from turning on during PR_SWAP + * as this is not a disconnect. + */ + tcpm_set_auto_vbus_discharge_threshold(port, TYPEC_PWR_MODE_USB, + port->pps_data.active, 0); + tcpm_set_charge(port, false); + tcpm_set_state(port, hard_reset_state(port), + PD_T_PS_SOURCE_OFF); + break; + case PR_SWAP_SNK_SRC_SOURCE_ON: + tcpm_enable_auto_vbus_discharge(port, true); + tcpm_set_cc(port, tcpm_rp_cc(port)); + tcpm_set_vbus(port, true); + /* + * allow time VBUS ramp-up, must be < tNewSrc + * Also, this window overlaps with CC debounce as well. + * So, Wait for the max of two which is PD_T_NEWSRC + */ + tcpm_set_state(port, PR_SWAP_SNK_SRC_SOURCE_ON_VBUS_RAMPED_UP, + PD_T_NEWSRC); + break; + case PR_SWAP_SNK_SRC_SOURCE_ON_VBUS_RAMPED_UP: + /* + * USB PD standard, 6.2.1.4: + * "Subsequent Messages initiated by the Policy Engine, + * such as the PS_RDY Message sent to indicate that Vbus + * is ready, will have the Port Power Role field set to + * Source." + */ + tcpm_set_pwr_role(port, TYPEC_SOURCE); + tcpm_pd_send_control(port, PD_CTRL_PS_RDY); + tcpm_set_state(port, SRC_STARTUP, PD_T_SWAP_SRC_START); + break; + + case VCONN_SWAP_ACCEPT: + tcpm_pd_send_control(port, PD_CTRL_ACCEPT); + tcpm_ams_finish(port); + tcpm_set_state(port, VCONN_SWAP_START, 0); + break; + case VCONN_SWAP_SEND: + tcpm_pd_send_control(port, PD_CTRL_VCONN_SWAP); + tcpm_set_state(port, VCONN_SWAP_SEND_TIMEOUT, + PD_T_SENDER_RESPONSE); + break; + case VCONN_SWAP_SEND_TIMEOUT: + tcpm_swap_complete(port, -ETIMEDOUT); + tcpm_set_state(port, ready_state(port), 0); + break; + case VCONN_SWAP_START: + if (port->vconn_role == TYPEC_SOURCE) + tcpm_set_state(port, VCONN_SWAP_WAIT_FOR_VCONN, 0); + else + tcpm_set_state(port, VCONN_SWAP_TURN_ON_VCONN, 0); + break; + case VCONN_SWAP_WAIT_FOR_VCONN: + tcpm_set_state(port, hard_reset_state(port), + PD_T_VCONN_SOURCE_ON); + break; + case VCONN_SWAP_TURN_ON_VCONN: + tcpm_set_vconn(port, true); + tcpm_pd_send_control(port, PD_CTRL_PS_RDY); + tcpm_set_state(port, ready_state(port), 0); + break; + case VCONN_SWAP_TURN_OFF_VCONN: + tcpm_set_vconn(port, false); + tcpm_set_state(port, ready_state(port), 0); + break; + + case DR_SWAP_CANCEL: + case PR_SWAP_CANCEL: + case VCONN_SWAP_CANCEL: + tcpm_swap_complete(port, port->swap_status); + if (port->pwr_role == TYPEC_SOURCE) + tcpm_set_state(port, SRC_READY, 0); + else + tcpm_set_state(port, SNK_READY, 0); + break; + case FR_SWAP_CANCEL: + if (port->pwr_role == TYPEC_SOURCE) + tcpm_set_state(port, SRC_READY, 0); + else + tcpm_set_state(port, SNK_READY, 0); + break; + + case BIST_RX: + switch (BDO_MODE_MASK(port->bist_request)) { + case BDO_MODE_CARRIER2: + tcpm_pd_transmit(port, TCPC_TX_BIST_MODE_2, NULL); + tcpm_set_state(port, unattached_state(port), + PD_T_BIST_CONT_MODE); + break; + case BDO_MODE_TESTDATA: + if (port->tcpc->set_bist_data) { + tcpm_log(port, "Enable BIST MODE TESTDATA"); + port->tcpc->set_bist_data(port->tcpc, true); + } + break; + default: + break; + } + break; + case GET_STATUS_SEND: + tcpm_pd_send_control(port, PD_CTRL_GET_STATUS); + tcpm_set_state(port, GET_STATUS_SEND_TIMEOUT, + PD_T_SENDER_RESPONSE); + break; + case GET_STATUS_SEND_TIMEOUT: + tcpm_set_state(port, ready_state(port), 0); + break; + case GET_PPS_STATUS_SEND: + tcpm_pd_send_control(port, PD_CTRL_GET_PPS_STATUS); + tcpm_set_state(port, GET_PPS_STATUS_SEND_TIMEOUT, + PD_T_SENDER_RESPONSE); + break; + case GET_PPS_STATUS_SEND_TIMEOUT: + tcpm_set_state(port, ready_state(port), 0); + break; + case GET_SINK_CAP: + tcpm_pd_send_control(port, PD_CTRL_GET_SINK_CAP); + tcpm_set_state(port, GET_SINK_CAP_TIMEOUT, PD_T_SENDER_RESPONSE); + break; + case GET_SINK_CAP_TIMEOUT: + port->sink_cap_done = true; + tcpm_set_state(port, ready_state(port), 0); + break; + case ERROR_RECOVERY: + tcpm_swap_complete(port, -EPROTO); + tcpm_pps_complete(port, -EPROTO); + tcpm_set_state(port, PORT_RESET, 0); + break; + case PORT_RESET: + tcpm_reset_port(port); + tcpm_set_cc(port, tcpm_default_state(port) == SNK_UNATTACHED ? + TYPEC_CC_RD : tcpm_rp_cc(port)); + tcpm_set_state(port, PORT_RESET_WAIT_OFF, + PD_T_ERROR_RECOVERY); + break; + case PORT_RESET_WAIT_OFF: + tcpm_set_state(port, + tcpm_default_state(port), + port->vbus_present ? PD_T_PS_SOURCE_OFF : 0); + break; + + /* AMS intermediate state */ + case AMS_START: + if (port->upcoming_state == INVALID_STATE) { + tcpm_set_state(port, port->pwr_role == TYPEC_SOURCE ? + SRC_READY : SNK_READY, 0); + break; + } + + upcoming_state = port->upcoming_state; + port->upcoming_state = INVALID_STATE; + tcpm_set_state(port, upcoming_state, 0); + break; + + /* Chunk state */ + case CHUNK_NOT_SUPP: + tcpm_pd_send_control(port, PD_CTRL_NOT_SUPP); + tcpm_set_state(port, port->pwr_role == TYPEC_SOURCE ? SRC_READY : SNK_READY, 0); + break; + default: + WARN(1, "Unexpected port state %d\n", port->state); + break; + } +} + +static void tcpm_state_machine_work(struct kthread_work *work) +{ + struct tcpm_port *port = container_of(work, struct tcpm_port, state_machine); + enum tcpm_state prev_state; + + mutex_lock(&port->lock); + port->state_machine_running = true; + + if (port->queued_message && tcpm_send_queued_message(port)) + goto done; + + /* If we were queued due to a delayed state change, update it now */ + if (port->delayed_state) { + tcpm_log(port, "state change %s -> %s [delayed %ld ms]", + tcpm_states[port->state], + tcpm_states[port->delayed_state], port->delay_ms); + port->prev_state = port->state; + port->state = port->delayed_state; + port->delayed_state = INVALID_STATE; + } + + /* + * Continue running as long as we have (non-delayed) state changes + * to make. + */ + do { + prev_state = port->state; + run_state_machine(port); + if (port->queued_message) + tcpm_send_queued_message(port); + } while (port->state != prev_state && !port->delayed_state); + +done: + port->state_machine_running = false; + mutex_unlock(&port->lock); +} + +static void _tcpm_cc_change(struct tcpm_port *port, enum typec_cc_status cc1, + enum typec_cc_status cc2) +{ + enum typec_cc_status old_cc1, old_cc2; + enum tcpm_state new_state; + + old_cc1 = port->cc1; + old_cc2 = port->cc2; + port->cc1 = cc1; + port->cc2 = cc2; + + tcpm_log_force(port, + "CC1: %u -> %u, CC2: %u -> %u [state %s, polarity %d, %s]", + old_cc1, cc1, old_cc2, cc2, tcpm_states[port->state], + port->polarity, + tcpm_port_is_disconnected(port) ? "disconnected" + : "connected"); + + switch (port->state) { + case TOGGLING: + if (tcpm_port_is_debug(port) || tcpm_port_is_audio(port) || + tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_ATTACH_WAIT, 0); + else if (tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_ATTACH_WAIT, 0); + break; + case CHECK_CONTAMINANT: + /* Wait for Toggling to be resumed */ + break; + case SRC_UNATTACHED: + case ACC_UNATTACHED: + if (tcpm_port_is_debug(port) || tcpm_port_is_audio(port) || + tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_ATTACH_WAIT, 0); + break; + case SRC_ATTACH_WAIT: + if (tcpm_port_is_disconnected(port) || + tcpm_port_is_audio_detached(port)) + tcpm_set_state(port, SRC_UNATTACHED, 0); + else if (cc1 != old_cc1 || cc2 != old_cc2) + tcpm_set_state(port, SRC_ATTACH_WAIT, 0); + break; + case SRC_ATTACHED: + case SRC_STARTUP: + case SRC_SEND_CAPABILITIES: + case SRC_READY: + if (tcpm_port_is_disconnected(port) || + !tcpm_port_is_source(port)) { + if (port->port_type == TYPEC_PORT_SRC) + tcpm_set_state(port, SRC_UNATTACHED, tcpm_wait_for_discharge(port)); + else + tcpm_set_state(port, SNK_UNATTACHED, tcpm_wait_for_discharge(port)); + } + break; + case SNK_UNATTACHED: + if (tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_ATTACH_WAIT, 0); + break; + case SNK_ATTACH_WAIT: + if ((port->cc1 == TYPEC_CC_OPEN && + port->cc2 != TYPEC_CC_OPEN) || + (port->cc1 != TYPEC_CC_OPEN && + port->cc2 == TYPEC_CC_OPEN)) + new_state = SNK_DEBOUNCED; + else if (tcpm_port_is_disconnected(port)) + new_state = SNK_UNATTACHED; + else + break; + if (new_state != port->delayed_state) + tcpm_set_state(port, SNK_ATTACH_WAIT, 0); + break; + case SNK_DEBOUNCED: + if (tcpm_port_is_disconnected(port)) + new_state = SNK_UNATTACHED; + else if (port->vbus_present) + new_state = tcpm_try_src(port) ? SRC_TRY : SNK_ATTACHED; + else + new_state = SNK_UNATTACHED; + if (new_state != port->delayed_state) + tcpm_set_state(port, SNK_DEBOUNCED, 0); + break; + case SNK_READY: + /* + * EXIT condition is based primarily on vbus disconnect and CC is secondary. + * "A port that has entered into USB PD communications with the Source and + * has seen the CC voltage exceed vRd-USB may monitor the CC pin to detect + * cable disconnect in addition to monitoring VBUS. + * + * A port that is monitoring the CC voltage for disconnect (but is not in + * the process of a USB PD PR_Swap or USB PD FR_Swap) shall transition to + * Unattached.SNK within tSinkDisconnect after the CC voltage remains below + * vRd-USB for tPDDebounce." + * + * When set_auto_vbus_discharge_threshold is enabled, CC pins go + * away before vbus decays to disconnect threshold. Allow + * disconnect to be driven by vbus disconnect when auto vbus + * discharge is enabled. + */ + if (!port->auto_vbus_discharge_enabled && tcpm_port_is_disconnected(port)) + tcpm_set_state(port, unattached_state(port), 0); + else if (!port->pd_capable && + (cc1 != old_cc1 || cc2 != old_cc2)) + tcpm_set_current_limit(port, + tcpm_get_current_limit(port), + 5000); + break; + + case AUDIO_ACC_ATTACHED: + if (cc1 == TYPEC_CC_OPEN || cc2 == TYPEC_CC_OPEN) + tcpm_set_state(port, AUDIO_ACC_DEBOUNCE, 0); + break; + case AUDIO_ACC_DEBOUNCE: + if (tcpm_port_is_audio(port)) + tcpm_set_state(port, AUDIO_ACC_ATTACHED, 0); + break; + + case DEBUG_ACC_ATTACHED: + if (cc1 == TYPEC_CC_OPEN || cc2 == TYPEC_CC_OPEN) + tcpm_set_state(port, ACC_UNATTACHED, 0); + break; + + case SNK_TRY: + /* Do nothing, waiting for timeout */ + break; + + case SNK_DISCOVERY: + /* CC line is unstable, wait for debounce */ + if (tcpm_port_is_disconnected(port)) + tcpm_set_state(port, SNK_DISCOVERY_DEBOUNCE, 0); + break; + case SNK_DISCOVERY_DEBOUNCE: + break; + + case SRC_TRYWAIT: + /* Hand over to state machine if needed */ + if (!port->vbus_present && tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_TRYWAIT_DEBOUNCE, 0); + break; + case SRC_TRYWAIT_DEBOUNCE: + if (port->vbus_present || !tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_TRYWAIT, 0); + break; + case SNK_TRY_WAIT_DEBOUNCE: + if (!tcpm_port_is_sink(port)) { + port->max_wait = 0; + tcpm_set_state(port, SRC_TRYWAIT, 0); + } + break; + case SRC_TRY_WAIT: + if (tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_TRY_DEBOUNCE, 0); + break; + case SRC_TRY_DEBOUNCE: + tcpm_set_state(port, SRC_TRY_WAIT, 0); + break; + case SNK_TRYWAIT_DEBOUNCE: + if (tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_TRYWAIT_VBUS, 0); + break; + case SNK_TRYWAIT_VBUS: + if (!tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_TRYWAIT_DEBOUNCE, 0); + break; + case SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS: + if (!tcpm_port_is_sink(port)) + tcpm_set_state(port, SRC_TRYWAIT, PD_T_TRY_CC_DEBOUNCE); + else + tcpm_set_state(port, SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS, 0); + break; + case SNK_TRYWAIT: + /* Do nothing, waiting for tCCDebounce */ + break; + case PR_SWAP_SNK_SRC_SINK_OFF: + case PR_SWAP_SRC_SNK_TRANSITION_OFF: + case PR_SWAP_SRC_SNK_SOURCE_OFF: + case PR_SWAP_SRC_SNK_SOURCE_OFF_CC_DEBOUNCED: + case PR_SWAP_SNK_SRC_SOURCE_ON: + /* + * CC state change is expected in PR_SWAP + * Ignore it. + */ + break; + case FR_SWAP_SEND: + case FR_SWAP_SEND_TIMEOUT: + case FR_SWAP_SNK_SRC_TRANSITION_TO_OFF: + case FR_SWAP_SNK_SRC_NEW_SINK_READY: + case FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED: + /* Do nothing, CC change expected */ + break; + + case PORT_RESET: + case PORT_RESET_WAIT_OFF: + /* + * State set back to default mode once the timer completes. + * Ignore CC changes here. + */ + break; + default: + /* + * While acting as sink and auto vbus discharge is enabled, Allow disconnect + * to be driven by vbus disconnect. + */ + if (tcpm_port_is_disconnected(port) && !(port->pwr_role == TYPEC_SINK && + port->auto_vbus_discharge_enabled)) + tcpm_set_state(port, unattached_state(port), 0); + break; + } +} + +static void _tcpm_pd_vbus_on(struct tcpm_port *port) +{ + tcpm_log_force(port, "VBUS on"); + port->vbus_present = true; + /* + * When vbus_present is true i.e. Voltage at VBUS is greater than VSAFE5V implicitly + * states that vbus is not at VSAFE0V, hence clear the vbus_vsafe0v flag here. + */ + port->vbus_vsafe0v = false; + + switch (port->state) { + case SNK_TRANSITION_SINK_VBUS: + port->explicit_contract = true; + tcpm_set_state(port, SNK_READY, 0); + break; + case SNK_DISCOVERY: + tcpm_set_state(port, SNK_DISCOVERY, 0); + break; + + case SNK_DEBOUNCED: + tcpm_set_state(port, tcpm_try_src(port) ? SRC_TRY + : SNK_ATTACHED, + 0); + break; + case SNK_HARD_RESET_WAIT_VBUS: + tcpm_set_state(port, SNK_HARD_RESET_SINK_ON, 0); + break; + case SRC_ATTACHED: + tcpm_set_state(port, SRC_STARTUP, 0); + break; + case SRC_HARD_RESET_VBUS_ON: + tcpm_set_state(port, SRC_STARTUP, 0); + break; + + case SNK_TRY: + /* Do nothing, waiting for timeout */ + break; + case SRC_TRYWAIT: + /* Do nothing, Waiting for Rd to be detected */ + break; + case SRC_TRYWAIT_DEBOUNCE: + tcpm_set_state(port, SRC_TRYWAIT, 0); + break; + case SNK_TRY_WAIT_DEBOUNCE: + /* Do nothing, waiting for PD_DEBOUNCE to do be done */ + break; + case SNK_TRYWAIT: + /* Do nothing, waiting for tCCDebounce */ + break; + case SNK_TRYWAIT_VBUS: + if (tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_ATTACHED, 0); + break; + case SNK_TRYWAIT_DEBOUNCE: + /* Do nothing, waiting for Rp */ + break; + case SNK_TRY_WAIT_DEBOUNCE_CHECK_VBUS: + if (port->vbus_present && tcpm_port_is_sink(port)) + tcpm_set_state(port, SNK_ATTACHED, 0); + break; + case SRC_TRY_WAIT: + case SRC_TRY_DEBOUNCE: + /* Do nothing, waiting for sink detection */ + break; + case FR_SWAP_SEND: + case FR_SWAP_SEND_TIMEOUT: + case FR_SWAP_SNK_SRC_TRANSITION_TO_OFF: + case FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED: + if (port->tcpc->frs_sourcing_vbus) + port->tcpc->frs_sourcing_vbus(port->tcpc); + break; + case FR_SWAP_SNK_SRC_NEW_SINK_READY: + if (port->tcpc->frs_sourcing_vbus) + port->tcpc->frs_sourcing_vbus(port->tcpc); + tcpm_set_state(port, FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED, 0); + break; + + case PORT_RESET: + case PORT_RESET_WAIT_OFF: + /* + * State set back to default mode once the timer completes. + * Ignore vbus changes here. + */ + break; + + default: + break; + } +} + +static void _tcpm_pd_vbus_off(struct tcpm_port *port) +{ + tcpm_log_force(port, "VBUS off"); + port->vbus_present = false; + port->vbus_never_low = false; + switch (port->state) { + case SNK_HARD_RESET_SINK_OFF: + tcpm_set_state(port, SNK_HARD_RESET_WAIT_VBUS, 0); + break; + case HARD_RESET_SEND: + break; + case SNK_TRY: + /* Do nothing, waiting for timeout */ + break; + case SRC_TRYWAIT: + /* Hand over to state machine if needed */ + if (tcpm_port_is_source(port)) + tcpm_set_state(port, SRC_TRYWAIT_DEBOUNCE, 0); + break; + case SNK_TRY_WAIT_DEBOUNCE: + /* Do nothing, waiting for PD_DEBOUNCE to do be done */ + break; + case SNK_TRYWAIT: + case SNK_TRYWAIT_VBUS: + case SNK_TRYWAIT_DEBOUNCE: + break; + case SNK_ATTACH_WAIT: + case SNK_DEBOUNCED: + /* Do nothing, as TCPM is still waiting for vbus to reaach VSAFE5V to connect */ + break; + + case SNK_NEGOTIATE_CAPABILITIES: + break; + + case PR_SWAP_SRC_SNK_TRANSITION_OFF: + tcpm_set_state(port, PR_SWAP_SRC_SNK_SOURCE_OFF, 0); + break; + + case PR_SWAP_SNK_SRC_SINK_OFF: + /* Do nothing, expected */ + break; + + case PR_SWAP_SNK_SRC_SOURCE_ON: + /* + * Do nothing when vbus off notification is received. + * TCPM can wait for PD_T_NEWSRC in PR_SWAP_SNK_SRC_SOURCE_ON + * for the vbus source to ramp up. + */ + break; + + case PORT_RESET_WAIT_OFF: + tcpm_set_state(port, tcpm_default_state(port), 0); + break; + + case SRC_TRY_WAIT: + case SRC_TRY_DEBOUNCE: + /* Do nothing, waiting for sink detection */ + break; + + case SRC_STARTUP: + case SRC_SEND_CAPABILITIES: + case SRC_SEND_CAPABILITIES_TIMEOUT: + case SRC_NEGOTIATE_CAPABILITIES: + case SRC_TRANSITION_SUPPLY: + case SRC_READY: + case SRC_WAIT_NEW_CAPABILITIES: + /* + * Force to unattached state to re-initiate connection. + * DRP port should move to Unattached.SNK instead of Unattached.SRC if + * sink removed. Although sink removal here is due to source's vbus collapse, + * treat it the same way for consistency. + */ + if (port->port_type == TYPEC_PORT_SRC) + tcpm_set_state(port, SRC_UNATTACHED, tcpm_wait_for_discharge(port)); + else + tcpm_set_state(port, SNK_UNATTACHED, tcpm_wait_for_discharge(port)); + break; + + case PORT_RESET: + /* + * State set back to default mode once the timer completes. + * Ignore vbus changes here. + */ + break; + + case FR_SWAP_SEND: + case FR_SWAP_SEND_TIMEOUT: + case FR_SWAP_SNK_SRC_TRANSITION_TO_OFF: + case FR_SWAP_SNK_SRC_NEW_SINK_READY: + case FR_SWAP_SNK_SRC_SOURCE_VBUS_APPLIED: + /* Do nothing, vbus drop expected */ + break; + + case SNK_HARD_RESET_WAIT_VBUS: + /* Do nothing, its OK to receive vbus off events */ + break; + + default: + if (port->pwr_role == TYPEC_SINK && port->attached) + tcpm_set_state(port, SNK_UNATTACHED, tcpm_wait_for_discharge(port)); + break; + } +} + +static void _tcpm_pd_vbus_vsafe0v(struct tcpm_port *port) +{ + tcpm_log_force(port, "VBUS VSAFE0V"); + port->vbus_vsafe0v = true; + switch (port->state) { + case SRC_HARD_RESET_VBUS_OFF: + /* + * After establishing the vSafe0V voltage condition on VBUS, the Source Shall wait + * tSrcRecover before re-applying VCONN and restoring VBUS to vSafe5V. + */ + tcpm_set_state(port, SRC_HARD_RESET_VBUS_ON, PD_T_SRC_RECOVER); + break; + case SRC_ATTACH_WAIT: + if (tcpm_port_is_source(port)) + tcpm_set_state(port, tcpm_try_snk(port) ? SNK_TRY : SRC_ATTACHED, + PD_T_CC_DEBOUNCE); + break; + case SRC_STARTUP: + case SRC_SEND_CAPABILITIES: + case SRC_SEND_CAPABILITIES_TIMEOUT: + case SRC_NEGOTIATE_CAPABILITIES: + case SRC_TRANSITION_SUPPLY: + case SRC_READY: + case SRC_WAIT_NEW_CAPABILITIES: + if (port->auto_vbus_discharge_enabled) { + if (port->port_type == TYPEC_PORT_SRC) + tcpm_set_state(port, SRC_UNATTACHED, 0); + else + tcpm_set_state(port, SNK_UNATTACHED, 0); + } + break; + case PR_SWAP_SNK_SRC_SINK_OFF: + case PR_SWAP_SNK_SRC_SOURCE_ON: + /* Do nothing, vsafe0v is expected during transition */ + break; + case SNK_ATTACH_WAIT: + case SNK_DEBOUNCED: + /*Do nothing, still waiting for VSAFE5V for connect */ + break; + case SNK_HARD_RESET_WAIT_VBUS: + /* Do nothing, its OK to receive vbus off events */ + break; + default: + if (port->pwr_role == TYPEC_SINK && port->auto_vbus_discharge_enabled) + tcpm_set_state(port, SNK_UNATTACHED, 0); + break; + } +} + +static void _tcpm_pd_hard_reset(struct tcpm_port *port) +{ + tcpm_log_force(port, "Received hard reset"); + if (port->bist_request == BDO_MODE_TESTDATA && port->tcpc->set_bist_data) + port->tcpc->set_bist_data(port->tcpc, false); + + switch (port->state) { + case ERROR_RECOVERY: + case PORT_RESET: + case PORT_RESET_WAIT_OFF: + return; + default: + break; + } + + if (port->ams != NONE_AMS) + port->ams = NONE_AMS; + if (port->hard_reset_count < PD_N_HARD_RESET_COUNT) + port->ams = HARD_RESET; + /* + * If we keep receiving hard reset requests, executing the hard reset + * must have failed. Revert to error recovery if that happens. + */ + tcpm_set_state(port, + port->hard_reset_count < PD_N_HARD_RESET_COUNT ? + HARD_RESET_START : ERROR_RECOVERY, + 0); +} + +static void tcpm_pd_event_handler(struct kthread_work *work) +{ + struct tcpm_port *port = container_of(work, struct tcpm_port, + event_work); + u32 events; + + mutex_lock(&port->lock); + + spin_lock(&port->pd_event_lock); + while (port->pd_events) { + events = port->pd_events; + port->pd_events = 0; + spin_unlock(&port->pd_event_lock); + if (events & TCPM_RESET_EVENT) + _tcpm_pd_hard_reset(port); + if (events & TCPM_VBUS_EVENT) { + bool vbus; + + vbus = port->tcpc->get_vbus(port->tcpc); + if (vbus) { + _tcpm_pd_vbus_on(port); + } else { + _tcpm_pd_vbus_off(port); + /* + * When TCPC does not support detecting vsafe0v voltage level, + * treat vbus absent as vsafe0v. Else invoke is_vbus_vsafe0v + * to see if vbus has discharge to VSAFE0V. + */ + if (!port->tcpc->is_vbus_vsafe0v || + port->tcpc->is_vbus_vsafe0v(port->tcpc)) + _tcpm_pd_vbus_vsafe0v(port); + } + } + if (events & TCPM_CC_EVENT) { + enum typec_cc_status cc1, cc2; + + if (port->tcpc->get_cc(port->tcpc, &cc1, &cc2) == 0) + _tcpm_cc_change(port, cc1, cc2); + } + if (events & TCPM_FRS_EVENT) { + if (port->state == SNK_READY) { + int ret; + + port->upcoming_state = FR_SWAP_SEND; + ret = tcpm_ams_start(port, FAST_ROLE_SWAP); + if (ret == -EAGAIN) + port->upcoming_state = INVALID_STATE; + } else { + tcpm_log(port, "Discarding FRS_SIGNAL! Not in sink ready"); + } + } + if (events & TCPM_SOURCING_VBUS) { + tcpm_log(port, "sourcing vbus"); + /* + * In fast role swap case TCPC autonomously sources vbus. Set vbus_source + * true as TCPM wouldn't have called tcpm_set_vbus. + * + * When vbus is sourced on the command on TCPM i.e. TCPM called + * tcpm_set_vbus to source vbus, vbus_source would already be true. + */ + port->vbus_source = true; + _tcpm_pd_vbus_on(port); + } + if (events & TCPM_PORT_CLEAN) { + tcpm_log(port, "port clean"); + if (port->state == CHECK_CONTAMINANT) { + if (tcpm_start_toggling(port, tcpm_rp_cc(port))) + tcpm_set_state(port, TOGGLING, 0); + else + tcpm_set_state(port, tcpm_default_state(port), 0); + } + } + + spin_lock(&port->pd_event_lock); + } + spin_unlock(&port->pd_event_lock); + mutex_unlock(&port->lock); +} + +void tcpm_cc_change(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events |= TCPM_CC_EVENT; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_cc_change); + +void tcpm_vbus_change(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events |= TCPM_VBUS_EVENT; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_vbus_change); + +void tcpm_pd_hard_reset(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events = TCPM_RESET_EVENT; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_pd_hard_reset); + +void tcpm_sink_frs(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events |= TCPM_FRS_EVENT; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_sink_frs); + +void tcpm_sourcing_vbus(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events |= TCPM_SOURCING_VBUS; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_sourcing_vbus); + +void tcpm_port_clean(struct tcpm_port *port) +{ + spin_lock(&port->pd_event_lock); + port->pd_events |= TCPM_PORT_CLEAN; + spin_unlock(&port->pd_event_lock); + kthread_queue_work(port->wq, &port->event_work); +} +EXPORT_SYMBOL_GPL(tcpm_port_clean); + +bool tcpm_port_is_toggling(struct tcpm_port *port) +{ + return port->port_type == TYPEC_PORT_DRP && port->state == TOGGLING; +} +EXPORT_SYMBOL_GPL(tcpm_port_is_toggling); + +static void tcpm_enable_frs_work(struct kthread_work *work) +{ + struct tcpm_port *port = container_of(work, struct tcpm_port, enable_frs); + int ret; + + mutex_lock(&port->lock); + /* Not FRS capable */ + if (!port->connected || port->port_type != TYPEC_PORT_DRP || + port->pwr_opmode != TYPEC_PWR_MODE_PD || + !port->tcpc->enable_frs || + /* Sink caps queried */ + port->sink_cap_done || port->negotiated_rev < PD_REV30) + goto unlock; + + /* Send when the state machine is idle */ + if (port->state != SNK_READY || port->vdm_sm_running || port->send_discover) + goto resched; + + port->upcoming_state = GET_SINK_CAP; + ret = tcpm_ams_start(port, GET_SINK_CAPABILITIES); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + } else { + port->sink_cap_done = true; + goto unlock; + } +resched: + mod_enable_frs_delayed_work(port, GET_SINK_CAP_RETRY_MS); +unlock: + mutex_unlock(&port->lock); +} + +static void tcpm_send_discover_work(struct kthread_work *work) +{ + struct tcpm_port *port = container_of(work, struct tcpm_port, send_discover_work); + + mutex_lock(&port->lock); + /* No need to send DISCOVER_IDENTITY anymore */ + if (!port->send_discover) + goto unlock; + + if (port->data_role == TYPEC_DEVICE && port->negotiated_rev < PD_REV30) { + port->send_discover = false; + goto unlock; + } + + /* Retry if the port is not idle */ + if ((port->state != SRC_READY && port->state != SNK_READY) || port->vdm_sm_running) { + mod_send_discover_delayed_work(port, SEND_DISCOVER_RETRY_MS); + goto unlock; + } + + tcpm_send_vdm(port, USB_SID_PD, CMD_DISCOVER_IDENT, NULL, 0); + +unlock: + mutex_unlock(&port->lock); +} + +static int tcpm_dr_set(struct typec_port *p, enum typec_data_role data) +{ + struct tcpm_port *port = typec_get_drvdata(p); + int ret; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (port->typec_caps.data != TYPEC_PORT_DRD) { + ret = -EINVAL; + goto port_unlock; + } + if (port->state != SRC_READY && port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + if (port->data_role == data) { + ret = 0; + goto port_unlock; + } + + /* + * XXX + * 6.3.9: If an alternate mode is active, a request to swap + * alternate modes shall trigger a port reset. + * Reject data role swap request in this case. + */ + + if (!port->pd_capable) { + /* + * If the partner is not PD capable, reset the port to + * trigger a role change. This can only work if a preferred + * role is configured, and if it matches the requested role. + */ + if (port->try_role == TYPEC_NO_PREFERRED_ROLE || + port->try_role == port->pwr_role) { + ret = -EINVAL; + goto port_unlock; + } + port->non_pd_role_swap = true; + tcpm_set_state(port, PORT_RESET, 0); + } else { + port->upcoming_state = DR_SWAP_SEND; + ret = tcpm_ams_start(port, DATA_ROLE_SWAP); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + } + + port->swap_status = 0; + port->swap_pending = true; + reinit_completion(&port->swap_complete); + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->swap_complete, + msecs_to_jiffies(PD_ROLE_SWAP_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->swap_status; + + port->non_pd_role_swap = false; + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + return ret; +} + +static int tcpm_pr_set(struct typec_port *p, enum typec_role role) +{ + struct tcpm_port *port = typec_get_drvdata(p); + int ret; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (port->port_type != TYPEC_PORT_DRP) { + ret = -EINVAL; + goto port_unlock; + } + if (port->state != SRC_READY && port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + if (role == port->pwr_role) { + ret = 0; + goto port_unlock; + } + + port->upcoming_state = PR_SWAP_SEND; + ret = tcpm_ams_start(port, POWER_ROLE_SWAP); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + + port->swap_status = 0; + port->swap_pending = true; + reinit_completion(&port->swap_complete); + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->swap_complete, + msecs_to_jiffies(PD_ROLE_SWAP_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->swap_status; + + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + return ret; +} + +static int tcpm_vconn_set(struct typec_port *p, enum typec_role role) +{ + struct tcpm_port *port = typec_get_drvdata(p); + int ret; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (port->state != SRC_READY && port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + if (role == port->vconn_role) { + ret = 0; + goto port_unlock; + } + + port->upcoming_state = VCONN_SWAP_SEND; + ret = tcpm_ams_start(port, VCONN_SWAP); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + + port->swap_status = 0; + port->swap_pending = true; + reinit_completion(&port->swap_complete); + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->swap_complete, + msecs_to_jiffies(PD_ROLE_SWAP_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->swap_status; + + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + return ret; +} + +static int tcpm_try_role(struct typec_port *p, int role) +{ + struct tcpm_port *port = typec_get_drvdata(p); + struct tcpc_dev *tcpc = port->tcpc; + int ret = 0; + + mutex_lock(&port->lock); + if (tcpc->try_role) + ret = tcpc->try_role(tcpc, role); + if (!ret) + port->try_role = role; + port->try_src_count = 0; + port->try_snk_count = 0; + mutex_unlock(&port->lock); + + return ret; +} + +static int tcpm_pps_set_op_curr(struct tcpm_port *port, u16 req_op_curr) +{ + unsigned int target_mw; + int ret; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (!port->pps_data.active) { + ret = -EOPNOTSUPP; + goto port_unlock; + } + + if (port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + if (req_op_curr > port->pps_data.max_curr) { + ret = -EINVAL; + goto port_unlock; + } + + target_mw = (req_op_curr * port->supply_voltage) / 1000; + if (target_mw < port->operating_snk_mw) { + ret = -EINVAL; + goto port_unlock; + } + + port->upcoming_state = SNK_NEGOTIATE_PPS_CAPABILITIES; + ret = tcpm_ams_start(port, POWER_NEGOTIATION); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + + /* Round down operating current to align with PPS valid steps */ + req_op_curr = req_op_curr - (req_op_curr % RDO_PROG_CURR_MA_STEP); + + reinit_completion(&port->pps_complete); + port->pps_data.req_op_curr = req_op_curr; + port->pps_status = 0; + port->pps_pending = true; + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->pps_complete, + msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->pps_status; + + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + + return ret; +} + +static int tcpm_pps_set_out_volt(struct tcpm_port *port, u16 req_out_volt) +{ + unsigned int target_mw; + int ret; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (!port->pps_data.active) { + ret = -EOPNOTSUPP; + goto port_unlock; + } + + if (port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + target_mw = (port->current_limit * req_out_volt) / 1000; + if (target_mw < port->operating_snk_mw) { + ret = -EINVAL; + goto port_unlock; + } + + port->upcoming_state = SNK_NEGOTIATE_PPS_CAPABILITIES; + ret = tcpm_ams_start(port, POWER_NEGOTIATION); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + + /* Round down output voltage to align with PPS valid steps */ + req_out_volt = req_out_volt - (req_out_volt % RDO_PROG_VOLT_MV_STEP); + + reinit_completion(&port->pps_complete); + port->pps_data.req_out_volt = req_out_volt; + port->pps_status = 0; + port->pps_pending = true; + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->pps_complete, + msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->pps_status; + + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + + return ret; +} + +static int tcpm_pps_activate(struct tcpm_port *port, bool activate) +{ + int ret = 0; + + mutex_lock(&port->swap_lock); + mutex_lock(&port->lock); + + if (!port->pps_data.supported) { + ret = -EOPNOTSUPP; + goto port_unlock; + } + + /* Trying to deactivate PPS when already deactivated so just bail */ + if (!port->pps_data.active && !activate) + goto port_unlock; + + if (port->state != SNK_READY) { + ret = -EAGAIN; + goto port_unlock; + } + + if (activate) + port->upcoming_state = SNK_NEGOTIATE_PPS_CAPABILITIES; + else + port->upcoming_state = SNK_NEGOTIATE_CAPABILITIES; + ret = tcpm_ams_start(port, POWER_NEGOTIATION); + if (ret == -EAGAIN) { + port->upcoming_state = INVALID_STATE; + goto port_unlock; + } + + reinit_completion(&port->pps_complete); + port->pps_status = 0; + port->pps_pending = true; + + /* Trigger PPS request or move back to standard PDO contract */ + if (activate) { + port->pps_data.req_out_volt = port->supply_voltage; + port->pps_data.req_op_curr = port->current_limit; + } + mutex_unlock(&port->lock); + + if (!wait_for_completion_timeout(&port->pps_complete, + msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT))) + ret = -ETIMEDOUT; + else + ret = port->pps_status; + + goto swap_unlock; + +port_unlock: + mutex_unlock(&port->lock); +swap_unlock: + mutex_unlock(&port->swap_lock); + + return ret; +} + +static void tcpm_init(struct tcpm_port *port) +{ + enum typec_cc_status cc1, cc2; + + port->tcpc->init(port->tcpc); + + tcpm_reset_port(port); + + /* + * XXX + * Should possibly wait for VBUS to settle if it was enabled locally + * since tcpm_reset_port() will disable VBUS. + */ + port->vbus_present = port->tcpc->get_vbus(port->tcpc); + if (port->vbus_present) + port->vbus_never_low = true; + + /* + * 1. When vbus_present is true, voltage on VBUS is already at VSAFE5V. + * So implicitly vbus_vsafe0v = false. + * + * 2. When vbus_present is false and TCPC does NOT support querying + * vsafe0v status, then, it's best to assume vbus is at VSAFE0V i.e. + * vbus_vsafe0v is true. + * + * 3. When vbus_present is false and TCPC does support querying vsafe0v, + * then, query tcpc for vsafe0v status. + */ + if (port->vbus_present) + port->vbus_vsafe0v = false; + else if (!port->tcpc->is_vbus_vsafe0v) + port->vbus_vsafe0v = true; + else + port->vbus_vsafe0v = port->tcpc->is_vbus_vsafe0v(port->tcpc); + + tcpm_set_state(port, tcpm_default_state(port), 0); + + if (port->tcpc->get_cc(port->tcpc, &cc1, &cc2) == 0) + _tcpm_cc_change(port, cc1, cc2); + + /* + * Some adapters need a clean slate at startup, and won't recover + * otherwise. So do not try to be fancy and force a clean disconnect. + */ + tcpm_set_state(port, PORT_RESET, 0); +} + +static int tcpm_port_type_set(struct typec_port *p, enum typec_port_type type) +{ + struct tcpm_port *port = typec_get_drvdata(p); + + mutex_lock(&port->lock); + if (type == port->port_type) + goto port_unlock; + + port->port_type = type; + + if (!port->connected) { + tcpm_set_state(port, PORT_RESET, 0); + } else if (type == TYPEC_PORT_SNK) { + if (!(port->pwr_role == TYPEC_SINK && + port->data_role == TYPEC_DEVICE)) + tcpm_set_state(port, PORT_RESET, 0); + } else if (type == TYPEC_PORT_SRC) { + if (!(port->pwr_role == TYPEC_SOURCE && + port->data_role == TYPEC_HOST)) + tcpm_set_state(port, PORT_RESET, 0); + } + +port_unlock: + mutex_unlock(&port->lock); + return 0; +} + +static const struct typec_operations tcpm_ops = { + .try_role = tcpm_try_role, + .dr_set = tcpm_dr_set, + .pr_set = tcpm_pr_set, + .vconn_set = tcpm_vconn_set, + .port_type_set = tcpm_port_type_set +}; + +void tcpm_tcpc_reset(struct tcpm_port *port) +{ + mutex_lock(&port->lock); + /* XXX: Maintain PD connection if possible? */ + tcpm_init(port); + mutex_unlock(&port->lock); +} +EXPORT_SYMBOL_GPL(tcpm_tcpc_reset); + +static void tcpm_port_unregister_pd(struct tcpm_port *port) +{ + usb_power_delivery_unregister_capabilities(port->port_sink_caps); + port->port_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(port->port_source_caps); + port->port_source_caps = NULL; + usb_power_delivery_unregister(port->pd); + port->pd = NULL; +} + +static int tcpm_port_register_pd(struct tcpm_port *port) +{ + struct usb_power_delivery_desc desc = { port->typec_caps.pd_revision }; + struct usb_power_delivery_capabilities_desc caps = { }; + struct usb_power_delivery_capabilities *cap; + int ret; + + if (!port->nr_src_pdo && !port->nr_snk_pdo) + return 0; + + port->pd = usb_power_delivery_register(port->dev, &desc); + if (IS_ERR(port->pd)) { + ret = PTR_ERR(port->pd); + goto err_unregister; + } + + if (port->nr_src_pdo) { + memcpy_and_pad(caps.pdo, sizeof(caps.pdo), port->src_pdo, + port->nr_src_pdo * sizeof(u32), 0); + caps.role = TYPEC_SOURCE; + + cap = usb_power_delivery_register_capabilities(port->pd, &caps); + if (IS_ERR(cap)) { + ret = PTR_ERR(cap); + goto err_unregister; + } + + port->port_source_caps = cap; + } + + if (port->nr_snk_pdo) { + memcpy_and_pad(caps.pdo, sizeof(caps.pdo), port->snk_pdo, + port->nr_snk_pdo * sizeof(u32), 0); + caps.role = TYPEC_SINK; + + cap = usb_power_delivery_register_capabilities(port->pd, &caps); + if (IS_ERR(cap)) { + ret = PTR_ERR(cap); + goto err_unregister; + } + + port->port_sink_caps = cap; + } + + return 0; + +err_unregister: + tcpm_port_unregister_pd(port); + + return ret; +} + +static int tcpm_fw_get_caps(struct tcpm_port *port, + struct fwnode_handle *fwnode) +{ + const char *opmode_str; + int ret; + u32 mw, frs_current; + + if (!fwnode) + return -EINVAL; + + /* + * This fwnode has a "compatible" property, but is never populated as a + * struct device. Instead we simply parse it to read the properties. + * This it breaks fw_devlink=on. To maintain backward compatibility + * with existing DT files, we work around this by deleting any + * fwnode_links to/from this fwnode. + */ + fw_devlink_purge_absent_suppliers(fwnode); + + ret = typec_get_fw_cap(&port->typec_caps, fwnode); + if (ret < 0) + return ret; + + port->port_type = port->typec_caps.type; + port->pd_supported = !fwnode_property_read_bool(fwnode, "pd-disable"); + + port->slow_charger_loop = fwnode_property_read_bool(fwnode, "slow-charger-loop"); + if (port->port_type == TYPEC_PORT_SNK) + goto sink; + + /* Get Source PDOs for the PD port or Source Rp value for the non-PD port */ + if (port->pd_supported) { + ret = fwnode_property_count_u32(fwnode, "source-pdos"); + if (ret == 0) + return -EINVAL; + else if (ret < 0) + return ret; + + port->nr_src_pdo = min(ret, PDO_MAX_OBJECTS); + ret = fwnode_property_read_u32_array(fwnode, "source-pdos", + port->src_pdo, port->nr_src_pdo); + if (ret) + return ret; + ret = tcpm_validate_caps(port, port->src_pdo, port->nr_src_pdo); + if (ret) + return ret; + } else { + ret = fwnode_property_read_string(fwnode, "typec-power-opmode", &opmode_str); + if (ret) + return ret; + ret = typec_find_pwr_opmode(opmode_str); + if (ret < 0) + return ret; + port->src_rp = tcpm_pwr_opmode_to_rp(ret); + } + + if (port->port_type == TYPEC_PORT_SRC) + return 0; + +sink: + port->self_powered = fwnode_property_read_bool(fwnode, "self-powered"); + + if (!port->pd_supported) + return 0; + + /* Get sink pdos */ + ret = fwnode_property_count_u32(fwnode, "sink-pdos"); + if (ret <= 0) + return -EINVAL; + + port->nr_snk_pdo = min(ret, PDO_MAX_OBJECTS); + ret = fwnode_property_read_u32_array(fwnode, "sink-pdos", + port->snk_pdo, port->nr_snk_pdo); + if ((ret < 0) || tcpm_validate_caps(port, port->snk_pdo, + port->nr_snk_pdo)) + return -EINVAL; + + if (fwnode_property_read_u32(fwnode, "op-sink-microwatt", &mw) < 0) + return -EINVAL; + port->operating_snk_mw = mw / 1000; + + /* FRS can only be supported by DRP ports */ + if (port->port_type == TYPEC_PORT_DRP) { + ret = fwnode_property_read_u32(fwnode, "new-source-frs-typec-current", + &frs_current); + if (ret >= 0 && frs_current <= FRS_5V_3A) + port->new_source_frs_current = frs_current; + } + + /* sink-vdos is optional */ + ret = fwnode_property_count_u32(fwnode, "sink-vdos"); + if (ret < 0) + ret = 0; + + port->nr_snk_vdo = min(ret, VDO_MAX_OBJECTS); + if (port->nr_snk_vdo) { + ret = fwnode_property_read_u32_array(fwnode, "sink-vdos", + port->snk_vdo, + port->nr_snk_vdo); + if (ret < 0) + return ret; + } + + /* If sink-vdos is found, sink-vdos-v1 is expected for backward compatibility. */ + if (port->nr_snk_vdo) { + ret = fwnode_property_count_u32(fwnode, "sink-vdos-v1"); + if (ret < 0) + return ret; + else if (ret == 0) + return -ENODATA; + + port->nr_snk_vdo_v1 = min(ret, VDO_MAX_OBJECTS); + ret = fwnode_property_read_u32_array(fwnode, "sink-vdos-v1", + port->snk_vdo_v1, + port->nr_snk_vdo_v1); + if (ret < 0) + return ret; + } + + return 0; +} + +/* Power Supply access to expose source power information */ +enum tcpm_psy_online_states { + TCPM_PSY_OFFLINE = 0, + TCPM_PSY_FIXED_ONLINE, + TCPM_PSY_PROG_ONLINE, +}; + +static enum power_supply_property tcpm_psy_props[] = { + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static int tcpm_psy_get_online(struct tcpm_port *port, + union power_supply_propval *val) +{ + if (port->vbus_charge) { + if (port->pps_data.active) + val->intval = TCPM_PSY_PROG_ONLINE; + else + val->intval = TCPM_PSY_FIXED_ONLINE; + } else { + val->intval = TCPM_PSY_OFFLINE; + } + + return 0; +} + +static int tcpm_psy_get_voltage_min(struct tcpm_port *port, + union power_supply_propval *val) +{ + if (port->pps_data.active) + val->intval = port->pps_data.min_volt * 1000; + else + val->intval = port->supply_voltage * 1000; + + return 0; +} + +static int tcpm_psy_get_voltage_max(struct tcpm_port *port, + union power_supply_propval *val) +{ + if (port->pps_data.active) + val->intval = port->pps_data.max_volt * 1000; + else + val->intval = port->supply_voltage * 1000; + + return 0; +} + +static int tcpm_psy_get_voltage_now(struct tcpm_port *port, + union power_supply_propval *val) +{ + val->intval = port->supply_voltage * 1000; + + return 0; +} + +static int tcpm_psy_get_current_max(struct tcpm_port *port, + union power_supply_propval *val) +{ + if (port->pps_data.active) + val->intval = port->pps_data.max_curr * 1000; + else + val->intval = port->current_limit * 1000; + + return 0; +} + +static int tcpm_psy_get_current_now(struct tcpm_port *port, + union power_supply_propval *val) +{ + val->intval = port->current_limit * 1000; + + return 0; +} + +static int tcpm_psy_get_input_power_limit(struct tcpm_port *port, + union power_supply_propval *val) +{ + unsigned int src_mv, src_ma, max_src_uw = 0; + unsigned int i, tmp; + + for (i = 0; i < port->nr_source_caps; i++) { + u32 pdo = port->source_caps[i]; + + if (pdo_type(pdo) == PDO_TYPE_FIXED) { + src_mv = pdo_fixed_voltage(pdo); + src_ma = pdo_max_current(pdo); + tmp = src_mv * src_ma; + max_src_uw = tmp > max_src_uw ? tmp : max_src_uw; + } + } + + val->intval = max_src_uw; + return 0; +} + +static int tcpm_psy_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct tcpm_port *port = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = port->usb_type; + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = tcpm_psy_get_online(port, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + ret = tcpm_psy_get_voltage_min(port, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + ret = tcpm_psy_get_voltage_max(port, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = tcpm_psy_get_voltage_now(port, val); + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + ret = tcpm_psy_get_current_max(port, val); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = tcpm_psy_get_current_now(port, val); + break; + case POWER_SUPPLY_PROP_INPUT_POWER_LIMIT: + tcpm_psy_get_input_power_limit(port, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int tcpm_psy_set_online(struct tcpm_port *port, + const union power_supply_propval *val) +{ + int ret; + + switch (val->intval) { + case TCPM_PSY_FIXED_ONLINE: + ret = tcpm_pps_activate(port, false); + break; + case TCPM_PSY_PROG_ONLINE: + ret = tcpm_pps_activate(port, true); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int tcpm_psy_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct tcpm_port *port = power_supply_get_drvdata(psy); + int ret; + + /* + * All the properties below are related to USB PD. The check needs to be + * property specific when a non-pd related property is added. + */ + if (!port->pd_supported) + return -EOPNOTSUPP; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = tcpm_psy_set_online(port, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = tcpm_pps_set_out_volt(port, val->intval / 1000); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (val->intval > port->pps_data.max_curr * 1000) + ret = -EINVAL; + else + ret = tcpm_pps_set_op_curr(port, val->intval / 1000); + break; + default: + ret = -EINVAL; + break; + } + power_supply_changed(port->psy); + return ret; +} + +static int tcpm_psy_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_CURRENT_NOW: + return 1; + default: + return 0; + } +} + +static enum power_supply_usb_type tcpm_psy_usb_types[] = { + POWER_SUPPLY_USB_TYPE_C, + POWER_SUPPLY_USB_TYPE_PD, + POWER_SUPPLY_USB_TYPE_PD_PPS, +}; + +static const char *tcpm_psy_name_prefix = "tcpm-source-psy-"; + +static int devm_tcpm_psy_register(struct tcpm_port *port) +{ + struct power_supply_config psy_cfg = {}; + const char *port_dev_name = dev_name(port->dev); + size_t psy_name_len = strlen(tcpm_psy_name_prefix) + + strlen(port_dev_name) + 1; + char *psy_name; + + psy_cfg.drv_data = port; + psy_cfg.fwnode = dev_fwnode(port->dev); + psy_name = devm_kzalloc(port->dev, psy_name_len, GFP_KERNEL); + if (!psy_name) + return -ENOMEM; + + snprintf(psy_name, psy_name_len, "%s%s", tcpm_psy_name_prefix, + port_dev_name); + port->psy_desc.name = psy_name; + port->psy_desc.type = POWER_SUPPLY_TYPE_USB; + port->psy_desc.usb_types = tcpm_psy_usb_types; + port->psy_desc.num_usb_types = ARRAY_SIZE(tcpm_psy_usb_types); + port->psy_desc.properties = tcpm_psy_props; + port->psy_desc.num_properties = ARRAY_SIZE(tcpm_psy_props); + port->psy_desc.get_property = tcpm_psy_get_prop; + port->psy_desc.set_property = tcpm_psy_set_prop; + port->psy_desc.property_is_writeable = tcpm_psy_prop_writeable; + + port->usb_type = POWER_SUPPLY_USB_TYPE_C; + + port->psy = devm_power_supply_register(port->dev, &port->psy_desc, + &psy_cfg); + + return PTR_ERR_OR_ZERO(port->psy); +} + +static enum hrtimer_restart state_machine_timer_handler(struct hrtimer *timer) +{ + struct tcpm_port *port = container_of(timer, struct tcpm_port, state_machine_timer); + + if (port->registered) + kthread_queue_work(port->wq, &port->state_machine); + return HRTIMER_NORESTART; +} + +static enum hrtimer_restart vdm_state_machine_timer_handler(struct hrtimer *timer) +{ + struct tcpm_port *port = container_of(timer, struct tcpm_port, vdm_state_machine_timer); + + if (port->registered) + kthread_queue_work(port->wq, &port->vdm_state_machine); + return HRTIMER_NORESTART; +} + +static enum hrtimer_restart enable_frs_timer_handler(struct hrtimer *timer) +{ + struct tcpm_port *port = container_of(timer, struct tcpm_port, enable_frs_timer); + + if (port->registered) + kthread_queue_work(port->wq, &port->enable_frs); + return HRTIMER_NORESTART; +} + +static enum hrtimer_restart send_discover_timer_handler(struct hrtimer *timer) +{ + struct tcpm_port *port = container_of(timer, struct tcpm_port, send_discover_timer); + + if (port->registered) + kthread_queue_work(port->wq, &port->send_discover_work); + return HRTIMER_NORESTART; +} + +struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc) +{ + struct tcpm_port *port; + int err; + + if (!dev || !tcpc || + !tcpc->get_vbus || !tcpc->set_cc || !tcpc->get_cc || + !tcpc->set_polarity || !tcpc->set_vconn || !tcpc->set_vbus || + !tcpc->set_pd_rx || !tcpc->set_roles || !tcpc->pd_transmit) + return ERR_PTR(-EINVAL); + + port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL); + if (!port) + return ERR_PTR(-ENOMEM); + + port->dev = dev; + port->tcpc = tcpc; + + mutex_init(&port->lock); + mutex_init(&port->swap_lock); + + port->wq = kthread_create_worker(0, dev_name(dev)); + if (IS_ERR(port->wq)) + return ERR_CAST(port->wq); + sched_set_fifo(port->wq->task); + + kthread_init_work(&port->state_machine, tcpm_state_machine_work); + kthread_init_work(&port->vdm_state_machine, vdm_state_machine_work); + kthread_init_work(&port->event_work, tcpm_pd_event_handler); + kthread_init_work(&port->enable_frs, tcpm_enable_frs_work); + kthread_init_work(&port->send_discover_work, tcpm_send_discover_work); + hrtimer_init(&port->state_machine_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + port->state_machine_timer.function = state_machine_timer_handler; + hrtimer_init(&port->vdm_state_machine_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + port->vdm_state_machine_timer.function = vdm_state_machine_timer_handler; + hrtimer_init(&port->enable_frs_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + port->enable_frs_timer.function = enable_frs_timer_handler; + hrtimer_init(&port->send_discover_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + port->send_discover_timer.function = send_discover_timer_handler; + + spin_lock_init(&port->pd_event_lock); + + init_completion(&port->tx_complete); + init_completion(&port->swap_complete); + init_completion(&port->pps_complete); + tcpm_debugfs_init(port); + + err = tcpm_fw_get_caps(port, tcpc->fwnode); + if (err < 0) + goto out_destroy_wq; + + port->try_role = port->typec_caps.prefer_role; + + port->typec_caps.fwnode = tcpc->fwnode; + port->typec_caps.revision = 0x0120; /* Type-C spec release 1.2 */ + port->typec_caps.pd_revision = 0x0300; /* USB-PD spec release 3.0 */ + port->typec_caps.svdm_version = SVDM_VER_2_0; + port->typec_caps.driver_data = port; + port->typec_caps.ops = &tcpm_ops; + port->typec_caps.orientation_aware = 1; + + port->partner_desc.identity = &port->partner_ident; + port->port_type = port->typec_caps.type; + + port->role_sw = usb_role_switch_get(port->dev); + if (!port->role_sw) + port->role_sw = fwnode_usb_role_switch_get(tcpc->fwnode); + if (IS_ERR(port->role_sw)) { + err = PTR_ERR(port->role_sw); + goto out_destroy_wq; + } + + err = devm_tcpm_psy_register(port); + if (err) + goto out_role_sw_put; + power_supply_changed(port->psy); + + err = tcpm_port_register_pd(port); + if (err) + goto out_role_sw_put; + + port->typec_caps.pd = port->pd; + + port->typec_port = typec_register_port(port->dev, &port->typec_caps); + if (IS_ERR(port->typec_port)) { + err = PTR_ERR(port->typec_port); + goto out_unregister_pd; + } + + typec_port_register_altmodes(port->typec_port, + &tcpm_altmode_ops, port, + port->port_altmode, ALTMODE_DISCOVERY_MAX); + port->registered = true; + + mutex_lock(&port->lock); + tcpm_init(port); + mutex_unlock(&port->lock); + + tcpm_log(port, "%s: registered", dev_name(dev)); + return port; + +out_unregister_pd: + tcpm_port_unregister_pd(port); +out_role_sw_put: + usb_role_switch_put(port->role_sw); +out_destroy_wq: + tcpm_debugfs_exit(port); + kthread_destroy_worker(port->wq); + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(tcpm_register_port); + +void tcpm_unregister_port(struct tcpm_port *port) +{ + int i; + + port->registered = false; + kthread_destroy_worker(port->wq); + + hrtimer_cancel(&port->send_discover_timer); + hrtimer_cancel(&port->enable_frs_timer); + hrtimer_cancel(&port->vdm_state_machine_timer); + hrtimer_cancel(&port->state_machine_timer); + + tcpm_reset_port(port); + + tcpm_port_unregister_pd(port); + + for (i = 0; i < ARRAY_SIZE(port->port_altmode); i++) + typec_unregister_altmode(port->port_altmode[i]); + typec_unregister_port(port->typec_port); + usb_role_switch_put(port->role_sw); + tcpm_debugfs_exit(port); +} +EXPORT_SYMBOL_GPL(tcpm_unregister_port); + +MODULE_AUTHOR("Guenter Roeck "); +MODULE_DESCRIPTION("USB Type-C Port Manager"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/typec/tcpm/wcove.c b/drivers/usb/typec/tcpm/wcove.c new file mode 100644 index 0000000000..87d4abde0e --- /dev/null +++ b/drivers/usb/typec/tcpm/wcove.c @@ -0,0 +1,702 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * typec_wcove.c - WhiskeyCove PMIC USB Type-C PHY driver + * + * Copyright (C) 2017 Intel Corporation + * Author: Heikki Krogerus + */ + +#include +#include +#include +#include +#include +#include + +/* Register offsets */ +#define WCOVE_CHGRIRQ0 0x4e09 + +#define USBC_CONTROL1 0x7001 +#define USBC_CONTROL2 0x7002 +#define USBC_CONTROL3 0x7003 +#define USBC_CC1_CTRL 0x7004 +#define USBC_CC2_CTRL 0x7005 +#define USBC_STATUS1 0x7007 +#define USBC_STATUS2 0x7008 +#define USBC_STATUS3 0x7009 +#define USBC_CC1 0x700a +#define USBC_CC2 0x700b +#define USBC_CC1_STATUS 0x700c +#define USBC_CC2_STATUS 0x700d +#define USBC_IRQ1 0x7015 +#define USBC_IRQ2 0x7016 +#define USBC_IRQMASK1 0x7017 +#define USBC_IRQMASK2 0x7018 +#define USBC_PDCFG2 0x701a +#define USBC_PDCFG3 0x701b +#define USBC_PDSTATUS 0x701c +#define USBC_RXSTATUS 0x701d +#define USBC_RXINFO 0x701e +#define USBC_TXCMD 0x701f +#define USBC_TXINFO 0x7020 +#define USBC_RX_DATA 0x7028 +#define USBC_TX_DATA 0x7047 + +/* Register bits */ + +#define USBC_CONTROL1_MODE_MASK 0x3 +#define USBC_CONTROL1_MODE_SNK 0 +#define USBC_CONTROL1_MODE_SNKACC 1 +#define USBC_CONTROL1_MODE_SRC 2 +#define USBC_CONTROL1_MODE_SRCACC 3 +#define USBC_CONTROL1_MODE_DRP 4 +#define USBC_CONTROL1_MODE_DRPACC 5 +#define USBC_CONTROL1_MODE_TEST 7 +#define USBC_CONTROL1_CURSRC_MASK 0xc +#define USBC_CONTROL1_CURSRC_UA_0 (0 << 3) +#define USBC_CONTROL1_CURSRC_UA_80 (1 << 3) +#define USBC_CONTROL1_CURSRC_UA_180 (2 << 3) +#define USBC_CONTROL1_CURSRC_UA_330 (3 << 3) +#define USBC_CONTROL1_DRPTOGGLE_RANDOM 0xe0 + +#define USBC_CONTROL2_UNATT_SNK BIT(0) +#define USBC_CONTROL2_UNATT_SRC BIT(1) +#define USBC_CONTROL2_DIS_ST BIT(2) + +#define USBC_CONTROL3_DET_DIS BIT(0) +#define USBC_CONTROL3_PD_DIS BIT(1) +#define USBC_CONTROL3_RESETPHY BIT(2) + +#define USBC_CC_CTRL_PU_EN BIT(0) +#define USBC_CC_CTRL_VCONN_EN BIT(1) +#define USBC_CC_CTRL_TX_EN BIT(2) +#define USBC_CC_CTRL_PD_EN BIT(3) +#define USBC_CC_CTRL_CDET_EN BIT(4) +#define USBC_CC_CTRL_RDET_EN BIT(5) +#define USBC_CC_CTRL_ADC_EN BIT(6) +#define USBC_CC_CTRL_VBUSOK BIT(7) + +#define USBC_STATUS1_DET_ONGOING BIT(6) +#define USBC_STATUS1_RSLT(r) ((r) & 0xf) +#define USBC_RSLT_NOTHING 0 +#define USBC_RSLT_SRC_DEFAULT 1 +#define USBC_RSLT_SRC_1_5A 2 +#define USBC_RSLT_SRC_3_0A 3 +#define USBC_RSLT_SNK 4 +#define USBC_RSLT_DEBUG_ACC 5 +#define USBC_RSLT_AUDIO_ACC 6 +#define USBC_RSLT_UNDEF 15 +#define USBC_STATUS1_ORIENT(r) (((r) >> 4) & 0x3) +#define USBC_ORIENT_NORMAL 1 +#define USBC_ORIENT_REVERSE 2 + +#define USBC_STATUS2_VBUS_REQ BIT(5) + +#define UCSC_CC_STATUS_SNK_RP BIT(0) +#define UCSC_CC_STATUS_PWRDEFSNK BIT(1) +#define UCSC_CC_STATUS_PWR_1P5A_SNK BIT(2) +#define UCSC_CC_STATUS_PWR_3A_SNK BIT(3) +#define UCSC_CC_STATUS_SRC_RP BIT(4) +#define UCSC_CC_STATUS_RX(r) (((r) >> 5) & 0x3) +#define USBC_CC_STATUS_RD 1 +#define USBC_CC_STATUS_RA 2 + +#define USBC_IRQ1_ADCDONE1 BIT(2) +#define USBC_IRQ1_OVERTEMP BIT(1) +#define USBC_IRQ1_SHORT BIT(0) + +#define USBC_IRQ2_CC_CHANGE BIT(7) +#define USBC_IRQ2_RX_PD BIT(6) +#define USBC_IRQ2_RX_HR BIT(5) +#define USBC_IRQ2_RX_CR BIT(4) +#define USBC_IRQ2_TX_SUCCESS BIT(3) +#define USBC_IRQ2_TX_FAIL BIT(2) + +#define USBC_IRQMASK1_ALL (USBC_IRQ1_ADCDONE1 | USBC_IRQ1_OVERTEMP | \ + USBC_IRQ1_SHORT) + +#define USBC_IRQMASK2_ALL (USBC_IRQ2_CC_CHANGE | USBC_IRQ2_RX_PD | \ + USBC_IRQ2_RX_HR | USBC_IRQ2_RX_CR | \ + USBC_IRQ2_TX_SUCCESS | USBC_IRQ2_TX_FAIL) + +#define USBC_PDCFG2_SOP BIT(0) +#define USBC_PDCFG2_SOP_P BIT(1) +#define USBC_PDCFG2_SOP_PP BIT(2) +#define USBC_PDCFG2_SOP_P_DEBUG BIT(3) +#define USBC_PDCFG2_SOP_PP_DEBUG BIT(4) + +#define USBC_PDCFG3_DATAROLE_SHIFT 1 +#define USBC_PDCFG3_SOP_SHIFT 2 + +#define USBC_RXSTATUS_RXCLEAR BIT(0) +#define USBC_RXSTATUS_RXDATA BIT(7) + +#define USBC_RXINFO_RXBYTES(i) (((i) >> 3) & 0x1f) + +#define USBC_TXCMD_BUF_RDY BIT(0) +#define USBC_TXCMD_START BIT(1) +#define USBC_TXCMD_NOP (0 << 5) +#define USBC_TXCMD_MSG (1 << 5) +#define USBC_TXCMD_CR (2 << 5) +#define USBC_TXCMD_HR (3 << 5) +#define USBC_TXCMD_BIST (4 << 5) + +#define USBC_TXINFO_RETRIES(d) (d << 3) + +struct wcove_typec { + struct mutex lock; /* device lock */ + struct device *dev; + struct regmap *regmap; + guid_t guid; + + bool vbus; + + struct tcpc_dev tcpc; + struct tcpm_port *tcpm; +}; + +#define tcpc_to_wcove(_tcpc_) container_of(_tcpc_, struct wcove_typec, tcpc) + +enum wcove_typec_func { + WCOVE_FUNC_DRIVE_VBUS = 1, + WCOVE_FUNC_ORIENTATION, + WCOVE_FUNC_ROLE, + WCOVE_FUNC_DRIVE_VCONN, +}; + +enum wcove_typec_orientation { + WCOVE_ORIENTATION_NORMAL, + WCOVE_ORIENTATION_REVERSE, +}; + +enum wcove_typec_role { + WCOVE_ROLE_HOST, + WCOVE_ROLE_DEVICE, +}; + +#define WCOVE_DSM_UUID "482383f0-2876-4e49-8685-db66211af037" + +static int wcove_typec_func(struct wcove_typec *wcove, + enum wcove_typec_func func, int param) +{ + union acpi_object *obj; + union acpi_object tmp; + union acpi_object argv4 = ACPI_INIT_DSM_ARGV4(1, &tmp); + + tmp.type = ACPI_TYPE_INTEGER; + tmp.integer.value = param; + + obj = acpi_evaluate_dsm(ACPI_HANDLE(wcove->dev), &wcove->guid, 1, func, + &argv4); + if (!obj) { + dev_err(wcove->dev, "%s: failed to evaluate _DSM\n", __func__); + return -EIO; + } + + ACPI_FREE(obj); + return 0; +} + +static int wcove_init(struct tcpc_dev *tcpc) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + int ret; + + ret = regmap_write(wcove->regmap, USBC_CONTROL1, 0); + if (ret) + return ret; + + /* Unmask everything */ + ret = regmap_write(wcove->regmap, USBC_IRQMASK1, 0); + if (ret) + return ret; + + return regmap_write(wcove->regmap, USBC_IRQMASK2, 0); +} + +static int wcove_get_vbus(struct tcpc_dev *tcpc) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int cc1ctrl; + int ret; + + ret = regmap_read(wcove->regmap, USBC_CC1_CTRL, &cc1ctrl); + if (ret) + return ret; + + wcove->vbus = !!(cc1ctrl & USBC_CC_CTRL_VBUSOK); + + return wcove->vbus; +} + +static int wcove_set_vbus(struct tcpc_dev *tcpc, bool on, bool sink) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + + return wcove_typec_func(wcove, WCOVE_FUNC_DRIVE_VBUS, on); +} + +static int wcove_set_vconn(struct tcpc_dev *tcpc, bool on) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + + return wcove_typec_func(wcove, WCOVE_FUNC_DRIVE_VCONN, on); +} + +static enum typec_cc_status wcove_to_typec_cc(unsigned int cc) +{ + if (cc & UCSC_CC_STATUS_SNK_RP) { + if (cc & UCSC_CC_STATUS_PWRDEFSNK) + return TYPEC_CC_RP_DEF; + else if (cc & UCSC_CC_STATUS_PWR_1P5A_SNK) + return TYPEC_CC_RP_1_5; + else if (cc & UCSC_CC_STATUS_PWR_3A_SNK) + return TYPEC_CC_RP_3_0; + } else { + switch (UCSC_CC_STATUS_RX(cc)) { + case USBC_CC_STATUS_RD: + return TYPEC_CC_RD; + case USBC_CC_STATUS_RA: + return TYPEC_CC_RA; + default: + break; + } + } + return TYPEC_CC_OPEN; +} + +static int wcove_get_cc(struct tcpc_dev *tcpc, enum typec_cc_status *cc1, + enum typec_cc_status *cc2) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int cc1_status; + unsigned int cc2_status; + int ret; + + ret = regmap_read(wcove->regmap, USBC_CC1_STATUS, &cc1_status); + if (ret) + return ret; + + ret = regmap_read(wcove->regmap, USBC_CC2_STATUS, &cc2_status); + if (ret) + return ret; + + *cc1 = wcove_to_typec_cc(cc1_status); + *cc2 = wcove_to_typec_cc(cc2_status); + + return 0; +} + +static int wcove_set_cc(struct tcpc_dev *tcpc, enum typec_cc_status cc) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int ctrl; + + switch (cc) { + case TYPEC_CC_RD: + ctrl = USBC_CONTROL1_MODE_SNK; + break; + case TYPEC_CC_RP_DEF: + ctrl = USBC_CONTROL1_CURSRC_UA_80 | USBC_CONTROL1_MODE_SRC; + break; + case TYPEC_CC_RP_1_5: + ctrl = USBC_CONTROL1_CURSRC_UA_180 | USBC_CONTROL1_MODE_SRC; + break; + case TYPEC_CC_RP_3_0: + ctrl = USBC_CONTROL1_CURSRC_UA_330 | USBC_CONTROL1_MODE_SRC; + break; + case TYPEC_CC_OPEN: + ctrl = 0; + break; + default: + return -EINVAL; + } + + return regmap_write(wcove->regmap, USBC_CONTROL1, ctrl); +} + +static int wcove_set_polarity(struct tcpc_dev *tcpc, enum typec_cc_polarity pol) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + + return wcove_typec_func(wcove, WCOVE_FUNC_ORIENTATION, pol); +} + +static int wcove_set_current_limit(struct tcpc_dev *tcpc, u32 max_ma, u32 mv) +{ + return 0; +} + +static int wcove_set_roles(struct tcpc_dev *tcpc, bool attached, + enum typec_role role, enum typec_data_role data) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int val; + int ret; + + ret = wcove_typec_func(wcove, WCOVE_FUNC_ROLE, data == TYPEC_HOST ? + WCOVE_ROLE_HOST : WCOVE_ROLE_DEVICE); + if (ret) + return ret; + + val = role; + val |= data << USBC_PDCFG3_DATAROLE_SHIFT; + val |= PD_REV20 << USBC_PDCFG3_SOP_SHIFT; + + return regmap_write(wcove->regmap, USBC_PDCFG3, val); +} + +static int wcove_set_pd_rx(struct tcpc_dev *tcpc, bool on) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + + return regmap_write(wcove->regmap, USBC_PDCFG2, + on ? USBC_PDCFG2_SOP : 0); +} + +static int wcove_pd_transmit(struct tcpc_dev *tcpc, + enum tcpm_transmit_type type, + const struct pd_message *msg, + unsigned int negotiated_rev) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int info = 0; + unsigned int cmd; + int ret; + + ret = regmap_read(wcove->regmap, USBC_TXCMD, &cmd); + if (ret) + return ret; + + if (!(cmd & USBC_TXCMD_BUF_RDY)) { + dev_warn(wcove->dev, "%s: Last transmission still ongoing!", + __func__); + return -EBUSY; + } + + if (msg) { + const u8 *data = (void *)msg; + int i; + + for (i = 0; i < pd_header_cnt_le(msg->header) * 4 + 2; i++) { + ret = regmap_write(wcove->regmap, USBC_TX_DATA + i, + data[i]); + if (ret) + return ret; + } + } + + switch (type) { + case TCPC_TX_SOP: + case TCPC_TX_SOP_PRIME: + case TCPC_TX_SOP_PRIME_PRIME: + case TCPC_TX_SOP_DEBUG_PRIME: + case TCPC_TX_SOP_DEBUG_PRIME_PRIME: + info = type + 1; + cmd = USBC_TXCMD_MSG; + break; + case TCPC_TX_HARD_RESET: + cmd = USBC_TXCMD_HR; + break; + case TCPC_TX_CABLE_RESET: + cmd = USBC_TXCMD_CR; + break; + case TCPC_TX_BIST_MODE_2: + cmd = USBC_TXCMD_BIST; + break; + default: + return -EINVAL; + } + + /* NOTE Setting maximum number of retries (7) */ + ret = regmap_write(wcove->regmap, USBC_TXINFO, + info | USBC_TXINFO_RETRIES(7)); + if (ret) + return ret; + + return regmap_write(wcove->regmap, USBC_TXCMD, cmd | USBC_TXCMD_START); +} + +static int wcove_start_toggling(struct tcpc_dev *tcpc, + enum typec_port_type port_type, + enum typec_cc_status cc) +{ + struct wcove_typec *wcove = tcpc_to_wcove(tcpc); + unsigned int usbc_ctrl; + + if (port_type != TYPEC_PORT_DRP) + return -EOPNOTSUPP; + + usbc_ctrl = USBC_CONTROL1_MODE_DRP | USBC_CONTROL1_DRPTOGGLE_RANDOM; + + switch (cc) { + case TYPEC_CC_RP_1_5: + usbc_ctrl |= USBC_CONTROL1_CURSRC_UA_180; + break; + case TYPEC_CC_RP_3_0: + usbc_ctrl |= USBC_CONTROL1_CURSRC_UA_330; + break; + default: + usbc_ctrl |= USBC_CONTROL1_CURSRC_UA_80; + break; + } + + return regmap_write(wcove->regmap, USBC_CONTROL1, usbc_ctrl); +} + +static int wcove_read_rx_buffer(struct wcove_typec *wcove, void *msg) +{ + unsigned int info; + int ret; + int i; + + ret = regmap_read(wcove->regmap, USBC_RXINFO, &info); + if (ret) + return ret; + + /* FIXME: Check that USBC_RXINFO_RXBYTES(info) matches the header */ + + for (i = 0; i < USBC_RXINFO_RXBYTES(info); i++) { + ret = regmap_read(wcove->regmap, USBC_RX_DATA + i, msg + i); + if (ret) + return ret; + } + + return regmap_write(wcove->regmap, USBC_RXSTATUS, + USBC_RXSTATUS_RXCLEAR); +} + +static irqreturn_t wcove_typec_irq(int irq, void *data) +{ + struct wcove_typec *wcove = data; + unsigned int usbc_irq1 = 0; + unsigned int usbc_irq2 = 0; + unsigned int cc1ctrl; + int ret; + + mutex_lock(&wcove->lock); + + /* Read.. */ + ret = regmap_read(wcove->regmap, USBC_IRQ1, &usbc_irq1); + if (ret) + goto err; + + ret = regmap_read(wcove->regmap, USBC_IRQ2, &usbc_irq2); + if (ret) + goto err; + + ret = regmap_read(wcove->regmap, USBC_CC1_CTRL, &cc1ctrl); + if (ret) + goto err; + + if (!wcove->tcpm) + goto err; + + /* ..check.. */ + if (usbc_irq1 & USBC_IRQ1_OVERTEMP) { + dev_err(wcove->dev, "VCONN Switch Over Temperature!\n"); + wcove_typec_func(wcove, WCOVE_FUNC_DRIVE_VCONN, false); + /* REVISIT: Report an error? */ + } + + if (usbc_irq1 & USBC_IRQ1_SHORT) { + dev_err(wcove->dev, "VCONN Switch Short Circuit!\n"); + wcove_typec_func(wcove, WCOVE_FUNC_DRIVE_VCONN, false); + /* REVISIT: Report an error? */ + } + + if (wcove->vbus != !!(cc1ctrl & USBC_CC_CTRL_VBUSOK)) + tcpm_vbus_change(wcove->tcpm); + + /* REVISIT: See if tcpm code can be made to consider Type-C HW FSMs */ + if (usbc_irq2 & USBC_IRQ2_CC_CHANGE) + tcpm_cc_change(wcove->tcpm); + + if (usbc_irq2 & USBC_IRQ2_RX_PD) { + unsigned int status; + + /* + * FIXME: Need to check if TX is ongoing and report + * TX_DIREGARDED if needed? + */ + + ret = regmap_read(wcove->regmap, USBC_RXSTATUS, &status); + if (ret) + goto err; + + /* Flush all buffers */ + while (status & USBC_RXSTATUS_RXDATA) { + struct pd_message msg; + + ret = wcove_read_rx_buffer(wcove, &msg); + if (ret) { + dev_err(wcove->dev, "%s: RX read failed\n", + __func__); + goto err; + } + + tcpm_pd_receive(wcove->tcpm, &msg); + + ret = regmap_read(wcove->regmap, USBC_RXSTATUS, + &status); + if (ret) + goto err; + } + } + + if (usbc_irq2 & USBC_IRQ2_RX_HR) + tcpm_pd_hard_reset(wcove->tcpm); + + /* REVISIT: if (usbc_irq2 & USBC_IRQ2_RX_CR) */ + + if (usbc_irq2 & USBC_IRQ2_TX_SUCCESS) + tcpm_pd_transmit_complete(wcove->tcpm, TCPC_TX_SUCCESS); + + if (usbc_irq2 & USBC_IRQ2_TX_FAIL) + tcpm_pd_transmit_complete(wcove->tcpm, TCPC_TX_FAILED); + +err: + /* ..and clear. */ + if (usbc_irq1) { + ret = regmap_write(wcove->regmap, USBC_IRQ1, usbc_irq1); + if (ret) + dev_WARN(wcove->dev, "%s failed to clear IRQ1\n", + __func__); + } + + if (usbc_irq2) { + ret = regmap_write(wcove->regmap, USBC_IRQ2, usbc_irq2); + if (ret) + dev_WARN(wcove->dev, "%s failed to clear IRQ2\n", + __func__); + } + + /* REVISIT: Clear WhiskeyCove CHGR Type-C interrupt */ + regmap_write(wcove->regmap, WCOVE_CHGRIRQ0, BIT(5)); + + mutex_unlock(&wcove->lock); + return IRQ_HANDLED; +} + +/* + * The following power levels should be safe to use with Joule board. + */ +static const u32 src_pdo[] = { + PDO_FIXED(5000, 1500, PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | + PDO_FIXED_USB_COMM), +}; + +static const u32 snk_pdo[] = { + PDO_FIXED(5000, 500, PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | + PDO_FIXED_USB_COMM), + PDO_VAR(5000, 12000, 3000), +}; + +static const struct property_entry wcove_props[] = { + PROPERTY_ENTRY_STRING("data-role", "dual"), + PROPERTY_ENTRY_STRING("power-role", "dual"), + PROPERTY_ENTRY_STRING("try-power-role", "sink"), + PROPERTY_ENTRY_U32_ARRAY("source-pdos", src_pdo), + PROPERTY_ENTRY_U32_ARRAY("sink-pdos", snk_pdo), + PROPERTY_ENTRY_U32("op-sink-microwatt", 15000000), + { } +}; + +static int wcove_typec_probe(struct platform_device *pdev) +{ + struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent); + struct wcove_typec *wcove; + int irq; + int ret; + + wcove = devm_kzalloc(&pdev->dev, sizeof(*wcove), GFP_KERNEL); + if (!wcove) + return -ENOMEM; + + mutex_init(&wcove->lock); + wcove->dev = &pdev->dev; + wcove->regmap = pmic->regmap; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + irq = regmap_irq_get_virq(pmic->irq_chip_data_chgr, irq); + if (irq < 0) + return irq; + + ret = guid_parse(WCOVE_DSM_UUID, &wcove->guid); + if (ret) + return ret; + + if (!acpi_check_dsm(ACPI_HANDLE(&pdev->dev), &wcove->guid, 0, 0x1f)) { + dev_err(&pdev->dev, "Missing _DSM functions\n"); + return -ENODEV; + } + + wcove->tcpc.init = wcove_init; + wcove->tcpc.get_vbus = wcove_get_vbus; + wcove->tcpc.set_vbus = wcove_set_vbus; + wcove->tcpc.set_cc = wcove_set_cc; + wcove->tcpc.get_cc = wcove_get_cc; + wcove->tcpc.set_polarity = wcove_set_polarity; + wcove->tcpc.set_vconn = wcove_set_vconn; + wcove->tcpc.set_current_limit = wcove_set_current_limit; + wcove->tcpc.start_toggling = wcove_start_toggling; + + wcove->tcpc.set_pd_rx = wcove_set_pd_rx; + wcove->tcpc.set_roles = wcove_set_roles; + wcove->tcpc.pd_transmit = wcove_pd_transmit; + + wcove->tcpc.fwnode = fwnode_create_software_node(wcove_props, NULL); + if (IS_ERR(wcove->tcpc.fwnode)) + return PTR_ERR(wcove->tcpc.fwnode); + + wcove->tcpm = tcpm_register_port(wcove->dev, &wcove->tcpc); + if (IS_ERR(wcove->tcpm)) { + fwnode_remove_software_node(wcove->tcpc.fwnode); + return PTR_ERR(wcove->tcpm); + } + + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + wcove_typec_irq, IRQF_ONESHOT, + "wcove_typec", wcove); + if (ret) { + tcpm_unregister_port(wcove->tcpm); + fwnode_remove_software_node(wcove->tcpc.fwnode); + return ret; + } + + platform_set_drvdata(pdev, wcove); + return 0; +} + +static void wcove_typec_remove(struct platform_device *pdev) +{ + struct wcove_typec *wcove = platform_get_drvdata(pdev); + unsigned int val; + + /* Mask everything */ + regmap_read(wcove->regmap, USBC_IRQMASK1, &val); + regmap_write(wcove->regmap, USBC_IRQMASK1, val | USBC_IRQMASK1_ALL); + regmap_read(wcove->regmap, USBC_IRQMASK2, &val); + regmap_write(wcove->regmap, USBC_IRQMASK2, val | USBC_IRQMASK2_ALL); + + tcpm_unregister_port(wcove->tcpm); + fwnode_remove_software_node(wcove->tcpc.fwnode); +} + +static struct platform_driver wcove_typec_driver = { + .driver = { + .name = "bxt_wcove_usbc", + }, + .probe = wcove_typec_probe, + .remove_new = wcove_typec_remove, +}; + +module_platform_driver(wcove_typec_driver); + +MODULE_AUTHOR("Intel Corporation"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("WhiskeyCove PMIC USB Type-C PHY driver"); +MODULE_ALIAS("platform:bxt_wcove_usbc"); -- cgit v1.2.3