summaryrefslogtreecommitdiffstats
path: root/drivers/usb/typec
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:49:45 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:49:45 +0000
commit2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch)
tree848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/usb/typec
parentInitial commit. (diff)
downloadlinux-2c3c1048746a4622d8c89a29670120dc8fab93c4.tar.xz
linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.zip
Adding upstream version 6.1.76.upstream/6.1.76
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/usb/typec')
-rw-r--r--drivers/usb/typec/Kconfig130
-rw-r--r--drivers/usb/typec/Makefile15
-rw-r--r--drivers/usb/typec/altmodes/Kconfig26
-rw-r--r--drivers/usb/typec/altmodes/Makefile6
-rw-r--r--drivers/usb/typec/altmodes/displayport.c638
-rw-r--r--drivers/usb/typec/altmodes/displayport.h8
-rw-r--r--drivers/usb/typec/altmodes/nvidia.c44
-rw-r--r--drivers/usb/typec/anx7411.c1599
-rw-r--r--drivers/usb/typec/bus.c420
-rw-r--r--drivers/usb/typec/bus.h34
-rw-r--r--drivers/usb/typec/class.c2359
-rw-r--r--drivers/usb/typec/class.h91
-rw-r--r--drivers/usb/typec/hd3ss3220.c275
-rw-r--r--drivers/usb/typec/mux.c518
-rw-r--r--drivers/usb/typec/mux.h27
-rw-r--r--drivers/usb/typec/mux/Kconfig32
-rw-r--r--drivers/usb/typec/mux/Makefile5
-rw-r--r--drivers/usb/typec/mux/fsa4480.c216
-rw-r--r--drivers/usb/typec/mux/intel_pmc_mux.c745
-rw-r--r--drivers/usb/typec/mux/pi3usb30532.c190
-rw-r--r--drivers/usb/typec/pd.c707
-rw-r--r--drivers/usb/typec/pd.h30
-rw-r--r--drivers/usb/typec/port-mapper.c84
-rw-r--r--drivers/usb/typec/qcom-pmic-typec.c261
-rw-r--r--drivers/usb/typec/retimer.c173
-rw-r--r--drivers/usb/typec/retimer.h15
-rw-r--r--drivers/usb/typec/rt1719.c959
-rw-r--r--drivers/usb/typec/stusb160x.c880
-rw-r--r--drivers/usb/typec/tcpm/Kconfig79
-rw-r--r--drivers/usb/typec/tcpm/Makefile10
-rw-r--r--drivers/usb/typec/tcpm/fusb302.c1848
-rw-r--r--drivers/usb/typec/tcpm/fusb302_reg.h177
-rw-r--r--drivers/usb/typec/tcpm/tcpci.c895
-rw-r--r--drivers/usb/typec/tcpm/tcpci_maxim.c530
-rw-r--r--drivers/usb/typec/tcpm/tcpci_mt6360.c237
-rw-r--r--drivers/usb/typec/tcpm/tcpci_mt6370.c207
-rw-r--r--drivers/usb/typec/tcpm/tcpci_rt1711h.c424
-rw-r--r--drivers/usb/typec/tcpm/tcpm.c6669
-rw-r--r--drivers/usb/typec/tcpm/wcove.c704
-rw-r--r--drivers/usb/typec/tipd/Kconfig12
-rw-r--r--drivers/usb/typec/tipd/Makefile6
-rw-r--r--drivers/usb/typec/tipd/core.c896
-rw-r--r--drivers/usb/typec/tipd/tps6598x.h202
-rw-r--r--drivers/usb/typec/tipd/trace.c9
-rw-r--r--drivers/usb/typec/tipd/trace.h306
-rw-r--r--drivers/usb/typec/ucsi/Kconfig61
-rw-r--r--drivers/usb/typec/ucsi/Makefile20
-rw-r--r--drivers/usb/typec/ucsi/displayport.c344
-rw-r--r--drivers/usb/typec/ucsi/psy.c273
-rw-r--r--drivers/usb/typec/ucsi/trace.c63
-rw-r--r--drivers/usb/typec/ucsi/trace.h120
-rw-r--r--drivers/usb/typec/ucsi/ucsi.c1460
-rw-r--r--drivers/usb/typec/ucsi/ucsi.h390
-rw-r--r--drivers/usb/typec/ucsi/ucsi_acpi.c257
-rw-r--r--drivers/usb/typec/ucsi/ucsi_ccg.c1494
-rw-r--r--drivers/usb/typec/ucsi/ucsi_stm32g0.c775
-rw-r--r--drivers/usb/typec/wusb3801.c435
57 files changed, 29390 insertions, 0 deletions
diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
new file mode 100644
index 000000000..831e70499
--- /dev/null
+++ b/drivers/usb/typec/Kconfig
@@ -0,0 +1,130 @@
+# SPDX-License-Identifier: GPL-2.0
+
+menuconfig TYPEC
+ tristate "USB Type-C Support"
+ help
+ USB Type-C Specification defines a cable and connector for USB where
+ only one type of plug is supported on both ends, i.e. there will not
+ be Type-A plug on one end of the cable and Type-B plug on the other.
+ Determination of the host-to-device relationship happens through a
+ specific Configuration Channel (CC) which goes through the USB Type-C
+ cable. The Configuration Channel may also be used to detect optional
+ Accessory Modes - Analog Audio and Debug - and if USB Power Delivery
+ is supported, the Alternate Modes, where the connector is used for
+ something else then USB communication.
+
+ USB Power Delivery Specification defines a protocol that can be used
+ to negotiate the voltage and current levels with the connected
+ partners. USB Power Delivery allows higher voltages then the normal
+ 5V, up to 20V, and current up to 5A over the cable. The USB Power
+ Delivery protocol is also used to negotiate the optional Alternate
+ Modes when they are supported. USB Power Delivery does not depend on
+ USB Type-C connector, however it is mostly used together with USB
+ Type-C connectors.
+
+ USB Type-C and USB Power Delivery Specifications define a set of state
+ machines that need to be implemented in either software or firmware.
+ Simple USB Type-C PHYs, for example USB Type-C Port Controller
+ Interface Specification compliant "Port Controllers" need the state
+ machines to be handled in the OS, but stand-alone USB Type-C and Power
+ Delivery controllers handle the state machines inside their firmware.
+ The USB Type-C and Power Delivery controllers usually function
+ autonomously, and do not necessarily require drivers.
+
+ Enable this configurations option if you have USB Type-C connectors on
+ your system and 1) you know your USB Type-C hardware requires OS
+ control (a driver) to function, or 2) if you need to be able to read
+ the status of the USB Type-C ports in your system, or 3) if you need
+ to be able to swap the power role (decide are you supplying or
+ consuming power over the cable) or data role (host or device) when
+ both roles are supported.
+
+ For more information, see the kernel documentation for USB Type-C
+ Connector Class API (Documentation/driver-api/usb/typec.rst)
+ <https://www.kernel.org/doc/html/latest/driver-api/usb/typec.html>
+ and ABI (Documentation/ABI/testing/sysfs-class-typec).
+
+if TYPEC
+
+source "drivers/usb/typec/tcpm/Kconfig"
+
+source "drivers/usb/typec/ucsi/Kconfig"
+
+source "drivers/usb/typec/tipd/Kconfig"
+
+config TYPEC_ANX7411
+ tristate "Analogix ANX7411 Type-C DRP Port controller driver"
+ depends on I2C
+ depends on USB_ROLE_SWITCH
+ depends on POWER_SUPPLY
+ help
+ Say Y or M here if your system has Analogix ANX7411 Type-C DRP Port
+ controller driver.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called anx7411.ko.
+
+config TYPEC_RT1719
+ tristate "Richtek RT1719 Sink Only Type-C controller driver"
+ depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+ depends on I2C
+ depends on POWER_SUPPLY
+ select REGMAP_I2C
+ help
+ Say Y or M here if your system has Richtek RT1719 sink only
+ Type-C port controller driver.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called rt1719.ko
+
+config TYPEC_HD3SS3220
+ tristate "TI HD3SS3220 Type-C DRP Port controller driver"
+ depends on I2C
+ depends on USB_ROLE_SWITCH
+ help
+ Say Y or M here if your system has TI HD3SS3220 Type-C DRP Port
+ controller driver.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called hd3ss3220.ko.
+
+config TYPEC_STUSB160X
+ tristate "STMicroelectronics STUSB160x Type-C controller driver"
+ depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say Y or M here if your system has STMicroelectronics STUSB160x
+ Type-C port controller.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called stusb160x.ko.
+
+config TYPEC_QCOM_PMIC
+ tristate "Qualcomm PMIC USB Type-C driver"
+ depends on ARCH_QCOM || COMPILE_TEST
+ depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+ help
+ Driver for supporting role switch over the Qualcomm PMIC. This will
+ handle the USB Type-C role and orientation detection reported by the
+ QCOM PMIC if the PMIC has the capability to handle USB Type-C
+ detection.
+
+ It will also enable the VBUS output to connected devices when a
+ DFP connection is made.
+
+config TYPEC_WUSB3801
+ tristate "Willsemi WUSB3801 Type-C port controller driver"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say Y or M here if your system has a WUSB3801 Type-C port controller.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called wusb3801.ko.
+
+source "drivers/usb/typec/mux/Kconfig"
+
+source "drivers/usb/typec/altmodes/Kconfig"
+
+endif # TYPEC
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
new file mode 100644
index 000000000..4a83dad51
--- /dev/null
+++ b/drivers/usb/typec/Makefile
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_TYPEC) += typec.o
+typec-y := class.o mux.o bus.o pd.o retimer.o
+typec-$(CONFIG_ACPI) += port-mapper.o
+obj-$(CONFIG_TYPEC) += altmodes/
+obj-$(CONFIG_TYPEC_TCPM) += tcpm/
+obj-$(CONFIG_TYPEC_UCSI) += ucsi/
+obj-$(CONFIG_TYPEC_TPS6598X) += tipd/
+obj-$(CONFIG_TYPEC_ANX7411) += anx7411.o
+obj-$(CONFIG_TYPEC_HD3SS3220) += hd3ss3220.o
+obj-$(CONFIG_TYPEC_QCOM_PMIC) += qcom-pmic-typec.o
+obj-$(CONFIG_TYPEC_STUSB160X) += stusb160x.o
+obj-$(CONFIG_TYPEC_RT1719) += rt1719.o
+obj-$(CONFIG_TYPEC_WUSB3801) += wusb3801.o
+obj-$(CONFIG_TYPEC) += mux/
diff --git a/drivers/usb/typec/altmodes/Kconfig b/drivers/usb/typec/altmodes/Kconfig
new file mode 100644
index 000000000..1a6b5e872
--- /dev/null
+++ b/drivers/usb/typec/altmodes/Kconfig
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: GPL-2.0
+
+menu "USB Type-C Alternate Mode drivers"
+
+config TYPEC_DP_ALTMODE
+ tristate "DisplayPort Alternate Mode driver"
+ depends on DRM
+ help
+ DisplayPort USB Type-C Alternate Mode allows DisplayPort
+ displays and adapters to be attached to the USB Type-C
+ connectors on the system.
+
+ To compile this driver as a module, choose M here: the
+ module will be called typec_displayport.
+
+config TYPEC_NVIDIA_ALTMODE
+ tristate "NVIDIA Alternate Mode driver"
+ depends on TYPEC_DP_ALTMODE
+ help
+ Latest NVIDIA GPUs support VirtualLink devices. Select this
+ to enable support for VirtualLink devices with NVIDIA GPUs.
+
+ To compile this driver as a module, choose M here: the
+ module will be called typec_nvidia.
+
+endmenu
diff --git a/drivers/usb/typec/altmodes/Makefile b/drivers/usb/typec/altmodes/Makefile
new file mode 100644
index 000000000..45717548b
--- /dev/null
+++ b/drivers/usb/typec/altmodes/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_TYPEC_DP_ALTMODE) += typec_displayport.o
+typec_displayport-y := displayport.o
+obj-$(CONFIG_TYPEC_NVIDIA_ALTMODE) += typec_nvidia.o
+typec_nvidia-y := nvidia.o
diff --git a/drivers/usb/typec/altmodes/displayport.c b/drivers/usb/typec/altmodes/displayport.c
new file mode 100644
index 000000000..f564d0d47
--- /dev/null
+++ b/drivers/usb/typec/altmodes/displayport.c
@@ -0,0 +1,638 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Typec-C DisplayPort Alternate Mode driver
+ *
+ * Copyright (C) 2018 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ *
+ * DisplayPort is trademark of VESA (www.vesa.org)
+ */
+
+#include <linux/delay.h>
+#include <linux/mutex.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/usb/pd_vdo.h>
+#include <linux/usb/typec_dp.h>
+#include <drm/drm_connector.h>
+#include "displayport.h"
+
+#define DP_HEADER(_dp, ver, cmd) (VDO((_dp)->alt->svid, 1, ver, cmd) \
+ | VDO_OPOS(USB_TYPEC_DP_MODE))
+
+enum {
+ DP_CONF_USB,
+ DP_CONF_DFP_D,
+ DP_CONF_UFP_D,
+ DP_CONF_DUAL_D,
+};
+
+/* Pin assignments that use USB3.1 Gen2 signaling to carry DP protocol */
+#define DP_PIN_ASSIGN_GEN2_BR_MASK (BIT(DP_PIN_ASSIGN_A) | \
+ BIT(DP_PIN_ASSIGN_B))
+
+/* Pin assignments that use DP v1.3 signaling to carry DP protocol */
+#define DP_PIN_ASSIGN_DP_BR_MASK (BIT(DP_PIN_ASSIGN_C) | \
+ BIT(DP_PIN_ASSIGN_D) | \
+ BIT(DP_PIN_ASSIGN_E) | \
+ BIT(DP_PIN_ASSIGN_F))
+
+/* DP only pin assignments */
+#define DP_PIN_ASSIGN_DP_ONLY_MASK (BIT(DP_PIN_ASSIGN_A) | \
+ BIT(DP_PIN_ASSIGN_C) | \
+ BIT(DP_PIN_ASSIGN_E))
+
+/* Pin assignments where one channel is for USB */
+#define DP_PIN_ASSIGN_MULTI_FUNC_MASK (BIT(DP_PIN_ASSIGN_B) | \
+ BIT(DP_PIN_ASSIGN_D) | \
+ BIT(DP_PIN_ASSIGN_F))
+
+enum dp_state {
+ DP_STATE_IDLE,
+ DP_STATE_ENTER,
+ DP_STATE_UPDATE,
+ DP_STATE_CONFIGURE,
+ DP_STATE_EXIT,
+};
+
+struct dp_altmode {
+ struct typec_displayport_data data;
+
+ enum dp_state state;
+ bool hpd;
+ bool pending_hpd;
+
+ struct mutex lock; /* device lock */
+ struct work_struct work;
+ struct typec_altmode *alt;
+ const struct typec_altmode *port;
+ struct fwnode_handle *connector_fwnode;
+};
+
+static int dp_altmode_notify(struct dp_altmode *dp)
+{
+ unsigned long conf;
+ u8 state;
+
+ if (dp->data.conf) {
+ state = get_count_order(DP_CONF_GET_PIN_ASSIGN(dp->data.conf));
+ conf = TYPEC_MODAL_STATE(state);
+ } else {
+ conf = TYPEC_STATE_USB;
+ }
+
+ return typec_altmode_notify(dp->alt, conf, &dp->data);
+}
+
+static int dp_altmode_configure(struct dp_altmode *dp, u8 con)
+{
+ u32 conf = DP_CONF_SIGNALING_DP; /* Only DP signaling supported */
+ u8 pin_assign = 0;
+
+ switch (con) {
+ case DP_STATUS_CON_DISABLED:
+ return 0;
+ case DP_STATUS_CON_DFP_D:
+ conf |= DP_CONF_UFP_U_AS_DFP_D;
+ pin_assign = DP_CAP_UFP_D_PIN_ASSIGN(dp->alt->vdo) &
+ DP_CAP_DFP_D_PIN_ASSIGN(dp->port->vdo);
+ break;
+ case DP_STATUS_CON_UFP_D:
+ case DP_STATUS_CON_BOTH: /* NOTE: First acting as DP source */
+ conf |= DP_CONF_UFP_U_AS_UFP_D;
+ pin_assign = DP_CAP_PIN_ASSIGN_UFP_D(dp->alt->vdo) &
+ DP_CAP_PIN_ASSIGN_DFP_D(dp->port->vdo);
+ break;
+ default:
+ break;
+ }
+
+ /* Determining the initial pin assignment. */
+ if (!DP_CONF_GET_PIN_ASSIGN(dp->data.conf)) {
+ /* Is USB together with DP preferred */
+ if (dp->data.status & DP_STATUS_PREFER_MULTI_FUNC &&
+ pin_assign & DP_PIN_ASSIGN_MULTI_FUNC_MASK)
+ pin_assign &= DP_PIN_ASSIGN_MULTI_FUNC_MASK;
+ else if (pin_assign & DP_PIN_ASSIGN_DP_ONLY_MASK) {
+ pin_assign &= DP_PIN_ASSIGN_DP_ONLY_MASK;
+ /* Default to pin assign C if available */
+ if (pin_assign & BIT(DP_PIN_ASSIGN_C))
+ pin_assign = BIT(DP_PIN_ASSIGN_C);
+ }
+
+ if (!pin_assign)
+ return -EINVAL;
+
+ conf |= DP_CONF_SET_PIN_ASSIGN(pin_assign);
+ }
+
+ dp->data.conf = conf;
+
+ return 0;
+}
+
+static int dp_altmode_status_update(struct dp_altmode *dp)
+{
+ bool configured = !!DP_CONF_GET_PIN_ASSIGN(dp->data.conf);
+ bool hpd = !!(dp->data.status & DP_STATUS_HPD_STATE);
+ u8 con = DP_STATUS_CONNECTION(dp->data.status);
+ int ret = 0;
+
+ if (configured && (dp->data.status & DP_STATUS_SWITCH_TO_USB)) {
+ dp->data.conf = 0;
+ dp->state = DP_STATE_CONFIGURE;
+ } else if (dp->data.status & DP_STATUS_EXIT_DP_MODE) {
+ dp->state = DP_STATE_EXIT;
+ } else if (!(con & DP_CONF_CURRENTLY(dp->data.conf))) {
+ ret = dp_altmode_configure(dp, con);
+ if (!ret) {
+ dp->state = DP_STATE_CONFIGURE;
+ if (dp->hpd != hpd) {
+ dp->hpd = hpd;
+ dp->pending_hpd = true;
+ }
+ }
+ } else {
+ if (dp->hpd != hpd) {
+ drm_connector_oob_hotplug_event(dp->connector_fwnode);
+ dp->hpd = hpd;
+ }
+ }
+
+ return ret;
+}
+
+static int dp_altmode_configured(struct dp_altmode *dp)
+{
+ sysfs_notify(&dp->alt->dev.kobj, "displayport", "configuration");
+ sysfs_notify(&dp->alt->dev.kobj, "displayport", "pin_assignment");
+ /*
+ * If the DFP_D/UFP_D sends a change in HPD when first notifying the
+ * DisplayPort driver that it is connected, then we wait until
+ * configuration is complete to signal HPD.
+ */
+ if (dp->pending_hpd) {
+ drm_connector_oob_hotplug_event(dp->connector_fwnode);
+ sysfs_notify(&dp->alt->dev.kobj, "displayport", "hpd");
+ dp->pending_hpd = false;
+ }
+
+ return dp_altmode_notify(dp);
+}
+
+static int dp_altmode_configure_vdm(struct dp_altmode *dp, u32 conf)
+{
+ int svdm_version = typec_altmode_get_svdm_version(dp->alt);
+ u32 header;
+ int ret;
+
+ if (svdm_version < 0)
+ return svdm_version;
+
+ header = DP_HEADER(dp, svdm_version, DP_CMD_CONFIGURE);
+ ret = typec_altmode_notify(dp->alt, TYPEC_STATE_SAFE, &dp->data);
+ if (ret) {
+ dev_err(&dp->alt->dev,
+ "unable to put to connector to safe mode\n");
+ return ret;
+ }
+
+ ret = typec_altmode_vdm(dp->alt, header, &conf, 2);
+ if (ret)
+ dp_altmode_notify(dp);
+
+ return ret;
+}
+
+static void dp_altmode_work(struct work_struct *work)
+{
+ struct dp_altmode *dp = container_of(work, struct dp_altmode, work);
+ int svdm_version;
+ u32 header;
+ u32 vdo;
+ int ret;
+
+ mutex_lock(&dp->lock);
+
+ switch (dp->state) {
+ case DP_STATE_ENTER:
+ ret = typec_altmode_enter(dp->alt, NULL);
+ if (ret && ret != -EBUSY)
+ dev_err(&dp->alt->dev, "failed to enter mode\n");
+ break;
+ case DP_STATE_UPDATE:
+ svdm_version = typec_altmode_get_svdm_version(dp->alt);
+ if (svdm_version < 0)
+ break;
+ header = DP_HEADER(dp, svdm_version, DP_CMD_STATUS_UPDATE);
+ vdo = 1;
+ ret = typec_altmode_vdm(dp->alt, header, &vdo, 2);
+ if (ret)
+ dev_err(&dp->alt->dev,
+ "unable to send Status Update command (%d)\n",
+ ret);
+ break;
+ case DP_STATE_CONFIGURE:
+ ret = dp_altmode_configure_vdm(dp, dp->data.conf);
+ if (ret)
+ dev_err(&dp->alt->dev,
+ "unable to send Configure command (%d)\n", ret);
+ break;
+ case DP_STATE_EXIT:
+ if (typec_altmode_exit(dp->alt))
+ dev_err(&dp->alt->dev, "Exit Mode Failed!\n");
+ break;
+ default:
+ break;
+ }
+
+ dp->state = DP_STATE_IDLE;
+
+ mutex_unlock(&dp->lock);
+}
+
+static void dp_altmode_attention(struct typec_altmode *alt, const u32 vdo)
+{
+ struct dp_altmode *dp = typec_altmode_get_drvdata(alt);
+ u8 old_state;
+
+ mutex_lock(&dp->lock);
+
+ old_state = dp->state;
+ dp->data.status = vdo;
+
+ if (old_state != DP_STATE_IDLE)
+ dev_warn(&alt->dev, "ATTENTION while processing state %d\n",
+ old_state);
+
+ if (dp_altmode_status_update(dp))
+ dev_warn(&alt->dev, "%s: status update failed\n", __func__);
+
+ if (dp_altmode_notify(dp))
+ dev_err(&alt->dev, "%s: notification failed\n", __func__);
+
+ if (old_state == DP_STATE_IDLE && dp->state != DP_STATE_IDLE)
+ schedule_work(&dp->work);
+
+ mutex_unlock(&dp->lock);
+}
+
+static int dp_altmode_vdm(struct typec_altmode *alt,
+ const u32 hdr, const u32 *vdo, int count)
+{
+ struct dp_altmode *dp = typec_altmode_get_drvdata(alt);
+ int cmd_type = PD_VDO_CMDT(hdr);
+ int cmd = PD_VDO_CMD(hdr);
+ int ret = 0;
+
+ mutex_lock(&dp->lock);
+
+ if (dp->state != DP_STATE_IDLE) {
+ ret = -EBUSY;
+ goto err_unlock;
+ }
+
+ switch (cmd_type) {
+ case CMDT_RSP_ACK:
+ switch (cmd) {
+ case CMD_ENTER_MODE:
+ dp->state = DP_STATE_UPDATE;
+ break;
+ case CMD_EXIT_MODE:
+ dp->data.status = 0;
+ dp->data.conf = 0;
+ if (dp->hpd) {
+ drm_connector_oob_hotplug_event(dp->connector_fwnode);
+ dp->hpd = false;
+ sysfs_notify(&dp->alt->dev.kobj, "displayport", "hpd");
+ }
+ break;
+ case DP_CMD_STATUS_UPDATE:
+ dp->data.status = *vdo;
+ ret = dp_altmode_status_update(dp);
+ break;
+ case DP_CMD_CONFIGURE:
+ ret = dp_altmode_configured(dp);
+ break;
+ default:
+ break;
+ }
+ break;
+ case CMDT_RSP_NAK:
+ switch (cmd) {
+ case DP_CMD_CONFIGURE:
+ dp->data.conf = 0;
+ ret = dp_altmode_configured(dp);
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (dp->state != DP_STATE_IDLE)
+ schedule_work(&dp->work);
+
+err_unlock:
+ mutex_unlock(&dp->lock);
+ return ret;
+}
+
+static int dp_altmode_activate(struct typec_altmode *alt, int activate)
+{
+ return activate ? typec_altmode_enter(alt, NULL) :
+ typec_altmode_exit(alt);
+}
+
+static const struct typec_altmode_ops dp_altmode_ops = {
+ .attention = dp_altmode_attention,
+ .vdm = dp_altmode_vdm,
+ .activate = dp_altmode_activate,
+};
+
+static const char * const configurations[] = {
+ [DP_CONF_USB] = "USB",
+ [DP_CONF_DFP_D] = "source",
+ [DP_CONF_UFP_D] = "sink",
+};
+
+static ssize_t
+configuration_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct dp_altmode *dp = dev_get_drvdata(dev);
+ u32 conf;
+ u32 cap;
+ int con;
+ int ret = 0;
+
+ con = sysfs_match_string(configurations, buf);
+ if (con < 0)
+ return con;
+
+ mutex_lock(&dp->lock);
+
+ if (dp->state != DP_STATE_IDLE) {
+ ret = -EBUSY;
+ goto err_unlock;
+ }
+
+ cap = DP_CAP_CAPABILITY(dp->alt->vdo);
+
+ if ((con == DP_CONF_DFP_D && !(cap & DP_CAP_DFP_D)) ||
+ (con == DP_CONF_UFP_D && !(cap & DP_CAP_UFP_D))) {
+ ret = -EINVAL;
+ goto err_unlock;
+ }
+
+ conf = dp->data.conf & ~DP_CONF_DUAL_D;
+ conf |= con;
+
+ if (dp->alt->active) {
+ ret = dp_altmode_configure_vdm(dp, conf);
+ if (ret)
+ goto err_unlock;
+ }
+
+ dp->data.conf = conf;
+
+err_unlock:
+ mutex_unlock(&dp->lock);
+
+ return ret ? ret : size;
+}
+
+static ssize_t configuration_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct dp_altmode *dp = dev_get_drvdata(dev);
+ int len;
+ u8 cap;
+ u8 cur;
+ int i;
+
+ mutex_lock(&dp->lock);
+
+ cap = DP_CAP_CAPABILITY(dp->alt->vdo);
+ cur = DP_CONF_CURRENTLY(dp->data.conf);
+
+ len = sprintf(buf, "%s ", cur ? "USB" : "[USB]");
+
+ for (i = 1; i < ARRAY_SIZE(configurations); i++) {
+ if (i == cur)
+ len += sprintf(buf + len, "[%s] ", configurations[i]);
+ else if ((i == DP_CONF_DFP_D && cap & DP_CAP_DFP_D) ||
+ (i == DP_CONF_UFP_D && cap & DP_CAP_UFP_D))
+ len += sprintf(buf + len, "%s ", configurations[i]);
+ }
+
+ mutex_unlock(&dp->lock);
+
+ buf[len - 1] = '\n';
+ return len;
+}
+static DEVICE_ATTR_RW(configuration);
+
+static const char * const pin_assignments[] = {
+ [DP_PIN_ASSIGN_A] = "A",
+ [DP_PIN_ASSIGN_B] = "B",
+ [DP_PIN_ASSIGN_C] = "C",
+ [DP_PIN_ASSIGN_D] = "D",
+ [DP_PIN_ASSIGN_E] = "E",
+ [DP_PIN_ASSIGN_F] = "F",
+};
+
+/*
+ * Helper function to extract a peripheral's currently supported
+ * Pin Assignments from its DisplayPort alternate mode state.
+ */
+static u8 get_current_pin_assignments(struct dp_altmode *dp)
+{
+ if (DP_CONF_CURRENTLY(dp->data.conf) == DP_CONF_DFP_D)
+ return DP_CAP_PIN_ASSIGN_DFP_D(dp->alt->vdo);
+ else
+ return DP_CAP_PIN_ASSIGN_UFP_D(dp->alt->vdo);
+}
+
+static ssize_t
+pin_assignment_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct dp_altmode *dp = dev_get_drvdata(dev);
+ u8 assignments;
+ u32 conf;
+ int ret;
+
+ ret = sysfs_match_string(pin_assignments, buf);
+ if (ret < 0)
+ return ret;
+
+ conf = DP_CONF_SET_PIN_ASSIGN(BIT(ret));
+ ret = 0;
+
+ mutex_lock(&dp->lock);
+
+ if (conf & dp->data.conf)
+ goto out_unlock;
+
+ if (dp->state != DP_STATE_IDLE) {
+ ret = -EBUSY;
+ goto out_unlock;
+ }
+
+ assignments = get_current_pin_assignments(dp);
+
+ if (!(DP_CONF_GET_PIN_ASSIGN(conf) & assignments)) {
+ ret = -EINVAL;
+ goto out_unlock;
+ }
+
+ conf |= dp->data.conf & ~DP_CONF_PIN_ASSIGNEMENT_MASK;
+
+ /* Only send Configure command if a configuration has been set */
+ if (dp->alt->active && DP_CONF_CURRENTLY(dp->data.conf)) {
+ ret = dp_altmode_configure_vdm(dp, conf);
+ if (ret)
+ goto out_unlock;
+ }
+
+ dp->data.conf = conf;
+
+out_unlock:
+ mutex_unlock(&dp->lock);
+
+ return ret ? ret : size;
+}
+
+static ssize_t pin_assignment_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct dp_altmode *dp = dev_get_drvdata(dev);
+ u8 assignments;
+ int len = 0;
+ u8 cur;
+ int i;
+
+ mutex_lock(&dp->lock);
+
+ cur = get_count_order(DP_CONF_GET_PIN_ASSIGN(dp->data.conf));
+
+ assignments = get_current_pin_assignments(dp);
+
+ for (i = 0; assignments; assignments >>= 1, i++) {
+ if (assignments & 1) {
+ if (i == cur)
+ len += sprintf(buf + len, "[%s] ",
+ pin_assignments[i]);
+ else
+ len += sprintf(buf + len, "%s ",
+ pin_assignments[i]);
+ }
+ }
+
+ mutex_unlock(&dp->lock);
+
+ /* get_current_pin_assignments can return 0 when no matching pin assignments are found */
+ if (len == 0)
+ len++;
+
+ buf[len - 1] = '\n';
+ return len;
+}
+static DEVICE_ATTR_RW(pin_assignment);
+
+static struct attribute *dp_altmode_attrs[] = {
+ &dev_attr_configuration.attr,
+ &dev_attr_pin_assignment.attr,
+ NULL
+};
+
+static const struct attribute_group dp_altmode_group = {
+ .name = "displayport",
+ .attrs = dp_altmode_attrs,
+};
+
+int dp_altmode_probe(struct typec_altmode *alt)
+{
+ const struct typec_altmode *port = typec_altmode_get_partner(alt);
+ struct fwnode_handle *fwnode;
+ struct dp_altmode *dp;
+ int ret;
+
+ /* FIXME: Port can only be DFP_U. */
+
+ /* Make sure we have compatiple pin configurations */
+ if (!(DP_CAP_PIN_ASSIGN_DFP_D(port->vdo) &
+ DP_CAP_PIN_ASSIGN_UFP_D(alt->vdo)) &&
+ !(DP_CAP_PIN_ASSIGN_UFP_D(port->vdo) &
+ DP_CAP_PIN_ASSIGN_DFP_D(alt->vdo)))
+ return -ENODEV;
+
+ ret = sysfs_create_group(&alt->dev.kobj, &dp_altmode_group);
+ if (ret)
+ return ret;
+
+ dp = devm_kzalloc(&alt->dev, sizeof(*dp), GFP_KERNEL);
+ if (!dp)
+ return -ENOMEM;
+
+ INIT_WORK(&dp->work, dp_altmode_work);
+ mutex_init(&dp->lock);
+ dp->port = port;
+ dp->alt = alt;
+
+ alt->desc = "DisplayPort";
+ alt->ops = &dp_altmode_ops;
+
+ fwnode = dev_fwnode(alt->dev.parent->parent); /* typec_port fwnode */
+ dp->connector_fwnode = fwnode_find_reference(fwnode, "displayport", 0);
+ if (IS_ERR(dp->connector_fwnode))
+ dp->connector_fwnode = NULL;
+
+ typec_altmode_set_drvdata(alt, dp);
+
+ dp->state = DP_STATE_ENTER;
+ schedule_work(&dp->work);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(dp_altmode_probe);
+
+void dp_altmode_remove(struct typec_altmode *alt)
+{
+ struct dp_altmode *dp = typec_altmode_get_drvdata(alt);
+
+ sysfs_remove_group(&alt->dev.kobj, &dp_altmode_group);
+ cancel_work_sync(&dp->work);
+
+ if (dp->connector_fwnode) {
+ if (dp->hpd)
+ drm_connector_oob_hotplug_event(dp->connector_fwnode);
+
+ fwnode_handle_put(dp->connector_fwnode);
+ }
+}
+EXPORT_SYMBOL_GPL(dp_altmode_remove);
+
+static const struct typec_device_id dp_typec_id[] = {
+ { USB_TYPEC_DP_SID, USB_TYPEC_DP_MODE },
+ { },
+};
+MODULE_DEVICE_TABLE(typec, dp_typec_id);
+
+static struct typec_altmode_driver dp_altmode_driver = {
+ .id_table = dp_typec_id,
+ .probe = dp_altmode_probe,
+ .remove = dp_altmode_remove,
+ .driver = {
+ .name = "typec_displayport",
+ .owner = THIS_MODULE,
+ },
+};
+module_typec_altmode_driver(dp_altmode_driver);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("DisplayPort Alternate Mode");
diff --git a/drivers/usb/typec/altmodes/displayport.h b/drivers/usb/typec/altmodes/displayport.h
new file mode 100644
index 000000000..e120364da
--- /dev/null
+++ b/drivers/usb/typec/altmodes/displayport.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#if IS_ENABLED(CONFIG_TYPEC_DP_ALTMODE)
+int dp_altmode_probe(struct typec_altmode *alt);
+void dp_altmode_remove(struct typec_altmode *alt);
+#else
+int dp_altmode_probe(struct typec_altmode *alt) { return -ENOTSUPP; }
+void dp_altmode_remove(struct typec_altmode *alt) { }
+#endif /* CONFIG_TYPEC_DP_ALTMODE */
diff --git a/drivers/usb/typec/altmodes/nvidia.c b/drivers/usb/typec/altmodes/nvidia.c
new file mode 100644
index 000000000..c36769736
--- /dev/null
+++ b/drivers/usb/typec/altmodes/nvidia.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2019 NVIDIA Corporation. All rights reserved.
+ *
+ * NVIDIA USB Type-C Alt Mode Driver
+ */
+#include <linux/module.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_dp.h>
+#include "displayport.h"
+
+static int nvidia_altmode_probe(struct typec_altmode *alt)
+{
+ if (alt->svid == USB_TYPEC_NVIDIA_VLINK_SID)
+ return dp_altmode_probe(alt);
+ else
+ return -ENOTSUPP;
+}
+
+static void nvidia_altmode_remove(struct typec_altmode *alt)
+{
+ if (alt->svid == USB_TYPEC_NVIDIA_VLINK_SID)
+ dp_altmode_remove(alt);
+}
+
+static const struct typec_device_id nvidia_typec_id[] = {
+ { USB_TYPEC_NVIDIA_VLINK_SID, TYPEC_ANY_MODE },
+ { },
+};
+MODULE_DEVICE_TABLE(typec, nvidia_typec_id);
+
+static struct typec_altmode_driver nvidia_altmode_driver = {
+ .id_table = nvidia_typec_id,
+ .probe = nvidia_altmode_probe,
+ .remove = nvidia_altmode_remove,
+ .driver = {
+ .name = "typec_nvidia",
+ .owner = THIS_MODULE,
+ },
+};
+module_typec_altmode_driver(nvidia_altmode_driver);
+
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("NVIDIA USB Type-C Alt Mode Driver");
diff --git a/drivers/usb/typec/anx7411.c b/drivers/usb/typec/anx7411.c
new file mode 100644
index 000000000..b8f3b75fd
--- /dev/null
+++ b/drivers/usb/typec/anx7411.c
@@ -0,0 +1,1599 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Driver for Analogix ANX7411 USB Type-C and PD controller
+ *
+ * Copyright(c) 2022, Analogix Semiconductor. All rights reserved.
+ *
+ */
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/iopoll.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of_graph.h>
+#include <linux/of_platform.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/role.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+#include <linux/workqueue.h>
+#include <linux/power_supply.h>
+
+#define TCPC_ADDRESS1 0x58
+#define TCPC_ADDRESS2 0x56
+#define TCPC_ADDRESS3 0x54
+#define TCPC_ADDRESS4 0x52
+#define SPI_ADDRESS1 0x7e
+#define SPI_ADDRESS2 0x6e
+#define SPI_ADDRESS3 0x64
+#define SPI_ADDRESS4 0x62
+
+struct anx7411_i2c_select {
+ u8 tcpc_address;
+ u8 spi_address;
+};
+
+#define VID_ANALOGIX 0x1F29
+#define PID_ANALOGIX 0x7411
+
+/* TCPC register define */
+
+#define ANALOG_CTRL_10 0xAA
+
+#define STATUS_LEN 2
+#define ALERT_0 0xCB
+#define RECEIVED_MSG BIT(7)
+#define SOFTWARE_INT BIT(6)
+#define MSG_LEN 32
+#define HEADER_LEN 2
+#define MSG_HEADER 0x00
+#define MSG_TYPE 0x01
+#define MSG_RAWDATA 0x02
+#define MSG_LEN_MASK 0x1F
+
+#define ALERT_1 0xCC
+#define INTP_POW_ON BIT(7)
+#define INTP_POW_OFF BIT(6)
+
+#define VBUS_THRESHOLD_H 0xDD
+#define VBUS_THRESHOLD_L 0xDE
+
+#define FW_CTRL_0 0xF0
+#define UNSTRUCT_VDM_EN BIT(0)
+#define DELAY_200MS BIT(1)
+#define VSAFE0 0
+#define VSAFE1 BIT(2)
+#define VSAFE2 BIT(3)
+#define VSAFE3 (BIT(2) | BIT(3))
+#define FRS_EN BIT(7)
+
+#define FW_PARAM 0xF1
+#define DONGLE_IOP BIT(0)
+
+#define FW_CTRL_2 0xF7
+#define SINK_CTRL_DIS_FLAG BIT(5)
+
+/* SPI register define */
+#define OCM_CTRL_0 0x6E
+#define OCM_RESET BIT(6)
+
+#define MAX_VOLTAGE 0xAC
+#define MAX_POWER 0xAD
+#define MIN_POWER 0xAE
+
+#define REQUEST_VOLTAGE 0xAF
+#define VOLTAGE_UNIT 100 /* mV per unit */
+
+#define REQUEST_CURRENT 0xB1
+#define CURRENT_UNIT 50 /* mA per unit */
+
+#define CMD_SEND_BUF 0xC0
+#define CMD_RECV_BUF 0xE0
+
+#define REQ_VOL_20V_IN_100MV 0xC8
+#define REQ_CUR_2_25A_IN_50MA 0x2D
+#define REQ_CUR_3_25A_IN_50MA 0x41
+
+#define DEF_5V 5000
+#define DEF_1_5A 1500
+
+#define LOBYTE(w) ((u8)((w) & 0xFF))
+#define HIBYTE(w) ((u8)(((u16)(w) >> 8) & 0xFF))
+
+enum anx7411_typec_message_type {
+ TYPE_SRC_CAP = 0x00,
+ TYPE_SNK_CAP = 0x01,
+ TYPE_SNK_IDENTITY = 0x02,
+ TYPE_SVID = 0x03,
+ TYPE_SET_SNK_DP_CAP = 0x08,
+ TYPE_PSWAP_REQ = 0x10,
+ TYPE_DSWAP_REQ = 0x11,
+ TYPE_VDM = 0x14,
+ TYPE_OBJ_REQ = 0x16,
+ TYPE_DP_ALT_ENTER = 0x19,
+ TYPE_DP_DISCOVER_MODES_INFO = 0x27,
+ TYPE_GET_DP_CONFIG = 0x29,
+ TYPE_DP_CONFIGURE = 0x2A,
+ TYPE_GET_DP_DISCOVER_MODES_INFO = 0x2E,
+ TYPE_GET_DP_ALT_ENTER = 0x2F,
+};
+
+#define FW_CTRL_1 0xB2
+#define AUTO_PD_EN BIT(1)
+#define TRYSRC_EN BIT(2)
+#define TRYSNK_EN BIT(3)
+#define FORCE_SEND_RDO BIT(6)
+
+#define FW_VER 0xB4
+#define FW_SUBVER 0xB5
+
+#define INT_MASK 0xB6
+#define INT_STS 0xB7
+#define OCM_BOOT_UP BIT(0)
+#define OC_OV_EVENT BIT(1)
+#define VCONN_CHANGE BIT(2)
+#define VBUS_CHANGE BIT(3)
+#define CC_STATUS_CHANGE BIT(4)
+#define DATA_ROLE_CHANGE BIT(5)
+#define PR_CONSUMER_GOT_POWER BIT(6)
+#define HPD_STATUS_CHANGE BIT(7)
+
+#define SYSTEM_STSTUS 0xB8
+/* 0: SINK off; 1: SINK on */
+#define SINK_STATUS BIT(1)
+/* 0: VCONN off; 1: VCONN on*/
+#define VCONN_STATUS BIT(2)
+/* 0: vbus off; 1: vbus on*/
+#define VBUS_STATUS BIT(3)
+/* 1: host; 0:device*/
+#define DATA_ROLE BIT(5)
+/* 0: Chunking; 1: Unchunked*/
+#define SUPPORT_UNCHUNKING BIT(6)
+/* 0: HPD low; 1: HPD high*/
+#define HPD_STATUS BIT(7)
+
+#define DATA_DFP 1
+#define DATA_UFP 2
+#define POWER_SOURCE 1
+#define POWER_SINK 2
+
+#define CC_STATUS 0xB9
+#define CC1_RD BIT(0)
+#define CC2_RD BIT(4)
+#define CC1_RA BIT(1)
+#define CC2_RA BIT(5)
+#define CC1_RD BIT(0)
+#define CC1_RP(cc) (((cc) >> 2) & 0x03)
+#define CC2_RP(cc) (((cc) >> 6) & 0x03)
+
+#define PD_REV_INIT 0xBA
+
+#define PD_EXT_MSG_CTRL 0xBB
+#define SRC_CAP_EXT_REPLY BIT(0)
+#define MANUFACTURER_INFO_REPLY BIT(1)
+#define BATTERY_STS_REPLY BIT(2)
+#define BATTERY_CAP_REPLY BIT(3)
+#define ALERT_REPLY BIT(4)
+#define STATUS_REPLY BIT(5)
+#define PPS_STATUS_REPLY BIT(6)
+#define SNK_CAP_EXT_REPLY BIT(7)
+
+#define NO_CONNECT 0x00
+#define USB3_1_CONNECTED 0x01
+#define DP_ALT_4LANES 0x02
+#define USB3_1_DP_2LANES 0x03
+#define CC1_CONNECTED 0x01
+#define CC2_CONNECTED 0x02
+#define SELECT_PIN_ASSIGMENT_C 0x04
+#define SELECT_PIN_ASSIGMENT_D 0x08
+#define SELECT_PIN_ASSIGMENT_E 0x10
+#define SELECT_PIN_ASSIGMENT_U 0x00
+#define REDRIVER_ADDRESS 0x20
+#define REDRIVER_OFFSET 0x00
+
+#define DP_SVID 0xFF01
+#define VDM_ACK 0x40
+#define VDM_CMD_RES 0x00
+#define VDM_CMD_DIS_ID 0x01
+#define VDM_CMD_DIS_SVID 0x02
+#define VDM_CMD_DIS_MOD 0x03
+#define VDM_CMD_ENTER_MODE 0x04
+#define VDM_CMD_EXIT_MODE 0x05
+#define VDM_CMD_ATTENTION 0x06
+#define VDM_CMD_GET_STS 0x10
+#define VDM_CMD_AND_ACK_MASK 0x5F
+
+#define MAX_ALTMODE 2
+
+#define HAS_SOURCE_CAP BIT(0)
+#define HAS_SINK_CAP BIT(1)
+#define HAS_SINK_WATT BIT(2)
+
+enum anx7411_psy_state {
+ /* copy from drivers/usb/typec/tcpm */
+ ANX7411_PSY_OFFLINE = 0,
+ ANX7411_PSY_FIXED_ONLINE,
+
+ /* private */
+ /* PD keep in, but disconnct power to bq25700,
+ * this state can be active when higher capacity adapter plug in,
+ * and change to ONLINE state when higher capacity adapter plug out
+ */
+ ANX7411_PSY_HANG = 0xff,
+};
+
+struct typec_params {
+ int request_current; /* ma */
+ int request_voltage; /* mv */
+ int cc_connect;
+ int cc_orientation_valid;
+ int cc_status;
+ int data_role;
+ int power_role;
+ int vconn_role;
+ int dp_altmode_enter;
+ int cust_altmode_enter;
+ struct usb_role_switch *role_sw;
+ struct typec_port *port;
+ struct typec_partner *partner;
+ struct typec_mux_dev *typec_mux;
+ struct typec_switch_dev *typec_switch;
+ struct typec_altmode *amode[MAX_ALTMODE];
+ struct typec_altmode *port_amode[MAX_ALTMODE];
+ struct typec_displayport_data data;
+ int pin_assignment;
+ struct typec_capability caps;
+ u32 src_pdo[PDO_MAX_OBJECTS];
+ u32 sink_pdo[PDO_MAX_OBJECTS];
+ u8 caps_flags;
+ u8 src_pdo_nr;
+ u8 sink_pdo_nr;
+ u8 sink_watt;
+ u8 sink_voltage;
+};
+
+#define MAX_BUF_LEN 30
+struct fw_msg {
+ u8 msg_len;
+ u8 msg_type;
+ u8 buf[MAX_BUF_LEN];
+} __packed;
+
+struct anx7411_data {
+ int fw_version;
+ int fw_subversion;
+ struct i2c_client *tcpc_client;
+ struct i2c_client *spi_client;
+ struct fw_msg send_msg;
+ struct fw_msg recv_msg;
+ struct gpio_desc *intp_gpiod;
+ struct fwnode_handle *connector_fwnode;
+ struct typec_params typec;
+ int intp_irq;
+ struct work_struct work;
+ struct workqueue_struct *workqueue;
+ /* Lock for interrupt work queue */
+ struct mutex lock;
+
+ enum anx7411_psy_state psy_online;
+ enum power_supply_usb_type usb_type;
+ struct power_supply *psy;
+ struct power_supply_desc psy_desc;
+ struct device *dev;
+};
+
+static u8 snk_identity[] = {
+ LOBYTE(VID_ANALOGIX), HIBYTE(VID_ANALOGIX), 0x00, 0x82, /* snk_id_hdr */
+ 0x00, 0x00, 0x00, 0x00, /* snk_cert */
+ 0x00, 0x00, LOBYTE(PID_ANALOGIX), HIBYTE(PID_ANALOGIX), /* 5snk_ama */
+};
+
+static u8 dp_caps[4] = {0xC6, 0x00, 0x00, 0x00};
+
+static int anx7411_reg_read(struct i2c_client *client,
+ u8 reg_addr)
+{
+ return i2c_smbus_read_byte_data(client, reg_addr);
+}
+
+static int anx7411_reg_block_read(struct i2c_client *client,
+ u8 reg_addr, u8 len, u8 *buf)
+{
+ return i2c_smbus_read_i2c_block_data(client, reg_addr, len, buf);
+}
+
+static int anx7411_reg_write(struct i2c_client *client,
+ u8 reg_addr, u8 reg_val)
+{
+ return i2c_smbus_write_byte_data(client, reg_addr, reg_val);
+}
+
+static int anx7411_reg_block_write(struct i2c_client *client,
+ u8 reg_addr, u8 len, u8 *buf)
+{
+ return i2c_smbus_write_i2c_block_data(client, reg_addr, len, buf);
+}
+
+static struct anx7411_i2c_select anx7411_i2c_addr[] = {
+ {TCPC_ADDRESS1, SPI_ADDRESS1},
+ {TCPC_ADDRESS2, SPI_ADDRESS2},
+ {TCPC_ADDRESS3, SPI_ADDRESS3},
+ {TCPC_ADDRESS4, SPI_ADDRESS4},
+};
+
+static int anx7411_detect_power_mode(struct anx7411_data *ctx)
+{
+ int ret;
+ int mode;
+
+ ret = anx7411_reg_read(ctx->spi_client, REQUEST_CURRENT);
+ if (ret < 0)
+ return ret;
+
+ ctx->typec.request_current = ret * CURRENT_UNIT; /* 50ma per unit */
+
+ ret = anx7411_reg_read(ctx->spi_client, REQUEST_VOLTAGE);
+ if (ret < 0)
+ return ret;
+
+ ctx->typec.request_voltage = ret * VOLTAGE_UNIT; /* 100mv per unit */
+
+ if (ctx->psy_online == ANX7411_PSY_OFFLINE) {
+ ctx->psy_online = ANX7411_PSY_FIXED_ONLINE;
+ ctx->usb_type = POWER_SUPPLY_USB_TYPE_PD;
+ power_supply_changed(ctx->psy);
+ }
+
+ if (!ctx->typec.cc_orientation_valid)
+ return 0;
+
+ if (ctx->typec.cc_connect == CC1_CONNECTED)
+ mode = CC1_RP(ctx->typec.cc_status);
+ else
+ mode = CC2_RP(ctx->typec.cc_status);
+ if (mode) {
+ typec_set_pwr_opmode(ctx->typec.port, mode - 1);
+ return 0;
+ }
+
+ typec_set_pwr_opmode(ctx->typec.port, TYPEC_PWR_MODE_PD);
+
+ return 0;
+}
+
+static int anx7411_register_partner(struct anx7411_data *ctx,
+ int pd, int accessory)
+{
+ struct typec_partner_desc desc;
+ struct typec_partner *partner;
+
+ if (ctx->typec.partner)
+ return 0;
+
+ desc.usb_pd = pd;
+ desc.accessory = accessory;
+ desc.identity = NULL;
+ partner = typec_register_partner(ctx->typec.port, &desc);
+ if (IS_ERR(partner))
+ return PTR_ERR(partner);
+
+ ctx->typec.partner = partner;
+
+ return 0;
+}
+
+static int anx7411_detect_cc_orientation(struct anx7411_data *ctx)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ int ret;
+ int cc1_rd, cc2_rd;
+ int cc1_ra, cc2_ra;
+ int cc1_rp, cc2_rp;
+
+ ret = anx7411_reg_read(ctx->spi_client, CC_STATUS);
+ if (ret < 0)
+ return ret;
+
+ ctx->typec.cc_status = ret;
+
+ cc1_rd = ret & CC1_RD ? 1 : 0;
+ cc2_rd = ret & CC2_RD ? 1 : 0;
+ cc1_ra = ret & CC1_RA ? 1 : 0;
+ cc2_ra = ret & CC2_RA ? 1 : 0;
+ cc1_rp = CC1_RP(ret);
+ cc2_rp = CC2_RP(ret);
+
+ /* Debug cable, nothing to do */
+ if (cc1_rd && cc2_rd) {
+ ctx->typec.cc_orientation_valid = 0;
+ return anx7411_register_partner(ctx, 0, TYPEC_ACCESSORY_DEBUG);
+ }
+
+ if (cc1_ra && cc2_ra) {
+ ctx->typec.cc_orientation_valid = 0;
+ return anx7411_register_partner(ctx, 0, TYPEC_ACCESSORY_AUDIO);
+ }
+
+ ctx->typec.cc_orientation_valid = 1;
+
+ ret = anx7411_register_partner(ctx, 1, TYPEC_ACCESSORY_NONE);
+ if (ret) {
+ dev_err(dev, "register partner\n");
+ return ret;
+ }
+
+ if (cc1_rd || cc1_rp) {
+ typec_set_orientation(ctx->typec.port, TYPEC_ORIENTATION_NORMAL);
+ ctx->typec.cc_connect = CC1_CONNECTED;
+ }
+
+ if (cc2_rd || cc2_rp) {
+ typec_set_orientation(ctx->typec.port, TYPEC_ORIENTATION_REVERSE);
+ ctx->typec.cc_connect = CC2_CONNECTED;
+ }
+
+ return 0;
+}
+
+static int anx7411_set_mux(struct anx7411_data *ctx, int pin_assignment)
+{
+ int mode = TYPEC_STATE_SAFE;
+
+ switch (pin_assignment) {
+ case SELECT_PIN_ASSIGMENT_U:
+ /* default 4 line USB 3.1 */
+ mode = TYPEC_STATE_MODAL;
+ break;
+ case SELECT_PIN_ASSIGMENT_C:
+ case SELECT_PIN_ASSIGMENT_E:
+ /* 4 line DP */
+ mode = TYPEC_STATE_SAFE;
+ break;
+ case SELECT_PIN_ASSIGMENT_D:
+ /* 2 line DP, 2 line USB */
+ mode = TYPEC_MODE_USB3;
+ break;
+ default:
+ mode = TYPEC_STATE_SAFE;
+ break;
+ }
+
+ ctx->typec.pin_assignment = pin_assignment;
+
+ return typec_set_mode(ctx->typec.port, mode);
+}
+
+static int anx7411_set_usb_role(struct anx7411_data *ctx, enum usb_role role)
+{
+ if (!ctx->typec.role_sw)
+ return 0;
+
+ return usb_role_switch_set_role(ctx->typec.role_sw, role);
+}
+
+static int anx7411_data_role_detect(struct anx7411_data *ctx)
+{
+ int ret;
+
+ ret = anx7411_reg_read(ctx->spi_client, SYSTEM_STSTUS);
+ if (ret < 0)
+ return ret;
+
+ ctx->typec.data_role = (ret & DATA_ROLE) ? TYPEC_HOST : TYPEC_DEVICE;
+ ctx->typec.vconn_role = (ret & VCONN_STATUS) ? TYPEC_SOURCE : TYPEC_SINK;
+
+ typec_set_data_role(ctx->typec.port, ctx->typec.data_role);
+
+ typec_set_vconn_role(ctx->typec.port, ctx->typec.vconn_role);
+
+ if (ctx->typec.data_role == TYPEC_HOST)
+ return anx7411_set_usb_role(ctx, USB_ROLE_HOST);
+
+ return anx7411_set_usb_role(ctx, USB_ROLE_DEVICE);
+}
+
+static int anx7411_power_role_detect(struct anx7411_data *ctx)
+{
+ int ret;
+
+ ret = anx7411_reg_read(ctx->spi_client, SYSTEM_STSTUS);
+ if (ret < 0)
+ return ret;
+
+ ctx->typec.power_role = (ret & SINK_STATUS) ? TYPEC_SINK : TYPEC_SOURCE;
+
+ if (ctx->typec.power_role == TYPEC_SOURCE) {
+ ctx->typec.request_current = DEF_1_5A;
+ ctx->typec.request_voltage = DEF_5V;
+ }
+
+ typec_set_pwr_role(ctx->typec.port, ctx->typec.power_role);
+
+ return 0;
+}
+
+static int anx7411_cc_status_detect(struct anx7411_data *ctx)
+{
+ anx7411_detect_cc_orientation(ctx);
+ anx7411_detect_power_mode(ctx);
+
+ return 0;
+}
+
+static void anx7411_partner_unregister_altmode(struct anx7411_data *ctx)
+{
+ int i;
+
+ ctx->typec.dp_altmode_enter = 0;
+ ctx->typec.cust_altmode_enter = 0;
+
+ for (i = 0; i < MAX_ALTMODE; i++)
+ if (ctx->typec.amode[i]) {
+ typec_unregister_altmode(ctx->typec.amode[i]);
+ ctx->typec.amode[i] = NULL;
+ }
+
+ ctx->typec.pin_assignment = 0;
+}
+
+static int anx7411_typec_register_altmode(struct anx7411_data *ctx,
+ int svid, int vdo)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ struct typec_altmode_desc desc;
+ int err;
+ int i;
+
+ desc.svid = svid;
+ desc.vdo = vdo;
+
+ for (i = 0; i < MAX_ALTMODE; i++)
+ if (!ctx->typec.amode[i])
+ break;
+
+ desc.mode = i + 1; /* start with 1 */
+
+ if (i >= MAX_ALTMODE) {
+ dev_err(dev, "no altmode space for registering\n");
+ return -ENOMEM;
+ }
+
+ ctx->typec.amode[i] = typec_partner_register_altmode(ctx->typec.partner,
+ &desc);
+ if (IS_ERR(ctx->typec.amode[i])) {
+ dev_err(dev, "failed to register altmode\n");
+ err = PTR_ERR(ctx->typec.amode[i]);
+ ctx->typec.amode[i] = NULL;
+ return err;
+ }
+
+ return 0;
+}
+
+static void anx7411_unregister_partner(struct anx7411_data *ctx)
+{
+ if (ctx->typec.partner) {
+ typec_unregister_partner(ctx->typec.partner);
+ ctx->typec.partner = NULL;
+ }
+}
+
+static int anx7411_update_altmode(struct anx7411_data *ctx, int svid)
+{
+ int i;
+
+ if (svid == DP_SVID)
+ ctx->typec.dp_altmode_enter = 1;
+ else
+ ctx->typec.cust_altmode_enter = 1;
+
+ for (i = 0; i < MAX_ALTMODE; i++) {
+ if (!ctx->typec.amode[i])
+ continue;
+
+ if (ctx->typec.amode[i]->svid == svid) {
+ typec_altmode_update_active(ctx->typec.amode[i], true);
+ typec_altmode_notify(ctx->typec.amode[i],
+ ctx->typec.pin_assignment,
+ &ctx->typec.data);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int anx7411_register_altmode(struct anx7411_data *ctx,
+ bool dp_altmode, u8 *buf)
+{
+ int ret;
+ int svid;
+ int mid;
+
+ if (!ctx->typec.partner)
+ return 0;
+
+ svid = DP_SVID;
+ if (dp_altmode) {
+ mid = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
+
+ return anx7411_typec_register_altmode(ctx, svid, mid);
+ }
+
+ svid = (buf[3] << 8) | buf[2];
+ if ((buf[0] & VDM_CMD_AND_ACK_MASK) != (VDM_ACK | VDM_CMD_ENTER_MODE))
+ return anx7411_update_altmode(ctx, svid);
+
+ if ((buf[0] & VDM_CMD_AND_ACK_MASK) != (VDM_ACK | VDM_CMD_DIS_MOD))
+ return 0;
+
+ mid = buf[4] | (buf[5] << 8) | (buf[6] << 16) | (buf[7] << 24);
+
+ ret = anx7411_typec_register_altmode(ctx, svid, mid);
+ if (ctx->typec.cust_altmode_enter)
+ ret |= anx7411_update_altmode(ctx, svid);
+
+ return ret;
+}
+
+static int anx7411_parse_cmd(struct anx7411_data *ctx, u8 type, u8 *buf, u8 len)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ u8 cur_50ma, vol_100mv;
+
+ switch (type) {
+ case TYPE_SRC_CAP:
+ cur_50ma = anx7411_reg_read(ctx->spi_client, REQUEST_CURRENT);
+ vol_100mv = anx7411_reg_read(ctx->spi_client, REQUEST_VOLTAGE);
+
+ ctx->typec.request_voltage = vol_100mv * VOLTAGE_UNIT;
+ ctx->typec.request_current = cur_50ma * CURRENT_UNIT;
+
+ ctx->psy_online = ANX7411_PSY_FIXED_ONLINE;
+ ctx->usb_type = POWER_SUPPLY_USB_TYPE_PD;
+ power_supply_changed(ctx->psy);
+ break;
+ case TYPE_SNK_CAP:
+ break;
+ case TYPE_SVID:
+ break;
+ case TYPE_SNK_IDENTITY:
+ break;
+ case TYPE_GET_DP_ALT_ENTER:
+ /* DP alt mode enter success */
+ if (buf[0])
+ anx7411_update_altmode(ctx, DP_SVID);
+ break;
+ case TYPE_DP_ALT_ENTER:
+ /* Update DP altmode */
+ anx7411_update_altmode(ctx, DP_SVID);
+ break;
+ case TYPE_OBJ_REQ:
+ anx7411_detect_power_mode(ctx);
+ break;
+ case TYPE_DP_CONFIGURE:
+ anx7411_set_mux(ctx, buf[1]);
+ break;
+ case TYPE_DP_DISCOVER_MODES_INFO:
+ /* Make sure discover modes valid */
+ if (buf[0] | buf[1])
+ /* Register DP Altmode */
+ anx7411_register_altmode(ctx, 1, buf);
+ break;
+ case TYPE_VDM:
+ /* Register other altmode */
+ anx7411_register_altmode(ctx, 0, buf);
+ break;
+ default:
+ dev_err(dev, "ignore message(0x%.02x).\n", type);
+ break;
+ }
+
+ return 0;
+}
+
+static u8 checksum(struct device *dev, u8 *buf, u8 len)
+{
+ u8 ret = 0;
+ u8 i;
+
+ for (i = 0; i < len; i++)
+ ret += buf[i];
+
+ return ret;
+}
+
+static int anx7411_read_msg_ctrl_status(struct i2c_client *client)
+{
+ return anx7411_reg_read(client, CMD_SEND_BUF);
+}
+
+static int anx7411_wait_msg_empty(struct i2c_client *client)
+{
+ int val;
+
+ return readx_poll_timeout(anx7411_read_msg_ctrl_status,
+ client, val, (val < 0) || (val == 0),
+ 2000, 2000 * 150);
+}
+
+static int anx7411_send_msg(struct anx7411_data *ctx, u8 type, u8 *buf, u8 size)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ struct fw_msg *msg = &ctx->send_msg;
+ u8 crc;
+ int ret;
+
+ size = min_t(u8, size, (u8)MAX_BUF_LEN);
+ memcpy(msg->buf, buf, size);
+ msg->msg_type = type;
+ /* msg len equals buffer length + msg_type */
+ msg->msg_len = size + 1;
+
+ /* Do CRC check for all buffer data and msg_len and msg_type */
+ crc = checksum(dev, (u8 *)msg, size + HEADER_LEN);
+ msg->buf[size] = 0 - crc;
+
+ ret = anx7411_wait_msg_empty(ctx->spi_client);
+ if (ret)
+ return ret;
+
+ ret = anx7411_reg_block_write(ctx->spi_client,
+ CMD_SEND_BUF + 1, size + HEADER_LEN,
+ &msg->msg_type);
+ ret |= anx7411_reg_write(ctx->spi_client, CMD_SEND_BUF,
+ msg->msg_len);
+ return ret;
+}
+
+static int anx7411_process_cmd(struct anx7411_data *ctx)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ struct fw_msg *msg = &ctx->recv_msg;
+ u8 len;
+ u8 crc;
+ int ret;
+
+ /* Read message from firmware */
+ ret = anx7411_reg_block_read(ctx->spi_client, CMD_RECV_BUF,
+ MSG_LEN, (u8 *)msg);
+ if (ret < 0)
+ return 0;
+
+ if (!msg->msg_len)
+ return 0;
+
+ ret = anx7411_reg_write(ctx->spi_client, CMD_RECV_BUF, 0);
+ if (ret)
+ return ret;
+
+ len = msg->msg_len & MSG_LEN_MASK;
+ crc = checksum(dev, (u8 *)msg, len + HEADER_LEN);
+ if (crc) {
+ dev_err(dev, "message error crc(0x%.02x)\n", crc);
+ return -ERANGE;
+ }
+
+ return anx7411_parse_cmd(ctx, msg->msg_type, msg->buf, len - 1);
+}
+
+static void anx7411_translate_payload(struct device *dev, __le32 *payload,
+ u32 *pdo, int nr, const char *type)
+{
+ int i;
+
+ if (nr > PDO_MAX_OBJECTS) {
+ dev_err(dev, "nr(%d) exceed PDO_MAX_OBJECTS(%d)\n",
+ nr, PDO_MAX_OBJECTS);
+
+ return;
+ }
+
+ for (i = 0; i < nr; i++)
+ payload[i] = cpu_to_le32(pdo[i]);
+}
+
+static int anx7411_config(struct anx7411_data *ctx)
+{
+ struct device *dev = &ctx->spi_client->dev;
+ struct typec_params *typecp = &ctx->typec;
+ __le32 payload[PDO_MAX_OBJECTS];
+ int ret;
+
+ /* Config PD FW work under PD 2.0 */
+ ret = anx7411_reg_write(ctx->spi_client, PD_REV_INIT, PD_REV20);
+ ret |= anx7411_reg_write(ctx->tcpc_client, FW_CTRL_0,
+ UNSTRUCT_VDM_EN | DELAY_200MS |
+ VSAFE1 | FRS_EN);
+ ret |= anx7411_reg_write(ctx->spi_client, FW_CTRL_1,
+ AUTO_PD_EN | FORCE_SEND_RDO);
+
+ /* Set VBUS current threshold */
+ ret |= anx7411_reg_write(ctx->tcpc_client, VBUS_THRESHOLD_H, 0xff);
+ ret |= anx7411_reg_write(ctx->tcpc_client, VBUS_THRESHOLD_L, 0x03);
+
+ /* Fix dongle compatible issue */
+ ret |= anx7411_reg_write(ctx->tcpc_client, FW_PARAM,
+ anx7411_reg_read(ctx->tcpc_client, FW_PARAM) |
+ DONGLE_IOP);
+ ret |= anx7411_reg_write(ctx->spi_client, INT_MASK, 0);
+
+ ret |= anx7411_reg_write(ctx->spi_client, PD_EXT_MSG_CTRL, 0xFF);
+ if (ret)
+ return ret;
+
+ if (typecp->caps_flags & HAS_SOURCE_CAP) {
+ anx7411_translate_payload(dev, payload, typecp->src_pdo,
+ typecp->src_pdo_nr, "source");
+ anx7411_send_msg(ctx, TYPE_SRC_CAP, (u8 *)&payload,
+ typecp->src_pdo_nr * 4);
+ anx7411_send_msg(ctx, TYPE_SNK_IDENTITY, snk_identity,
+ sizeof(snk_identity));
+ anx7411_send_msg(ctx, TYPE_SET_SNK_DP_CAP, dp_caps,
+ sizeof(dp_caps));
+ }
+
+ if (typecp->caps_flags & HAS_SINK_CAP) {
+ anx7411_translate_payload(dev, payload, typecp->sink_pdo,
+ typecp->sink_pdo_nr, "sink");
+ anx7411_send_msg(ctx, TYPE_SNK_CAP, (u8 *)&payload,
+ typecp->sink_pdo_nr * 4);
+ }
+
+ if (typecp->caps_flags & HAS_SINK_WATT) {
+ if (typecp->sink_watt) {
+ ret |= anx7411_reg_write(ctx->spi_client, MAX_POWER,
+ typecp->sink_watt);
+ /* Set min power to 1W */
+ ret |= anx7411_reg_write(ctx->spi_client, MIN_POWER, 2);
+ }
+
+ if (typecp->sink_voltage)
+ ret |= anx7411_reg_write(ctx->spi_client, MAX_VOLTAGE,
+ typecp->sink_voltage);
+ if (ret)
+ return ret;
+ }
+
+ if (!typecp->caps_flags)
+ usleep_range(5000, 6000);
+
+ ctx->fw_version = anx7411_reg_read(ctx->spi_client, FW_VER);
+ ctx->fw_subversion = anx7411_reg_read(ctx->spi_client, FW_SUBVER);
+
+ return 0;
+}
+
+static void anx7411_chip_standby(struct anx7411_data *ctx)
+{
+ int ret;
+ u8 cc1, cc2;
+ struct device *dev = &ctx->spi_client->dev;
+
+ ret = anx7411_reg_write(ctx->spi_client, OCM_CTRL_0,
+ anx7411_reg_read(ctx->spi_client, OCM_CTRL_0) |
+ OCM_RESET);
+ ret |= anx7411_reg_write(ctx->tcpc_client, ANALOG_CTRL_10, 0x80);
+ /* Set TCPC to RD and DRP enable */
+ cc1 = TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT;
+ cc2 = TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT;
+ ret |= anx7411_reg_write(ctx->tcpc_client, TCPC_ROLE_CTRL,
+ TCPC_ROLE_CTRL_DRP | cc1 | cc2);
+
+ /* Send DRP toggle command */
+ ret |= anx7411_reg_write(ctx->tcpc_client, TCPC_COMMAND,
+ TCPC_CMD_LOOK4CONNECTION);
+
+ /* Send TCPC enter standby command */
+ ret |= anx7411_reg_write(ctx->tcpc_client,
+ TCPC_COMMAND, TCPC_CMD_I2C_IDLE);
+ if (ret)
+ dev_err(dev, "Chip standby failed\n");
+}
+
+static void anx7411_work_func(struct work_struct *work)
+{
+ int ret;
+ u8 buf[STATUS_LEN];
+ u8 int_change; /* Interrupt change */
+ u8 int_status; /* Firmware status update */
+ u8 alert0, alert1; /* Interrupt alert source */
+ struct anx7411_data *ctx = container_of(work, struct anx7411_data, work);
+ struct device *dev = &ctx->spi_client->dev;
+
+ mutex_lock(&ctx->lock);
+
+ /* Read interrupt change status */
+ ret = anx7411_reg_block_read(ctx->spi_client, INT_STS, STATUS_LEN, buf);
+ if (ret < 0) {
+ /* Power standby mode, just return */
+ goto unlock;
+ }
+ int_change = buf[0];
+ int_status = buf[1];
+
+ /* Read alert register */
+ ret = anx7411_reg_block_read(ctx->tcpc_client, ALERT_0, STATUS_LEN, buf);
+ if (ret < 0)
+ goto unlock;
+
+ alert0 = buf[0];
+ alert1 = buf[1];
+
+ /* Clear interrupt and alert status */
+ ret = anx7411_reg_write(ctx->spi_client, INT_STS, 0);
+ ret |= anx7411_reg_write(ctx->tcpc_client, ALERT_0, alert0);
+ ret |= anx7411_reg_write(ctx->tcpc_client, ALERT_1, alert1);
+ if (ret)
+ goto unlock;
+
+ if (alert1 & INTP_POW_OFF) {
+ anx7411_partner_unregister_altmode(ctx);
+ if (anx7411_set_usb_role(ctx, USB_ROLE_NONE))
+ dev_err(dev, "Set usb role\n");
+ anx7411_unregister_partner(ctx);
+ ctx->psy_online = ANX7411_PSY_OFFLINE;
+ ctx->usb_type = POWER_SUPPLY_USB_TYPE_C;
+ ctx->typec.request_voltage = 0;
+ ctx->typec.request_current = 0;
+ power_supply_changed(ctx->psy);
+ anx7411_chip_standby(ctx);
+ goto unlock;
+ }
+
+ if ((alert0 & SOFTWARE_INT) && (int_change & OCM_BOOT_UP)) {
+ if (anx7411_config(ctx))
+ dev_err(dev, "Config failed\n");
+ if (anx7411_data_role_detect(ctx))
+ dev_err(dev, "set PD data role\n");
+ if (anx7411_power_role_detect(ctx))
+ dev_err(dev, "set PD power role\n");
+ anx7411_set_mux(ctx, SELECT_PIN_ASSIGMENT_C);
+ }
+
+ if (alert0 & RECEIVED_MSG)
+ anx7411_process_cmd(ctx);
+
+ ret = (int_status & DATA_ROLE) ? TYPEC_HOST : TYPEC_DEVICE;
+ if (ctx->typec.data_role != ret)
+ if (anx7411_data_role_detect(ctx))
+ dev_err(dev, "set PD data role\n");
+
+ ret = (int_status & SINK_STATUS) ? TYPEC_SINK : TYPEC_SOURCE;
+ if (ctx->typec.power_role != ret)
+ if (anx7411_power_role_detect(ctx))
+ dev_err(dev, "set PD power role\n");
+
+ if ((alert0 & SOFTWARE_INT) && (int_change & CC_STATUS_CHANGE))
+ anx7411_cc_status_detect(ctx);
+
+unlock:
+ mutex_unlock(&ctx->lock);
+}
+
+static irqreturn_t anx7411_intr_isr(int irq, void *data)
+{
+ struct anx7411_data *ctx = (struct anx7411_data *)data;
+
+ queue_work(ctx->workqueue, &ctx->work);
+
+ return IRQ_HANDLED;
+}
+
+static int anx7411_register_i2c_dummy_clients(struct anx7411_data *ctx,
+ struct i2c_client *client)
+{
+ int i;
+ u8 spi_addr;
+
+ for (i = 0; i < ARRAY_SIZE(anx7411_i2c_addr); i++) {
+ if (client->addr == (anx7411_i2c_addr[i].tcpc_address >> 1)) {
+ spi_addr = anx7411_i2c_addr[i].spi_address >> 1;
+ ctx->spi_client = i2c_new_dummy_device(client->adapter,
+ spi_addr);
+ if (!IS_ERR(ctx->spi_client))
+ return 0;
+ }
+ }
+
+ dev_err(&client->dev, "unable to get SPI slave\n");
+ return -ENOMEM;
+}
+
+static void anx7411_port_unregister_altmodes(struct typec_altmode **adev)
+{
+ int i;
+
+ for (i = 0; i < MAX_ALTMODE; i++)
+ if (adev[i]) {
+ typec_unregister_altmode(adev[i]);
+ adev[i] = NULL;
+ }
+}
+
+static int anx7411_usb_mux_set(struct typec_mux_dev *mux,
+ struct typec_mux_state *state)
+{
+ struct anx7411_data *ctx = typec_mux_get_drvdata(mux);
+ struct device *dev = &ctx->spi_client->dev;
+ int has_dp;
+
+ has_dp = (state->alt && state->alt->svid == USB_TYPEC_DP_SID &&
+ state->alt->mode == USB_TYPEC_DP_MODE);
+ if (!has_dp)
+ dev_err(dev, "dp altmode not register\n");
+
+ return 0;
+}
+
+static int anx7411_usb_set_orientation(struct typec_switch_dev *sw,
+ enum typec_orientation orientation)
+{
+ /* No need set */
+
+ return 0;
+}
+
+static int anx7411_register_switch(struct anx7411_data *ctx,
+ struct device *dev,
+ struct fwnode_handle *fwnode)
+{
+ struct typec_switch_desc sw_desc = { };
+
+ sw_desc.fwnode = fwnode;
+ sw_desc.drvdata = ctx;
+ sw_desc.name = fwnode_get_name(fwnode);
+ sw_desc.set = anx7411_usb_set_orientation;
+
+ ctx->typec.typec_switch = typec_switch_register(dev, &sw_desc);
+ if (IS_ERR(ctx->typec.typec_switch)) {
+ dev_err(dev, "switch register failed\n");
+ return PTR_ERR(ctx->typec.typec_switch);
+ }
+
+ return 0;
+}
+
+static int anx7411_register_mux(struct anx7411_data *ctx,
+ struct device *dev,
+ struct fwnode_handle *fwnode)
+{
+ struct typec_mux_desc mux_desc = { };
+
+ mux_desc.fwnode = fwnode;
+ mux_desc.drvdata = ctx;
+ mux_desc.name = fwnode_get_name(fwnode);
+ mux_desc.set = anx7411_usb_mux_set;
+
+ ctx->typec.typec_mux = typec_mux_register(dev, &mux_desc);
+ if (IS_ERR(ctx->typec.typec_mux)) {
+ dev_err(dev, "mux register failed\n");
+ return PTR_ERR(ctx->typec.typec_mux);
+ }
+
+ return 0;
+}
+
+static void anx7411_unregister_mux(struct anx7411_data *ctx)
+{
+ if (ctx->typec.typec_mux) {
+ typec_mux_unregister(ctx->typec.typec_mux);
+ ctx->typec.typec_mux = NULL;
+ }
+}
+
+static void anx7411_unregister_switch(struct anx7411_data *ctx)
+{
+ if (ctx->typec.typec_switch) {
+ typec_switch_unregister(ctx->typec.typec_switch);
+ ctx->typec.typec_switch = NULL;
+ }
+}
+
+static int anx7411_typec_switch_probe(struct anx7411_data *ctx,
+ struct device *dev)
+{
+ int ret;
+ struct device_node *node;
+
+ node = of_get_child_by_name(dev->of_node, "orientation_switch");
+ if (!node)
+ return 0;
+
+ ret = anx7411_register_switch(ctx, dev, &node->fwnode);
+ if (ret) {
+ dev_err(dev, "failed register switch");
+ return ret;
+ }
+
+ node = of_get_child_by_name(dev->of_node, "mode_switch");
+ if (!node) {
+ dev_err(dev, "no typec mux exist");
+ ret = -ENODEV;
+ goto unregister_switch;
+ }
+
+ ret = anx7411_register_mux(ctx, dev, &node->fwnode);
+ if (ret) {
+ dev_err(dev, "failed register mode switch");
+ ret = -ENODEV;
+ goto unregister_switch;
+ }
+
+ return 0;
+
+unregister_switch:
+ anx7411_unregister_switch(ctx);
+
+ return ret;
+}
+
+static int anx7411_typec_port_probe(struct anx7411_data *ctx,
+ struct device *dev)
+{
+ struct typec_capability *cap = &ctx->typec.caps;
+ struct typec_params *typecp = &ctx->typec;
+ struct fwnode_handle *fwnode;
+ const char *buf;
+ int ret, i;
+
+ fwnode = device_get_named_child_node(dev, "connector");
+ if (!fwnode)
+ return -EINVAL;
+
+ ret = fwnode_property_read_string(fwnode, "power-role", &buf);
+ if (ret) {
+ dev_err(dev, "power-role not found: %d\n", ret);
+ return ret;
+ }
+
+ ret = typec_find_port_power_role(buf);
+ if (ret < 0)
+ return ret;
+ cap->type = ret;
+
+ ret = fwnode_property_read_string(fwnode, "data-role", &buf);
+ if (ret) {
+ dev_err(dev, "data-role not found: %d\n", ret);
+ return ret;
+ }
+
+ ret = typec_find_port_data_role(buf);
+ if (ret < 0)
+ return ret;
+ cap->data = ret;
+
+ ret = fwnode_property_read_string(fwnode, "try-power-role", &buf);
+ if (ret) {
+ dev_err(dev, "try-power-role not found: %d\n", ret);
+ return ret;
+ }
+
+ ret = typec_find_power_role(buf);
+ if (ret < 0)
+ return ret;
+ cap->prefer_role = ret;
+
+ /* Get source pdos */
+ ret = fwnode_property_count_u32(fwnode, "source-pdos");
+ if (ret > 0) {
+ typecp->src_pdo_nr = min_t(u8, ret, PDO_MAX_OBJECTS);
+ ret = fwnode_property_read_u32_array(fwnode, "source-pdos",
+ typecp->src_pdo,
+ typecp->src_pdo_nr);
+ if (ret < 0) {
+ dev_err(dev, "source cap validate failed: %d\n", ret);
+ return -EINVAL;
+ }
+
+ typecp->caps_flags |= HAS_SOURCE_CAP;
+ }
+
+ ret = fwnode_property_count_u32(fwnode, "sink-pdos");
+ if (ret > 0) {
+ typecp->sink_pdo_nr = min_t(u8, ret, PDO_MAX_OBJECTS);
+ ret = fwnode_property_read_u32_array(fwnode, "sink-pdos",
+ typecp->sink_pdo,
+ typecp->sink_pdo_nr);
+ if (ret < 0) {
+ dev_err(dev, "sink cap validate failed: %d\n", ret);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < typecp->sink_pdo_nr; i++) {
+ ret = 0;
+ switch (pdo_type(typecp->sink_pdo[i])) {
+ case PDO_TYPE_FIXED:
+ ret = pdo_fixed_voltage(typecp->sink_pdo[i]);
+ break;
+ case PDO_TYPE_BATT:
+ case PDO_TYPE_VAR:
+ ret = pdo_max_voltage(typecp->sink_pdo[i]);
+ break;
+ case PDO_TYPE_APDO:
+ default:
+ ret = 0;
+ break;
+ }
+
+ /* 100mv per unit */
+ typecp->sink_voltage = max(5000, ret) / 100;
+ }
+
+ typecp->caps_flags |= HAS_SINK_CAP;
+ }
+
+ if (!fwnode_property_read_u32(fwnode, "op-sink-microwatt", &ret)) {
+ typecp->sink_watt = ret / 500000; /* 500mw per unit */
+ typecp->caps_flags |= HAS_SINK_WATT;
+ }
+
+ cap->fwnode = fwnode;
+
+ ctx->typec.role_sw = usb_role_switch_get(dev);
+ if (IS_ERR(ctx->typec.role_sw)) {
+ dev_err(dev, "USB role switch not found.\n");
+ ctx->typec.role_sw = NULL;
+ }
+
+ ctx->typec.port = typec_register_port(dev, cap);
+ if (IS_ERR(ctx->typec.port)) {
+ ret = PTR_ERR(ctx->typec.port);
+ ctx->typec.port = NULL;
+ dev_err(dev, "Failed to register type c port %d\n", ret);
+ return ret;
+ }
+
+ typec_port_register_altmodes(ctx->typec.port, NULL, ctx,
+ ctx->typec.port_amode,
+ MAX_ALTMODE);
+ return 0;
+}
+
+static int anx7411_typec_check_connection(struct anx7411_data *ctx)
+{
+ int ret;
+
+ ret = anx7411_reg_read(ctx->spi_client, FW_VER);
+ if (ret < 0)
+ return 0; /* No device attached in typec port */
+
+ /* Clear interrupt and alert status */
+ ret = anx7411_reg_write(ctx->spi_client, INT_STS, 0);
+ ret |= anx7411_reg_write(ctx->tcpc_client, ALERT_0, 0xFF);
+ ret |= anx7411_reg_write(ctx->tcpc_client, ALERT_1, 0xFF);
+ if (ret)
+ return ret;
+
+ ret = anx7411_cc_status_detect(ctx);
+ ret |= anx7411_power_role_detect(ctx);
+ ret |= anx7411_data_role_detect(ctx);
+ ret |= anx7411_set_mux(ctx, SELECT_PIN_ASSIGMENT_C);
+ if (ret)
+ return ret;
+
+ ret = anx7411_send_msg(ctx, TYPE_GET_DP_ALT_ENTER, NULL, 0);
+ ret |= anx7411_send_msg(ctx, TYPE_GET_DP_DISCOVER_MODES_INFO, NULL, 0);
+
+ return ret;
+}
+
+static int __maybe_unused anx7411_runtime_pm_suspend(struct device *dev)
+{
+ struct anx7411_data *ctx = dev_get_drvdata(dev);
+
+ mutex_lock(&ctx->lock);
+
+ anx7411_partner_unregister_altmode(ctx);
+
+ if (ctx->typec.partner)
+ anx7411_unregister_partner(ctx);
+
+ mutex_unlock(&ctx->lock);
+
+ return 0;
+}
+
+static int __maybe_unused anx7411_runtime_pm_resume(struct device *dev)
+{
+ struct anx7411_data *ctx = dev_get_drvdata(dev);
+
+ mutex_lock(&ctx->lock);
+ /* Detect PD connection */
+ if (anx7411_typec_check_connection(ctx))
+ dev_err(dev, "check connection");
+
+ mutex_unlock(&ctx->lock);
+
+ return 0;
+}
+
+static const struct dev_pm_ops anx7411_pm_ops = {
+ SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend,
+ pm_runtime_force_resume)
+ SET_RUNTIME_PM_OPS(anx7411_runtime_pm_suspend,
+ anx7411_runtime_pm_resume, NULL)
+};
+
+static void anx7411_get_gpio_irq(struct anx7411_data *ctx)
+{
+ struct device *dev = &ctx->tcpc_client->dev;
+
+ ctx->intp_gpiod = devm_gpiod_get_optional(dev, "interrupt", GPIOD_IN);
+ if (IS_ERR_OR_NULL(ctx->intp_gpiod)) {
+ dev_err(dev, "no interrupt gpio property\n");
+ return;
+ }
+
+ ctx->intp_irq = gpiod_to_irq(ctx->intp_gpiod);
+ if (ctx->intp_irq < 0)
+ dev_err(dev, "failed to get GPIO IRQ\n");
+}
+
+static enum power_supply_usb_type anx7411_psy_usb_types[] = {
+ POWER_SUPPLY_USB_TYPE_C,
+ POWER_SUPPLY_USB_TYPE_PD,
+ POWER_SUPPLY_USB_TYPE_PD_PPS,
+};
+
+static enum power_supply_property anx7411_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 anx7411_psy_set_prop(struct power_supply *psy,
+ enum power_supply_property psp,
+ const union power_supply_propval *val)
+{
+ struct anx7411_data *ctx = power_supply_get_drvdata(psy);
+ int ret = 0;
+
+ if (psp == POWER_SUPPLY_PROP_ONLINE)
+ ctx->psy_online = val->intval;
+ else
+ ret = -EINVAL;
+
+ power_supply_changed(ctx->psy);
+ return ret;
+}
+
+static int anx7411_psy_prop_writeable(struct power_supply *psy,
+ enum power_supply_property psp)
+{
+ return psp == POWER_SUPPLY_PROP_ONLINE;
+}
+
+static int anx7411_psy_get_prop(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct anx7411_data *ctx = power_supply_get_drvdata(psy);
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_USB_TYPE:
+ val->intval = ctx->usb_type;
+ break;
+ case POWER_SUPPLY_PROP_ONLINE:
+ val->intval = ctx->psy_online;
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ case POWER_SUPPLY_PROP_VOLTAGE_MIN:
+ case POWER_SUPPLY_PROP_VOLTAGE_MAX:
+ val->intval = (ctx->psy_online) ?
+ ctx->typec.request_voltage * 1000 : 0;
+ break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ case POWER_SUPPLY_PROP_CURRENT_MAX:
+ val->intval = (ctx->psy_online) ?
+ ctx->typec.request_current * 1000 : 0;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
+static int anx7411_psy_register(struct anx7411_data *ctx)
+{
+ struct power_supply_desc *psy_desc = &ctx->psy_desc;
+ struct power_supply_config psy_cfg = {};
+ char *psy_name;
+
+ psy_name = devm_kasprintf(ctx->dev, GFP_KERNEL, "anx7411-source-psy-%s",
+ dev_name(ctx->dev));
+ if (!psy_name)
+ return -ENOMEM;
+
+ psy_desc->name = psy_name;
+ psy_desc->type = POWER_SUPPLY_TYPE_USB;
+ psy_desc->usb_types = anx7411_psy_usb_types;
+ psy_desc->num_usb_types = ARRAY_SIZE(anx7411_psy_usb_types);
+ psy_desc->properties = anx7411_psy_props;
+ psy_desc->num_properties = ARRAY_SIZE(anx7411_psy_props);
+
+ psy_desc->get_property = anx7411_psy_get_prop;
+ psy_desc->set_property = anx7411_psy_set_prop;
+ psy_desc->property_is_writeable = anx7411_psy_prop_writeable;
+
+ ctx->usb_type = POWER_SUPPLY_USB_TYPE_C;
+ ctx->psy = devm_power_supply_register(ctx->dev, psy_desc, &psy_cfg);
+
+ if (IS_ERR(ctx->psy))
+ dev_warn(ctx->dev, "unable to register psy\n");
+
+ return PTR_ERR_OR_ZERO(ctx->psy);
+}
+
+static int anx7411_i2c_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct anx7411_data *plat;
+ struct device *dev = &client->dev;
+ int ret;
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_I2C_BLOCK))
+ return -ENODEV;
+
+ plat = devm_kzalloc(dev, sizeof(*plat), GFP_KERNEL);
+ if (!plat)
+ return -ENOMEM;
+
+ plat->tcpc_client = client;
+ i2c_set_clientdata(client, plat);
+
+ mutex_init(&plat->lock);
+
+ ret = anx7411_register_i2c_dummy_clients(plat, client);
+ if (ret) {
+ dev_err(dev, "fail to reserve I2C bus\n");
+ return ret;
+ }
+
+ ret = anx7411_typec_switch_probe(plat, dev);
+ if (ret) {
+ dev_err(dev, "fail to probe typec switch\n");
+ goto free_i2c_dummy;
+ }
+
+ ret = anx7411_typec_port_probe(plat, dev);
+ if (ret) {
+ dev_err(dev, "fail to probe typec property.\n");
+ ret = -ENODEV;
+ goto free_typec_switch;
+ }
+
+ plat->intp_irq = client->irq;
+ if (!client->irq)
+ anx7411_get_gpio_irq(plat);
+
+ if (!plat->intp_irq) {
+ dev_err(dev, "fail to get interrupt IRQ\n");
+ ret = -EINVAL;
+ goto free_typec_port;
+ }
+
+ plat->dev = dev;
+ plat->psy_online = ANX7411_PSY_OFFLINE;
+ ret = anx7411_psy_register(plat);
+ if (ret) {
+ dev_err(dev, "register psy\n");
+ goto free_typec_port;
+ }
+
+ INIT_WORK(&plat->work, anx7411_work_func);
+ plat->workqueue = alloc_workqueue("anx7411_work",
+ WQ_FREEZABLE |
+ WQ_MEM_RECLAIM,
+ 1);
+ if (!plat->workqueue) {
+ dev_err(dev, "fail to create work queue\n");
+ ret = -ENOMEM;
+ goto free_typec_port;
+ }
+
+ ret = devm_request_threaded_irq(dev, plat->intp_irq,
+ NULL, anx7411_intr_isr,
+ IRQF_TRIGGER_FALLING |
+ IRQF_ONESHOT,
+ "anx7411-intp", plat);
+ if (ret) {
+ dev_err(dev, "fail to request irq\n");
+ goto free_wq;
+ }
+
+ if (anx7411_typec_check_connection(plat))
+ dev_err(dev, "check status\n");
+
+ pm_runtime_enable(dev);
+
+ return 0;
+
+free_wq:
+ destroy_workqueue(plat->workqueue);
+
+free_typec_port:
+ typec_unregister_port(plat->typec.port);
+ anx7411_port_unregister_altmodes(plat->typec.port_amode);
+
+free_typec_switch:
+ anx7411_unregister_switch(plat);
+ anx7411_unregister_mux(plat);
+
+free_i2c_dummy:
+ i2c_unregister_device(plat->spi_client);
+
+ return ret;
+}
+
+static void anx7411_i2c_remove(struct i2c_client *client)
+{
+ struct anx7411_data *plat = i2c_get_clientdata(client);
+
+ anx7411_partner_unregister_altmode(plat);
+ anx7411_unregister_partner(plat);
+
+ if (plat->workqueue)
+ destroy_workqueue(plat->workqueue);
+
+ if (plat->spi_client)
+ i2c_unregister_device(plat->spi_client);
+
+ if (plat->typec.role_sw)
+ usb_role_switch_put(plat->typec.role_sw);
+
+ anx7411_unregister_mux(plat);
+
+ anx7411_unregister_switch(plat);
+
+ if (plat->typec.port)
+ typec_unregister_port(plat->typec.port);
+
+ anx7411_port_unregister_altmodes(plat->typec.port_amode);
+}
+
+static const struct i2c_device_id anx7411_id[] = {
+ {"anx7411", 0},
+ {}
+};
+
+MODULE_DEVICE_TABLE(i2c, anx7411_id);
+
+static const struct of_device_id anx_match_table[] = {
+ {.compatible = "analogix,anx7411",},
+ {},
+};
+
+static struct i2c_driver anx7411_driver = {
+ .driver = {
+ .name = "anx7411",
+ .of_match_table = anx_match_table,
+ .pm = &anx7411_pm_ops,
+ },
+ .probe = anx7411_i2c_probe,
+ .remove = anx7411_i2c_remove,
+
+ .id_table = anx7411_id,
+};
+
+module_i2c_driver(anx7411_driver);
+
+MODULE_DESCRIPTION("Anx7411 USB Type-C PD driver");
+MODULE_AUTHOR("Xin Ji <xji@analogixsemi.com>");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.5");
diff --git a/drivers/usb/typec/bus.c b/drivers/usb/typec/bus.c
new file mode 100644
index 000000000..69442a813
--- /dev/null
+++ b/drivers/usb/typec/bus.c
@@ -0,0 +1,420 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Bus for USB Type-C Alternate Modes
+ *
+ * Copyright (C) 2018 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/usb/pd_vdo.h>
+
+#include "bus.h"
+#include "class.h"
+#include "mux.h"
+
+static inline int
+typec_altmode_set_mux(struct altmode *alt, unsigned long conf, void *data)
+{
+ struct typec_mux_state state;
+
+ if (!alt->mux)
+ return 0;
+
+ state.alt = &alt->adev;
+ state.mode = conf;
+ state.data = data;
+
+ return typec_mux_set(alt->mux, &state);
+}
+
+static int typec_altmode_set_state(struct typec_altmode *adev,
+ unsigned long conf, void *data)
+{
+ bool is_port = is_typec_port(adev->dev.parent);
+ struct altmode *port_altmode;
+
+ port_altmode = is_port ? to_altmode(adev) : to_altmode(adev)->partner;
+
+ return typec_altmode_set_mux(port_altmode, conf, data);
+}
+
+/* -------------------------------------------------------------------------- */
+/* Common API */
+
+/**
+ * typec_altmode_notify - Communication between the OS and alternate mode driver
+ * @adev: Handle to the alternate mode
+ * @conf: Alternate mode specific configuration value
+ * @data: Alternate mode specific data
+ *
+ * The primary purpose for this function is to allow the alternate mode drivers
+ * to tell which pin configuration has been negotiated with the partner. That
+ * information will then be used for example to configure the muxes.
+ * Communication to the other direction is also possible, and low level device
+ * drivers can also send notifications to the alternate mode drivers. The actual
+ * communication will be specific for every SVID.
+ */
+int typec_altmode_notify(struct typec_altmode *adev,
+ unsigned long conf, void *data)
+{
+ bool is_port;
+ struct altmode *altmode;
+ struct altmode *partner;
+ int ret;
+
+ if (!adev)
+ return 0;
+
+ altmode = to_altmode(adev);
+
+ if (!altmode->partner)
+ return -ENODEV;
+
+ is_port = is_typec_port(adev->dev.parent);
+ partner = altmode->partner;
+
+ ret = typec_altmode_set_mux(is_port ? altmode : partner, conf, data);
+ if (ret)
+ return ret;
+
+ if (partner->adev.ops && partner->adev.ops->notify)
+ return partner->adev.ops->notify(&partner->adev, conf, data);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_altmode_notify);
+
+/**
+ * typec_altmode_enter - Enter Mode
+ * @adev: The alternate mode
+ * @vdo: VDO for the Enter Mode command
+ *
+ * The alternate mode drivers use this function to enter mode. The port drivers
+ * use this to inform the alternate mode drivers that the partner has initiated
+ * Enter Mode command. If the alternate mode does not require VDO, @vdo must be
+ * NULL.
+ */
+int typec_altmode_enter(struct typec_altmode *adev, u32 *vdo)
+{
+ struct altmode *partner = to_altmode(adev)->partner;
+ struct typec_altmode *pdev = &partner->adev;
+ int ret;
+
+ if (!adev || adev->active)
+ return 0;
+
+ if (!pdev->ops || !pdev->ops->enter)
+ return -EOPNOTSUPP;
+
+ if (is_typec_port(pdev->dev.parent) && !pdev->active)
+ return -EPERM;
+
+ /* Moving to USB Safe State */
+ ret = typec_altmode_set_state(adev, TYPEC_STATE_SAFE, NULL);
+ if (ret)
+ return ret;
+
+ /* Enter Mode */
+ return pdev->ops->enter(pdev, vdo);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_enter);
+
+/**
+ * typec_altmode_exit - Exit Mode
+ * @adev: The alternate mode
+ *
+ * The partner of @adev has initiated Exit Mode command.
+ */
+int typec_altmode_exit(struct typec_altmode *adev)
+{
+ struct altmode *partner = to_altmode(adev)->partner;
+ struct typec_altmode *pdev = &partner->adev;
+ int ret;
+
+ if (!adev || !adev->active)
+ return 0;
+
+ if (!pdev->ops || !pdev->ops->exit)
+ return -EOPNOTSUPP;
+
+ /* Moving to USB Safe State */
+ ret = typec_altmode_set_state(adev, TYPEC_STATE_SAFE, NULL);
+ if (ret)
+ return ret;
+
+ /* Exit Mode command */
+ return pdev->ops->exit(pdev);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_exit);
+
+/**
+ * typec_altmode_attention - Attention command
+ * @adev: The alternate mode
+ * @vdo: VDO for the Attention command
+ *
+ * Notifies the partner of @adev about Attention command.
+ */
+int typec_altmode_attention(struct typec_altmode *adev, u32 vdo)
+{
+ struct altmode *partner = to_altmode(adev)->partner;
+ struct typec_altmode *pdev;
+
+ if (!partner)
+ return -ENODEV;
+
+ pdev = &partner->adev;
+
+ if (pdev->ops && pdev->ops->attention)
+ pdev->ops->attention(pdev, vdo);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_altmode_attention);
+
+/**
+ * typec_altmode_vdm - Send Vendor Defined Messages (VDM) to the partner
+ * @adev: Alternate mode handle
+ * @header: VDM Header
+ * @vdo: Array of Vendor Defined Data Objects
+ * @count: Number of Data Objects
+ *
+ * The alternate mode drivers use this function for SVID specific communication
+ * with the partner. The port drivers use it to deliver the Structured VDMs
+ * received from the partners to the alternate mode drivers.
+ */
+int typec_altmode_vdm(struct typec_altmode *adev,
+ const u32 header, const u32 *vdo, int count)
+{
+ struct typec_altmode *pdev;
+ struct altmode *altmode;
+
+ if (!adev)
+ return 0;
+
+ altmode = to_altmode(adev);
+
+ if (!altmode->partner)
+ return -ENODEV;
+
+ pdev = &altmode->partner->adev;
+
+ if (!pdev->ops || !pdev->ops->vdm)
+ return -EOPNOTSUPP;
+
+ return pdev->ops->vdm(pdev, header, vdo, count);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_vdm);
+
+const struct typec_altmode *
+typec_altmode_get_partner(struct typec_altmode *adev)
+{
+ if (!adev || !to_altmode(adev)->partner)
+ return NULL;
+
+ return &to_altmode(adev)->partner->adev;
+}
+EXPORT_SYMBOL_GPL(typec_altmode_get_partner);
+
+/* -------------------------------------------------------------------------- */
+/* API for the alternate mode drivers */
+
+/**
+ * typec_altmode_get_plug - Find cable plug alternate mode
+ * @adev: Handle to partner alternate mode
+ * @index: Cable plug index
+ *
+ * Increment reference count for cable plug alternate mode device. Returns
+ * handle to the cable plug alternate mode, or NULL if none is found.
+ */
+struct typec_altmode *typec_altmode_get_plug(struct typec_altmode *adev,
+ enum typec_plug_index index)
+{
+ struct altmode *port = to_altmode(adev)->partner;
+
+ if (port->plug[index]) {
+ get_device(&port->plug[index]->adev.dev);
+ return &port->plug[index]->adev;
+ }
+
+ return NULL;
+}
+EXPORT_SYMBOL_GPL(typec_altmode_get_plug);
+
+/**
+ * typec_altmode_put_plug - Decrement cable plug alternate mode reference count
+ * @plug: Handle to the cable plug alternate mode
+ */
+void typec_altmode_put_plug(struct typec_altmode *plug)
+{
+ if (plug)
+ put_device(&plug->dev);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_put_plug);
+
+int __typec_altmode_register_driver(struct typec_altmode_driver *drv,
+ struct module *module)
+{
+ if (!drv->probe)
+ return -EINVAL;
+
+ drv->driver.owner = module;
+ drv->driver.bus = &typec_bus;
+
+ return driver_register(&drv->driver);
+}
+EXPORT_SYMBOL_GPL(__typec_altmode_register_driver);
+
+void typec_altmode_unregister_driver(struct typec_altmode_driver *drv)
+{
+ driver_unregister(&drv->driver);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_unregister_driver);
+
+/* -------------------------------------------------------------------------- */
+/* API for the port drivers */
+
+/**
+ * typec_match_altmode - Match SVID and mode to an array of alternate modes
+ * @altmodes: Array of alternate modes
+ * @n: Number of elements in the array, or -1 for NULL terminated arrays
+ * @svid: Standard or Vendor ID to match with
+ * @mode: Mode to match with
+ *
+ * Return pointer to an alternate mode with SVID matching @svid, or NULL when no
+ * match is found.
+ */
+struct typec_altmode *typec_match_altmode(struct typec_altmode **altmodes,
+ size_t n, u16 svid, u8 mode)
+{
+ int i;
+
+ for (i = 0; i < n; i++) {
+ if (!altmodes[i])
+ break;
+ if (altmodes[i]->svid == svid && altmodes[i]->mode == mode)
+ return altmodes[i];
+ }
+
+ return NULL;
+}
+EXPORT_SYMBOL_GPL(typec_match_altmode);
+
+/* -------------------------------------------------------------------------- */
+
+static ssize_t
+description_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ return sprintf(buf, "%s\n", alt->desc ? alt->desc : "");
+}
+static DEVICE_ATTR_RO(description);
+
+static struct attribute *typec_attrs[] = {
+ &dev_attr_description.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(typec);
+
+static int typec_match(struct device *dev, struct device_driver *driver)
+{
+ struct typec_altmode_driver *drv = to_altmode_driver(driver);
+ struct typec_altmode *altmode = to_typec_altmode(dev);
+ const struct typec_device_id *id;
+
+ for (id = drv->id_table; id->svid; id++)
+ if (id->svid == altmode->svid &&
+ (id->mode == TYPEC_ANY_MODE || id->mode == altmode->mode))
+ return 1;
+ return 0;
+}
+
+static int typec_uevent(struct device *dev, struct kobj_uevent_env *env)
+{
+ struct typec_altmode *altmode = to_typec_altmode(dev);
+
+ if (add_uevent_var(env, "SVID=%04X", altmode->svid))
+ return -ENOMEM;
+
+ if (add_uevent_var(env, "MODE=%u", altmode->mode))
+ return -ENOMEM;
+
+ return add_uevent_var(env, "MODALIAS=typec:id%04Xm%02X",
+ altmode->svid, altmode->mode);
+}
+
+static int typec_altmode_create_links(struct altmode *alt)
+{
+ struct device *port_dev = &alt->partner->adev.dev;
+ struct device *dev = &alt->adev.dev;
+ int err;
+
+ err = sysfs_create_link(&dev->kobj, &port_dev->kobj, "port");
+ if (err)
+ return err;
+
+ err = sysfs_create_link(&port_dev->kobj, &dev->kobj, "partner");
+ if (err)
+ sysfs_remove_link(&dev->kobj, "port");
+
+ return err;
+}
+
+static void typec_altmode_remove_links(struct altmode *alt)
+{
+ sysfs_remove_link(&alt->partner->adev.dev.kobj, "partner");
+ sysfs_remove_link(&alt->adev.dev.kobj, "port");
+}
+
+static int typec_probe(struct device *dev)
+{
+ struct typec_altmode_driver *drv = to_altmode_driver(dev->driver);
+ struct typec_altmode *adev = to_typec_altmode(dev);
+ struct altmode *altmode = to_altmode(adev);
+ int ret;
+
+ /* Fail if the port does not support the alternate mode */
+ if (!altmode->partner)
+ return -ENODEV;
+
+ ret = typec_altmode_create_links(altmode);
+ if (ret) {
+ dev_warn(dev, "failed to create symlinks\n");
+ return ret;
+ }
+
+ ret = drv->probe(adev);
+ if (ret)
+ typec_altmode_remove_links(altmode);
+
+ return ret;
+}
+
+static void typec_remove(struct device *dev)
+{
+ struct typec_altmode_driver *drv = to_altmode_driver(dev->driver);
+ struct typec_altmode *adev = to_typec_altmode(dev);
+ struct altmode *altmode = to_altmode(adev);
+
+ typec_altmode_remove_links(altmode);
+
+ if (drv->remove)
+ drv->remove(to_typec_altmode(dev));
+
+ if (adev->active) {
+ WARN_ON(typec_altmode_set_state(adev, TYPEC_STATE_SAFE, NULL));
+ typec_altmode_update_active(adev, false);
+ }
+
+ adev->desc = NULL;
+ adev->ops = NULL;
+}
+
+struct bus_type typec_bus = {
+ .name = "typec",
+ .dev_groups = typec_groups,
+ .match = typec_match,
+ .uevent = typec_uevent,
+ .probe = typec_probe,
+ .remove = typec_remove,
+};
diff --git a/drivers/usb/typec/bus.h b/drivers/usb/typec/bus.h
new file mode 100644
index 000000000..56dec268d
--- /dev/null
+++ b/drivers/usb/typec/bus.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_TYPEC_ALTMODE_H__
+#define __USB_TYPEC_ALTMODE_H__
+
+#include <linux/usb/typec_altmode.h>
+
+struct bus_type;
+struct typec_mux;
+
+struct altmode {
+ unsigned int id;
+ struct typec_altmode adev;
+ struct typec_mux *mux;
+
+ enum typec_port_data roles;
+
+ struct attribute *attrs[5];
+ char group_name[8];
+ struct attribute_group group;
+ const struct attribute_group *groups[2];
+
+ struct altmode *partner;
+ struct altmode *plug[2];
+};
+
+#define to_altmode(d) container_of(d, struct altmode, adev)
+
+extern struct bus_type typec_bus;
+extern const struct device_type typec_altmode_dev_type;
+
+#define is_typec_altmode(_dev_) (_dev_->type == &typec_altmode_dev_type)
+
+#endif /* __USB_TYPEC_ALTMODE_H__ */
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
new file mode 100644
index 000000000..3da404d51
--- /dev/null
+++ b/drivers/usb/typec/class.c
@@ -0,0 +1,2359 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Type-C Connector Class
+ *
+ * Copyright (C) 2017, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/usb/pd_vdo.h>
+#include <linux/usb/typec_mux.h>
+#include <linux/usb/typec_retimer.h>
+
+#include "bus.h"
+#include "class.h"
+#include "pd.h"
+
+static DEFINE_IDA(typec_index_ida);
+
+struct class typec_class = {
+ .name = "typec",
+ .owner = THIS_MODULE,
+};
+
+/* ------------------------------------------------------------------------- */
+/* Common attributes */
+
+static const char * const typec_accessory_modes[] = {
+ [TYPEC_ACCESSORY_NONE] = "none",
+ [TYPEC_ACCESSORY_AUDIO] = "analog_audio",
+ [TYPEC_ACCESSORY_DEBUG] = "debug",
+};
+
+/* Product types defined in USB PD Specification R3.0 V2.0 */
+static const char * const product_type_ufp[8] = {
+ [IDH_PTYPE_NOT_UFP] = "not_ufp",
+ [IDH_PTYPE_HUB] = "hub",
+ [IDH_PTYPE_PERIPH] = "peripheral",
+ [IDH_PTYPE_PSD] = "psd",
+ [IDH_PTYPE_AMA] = "ama",
+};
+
+static const char * const product_type_dfp[8] = {
+ [IDH_PTYPE_NOT_DFP] = "not_dfp",
+ [IDH_PTYPE_DFP_HUB] = "hub",
+ [IDH_PTYPE_DFP_HOST] = "host",
+ [IDH_PTYPE_DFP_PB] = "power_brick",
+};
+
+static const char * const product_type_cable[8] = {
+ [IDH_PTYPE_NOT_CABLE] = "not_cable",
+ [IDH_PTYPE_PCABLE] = "passive",
+ [IDH_PTYPE_ACABLE] = "active",
+ [IDH_PTYPE_VPD] = "vpd",
+};
+
+static struct usb_pd_identity *get_pd_identity(struct device *dev)
+{
+ if (is_typec_partner(dev)) {
+ struct typec_partner *partner = to_typec_partner(dev);
+
+ return partner->identity;
+ } else if (is_typec_cable(dev)) {
+ struct typec_cable *cable = to_typec_cable(dev);
+
+ return cable->identity;
+ }
+ return NULL;
+}
+
+static const char *get_pd_product_type(struct device *dev)
+{
+ struct typec_port *port = to_typec_port(dev->parent);
+ struct usb_pd_identity *id = get_pd_identity(dev);
+ const char *ptype = NULL;
+
+ if (is_typec_partner(dev)) {
+ if (!id)
+ return NULL;
+
+ if (port->data_role == TYPEC_HOST)
+ ptype = product_type_ufp[PD_IDH_PTYPE(id->id_header)];
+ else
+ ptype = product_type_dfp[PD_IDH_DFP_PTYPE(id->id_header)];
+ } else if (is_typec_cable(dev)) {
+ if (id)
+ ptype = product_type_cable[PD_IDH_PTYPE(id->id_header)];
+ else
+ ptype = to_typec_cable(dev)->active ?
+ product_type_cable[IDH_PTYPE_ACABLE] :
+ product_type_cable[IDH_PTYPE_PCABLE];
+ }
+
+ return ptype;
+}
+
+static ssize_t id_header_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sprintf(buf, "0x%08x\n", id->id_header);
+}
+static DEVICE_ATTR_RO(id_header);
+
+static ssize_t cert_stat_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sprintf(buf, "0x%08x\n", id->cert_stat);
+}
+static DEVICE_ATTR_RO(cert_stat);
+
+static ssize_t product_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sprintf(buf, "0x%08x\n", id->product);
+}
+static DEVICE_ATTR_RO(product);
+
+static ssize_t product_type_vdo1_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sysfs_emit(buf, "0x%08x\n", id->vdo[0]);
+}
+static DEVICE_ATTR_RO(product_type_vdo1);
+
+static ssize_t product_type_vdo2_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sysfs_emit(buf, "0x%08x\n", id->vdo[1]);
+}
+static DEVICE_ATTR_RO(product_type_vdo2);
+
+static ssize_t product_type_vdo3_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct usb_pd_identity *id = get_pd_identity(dev);
+
+ return sysfs_emit(buf, "0x%08x\n", id->vdo[2]);
+}
+static DEVICE_ATTR_RO(product_type_vdo3);
+
+static struct attribute *usb_pd_id_attrs[] = {
+ &dev_attr_id_header.attr,
+ &dev_attr_cert_stat.attr,
+ &dev_attr_product.attr,
+ &dev_attr_product_type_vdo1.attr,
+ &dev_attr_product_type_vdo2.attr,
+ &dev_attr_product_type_vdo3.attr,
+ NULL
+};
+
+static const struct attribute_group usb_pd_id_group = {
+ .name = "identity",
+ .attrs = usb_pd_id_attrs,
+};
+
+static const struct attribute_group *usb_pd_id_groups[] = {
+ &usb_pd_id_group,
+ NULL,
+};
+
+static void typec_product_type_notify(struct device *dev)
+{
+ char *envp[2] = { };
+ const char *ptype;
+
+ ptype = get_pd_product_type(dev);
+ if (!ptype)
+ return;
+
+ sysfs_notify(&dev->kobj, NULL, "type");
+
+ envp[0] = kasprintf(GFP_KERNEL, "PRODUCT_TYPE=%s", ptype);
+ if (!envp[0])
+ return;
+
+ kobject_uevent_env(&dev->kobj, KOBJ_CHANGE, envp);
+ kfree(envp[0]);
+}
+
+static void typec_report_identity(struct device *dev)
+{
+ sysfs_notify(&dev->kobj, "identity", "id_header");
+ sysfs_notify(&dev->kobj, "identity", "cert_stat");
+ sysfs_notify(&dev->kobj, "identity", "product");
+ sysfs_notify(&dev->kobj, "identity", "product_type_vdo1");
+ sysfs_notify(&dev->kobj, "identity", "product_type_vdo2");
+ sysfs_notify(&dev->kobj, "identity", "product_type_vdo3");
+ typec_product_type_notify(dev);
+}
+
+static ssize_t
+type_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ const char *ptype;
+
+ ptype = get_pd_product_type(dev);
+ if (!ptype)
+ return 0;
+
+ return sysfs_emit(buf, "%s\n", ptype);
+}
+static DEVICE_ATTR_RO(type);
+
+static ssize_t usb_power_delivery_revision_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf);
+static DEVICE_ATTR_RO(usb_power_delivery_revision);
+
+/* ------------------------------------------------------------------------- */
+/* Alternate Modes */
+
+static int altmode_match(struct device *dev, void *data)
+{
+ struct typec_altmode *adev = to_typec_altmode(dev);
+ struct typec_device_id *id = data;
+
+ if (!is_typec_altmode(dev))
+ return 0;
+
+ return ((adev->svid == id->svid) && (adev->mode == id->mode));
+}
+
+static void typec_altmode_set_partner(struct altmode *altmode)
+{
+ struct typec_altmode *adev = &altmode->adev;
+ struct typec_device_id id = { adev->svid, adev->mode, };
+ struct typec_port *port = typec_altmode2port(adev);
+ struct altmode *partner;
+ struct device *dev;
+
+ dev = device_find_child(&port->dev, &id, altmode_match);
+ if (!dev)
+ return;
+
+ /* Bind the port alt mode to the partner/plug alt mode. */
+ partner = to_altmode(to_typec_altmode(dev));
+ altmode->partner = partner;
+
+ /* Bind the partner/plug alt mode to the port alt mode. */
+ if (is_typec_plug(adev->dev.parent)) {
+ struct typec_plug *plug = to_typec_plug(adev->dev.parent);
+
+ partner->plug[plug->index] = altmode;
+ } else {
+ partner->partner = altmode;
+ }
+}
+
+static void typec_altmode_put_partner(struct altmode *altmode)
+{
+ struct altmode *partner = altmode->partner;
+ struct typec_altmode *adev;
+ struct typec_altmode *partner_adev;
+
+ if (!partner)
+ return;
+
+ adev = &altmode->adev;
+ partner_adev = &partner->adev;
+
+ if (is_typec_plug(adev->dev.parent)) {
+ struct typec_plug *plug = to_typec_plug(adev->dev.parent);
+
+ partner->plug[plug->index] = NULL;
+ } else {
+ partner->partner = NULL;
+ }
+ put_device(&partner_adev->dev);
+}
+
+/**
+ * typec_altmode_update_active - Report Enter/Exit mode
+ * @adev: Handle to the alternate mode
+ * @active: True when the mode has been entered
+ *
+ * If a partner or cable plug executes Enter/Exit Mode command successfully, the
+ * drivers use this routine to report the updated state of the mode.
+ */
+void typec_altmode_update_active(struct typec_altmode *adev, bool active)
+{
+ char dir[6];
+
+ if (adev->active == active)
+ return;
+
+ if (!is_typec_port(adev->dev.parent) && adev->dev.driver) {
+ if (!active)
+ module_put(adev->dev.driver->owner);
+ else
+ WARN_ON(!try_module_get(adev->dev.driver->owner));
+ }
+
+ adev->active = active;
+ snprintf(dir, sizeof(dir), "mode%d", adev->mode);
+ sysfs_notify(&adev->dev.kobj, dir, "active");
+ sysfs_notify(&adev->dev.kobj, NULL, "active");
+ kobject_uevent(&adev->dev.kobj, KOBJ_CHANGE);
+}
+EXPORT_SYMBOL_GPL(typec_altmode_update_active);
+
+/**
+ * typec_altmode2port - Alternate Mode to USB Type-C port
+ * @alt: The Alternate Mode
+ *
+ * Returns handle to the port that a cable plug or partner with @alt is
+ * connected to.
+ */
+struct typec_port *typec_altmode2port(struct typec_altmode *alt)
+{
+ if (is_typec_plug(alt->dev.parent))
+ return to_typec_port(alt->dev.parent->parent->parent);
+ if (is_typec_partner(alt->dev.parent))
+ return to_typec_port(alt->dev.parent->parent);
+ if (is_typec_port(alt->dev.parent))
+ return to_typec_port(alt->dev.parent);
+
+ return NULL;
+}
+EXPORT_SYMBOL_GPL(typec_altmode2port);
+
+static ssize_t
+vdo_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ return sprintf(buf, "0x%08x\n", alt->vdo);
+}
+static DEVICE_ATTR_RO(vdo);
+
+static ssize_t
+description_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ return sprintf(buf, "%s\n", alt->desc ? alt->desc : "");
+}
+static DEVICE_ATTR_RO(description);
+
+static ssize_t
+active_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ return sprintf(buf, "%s\n", alt->active ? "yes" : "no");
+}
+
+static ssize_t active_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_altmode *adev = to_typec_altmode(dev);
+ struct altmode *altmode = to_altmode(adev);
+ bool enter;
+ int ret;
+
+ ret = kstrtobool(buf, &enter);
+ if (ret)
+ return ret;
+
+ if (adev->active == enter)
+ return size;
+
+ if (is_typec_port(adev->dev.parent)) {
+ typec_altmode_update_active(adev, enter);
+
+ /* Make sure that the partner exits the mode before disabling */
+ if (altmode->partner && !enter && altmode->partner->adev.active)
+ typec_altmode_exit(&altmode->partner->adev);
+ } else if (altmode->partner) {
+ if (enter && !altmode->partner->adev.active) {
+ dev_warn(dev, "port has the mode disabled\n");
+ return -EPERM;
+ }
+ }
+
+ /* Note: If there is no driver, the mode will not be entered */
+ if (adev->ops && adev->ops->activate) {
+ ret = adev->ops->activate(adev, enter);
+ if (ret)
+ return ret;
+ }
+
+ return size;
+}
+static DEVICE_ATTR_RW(active);
+
+static ssize_t
+supported_roles_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct altmode *alt = to_altmode(to_typec_altmode(dev));
+ ssize_t ret;
+
+ switch (alt->roles) {
+ case TYPEC_PORT_SRC:
+ ret = sprintf(buf, "source\n");
+ break;
+ case TYPEC_PORT_SNK:
+ ret = sprintf(buf, "sink\n");
+ break;
+ case TYPEC_PORT_DRP:
+ default:
+ ret = sprintf(buf, "source sink\n");
+ break;
+ }
+ return ret;
+}
+static DEVICE_ATTR_RO(supported_roles);
+
+static ssize_t
+mode_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *adev = to_typec_altmode(dev);
+
+ return sprintf(buf, "%u\n", adev->mode);
+}
+static DEVICE_ATTR_RO(mode);
+
+static ssize_t
+svid_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *adev = to_typec_altmode(dev);
+
+ return sprintf(buf, "%04x\n", adev->svid);
+}
+static DEVICE_ATTR_RO(svid);
+
+static struct attribute *typec_altmode_attrs[] = {
+ &dev_attr_active.attr,
+ &dev_attr_mode.attr,
+ &dev_attr_svid.attr,
+ &dev_attr_vdo.attr,
+ NULL
+};
+
+static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct typec_altmode *adev = to_typec_altmode(kobj_to_dev(kobj));
+
+ if (attr == &dev_attr_active.attr)
+ if (!adev->ops || !adev->ops->activate)
+ return 0444;
+
+ return attr->mode;
+}
+
+static const struct attribute_group typec_altmode_group = {
+ .is_visible = typec_altmode_attr_is_visible,
+ .attrs = typec_altmode_attrs,
+};
+
+static const struct attribute_group *typec_altmode_groups[] = {
+ &typec_altmode_group,
+ NULL
+};
+
+static int altmode_id_get(struct device *dev)
+{
+ struct ida *ids;
+
+ if (is_typec_partner(dev))
+ ids = &to_typec_partner(dev)->mode_ids;
+ else if (is_typec_plug(dev))
+ ids = &to_typec_plug(dev)->mode_ids;
+ else
+ ids = &to_typec_port(dev)->mode_ids;
+
+ return ida_simple_get(ids, 0, 0, GFP_KERNEL);
+}
+
+static void altmode_id_remove(struct device *dev, int id)
+{
+ struct ida *ids;
+
+ if (is_typec_partner(dev))
+ ids = &to_typec_partner(dev)->mode_ids;
+ else if (is_typec_plug(dev))
+ ids = &to_typec_plug(dev)->mode_ids;
+ else
+ ids = &to_typec_port(dev)->mode_ids;
+
+ ida_simple_remove(ids, id);
+}
+
+static void typec_altmode_release(struct device *dev)
+{
+ struct altmode *alt = to_altmode(to_typec_altmode(dev));
+
+ if (!is_typec_port(dev->parent))
+ typec_altmode_put_partner(alt);
+
+ altmode_id_remove(alt->adev.dev.parent, alt->id);
+ kfree(alt);
+}
+
+const struct device_type typec_altmode_dev_type = {
+ .name = "typec_alternate_mode",
+ .groups = typec_altmode_groups,
+ .release = typec_altmode_release,
+};
+
+static struct typec_altmode *
+typec_register_altmode(struct device *parent,
+ const struct typec_altmode_desc *desc)
+{
+ unsigned int id = altmode_id_get(parent);
+ bool is_port = is_typec_port(parent);
+ struct altmode *alt;
+ int ret;
+
+ alt = kzalloc(sizeof(*alt), GFP_KERNEL);
+ if (!alt) {
+ altmode_id_remove(parent, id);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ alt->adev.svid = desc->svid;
+ alt->adev.mode = desc->mode;
+ alt->adev.vdo = desc->vdo;
+ alt->roles = desc->roles;
+ alt->id = id;
+
+ alt->attrs[0] = &dev_attr_vdo.attr;
+ alt->attrs[1] = &dev_attr_description.attr;
+ alt->attrs[2] = &dev_attr_active.attr;
+
+ if (is_port) {
+ alt->attrs[3] = &dev_attr_supported_roles.attr;
+ alt->adev.active = true; /* Enabled by default */
+ }
+
+ sprintf(alt->group_name, "mode%d", desc->mode);
+ alt->group.name = alt->group_name;
+ alt->group.attrs = alt->attrs;
+ alt->groups[0] = &alt->group;
+
+ alt->adev.dev.parent = parent;
+ alt->adev.dev.groups = alt->groups;
+ alt->adev.dev.type = &typec_altmode_dev_type;
+ dev_set_name(&alt->adev.dev, "%s.%u", dev_name(parent), id);
+
+ /* Link partners and plugs with the ports */
+ if (!is_port)
+ typec_altmode_set_partner(alt);
+
+ /* The partners are bind to drivers */
+ if (is_typec_partner(parent))
+ alt->adev.dev.bus = &typec_bus;
+
+ /* Plug alt modes need a class to generate udev events. */
+ if (is_typec_plug(parent))
+ alt->adev.dev.class = &typec_class;
+
+ ret = device_register(&alt->adev.dev);
+ if (ret) {
+ dev_err(parent, "failed to register alternate mode (%d)\n",
+ ret);
+ put_device(&alt->adev.dev);
+ return ERR_PTR(ret);
+ }
+
+ return &alt->adev;
+}
+
+/**
+ * typec_unregister_altmode - Unregister Alternate Mode
+ * @adev: The alternate mode to be unregistered
+ *
+ * Unregister device created with typec_partner_register_altmode(),
+ * typec_plug_register_altmode() or typec_port_register_altmode().
+ */
+void typec_unregister_altmode(struct typec_altmode *adev)
+{
+ if (IS_ERR_OR_NULL(adev))
+ return;
+ typec_mux_put(to_altmode(adev)->mux);
+ device_unregister(&adev->dev);
+}
+EXPORT_SYMBOL_GPL(typec_unregister_altmode);
+
+/* ------------------------------------------------------------------------- */
+/* Type-C Partners */
+
+static ssize_t accessory_mode_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_partner *p = to_typec_partner(dev);
+
+ return sprintf(buf, "%s\n", typec_accessory_modes[p->accessory]);
+}
+static DEVICE_ATTR_RO(accessory_mode);
+
+static ssize_t supports_usb_power_delivery_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_partner *p = to_typec_partner(dev);
+
+ return sprintf(buf, "%s\n", p->usb_pd ? "yes" : "no");
+}
+static DEVICE_ATTR_RO(supports_usb_power_delivery);
+
+static ssize_t number_of_alternate_modes_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_partner *partner;
+ struct typec_plug *plug;
+ int num_altmodes;
+
+ if (is_typec_partner(dev)) {
+ partner = to_typec_partner(dev);
+ num_altmodes = partner->num_altmodes;
+ } else if (is_typec_plug(dev)) {
+ plug = to_typec_plug(dev);
+ num_altmodes = plug->num_altmodes;
+ } else {
+ return 0;
+ }
+
+ return sysfs_emit(buf, "%d\n", num_altmodes);
+}
+static DEVICE_ATTR_RO(number_of_alternate_modes);
+
+static struct attribute *typec_partner_attrs[] = {
+ &dev_attr_accessory_mode.attr,
+ &dev_attr_supports_usb_power_delivery.attr,
+ &dev_attr_number_of_alternate_modes.attr,
+ &dev_attr_type.attr,
+ &dev_attr_usb_power_delivery_revision.attr,
+ NULL
+};
+
+static umode_t typec_partner_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ struct typec_partner *partner = to_typec_partner(kobj_to_dev(kobj));
+
+ if (attr == &dev_attr_number_of_alternate_modes.attr) {
+ if (partner->num_altmodes < 0)
+ return 0;
+ }
+
+ if (attr == &dev_attr_type.attr)
+ if (!get_pd_product_type(kobj_to_dev(kobj)))
+ return 0;
+
+ return attr->mode;
+}
+
+static const struct attribute_group typec_partner_group = {
+ .is_visible = typec_partner_attr_is_visible,
+ .attrs = typec_partner_attrs
+};
+
+static const struct attribute_group *typec_partner_groups[] = {
+ &typec_partner_group,
+ NULL
+};
+
+static void typec_partner_release(struct device *dev)
+{
+ struct typec_partner *partner = to_typec_partner(dev);
+
+ ida_destroy(&partner->mode_ids);
+ kfree(partner);
+}
+
+const struct device_type typec_partner_dev_type = {
+ .name = "typec_partner",
+ .groups = typec_partner_groups,
+ .release = typec_partner_release,
+};
+
+/**
+ * typec_partner_set_identity - Report result from Discover Identity command
+ * @partner: The partner updated identity values
+ *
+ * This routine is used to report that the result of Discover Identity USB power
+ * delivery command has become available.
+ */
+int typec_partner_set_identity(struct typec_partner *partner)
+{
+ if (!partner->identity)
+ return -EINVAL;
+
+ typec_report_identity(&partner->dev);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_partner_set_identity);
+
+/**
+ * typec_partner_set_pd_revision - Set the PD revision supported by the partner
+ * @partner: The partner to be updated.
+ * @pd_revision: USB Power Delivery Specification Revision supported by partner
+ *
+ * This routine is used to report that the PD revision of the port partner has
+ * become available.
+ */
+void typec_partner_set_pd_revision(struct typec_partner *partner, u16 pd_revision)
+{
+ if (partner->pd_revision == pd_revision)
+ return;
+
+ partner->pd_revision = pd_revision;
+ sysfs_notify(&partner->dev.kobj, NULL, "usb_power_delivery_revision");
+ if (pd_revision != 0 && !partner->usb_pd) {
+ partner->usb_pd = 1;
+ sysfs_notify(&partner->dev.kobj, NULL,
+ "supports_usb_power_delivery");
+ }
+ kobject_uevent(&partner->dev.kobj, KOBJ_CHANGE);
+}
+EXPORT_SYMBOL_GPL(typec_partner_set_pd_revision);
+
+/**
+ * typec_partner_set_usb_power_delivery - Declare USB Power Delivery Contract.
+ * @partner: The partner device.
+ * @pd: The USB PD instance.
+ *
+ * This routine can be used to declare USB Power Delivery Contract with @partner
+ * by linking @partner to @pd which contains the objects that were used during the
+ * negotiation of the contract.
+ *
+ * If @pd is NULL, the link is removed and the contract with @partner has ended.
+ */
+int typec_partner_set_usb_power_delivery(struct typec_partner *partner,
+ struct usb_power_delivery *pd)
+{
+ int ret;
+
+ if (IS_ERR_OR_NULL(partner) || partner->pd == pd)
+ return 0;
+
+ if (pd) {
+ ret = usb_power_delivery_link_device(pd, &partner->dev);
+ if (ret)
+ return ret;
+ } else {
+ usb_power_delivery_unlink_device(partner->pd, &partner->dev);
+ }
+
+ partner->pd = pd;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_partner_set_usb_power_delivery);
+
+/**
+ * typec_partner_set_num_altmodes - Set the number of available partner altmodes
+ * @partner: The partner to be updated.
+ * @num_altmodes: The number of altmodes we want to specify as available.
+ *
+ * This routine is used to report the number of alternate modes supported by the
+ * partner. This value is *not* enforced in alternate mode registration routines.
+ *
+ * @partner.num_altmodes is set to -1 on partner registration, denoting that
+ * a valid value has not been set for it yet.
+ *
+ * Returns 0 on success or negative error number on failure.
+ */
+int typec_partner_set_num_altmodes(struct typec_partner *partner, int num_altmodes)
+{
+ int ret;
+
+ if (num_altmodes < 0)
+ return -EINVAL;
+
+ partner->num_altmodes = num_altmodes;
+ ret = sysfs_update_group(&partner->dev.kobj, &typec_partner_group);
+ if (ret < 0)
+ return ret;
+
+ sysfs_notify(&partner->dev.kobj, NULL, "number_of_alternate_modes");
+ kobject_uevent(&partner->dev.kobj, KOBJ_CHANGE);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_partner_set_num_altmodes);
+
+/**
+ * typec_partner_register_altmode - Register USB Type-C Partner Alternate Mode
+ * @partner: USB Type-C Partner that supports the alternate mode
+ * @desc: Description of the alternate mode
+ *
+ * This routine is used to register each alternate mode individually that
+ * @partner has listed in response to Discover SVIDs command. The modes for a
+ * SVID listed in response to Discover Modes command need to be listed in an
+ * array in @desc.
+ *
+ * Returns handle to the alternate mode on success or ERR_PTR on failure.
+ */
+struct typec_altmode *
+typec_partner_register_altmode(struct typec_partner *partner,
+ const struct typec_altmode_desc *desc)
+{
+ return typec_register_altmode(&partner->dev, desc);
+}
+EXPORT_SYMBOL_GPL(typec_partner_register_altmode);
+
+/**
+ * typec_partner_set_svdm_version - Set negotiated Structured VDM (SVDM) Version
+ * @partner: USB Type-C Partner that supports SVDM
+ * @svdm_version: Negotiated SVDM Version
+ *
+ * This routine is used to save the negotiated SVDM Version.
+ */
+void typec_partner_set_svdm_version(struct typec_partner *partner,
+ enum usb_pd_svdm_ver svdm_version)
+{
+ partner->svdm_version = svdm_version;
+}
+EXPORT_SYMBOL_GPL(typec_partner_set_svdm_version);
+
+/**
+ * typec_register_partner - Register a USB Type-C Partner
+ * @port: The USB Type-C Port the partner is connected to
+ * @desc: Description of the partner
+ *
+ * Registers a device for USB Type-C Partner described in @desc.
+ *
+ * Returns handle to the partner on success or ERR_PTR on failure.
+ */
+struct typec_partner *typec_register_partner(struct typec_port *port,
+ struct typec_partner_desc *desc)
+{
+ struct typec_partner *partner;
+ int ret;
+
+ partner = kzalloc(sizeof(*partner), GFP_KERNEL);
+ if (!partner)
+ return ERR_PTR(-ENOMEM);
+
+ ida_init(&partner->mode_ids);
+ partner->usb_pd = desc->usb_pd;
+ partner->accessory = desc->accessory;
+ partner->num_altmodes = -1;
+ partner->pd_revision = desc->pd_revision;
+ partner->svdm_version = port->cap->svdm_version;
+
+ if (desc->identity) {
+ /*
+ * Creating directory for the identity only if the driver is
+ * able to provide data to it.
+ */
+ partner->dev.groups = usb_pd_id_groups;
+ partner->identity = desc->identity;
+ }
+
+ partner->dev.class = &typec_class;
+ partner->dev.parent = &port->dev;
+ partner->dev.type = &typec_partner_dev_type;
+ dev_set_name(&partner->dev, "%s-partner", dev_name(&port->dev));
+
+ ret = device_register(&partner->dev);
+ if (ret) {
+ dev_err(&port->dev, "failed to register partner (%d)\n", ret);
+ put_device(&partner->dev);
+ return ERR_PTR(ret);
+ }
+
+ return partner;
+}
+EXPORT_SYMBOL_GPL(typec_register_partner);
+
+/**
+ * typec_unregister_partner - Unregister a USB Type-C Partner
+ * @partner: The partner to be unregistered
+ *
+ * Unregister device created with typec_register_partner().
+ */
+void typec_unregister_partner(struct typec_partner *partner)
+{
+ if (!IS_ERR_OR_NULL(partner))
+ device_unregister(&partner->dev);
+}
+EXPORT_SYMBOL_GPL(typec_unregister_partner);
+
+/* ------------------------------------------------------------------------- */
+/* Type-C Cable Plugs */
+
+static void typec_plug_release(struct device *dev)
+{
+ struct typec_plug *plug = to_typec_plug(dev);
+
+ ida_destroy(&plug->mode_ids);
+ kfree(plug);
+}
+
+static struct attribute *typec_plug_attrs[] = {
+ &dev_attr_number_of_alternate_modes.attr,
+ NULL
+};
+
+static umode_t typec_plug_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ struct typec_plug *plug = to_typec_plug(kobj_to_dev(kobj));
+
+ if (attr == &dev_attr_number_of_alternate_modes.attr) {
+ if (plug->num_altmodes < 0)
+ return 0;
+ }
+
+ return attr->mode;
+}
+
+static const struct attribute_group typec_plug_group = {
+ .is_visible = typec_plug_attr_is_visible,
+ .attrs = typec_plug_attrs
+};
+
+static const struct attribute_group *typec_plug_groups[] = {
+ &typec_plug_group,
+ NULL
+};
+
+const struct device_type typec_plug_dev_type = {
+ .name = "typec_plug",
+ .groups = typec_plug_groups,
+ .release = typec_plug_release,
+};
+
+/**
+ * typec_plug_set_num_altmodes - Set the number of available plug altmodes
+ * @plug: The plug to be updated.
+ * @num_altmodes: The number of altmodes we want to specify as available.
+ *
+ * This routine is used to report the number of alternate modes supported by the
+ * plug. This value is *not* enforced in alternate mode registration routines.
+ *
+ * @plug.num_altmodes is set to -1 on plug registration, denoting that
+ * a valid value has not been set for it yet.
+ *
+ * Returns 0 on success or negative error number on failure.
+ */
+int typec_plug_set_num_altmodes(struct typec_plug *plug, int num_altmodes)
+{
+ int ret;
+
+ if (num_altmodes < 0)
+ return -EINVAL;
+
+ plug->num_altmodes = num_altmodes;
+ ret = sysfs_update_group(&plug->dev.kobj, &typec_plug_group);
+ if (ret < 0)
+ return ret;
+
+ sysfs_notify(&plug->dev.kobj, NULL, "number_of_alternate_modes");
+ kobject_uevent(&plug->dev.kobj, KOBJ_CHANGE);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_plug_set_num_altmodes);
+
+/**
+ * typec_plug_register_altmode - Register USB Type-C Cable Plug Alternate Mode
+ * @plug: USB Type-C Cable Plug that supports the alternate mode
+ * @desc: Description of the alternate mode
+ *
+ * This routine is used to register each alternate mode individually that @plug
+ * has listed in response to Discover SVIDs command. The modes for a SVID that
+ * the plug lists in response to Discover Modes command need to be listed in an
+ * array in @desc.
+ *
+ * Returns handle to the alternate mode on success or ERR_PTR on failure.
+ */
+struct typec_altmode *
+typec_plug_register_altmode(struct typec_plug *plug,
+ const struct typec_altmode_desc *desc)
+{
+ return typec_register_altmode(&plug->dev, desc);
+}
+EXPORT_SYMBOL_GPL(typec_plug_register_altmode);
+
+/**
+ * typec_register_plug - Register a USB Type-C Cable Plug
+ * @cable: USB Type-C Cable with the plug
+ * @desc: Description of the cable plug
+ *
+ * Registers a device for USB Type-C Cable Plug described in @desc. A USB Type-C
+ * Cable Plug represents a plug with electronics in it that can response to USB
+ * Power Delivery SOP Prime or SOP Double Prime packages.
+ *
+ * Returns handle to the cable plug on success or ERR_PTR on failure.
+ */
+struct typec_plug *typec_register_plug(struct typec_cable *cable,
+ struct typec_plug_desc *desc)
+{
+ struct typec_plug *plug;
+ char name[8];
+ int ret;
+
+ plug = kzalloc(sizeof(*plug), GFP_KERNEL);
+ if (!plug)
+ return ERR_PTR(-ENOMEM);
+
+ sprintf(name, "plug%d", desc->index);
+
+ ida_init(&plug->mode_ids);
+ plug->num_altmodes = -1;
+ plug->index = desc->index;
+ plug->dev.class = &typec_class;
+ plug->dev.parent = &cable->dev;
+ plug->dev.type = &typec_plug_dev_type;
+ dev_set_name(&plug->dev, "%s-%s", dev_name(cable->dev.parent), name);
+
+ ret = device_register(&plug->dev);
+ if (ret) {
+ dev_err(&cable->dev, "failed to register plug (%d)\n", ret);
+ put_device(&plug->dev);
+ return ERR_PTR(ret);
+ }
+
+ return plug;
+}
+EXPORT_SYMBOL_GPL(typec_register_plug);
+
+/**
+ * typec_unregister_plug - Unregister a USB Type-C Cable Plug
+ * @plug: The cable plug to be unregistered
+ *
+ * Unregister device created with typec_register_plug().
+ */
+void typec_unregister_plug(struct typec_plug *plug)
+{
+ if (!IS_ERR_OR_NULL(plug))
+ device_unregister(&plug->dev);
+}
+EXPORT_SYMBOL_GPL(typec_unregister_plug);
+
+/* Type-C Cables */
+
+static const char * const typec_plug_types[] = {
+ [USB_PLUG_NONE] = "unknown",
+ [USB_PLUG_TYPE_A] = "type-a",
+ [USB_PLUG_TYPE_B] = "type-b",
+ [USB_PLUG_TYPE_C] = "type-c",
+ [USB_PLUG_CAPTIVE] = "captive",
+};
+
+static ssize_t plug_type_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_cable *cable = to_typec_cable(dev);
+
+ return sprintf(buf, "%s\n", typec_plug_types[cable->type]);
+}
+static DEVICE_ATTR_RO(plug_type);
+
+static struct attribute *typec_cable_attrs[] = {
+ &dev_attr_type.attr,
+ &dev_attr_plug_type.attr,
+ &dev_attr_usb_power_delivery_revision.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(typec_cable);
+
+static void typec_cable_release(struct device *dev)
+{
+ struct typec_cable *cable = to_typec_cable(dev);
+
+ kfree(cable);
+}
+
+const struct device_type typec_cable_dev_type = {
+ .name = "typec_cable",
+ .groups = typec_cable_groups,
+ .release = typec_cable_release,
+};
+
+static int cable_match(struct device *dev, void *data)
+{
+ return is_typec_cable(dev);
+}
+
+/**
+ * typec_cable_get - Get a reference to the USB Type-C cable
+ * @port: The USB Type-C Port the cable is connected to
+ *
+ * The caller must decrement the reference count with typec_cable_put() after
+ * use.
+ */
+struct typec_cable *typec_cable_get(struct typec_port *port)
+{
+ struct device *dev;
+
+ dev = device_find_child(&port->dev, NULL, cable_match);
+ if (!dev)
+ return NULL;
+
+ return to_typec_cable(dev);
+}
+EXPORT_SYMBOL_GPL(typec_cable_get);
+
+/**
+ * typec_cable_put - Decrement the reference count on USB Type-C cable
+ * @cable: The USB Type-C cable
+ */
+void typec_cable_put(struct typec_cable *cable)
+{
+ put_device(&cable->dev);
+}
+EXPORT_SYMBOL_GPL(typec_cable_put);
+
+/**
+ * typec_cable_is_active - Check is the USB Type-C cable active or passive
+ * @cable: The USB Type-C Cable
+ *
+ * Return 1 if the cable is active or 0 if it's passive.
+ */
+int typec_cable_is_active(struct typec_cable *cable)
+{
+ return cable->active;
+}
+EXPORT_SYMBOL_GPL(typec_cable_is_active);
+
+/**
+ * typec_cable_set_identity - Report result from Discover Identity command
+ * @cable: The cable updated identity values
+ *
+ * This routine is used to report that the result of Discover Identity USB power
+ * delivery command has become available.
+ */
+int typec_cable_set_identity(struct typec_cable *cable)
+{
+ if (!cable->identity)
+ return -EINVAL;
+
+ typec_report_identity(&cable->dev);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_cable_set_identity);
+
+/**
+ * typec_register_cable - Register a USB Type-C Cable
+ * @port: The USB Type-C Port the cable is connected to
+ * @desc: Description of the cable
+ *
+ * Registers a device for USB Type-C Cable described in @desc. The cable will be
+ * parent for the optional cable plug devises.
+ *
+ * Returns handle to the cable on success or ERR_PTR on failure.
+ */
+struct typec_cable *typec_register_cable(struct typec_port *port,
+ struct typec_cable_desc *desc)
+{
+ struct typec_cable *cable;
+ int ret;
+
+ cable = kzalloc(sizeof(*cable), GFP_KERNEL);
+ if (!cable)
+ return ERR_PTR(-ENOMEM);
+
+ cable->type = desc->type;
+ cable->active = desc->active;
+ cable->pd_revision = desc->pd_revision;
+
+ if (desc->identity) {
+ /*
+ * Creating directory for the identity only if the driver is
+ * able to provide data to it.
+ */
+ cable->dev.groups = usb_pd_id_groups;
+ cable->identity = desc->identity;
+ }
+
+ cable->dev.class = &typec_class;
+ cable->dev.parent = &port->dev;
+ cable->dev.type = &typec_cable_dev_type;
+ dev_set_name(&cable->dev, "%s-cable", dev_name(&port->dev));
+
+ ret = device_register(&cable->dev);
+ if (ret) {
+ dev_err(&port->dev, "failed to register cable (%d)\n", ret);
+ put_device(&cable->dev);
+ return ERR_PTR(ret);
+ }
+
+ return cable;
+}
+EXPORT_SYMBOL_GPL(typec_register_cable);
+
+/**
+ * typec_unregister_cable - Unregister a USB Type-C Cable
+ * @cable: The cable to be unregistered
+ *
+ * Unregister device created with typec_register_cable().
+ */
+void typec_unregister_cable(struct typec_cable *cable)
+{
+ if (!IS_ERR_OR_NULL(cable))
+ device_unregister(&cable->dev);
+}
+EXPORT_SYMBOL_GPL(typec_unregister_cable);
+
+/* ------------------------------------------------------------------------- */
+/* USB Type-C ports */
+
+/**
+ * typec_port_set_usb_power_delivery - Assign USB PD for port.
+ * @port: USB Type-C port.
+ * @pd: USB PD instance.
+ *
+ * This routine can be used to set the USB Power Delivery Capabilities for @port
+ * that it will advertise to the partner.
+ *
+ * If @pd is NULL, the assignment is removed.
+ */
+int typec_port_set_usb_power_delivery(struct typec_port *port, struct usb_power_delivery *pd)
+{
+ int ret;
+
+ if (IS_ERR_OR_NULL(port) || port->pd == pd)
+ return 0;
+
+ if (pd) {
+ ret = usb_power_delivery_link_device(pd, &port->dev);
+ if (ret)
+ return ret;
+ } else {
+ usb_power_delivery_unlink_device(port->pd, &port->dev);
+ }
+
+ port->pd = pd;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_port_set_usb_power_delivery);
+
+static ssize_t select_usb_power_delivery_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ struct usb_power_delivery *pd;
+
+ if (!port->ops || !port->ops->pd_set)
+ return -EOPNOTSUPP;
+
+ pd = usb_power_delivery_find(buf);
+ if (!pd)
+ return -EINVAL;
+
+ return port->ops->pd_set(port, pd);
+}
+
+static ssize_t select_usb_power_delivery_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+ struct usb_power_delivery **pds;
+ int i, ret = 0;
+
+ if (!port->ops || !port->ops->pd_get)
+ return -EOPNOTSUPP;
+
+ pds = port->ops->pd_get(port);
+ if (!pds)
+ return 0;
+
+ for (i = 0; pds[i]; i++) {
+ if (pds[i] == port->pd)
+ ret += sysfs_emit_at(buf, ret, "[%s] ", dev_name(&pds[i]->dev));
+ else
+ ret += sysfs_emit_at(buf, ret, "%s ", dev_name(&pds[i]->dev));
+ }
+
+ buf[ret - 1] = '\n';
+
+ return ret;
+}
+static DEVICE_ATTR_RW(select_usb_power_delivery);
+
+static struct attribute *port_attrs[] = {
+ &dev_attr_select_usb_power_delivery.attr,
+ NULL
+};
+
+static umode_t port_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ struct typec_port *port = to_typec_port(kobj_to_dev(kobj));
+
+ if (!port->pd || !port->ops || !port->ops->pd_get)
+ return 0;
+ if (!port->ops->pd_set)
+ return 0444;
+
+ return attr->mode;
+}
+
+static const struct attribute_group pd_group = {
+ .is_visible = port_attr_is_visible,
+ .attrs = port_attrs,
+};
+
+static const char * const typec_orientations[] = {
+ [TYPEC_ORIENTATION_NONE] = "unknown",
+ [TYPEC_ORIENTATION_NORMAL] = "normal",
+ [TYPEC_ORIENTATION_REVERSE] = "reverse",
+};
+
+static const char * const typec_roles[] = {
+ [TYPEC_SINK] = "sink",
+ [TYPEC_SOURCE] = "source",
+};
+
+static const char * const typec_data_roles[] = {
+ [TYPEC_DEVICE] = "device",
+ [TYPEC_HOST] = "host",
+};
+
+static const char * const typec_port_power_roles[] = {
+ [TYPEC_PORT_SRC] = "source",
+ [TYPEC_PORT_SNK] = "sink",
+ [TYPEC_PORT_DRP] = "dual",
+};
+
+static const char * const typec_port_data_roles[] = {
+ [TYPEC_PORT_DFP] = "host",
+ [TYPEC_PORT_UFP] = "device",
+ [TYPEC_PORT_DRD] = "dual",
+};
+
+static const char * const typec_port_types_drp[] = {
+ [TYPEC_PORT_SRC] = "dual [source] sink",
+ [TYPEC_PORT_SNK] = "dual source [sink]",
+ [TYPEC_PORT_DRP] = "[dual] source sink",
+};
+
+static ssize_t
+preferred_role_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ int role;
+ int ret;
+
+ if (port->cap->type != TYPEC_PORT_DRP) {
+ dev_dbg(dev, "Preferred role only supported with DRP ports\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->ops || !port->ops->try_role) {
+ dev_dbg(dev, "Setting preferred role not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ role = sysfs_match_string(typec_roles, buf);
+ if (role < 0) {
+ if (sysfs_streq(buf, "none"))
+ role = TYPEC_NO_PREFERRED_ROLE;
+ else
+ return -EINVAL;
+ }
+
+ ret = port->ops->try_role(port, role);
+ if (ret)
+ return ret;
+
+ port->prefer_role = role;
+ return size;
+}
+
+static ssize_t
+preferred_role_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->type != TYPEC_PORT_DRP)
+ return 0;
+
+ if (port->prefer_role < 0)
+ return 0;
+
+ return sprintf(buf, "%s\n", typec_roles[port->prefer_role]);
+}
+static DEVICE_ATTR_RW(preferred_role);
+
+static ssize_t data_role_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ int ret;
+
+ if (!port->ops || !port->ops->dr_set) {
+ dev_dbg(dev, "data role swapping not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ ret = sysfs_match_string(typec_data_roles, buf);
+ if (ret < 0)
+ return ret;
+
+ mutex_lock(&port->port_type_lock);
+ if (port->cap->data != TYPEC_PORT_DRD) {
+ ret = -EOPNOTSUPP;
+ goto unlock_and_ret;
+ }
+
+ ret = port->ops->dr_set(port, ret);
+ if (ret)
+ goto unlock_and_ret;
+
+ ret = size;
+unlock_and_ret:
+ mutex_unlock(&port->port_type_lock);
+ return ret;
+}
+
+static ssize_t data_role_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->data == TYPEC_PORT_DRD)
+ return sprintf(buf, "%s\n", port->data_role == TYPEC_HOST ?
+ "[host] device" : "host [device]");
+
+ return sprintf(buf, "[%s]\n", typec_data_roles[port->data_role]);
+}
+static DEVICE_ATTR_RW(data_role);
+
+static ssize_t power_role_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ int ret;
+
+ if (!port->ops || !port->ops->pr_set) {
+ dev_dbg(dev, "power role swapping not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (port->pwr_opmode != TYPEC_PWR_MODE_PD) {
+ dev_dbg(dev, "partner unable to swap power role\n");
+ return -EIO;
+ }
+
+ ret = sysfs_match_string(typec_roles, buf);
+ if (ret < 0)
+ return ret;
+
+ mutex_lock(&port->port_type_lock);
+ if (port->port_type != TYPEC_PORT_DRP) {
+ dev_dbg(dev, "port type fixed at \"%s\"",
+ typec_port_power_roles[port->port_type]);
+ ret = -EOPNOTSUPP;
+ goto unlock_and_ret;
+ }
+
+ ret = port->ops->pr_set(port, ret);
+ if (ret)
+ goto unlock_and_ret;
+
+ ret = size;
+unlock_and_ret:
+ mutex_unlock(&port->port_type_lock);
+ return ret;
+}
+
+static ssize_t power_role_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->type == TYPEC_PORT_DRP)
+ return sprintf(buf, "%s\n", port->pwr_role == TYPEC_SOURCE ?
+ "[source] sink" : "source [sink]");
+
+ return sprintf(buf, "[%s]\n", typec_roles[port->pwr_role]);
+}
+static DEVICE_ATTR_RW(power_role);
+
+static ssize_t
+port_type_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ int ret;
+ enum typec_port_type type;
+
+ if (port->cap->type != TYPEC_PORT_DRP ||
+ !port->ops || !port->ops->port_type_set) {
+ dev_dbg(dev, "changing port type not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ ret = sysfs_match_string(typec_port_power_roles, buf);
+ if (ret < 0)
+ return ret;
+
+ type = ret;
+ mutex_lock(&port->port_type_lock);
+
+ if (port->port_type == type) {
+ ret = size;
+ goto unlock_and_ret;
+ }
+
+ ret = port->ops->port_type_set(port, type);
+ if (ret)
+ goto unlock_and_ret;
+
+ port->port_type = type;
+ ret = size;
+
+unlock_and_ret:
+ mutex_unlock(&port->port_type_lock);
+ return ret;
+}
+
+static ssize_t
+port_type_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->type == TYPEC_PORT_DRP)
+ return sprintf(buf, "%s\n",
+ typec_port_types_drp[port->port_type]);
+
+ return sprintf(buf, "[%s]\n", typec_port_power_roles[port->cap->type]);
+}
+static DEVICE_ATTR_RW(port_type);
+
+static const char * const typec_pwr_opmodes[] = {
+ [TYPEC_PWR_MODE_USB] = "default",
+ [TYPEC_PWR_MODE_1_5A] = "1.5A",
+ [TYPEC_PWR_MODE_3_0A] = "3.0A",
+ [TYPEC_PWR_MODE_PD] = "usb_power_delivery",
+};
+
+static ssize_t power_operation_mode_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%s\n", typec_pwr_opmodes[port->pwr_opmode]);
+}
+static DEVICE_ATTR_RO(power_operation_mode);
+
+static ssize_t vconn_source_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ bool source;
+ int ret;
+
+ if (!port->cap->pd_revision) {
+ dev_dbg(dev, "VCONN swap depends on USB Power Delivery\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->ops || !port->ops->vconn_set) {
+ dev_dbg(dev, "VCONN swapping not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ ret = kstrtobool(buf, &source);
+ if (ret)
+ return ret;
+
+ ret = port->ops->vconn_set(port, (enum typec_role)source);
+ if (ret)
+ return ret;
+
+ return size;
+}
+
+static ssize_t vconn_source_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%s\n",
+ port->vconn_role == TYPEC_SOURCE ? "yes" : "no");
+}
+static DEVICE_ATTR_RW(vconn_source);
+
+static ssize_t supported_accessory_modes_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+ ssize_t ret = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(port->cap->accessory); i++) {
+ if (port->cap->accessory[i])
+ ret += sprintf(buf + ret, "%s ",
+ typec_accessory_modes[port->cap->accessory[i]]);
+ }
+
+ if (!ret)
+ return sprintf(buf, "none\n");
+
+ buf[ret - 1] = '\n';
+
+ return ret;
+}
+static DEVICE_ATTR_RO(supported_accessory_modes);
+
+static ssize_t usb_typec_revision_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+ u16 rev = port->cap->revision;
+
+ return sprintf(buf, "%d.%d\n", (rev >> 8) & 0xff, (rev >> 4) & 0xf);
+}
+static DEVICE_ATTR_RO(usb_typec_revision);
+
+static ssize_t usb_power_delivery_revision_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ u16 rev = 0;
+
+ if (is_typec_partner(dev)) {
+ struct typec_partner *partner = to_typec_partner(dev);
+
+ rev = partner->pd_revision;
+ } else if (is_typec_cable(dev)) {
+ struct typec_cable *cable = to_typec_cable(dev);
+
+ rev = cable->pd_revision;
+ } else if (is_typec_port(dev)) {
+ struct typec_port *p = to_typec_port(dev);
+
+ rev = p->cap->pd_revision;
+ }
+ return sysfs_emit(buf, "%d.%d\n", (rev >> 8) & 0xff, (rev >> 4) & 0xf);
+}
+
+static ssize_t orientation_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%s\n", typec_orientations[port->orientation]);
+}
+static DEVICE_ATTR_RO(orientation);
+
+static struct attribute *typec_attrs[] = {
+ &dev_attr_data_role.attr,
+ &dev_attr_power_operation_mode.attr,
+ &dev_attr_power_role.attr,
+ &dev_attr_preferred_role.attr,
+ &dev_attr_supported_accessory_modes.attr,
+ &dev_attr_usb_power_delivery_revision.attr,
+ &dev_attr_usb_typec_revision.attr,
+ &dev_attr_vconn_source.attr,
+ &dev_attr_port_type.attr,
+ &dev_attr_orientation.attr,
+ NULL,
+};
+
+static umode_t typec_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct typec_port *port = to_typec_port(kobj_to_dev(kobj));
+
+ if (attr == &dev_attr_data_role.attr) {
+ if (port->cap->data != TYPEC_PORT_DRD ||
+ !port->ops || !port->ops->dr_set)
+ return 0444;
+ } else if (attr == &dev_attr_power_role.attr) {
+ if (port->cap->type != TYPEC_PORT_DRP ||
+ !port->ops || !port->ops->pr_set)
+ return 0444;
+ } else if (attr == &dev_attr_vconn_source.attr) {
+ if (!port->cap->pd_revision ||
+ !port->ops || !port->ops->vconn_set)
+ return 0444;
+ } else if (attr == &dev_attr_preferred_role.attr) {
+ if (port->cap->type != TYPEC_PORT_DRP ||
+ !port->ops || !port->ops->try_role)
+ return 0444;
+ } else if (attr == &dev_attr_port_type.attr) {
+ if (!port->ops || !port->ops->port_type_set)
+ return 0;
+ if (port->cap->type != TYPEC_PORT_DRP)
+ return 0444;
+ } else if (attr == &dev_attr_orientation.attr) {
+ if (port->cap->orientation_aware)
+ return 0444;
+ return 0;
+ }
+
+ return attr->mode;
+}
+
+static const struct attribute_group typec_group = {
+ .is_visible = typec_attr_is_visible,
+ .attrs = typec_attrs,
+};
+
+static const struct attribute_group *typec_groups[] = {
+ &typec_group,
+ &pd_group,
+ NULL
+};
+
+static int typec_uevent(struct device *dev, struct kobj_uevent_env *env)
+{
+ int ret;
+
+ ret = add_uevent_var(env, "TYPEC_PORT=%s", dev_name(dev));
+ if (ret)
+ dev_err(dev, "failed to add uevent TYPEC_PORT\n");
+
+ return ret;
+}
+
+static void typec_release(struct device *dev)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ ida_simple_remove(&typec_index_ida, port->id);
+ ida_destroy(&port->mode_ids);
+ typec_switch_put(port->sw);
+ typec_mux_put(port->mux);
+ typec_retimer_put(port->retimer);
+ kfree(port->cap);
+ kfree(port);
+}
+
+const struct device_type typec_port_dev_type = {
+ .name = "typec_port",
+ .groups = typec_groups,
+ .uevent = typec_uevent,
+ .release = typec_release,
+};
+
+/* --------------------------------------- */
+/* Driver callbacks to report role updates */
+
+static int partner_match(struct device *dev, void *data)
+{
+ return is_typec_partner(dev);
+}
+
+/**
+ * typec_set_data_role - Report data role change
+ * @port: The USB Type-C Port where the role was changed
+ * @role: The new data role
+ *
+ * This routine is used by the port drivers to report data role changes.
+ */
+void typec_set_data_role(struct typec_port *port, enum typec_data_role role)
+{
+ struct device *partner_dev;
+
+ if (port->data_role == role)
+ return;
+
+ port->data_role = role;
+ sysfs_notify(&port->dev.kobj, NULL, "data_role");
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+
+ partner_dev = device_find_child(&port->dev, NULL, partner_match);
+ if (!partner_dev)
+ return;
+
+ if (to_typec_partner(partner_dev)->identity)
+ typec_product_type_notify(partner_dev);
+
+ put_device(partner_dev);
+}
+EXPORT_SYMBOL_GPL(typec_set_data_role);
+
+/**
+ * typec_set_pwr_role - Report power role change
+ * @port: The USB Type-C Port where the role was changed
+ * @role: The new data role
+ *
+ * This routine is used by the port drivers to report power role changes.
+ */
+void typec_set_pwr_role(struct typec_port *port, enum typec_role role)
+{
+ if (port->pwr_role == role)
+ return;
+
+ port->pwr_role = role;
+ sysfs_notify(&port->dev.kobj, NULL, "power_role");
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+}
+EXPORT_SYMBOL_GPL(typec_set_pwr_role);
+
+/**
+ * typec_set_vconn_role - Report VCONN source change
+ * @port: The USB Type-C Port which VCONN role changed
+ * @role: Source when @port is sourcing VCONN, or Sink when it's not
+ *
+ * This routine is used by the port drivers to report if the VCONN source is
+ * changes.
+ */
+void typec_set_vconn_role(struct typec_port *port, enum typec_role role)
+{
+ if (port->vconn_role == role)
+ return;
+
+ port->vconn_role = role;
+ sysfs_notify(&port->dev.kobj, NULL, "vconn_source");
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+}
+EXPORT_SYMBOL_GPL(typec_set_vconn_role);
+
+/**
+ * typec_set_pwr_opmode - Report changed power operation mode
+ * @port: The USB Type-C Port where the mode was changed
+ * @opmode: New power operation mode
+ *
+ * This routine is used by the port drivers to report changed power operation
+ * mode in @port. The modes are USB (default), 1.5A, 3.0A as defined in USB
+ * Type-C specification, and "USB Power Delivery" when the power levels are
+ * negotiated with methods defined in USB Power Delivery specification.
+ */
+void typec_set_pwr_opmode(struct typec_port *port,
+ enum typec_pwr_opmode opmode)
+{
+ struct device *partner_dev;
+
+ if (port->pwr_opmode == opmode)
+ return;
+
+ port->pwr_opmode = opmode;
+ sysfs_notify(&port->dev.kobj, NULL, "power_operation_mode");
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+
+ partner_dev = device_find_child(&port->dev, NULL, partner_match);
+ if (partner_dev) {
+ struct typec_partner *partner = to_typec_partner(partner_dev);
+
+ if (opmode == TYPEC_PWR_MODE_PD && !partner->usb_pd) {
+ partner->usb_pd = 1;
+ sysfs_notify(&partner_dev->kobj, NULL,
+ "supports_usb_power_delivery");
+ kobject_uevent(&partner_dev->kobj, KOBJ_CHANGE);
+ }
+ put_device(partner_dev);
+ }
+}
+EXPORT_SYMBOL_GPL(typec_set_pwr_opmode);
+
+/**
+ * typec_find_pwr_opmode - Get the typec power operation mode capability
+ * @name: power operation mode string
+ *
+ * This routine is used to find the typec_pwr_opmode by its string @name.
+ *
+ * Returns typec_pwr_opmode if success, otherwise negative error code.
+ */
+int typec_find_pwr_opmode(const char *name)
+{
+ return match_string(typec_pwr_opmodes,
+ ARRAY_SIZE(typec_pwr_opmodes), name);
+}
+EXPORT_SYMBOL_GPL(typec_find_pwr_opmode);
+
+/**
+ * typec_find_orientation - Convert orientation string to enum typec_orientation
+ * @name: Orientation string
+ *
+ * This routine is used to find the typec_orientation by its string name @name.
+ *
+ * Returns the orientation value on success, otherwise negative error code.
+ */
+int typec_find_orientation(const char *name)
+{
+ return match_string(typec_orientations, ARRAY_SIZE(typec_orientations),
+ name);
+}
+EXPORT_SYMBOL_GPL(typec_find_orientation);
+
+/**
+ * typec_find_port_power_role - Get the typec port power capability
+ * @name: port power capability string
+ *
+ * This routine is used to find the typec_port_type by its string name.
+ *
+ * Returns typec_port_type if success, otherwise negative error code.
+ */
+int typec_find_port_power_role(const char *name)
+{
+ return match_string(typec_port_power_roles,
+ ARRAY_SIZE(typec_port_power_roles), name);
+}
+EXPORT_SYMBOL_GPL(typec_find_port_power_role);
+
+/**
+ * typec_find_power_role - Find the typec one specific power role
+ * @name: power role string
+ *
+ * This routine is used to find the typec_role by its string name.
+ *
+ * Returns typec_role if success, otherwise negative error code.
+ */
+int typec_find_power_role(const char *name)
+{
+ return match_string(typec_roles, ARRAY_SIZE(typec_roles), name);
+}
+EXPORT_SYMBOL_GPL(typec_find_power_role);
+
+/**
+ * typec_find_port_data_role - Get the typec port data capability
+ * @name: port data capability string
+ *
+ * This routine is used to find the typec_port_data by its string name.
+ *
+ * Returns typec_port_data if success, otherwise negative error code.
+ */
+int typec_find_port_data_role(const char *name)
+{
+ return match_string(typec_port_data_roles,
+ ARRAY_SIZE(typec_port_data_roles), name);
+}
+EXPORT_SYMBOL_GPL(typec_find_port_data_role);
+
+/* ------------------------------------------ */
+/* API for Multiplexer/DeMultiplexer Switches */
+
+/**
+ * typec_set_orientation - Set USB Type-C cable plug orientation
+ * @port: USB Type-C Port
+ * @orientation: USB Type-C cable plug orientation
+ *
+ * Set cable plug orientation for @port.
+ */
+int typec_set_orientation(struct typec_port *port,
+ enum typec_orientation orientation)
+{
+ int ret;
+
+ ret = typec_switch_set(port->sw, orientation);
+ if (ret)
+ return ret;
+
+ port->orientation = orientation;
+ sysfs_notify(&port->dev.kobj, NULL, "orientation");
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_set_orientation);
+
+/**
+ * typec_get_orientation - Get USB Type-C cable plug orientation
+ * @port: USB Type-C Port
+ *
+ * Get current cable plug orientation for @port.
+ */
+enum typec_orientation typec_get_orientation(struct typec_port *port)
+{
+ return port->orientation;
+}
+EXPORT_SYMBOL_GPL(typec_get_orientation);
+
+/**
+ * typec_set_mode - Set mode of operation for USB Type-C connector
+ * @port: USB Type-C connector
+ * @mode: Accessory Mode, USB Operation or Safe State
+ *
+ * Configure @port for Accessory Mode @mode. This function will configure the
+ * muxes needed for @mode.
+ */
+int typec_set_mode(struct typec_port *port, int mode)
+{
+ struct typec_mux_state state = { };
+
+ state.mode = mode;
+
+ return typec_mux_set(port->mux, &state);
+}
+EXPORT_SYMBOL_GPL(typec_set_mode);
+
+/* --------------------------------------- */
+
+/**
+ * typec_get_negotiated_svdm_version - Get negotiated SVDM Version
+ * @port: USB Type-C Port.
+ *
+ * Get the negotiated SVDM Version. The Version is set to the port default
+ * value stored in typec_capability on partner registration, and updated after
+ * a successful Discover Identity if the negotiated value is less than the
+ * default value.
+ *
+ * Returns usb_pd_svdm_ver if the partner has been registered otherwise -ENODEV.
+ */
+int typec_get_negotiated_svdm_version(struct typec_port *port)
+{
+ enum usb_pd_svdm_ver svdm_version;
+ struct device *partner_dev;
+
+ partner_dev = device_find_child(&port->dev, NULL, partner_match);
+ if (!partner_dev)
+ return -ENODEV;
+
+ svdm_version = to_typec_partner(partner_dev)->svdm_version;
+ put_device(partner_dev);
+
+ return svdm_version;
+}
+EXPORT_SYMBOL_GPL(typec_get_negotiated_svdm_version);
+
+/**
+ * typec_get_drvdata - Return private driver data pointer
+ * @port: USB Type-C port
+ */
+void *typec_get_drvdata(struct typec_port *port)
+{
+ return dev_get_drvdata(&port->dev);
+}
+EXPORT_SYMBOL_GPL(typec_get_drvdata);
+
+int typec_get_fw_cap(struct typec_capability *cap,
+ struct fwnode_handle *fwnode)
+{
+ const char *cap_str;
+ int ret;
+
+ cap->fwnode = fwnode;
+
+ ret = fwnode_property_read_string(fwnode, "power-role", &cap_str);
+ if (ret < 0)
+ return ret;
+
+ ret = typec_find_port_power_role(cap_str);
+ if (ret < 0)
+ return ret;
+ cap->type = ret;
+
+ /* USB data support is optional */
+ ret = fwnode_property_read_string(fwnode, "data-role", &cap_str);
+ if (ret == 0) {
+ ret = typec_find_port_data_role(cap_str);
+ if (ret < 0)
+ return ret;
+ cap->data = ret;
+ }
+
+ /* Get the preferred power role for a DRP */
+ if (cap->type == TYPEC_PORT_DRP) {
+ cap->prefer_role = TYPEC_NO_PREFERRED_ROLE;
+
+ ret = fwnode_property_read_string(fwnode, "try-power-role", &cap_str);
+ if (ret == 0) {
+ ret = typec_find_power_role(cap_str);
+ if (ret < 0)
+ return ret;
+ cap->prefer_role = ret;
+ }
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_get_fw_cap);
+
+/**
+ * typec_port_register_altmode - Register USB Type-C Port Alternate Mode
+ * @port: USB Type-C Port that supports the alternate mode
+ * @desc: Description of the alternate mode
+ *
+ * This routine is used to register an alternate mode that @port is capable of
+ * supporting.
+ *
+ * Returns handle to the alternate mode on success or ERR_PTR on failure.
+ */
+struct typec_altmode *
+typec_port_register_altmode(struct typec_port *port,
+ const struct typec_altmode_desc *desc)
+{
+ struct typec_altmode *adev;
+ struct typec_mux *mux;
+
+ mux = typec_mux_get(&port->dev, desc);
+ if (IS_ERR(mux))
+ return ERR_CAST(mux);
+
+ adev = typec_register_altmode(&port->dev, desc);
+ if (IS_ERR(adev))
+ typec_mux_put(mux);
+ else
+ to_altmode(adev)->mux = mux;
+
+ return adev;
+}
+EXPORT_SYMBOL_GPL(typec_port_register_altmode);
+
+void typec_port_register_altmodes(struct typec_port *port,
+ const struct typec_altmode_ops *ops, void *drvdata,
+ struct typec_altmode **altmodes, size_t n)
+{
+ struct fwnode_handle *altmodes_node, *child;
+ struct typec_altmode_desc desc;
+ struct typec_altmode *alt;
+ size_t index = 0;
+ u32 svid, vdo;
+ int ret;
+
+ altmodes_node = device_get_named_child_node(&port->dev, "altmodes");
+ if (!altmodes_node)
+ return; /* No altmodes specified */
+
+ fwnode_for_each_child_node(altmodes_node, child) {
+ ret = fwnode_property_read_u32(child, "svid", &svid);
+ if (ret) {
+ dev_err(&port->dev, "Error reading svid for altmode %s\n",
+ fwnode_get_name(child));
+ continue;
+ }
+
+ ret = fwnode_property_read_u32(child, "vdo", &vdo);
+ if (ret) {
+ dev_err(&port->dev, "Error reading vdo for altmode %s\n",
+ fwnode_get_name(child));
+ continue;
+ }
+
+ if (index >= n) {
+ dev_err(&port->dev, "Error not enough space for altmode %s\n",
+ fwnode_get_name(child));
+ continue;
+ }
+
+ desc.svid = svid;
+ desc.vdo = vdo;
+ desc.mode = index + 1;
+ alt = typec_port_register_altmode(port, &desc);
+ if (IS_ERR(alt)) {
+ dev_err(&port->dev, "Error registering altmode %s\n",
+ fwnode_get_name(child));
+ continue;
+ }
+
+ alt->ops = ops;
+ typec_altmode_set_drvdata(alt, drvdata);
+ altmodes[index] = alt;
+ index++;
+ }
+}
+EXPORT_SYMBOL_GPL(typec_port_register_altmodes);
+
+/**
+ * typec_register_port - Register a USB Type-C Port
+ * @parent: Parent device
+ * @cap: Description of the port
+ *
+ * Registers a device for USB Type-C Port described in @cap.
+ *
+ * Returns handle to the port on success or ERR_PTR on failure.
+ */
+struct typec_port *typec_register_port(struct device *parent,
+ const struct typec_capability *cap)
+{
+ struct typec_port *port;
+ int ret;
+ int id;
+
+ port = kzalloc(sizeof(*port), GFP_KERNEL);
+ if (!port)
+ return ERR_PTR(-ENOMEM);
+
+ id = ida_simple_get(&typec_index_ida, 0, 0, GFP_KERNEL);
+ if (id < 0) {
+ kfree(port);
+ return ERR_PTR(id);
+ }
+
+ switch (cap->type) {
+ case TYPEC_PORT_SRC:
+ port->pwr_role = TYPEC_SOURCE;
+ port->vconn_role = TYPEC_SOURCE;
+ break;
+ case TYPEC_PORT_SNK:
+ port->pwr_role = TYPEC_SINK;
+ port->vconn_role = TYPEC_SINK;
+ break;
+ case TYPEC_PORT_DRP:
+ if (cap->prefer_role != TYPEC_NO_PREFERRED_ROLE)
+ port->pwr_role = cap->prefer_role;
+ else
+ port->pwr_role = TYPEC_SINK;
+ break;
+ }
+
+ switch (cap->data) {
+ case TYPEC_PORT_DFP:
+ port->data_role = TYPEC_HOST;
+ break;
+ case TYPEC_PORT_UFP:
+ port->data_role = TYPEC_DEVICE;
+ break;
+ case TYPEC_PORT_DRD:
+ if (cap->prefer_role == TYPEC_SOURCE)
+ port->data_role = TYPEC_HOST;
+ else
+ port->data_role = TYPEC_DEVICE;
+ break;
+ }
+
+ ida_init(&port->mode_ids);
+ mutex_init(&port->port_type_lock);
+
+ port->id = id;
+ port->ops = cap->ops;
+ port->port_type = cap->type;
+ port->prefer_role = cap->prefer_role;
+
+ device_initialize(&port->dev);
+ port->dev.class = &typec_class;
+ port->dev.parent = parent;
+ port->dev.fwnode = cap->fwnode;
+ port->dev.type = &typec_port_dev_type;
+ dev_set_name(&port->dev, "port%d", id);
+ dev_set_drvdata(&port->dev, cap->driver_data);
+
+ port->cap = kmemdup(cap, sizeof(*cap), GFP_KERNEL);
+ if (!port->cap) {
+ put_device(&port->dev);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ port->sw = typec_switch_get(&port->dev);
+ if (IS_ERR(port->sw)) {
+ ret = PTR_ERR(port->sw);
+ put_device(&port->dev);
+ return ERR_PTR(ret);
+ }
+
+ port->mux = typec_mux_get(&port->dev, NULL);
+ if (IS_ERR(port->mux)) {
+ ret = PTR_ERR(port->mux);
+ put_device(&port->dev);
+ return ERR_PTR(ret);
+ }
+
+ port->retimer = typec_retimer_get(&port->dev);
+ if (IS_ERR(port->retimer)) {
+ ret = PTR_ERR(port->retimer);
+ put_device(&port->dev);
+ return ERR_PTR(ret);
+ }
+
+ port->pd = cap->pd;
+
+ ret = device_add(&port->dev);
+ if (ret) {
+ dev_err(parent, "failed to register port (%d)\n", ret);
+ put_device(&port->dev);
+ return ERR_PTR(ret);
+ }
+
+ ret = usb_power_delivery_link_device(port->pd, &port->dev);
+ if (ret) {
+ dev_err(&port->dev, "failed to link pd\n");
+ device_unregister(&port->dev);
+ return ERR_PTR(ret);
+ }
+
+ ret = typec_link_ports(port);
+ if (ret)
+ dev_warn(&port->dev, "failed to create symlinks (%d)\n", ret);
+
+ return port;
+}
+EXPORT_SYMBOL_GPL(typec_register_port);
+
+/**
+ * typec_unregister_port - Unregister a USB Type-C Port
+ * @port: The port to be unregistered
+ *
+ * Unregister device created with typec_register_port().
+ */
+void typec_unregister_port(struct typec_port *port)
+{
+ if (!IS_ERR_OR_NULL(port)) {
+ typec_unlink_ports(port);
+ typec_port_set_usb_power_delivery(port, NULL);
+ device_unregister(&port->dev);
+ }
+}
+EXPORT_SYMBOL_GPL(typec_unregister_port);
+
+static int __init typec_init(void)
+{
+ int ret;
+
+ ret = bus_register(&typec_bus);
+ if (ret)
+ return ret;
+
+ ret = class_register(&typec_mux_class);
+ if (ret)
+ goto err_unregister_bus;
+
+ ret = class_register(&retimer_class);
+ if (ret)
+ goto err_unregister_mux_class;
+
+ ret = class_register(&typec_class);
+ if (ret)
+ goto err_unregister_retimer_class;
+
+ ret = usb_power_delivery_init();
+ if (ret)
+ goto err_unregister_class;
+
+ return 0;
+
+err_unregister_class:
+ class_unregister(&typec_class);
+
+err_unregister_retimer_class:
+ class_unregister(&retimer_class);
+
+err_unregister_mux_class:
+ class_unregister(&typec_mux_class);
+
+err_unregister_bus:
+ bus_unregister(&typec_bus);
+
+ return ret;
+}
+subsys_initcall(typec_init);
+
+static void __exit typec_exit(void)
+{
+ usb_power_delivery_exit();
+ class_unregister(&typec_class);
+ ida_destroy(&typec_index_ida);
+ bus_unregister(&typec_bus);
+ class_unregister(&typec_mux_class);
+ class_unregister(&retimer_class);
+}
+module_exit(typec_exit);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("USB Type-C Connector Class");
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
new file mode 100644
index 000000000..673b2952b
--- /dev/null
+++ b/drivers/usb/typec/class.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_TYPEC_CLASS__
+#define __USB_TYPEC_CLASS__
+
+#include <linux/device.h>
+#include <linux/usb/typec.h>
+
+struct typec_mux;
+struct typec_switch;
+
+struct typec_plug {
+ struct device dev;
+ enum typec_plug_index index;
+ struct ida mode_ids;
+ int num_altmodes;
+};
+
+struct typec_cable {
+ struct device dev;
+ enum typec_plug_type type;
+ struct usb_pd_identity *identity;
+ unsigned int active:1;
+ u16 pd_revision; /* 0300H = "3.0" */
+};
+
+struct typec_partner {
+ struct device dev;
+ unsigned int usb_pd:1;
+ struct usb_pd_identity *identity;
+ enum typec_accessory accessory;
+ struct ida mode_ids;
+ int num_altmodes;
+ u16 pd_revision; /* 0300H = "3.0" */
+ enum usb_pd_svdm_ver svdm_version;
+
+ struct usb_power_delivery *pd;
+};
+
+struct typec_port {
+ unsigned int id;
+ struct device dev;
+ struct ida mode_ids;
+
+ struct usb_power_delivery *pd;
+
+ int prefer_role;
+ enum typec_data_role data_role;
+ enum typec_role pwr_role;
+ enum typec_role vconn_role;
+ enum typec_pwr_opmode pwr_opmode;
+ enum typec_port_type port_type;
+ struct mutex port_type_lock;
+
+ enum typec_orientation orientation;
+ struct typec_switch *sw;
+ struct typec_mux *mux;
+ struct typec_retimer *retimer;
+
+ const struct typec_capability *cap;
+ const struct typec_operations *ops;
+};
+
+#define to_typec_port(_dev_) container_of(_dev_, struct typec_port, dev)
+#define to_typec_plug(_dev_) container_of(_dev_, struct typec_plug, dev)
+#define to_typec_cable(_dev_) container_of(_dev_, struct typec_cable, dev)
+#define to_typec_partner(_dev_) container_of(_dev_, struct typec_partner, dev)
+
+extern const struct device_type typec_partner_dev_type;
+extern const struct device_type typec_cable_dev_type;
+extern const struct device_type typec_plug_dev_type;
+extern const struct device_type typec_port_dev_type;
+
+#define is_typec_partner(dev) ((dev)->type == &typec_partner_dev_type)
+#define is_typec_cable(dev) ((dev)->type == &typec_cable_dev_type)
+#define is_typec_plug(dev) ((dev)->type == &typec_plug_dev_type)
+#define is_typec_port(dev) ((dev)->type == &typec_port_dev_type)
+
+extern struct class typec_mux_class;
+extern struct class retimer_class;
+extern struct class typec_class;
+
+#if defined(CONFIG_ACPI)
+int typec_link_ports(struct typec_port *connector);
+void typec_unlink_ports(struct typec_port *connector);
+#else
+static inline int typec_link_ports(struct typec_port *connector) { return 0; }
+static inline void typec_unlink_ports(struct typec_port *connector) { }
+#endif
+
+#endif /* __USB_TYPEC_CLASS__ */
diff --git a/drivers/usb/typec/hd3ss3220.c b/drivers/usb/typec/hd3ss3220.c
new file mode 100644
index 000000000..2a58185fb
--- /dev/null
+++ b/drivers/usb/typec/hd3ss3220.c
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * TI HD3SS3220 Type-C DRP Port Controller Driver
+ *
+ * Copyright (C) 2019 Renesas Electronics Corp.
+ */
+
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/usb/role.h>
+#include <linux/irqreturn.h>
+#include <linux/interrupt.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/usb/typec.h>
+#include <linux/delay.h>
+
+#define HD3SS3220_REG_CN_STAT_CTRL 0x09
+#define HD3SS3220_REG_GEN_CTRL 0x0A
+#define HD3SS3220_REG_DEV_REV 0xA0
+
+/* Register HD3SS3220_REG_CN_STAT_CTRL*/
+#define HD3SS3220_REG_CN_STAT_CTRL_ATTACHED_STATE_MASK (BIT(7) | BIT(6))
+#define HD3SS3220_REG_CN_STAT_CTRL_AS_DFP BIT(6)
+#define HD3SS3220_REG_CN_STAT_CTRL_AS_UFP BIT(7)
+#define HD3SS3220_REG_CN_STAT_CTRL_TO_ACCESSORY (BIT(7) | BIT(6))
+#define HD3SS3220_REG_CN_STAT_CTRL_INT_STATUS BIT(4)
+
+/* Register HD3SS3220_REG_GEN_CTRL*/
+#define HD3SS3220_REG_GEN_CTRL_SRC_PREF_MASK (BIT(2) | BIT(1))
+#define HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_DEFAULT 0x00
+#define HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_TRY_SNK BIT(1)
+#define HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_TRY_SRC (BIT(2) | BIT(1))
+
+struct hd3ss3220 {
+ struct device *dev;
+ struct regmap *regmap;
+ struct usb_role_switch *role_sw;
+ struct typec_port *port;
+};
+
+static int hd3ss3220_set_source_pref(struct hd3ss3220 *hd3ss3220, int src_pref)
+{
+ return regmap_update_bits(hd3ss3220->regmap, HD3SS3220_REG_GEN_CTRL,
+ HD3SS3220_REG_GEN_CTRL_SRC_PREF_MASK,
+ src_pref);
+}
+
+static enum usb_role hd3ss3220_get_attached_state(struct hd3ss3220 *hd3ss3220)
+{
+ unsigned int reg_val;
+ enum usb_role attached_state;
+ int ret;
+
+ ret = regmap_read(hd3ss3220->regmap, HD3SS3220_REG_CN_STAT_CTRL,
+ &reg_val);
+ if (ret < 0)
+ return ret;
+
+ switch (reg_val & HD3SS3220_REG_CN_STAT_CTRL_ATTACHED_STATE_MASK) {
+ case HD3SS3220_REG_CN_STAT_CTRL_AS_DFP:
+ attached_state = USB_ROLE_HOST;
+ break;
+ case HD3SS3220_REG_CN_STAT_CTRL_AS_UFP:
+ attached_state = USB_ROLE_DEVICE;
+ break;
+ default:
+ attached_state = USB_ROLE_NONE;
+ break;
+ }
+
+ return attached_state;
+}
+
+static int hd3ss3220_dr_set(struct typec_port *port, enum typec_data_role role)
+{
+ struct hd3ss3220 *hd3ss3220 = typec_get_drvdata(port);
+ enum usb_role role_val;
+ int pref, ret = 0;
+
+ if (role == TYPEC_HOST) {
+ role_val = USB_ROLE_HOST;
+ pref = HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_TRY_SRC;
+ } else {
+ role_val = USB_ROLE_DEVICE;
+ pref = HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_TRY_SNK;
+ }
+
+ ret = hd3ss3220_set_source_pref(hd3ss3220, pref);
+ usleep_range(10, 100);
+
+ usb_role_switch_set_role(hd3ss3220->role_sw, role_val);
+ typec_set_data_role(hd3ss3220->port, role);
+
+ return ret;
+}
+
+static const struct typec_operations hd3ss3220_ops = {
+ .dr_set = hd3ss3220_dr_set
+};
+
+static void hd3ss3220_set_role(struct hd3ss3220 *hd3ss3220)
+{
+ enum usb_role role_state = hd3ss3220_get_attached_state(hd3ss3220);
+
+ usb_role_switch_set_role(hd3ss3220->role_sw, role_state);
+ if (role_state == USB_ROLE_NONE)
+ hd3ss3220_set_source_pref(hd3ss3220,
+ HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_DEFAULT);
+
+ switch (role_state) {
+ case USB_ROLE_HOST:
+ typec_set_data_role(hd3ss3220->port, TYPEC_HOST);
+ break;
+ case USB_ROLE_DEVICE:
+ typec_set_data_role(hd3ss3220->port, TYPEC_DEVICE);
+ break;
+ default:
+ break;
+ }
+}
+
+static irqreturn_t hd3ss3220_irq(struct hd3ss3220 *hd3ss3220)
+{
+ int err;
+
+ hd3ss3220_set_role(hd3ss3220);
+ err = regmap_write_bits(hd3ss3220->regmap, HD3SS3220_REG_CN_STAT_CTRL,
+ HD3SS3220_REG_CN_STAT_CTRL_INT_STATUS,
+ HD3SS3220_REG_CN_STAT_CTRL_INT_STATUS);
+ if (err < 0)
+ return IRQ_NONE;
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t hd3ss3220_irq_handler(int irq, void *data)
+{
+ struct i2c_client *client = to_i2c_client(data);
+ struct hd3ss3220 *hd3ss3220 = i2c_get_clientdata(client);
+
+ return hd3ss3220_irq(hd3ss3220);
+}
+
+static const struct regmap_config config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x0A,
+};
+
+static int hd3ss3220_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct typec_capability typec_cap = { };
+ struct hd3ss3220 *hd3ss3220;
+ struct fwnode_handle *connector, *ep;
+ int ret;
+ unsigned int data;
+
+ hd3ss3220 = devm_kzalloc(&client->dev, sizeof(struct hd3ss3220),
+ GFP_KERNEL);
+ if (!hd3ss3220)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, hd3ss3220);
+
+ hd3ss3220->dev = &client->dev;
+ hd3ss3220->regmap = devm_regmap_init_i2c(client, &config);
+ if (IS_ERR(hd3ss3220->regmap))
+ return PTR_ERR(hd3ss3220->regmap);
+
+ hd3ss3220_set_source_pref(hd3ss3220,
+ HD3SS3220_REG_GEN_CTRL_SRC_PREF_DRP_DEFAULT);
+ /* For backward compatibility check the connector child node first */
+ connector = device_get_named_child_node(hd3ss3220->dev, "connector");
+ if (connector) {
+ hd3ss3220->role_sw = fwnode_usb_role_switch_get(connector);
+ } else {
+ ep = fwnode_graph_get_next_endpoint(dev_fwnode(hd3ss3220->dev), NULL);
+ if (!ep)
+ return -ENODEV;
+ connector = fwnode_graph_get_remote_port_parent(ep);
+ fwnode_handle_put(ep);
+ if (!connector)
+ return -ENODEV;
+ hd3ss3220->role_sw = usb_role_switch_get(hd3ss3220->dev);
+ }
+
+ if (IS_ERR(hd3ss3220->role_sw)) {
+ ret = PTR_ERR(hd3ss3220->role_sw);
+ goto err_put_fwnode;
+ }
+
+ typec_cap.prefer_role = TYPEC_NO_PREFERRED_ROLE;
+ typec_cap.driver_data = hd3ss3220;
+ typec_cap.type = TYPEC_PORT_DRP;
+ typec_cap.data = TYPEC_PORT_DRD;
+ typec_cap.ops = &hd3ss3220_ops;
+ typec_cap.fwnode = connector;
+
+ hd3ss3220->port = typec_register_port(&client->dev, &typec_cap);
+ if (IS_ERR(hd3ss3220->port)) {
+ ret = PTR_ERR(hd3ss3220->port);
+ goto err_put_role;
+ }
+
+ hd3ss3220_set_role(hd3ss3220);
+ ret = regmap_read(hd3ss3220->regmap, HD3SS3220_REG_CN_STAT_CTRL, &data);
+ if (ret < 0)
+ goto err_unreg_port;
+
+ if (data & HD3SS3220_REG_CN_STAT_CTRL_INT_STATUS) {
+ ret = regmap_write(hd3ss3220->regmap,
+ HD3SS3220_REG_CN_STAT_CTRL,
+ data | HD3SS3220_REG_CN_STAT_CTRL_INT_STATUS);
+ if (ret < 0)
+ goto err_unreg_port;
+ }
+
+ if (client->irq > 0) {
+ ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ hd3ss3220_irq_handler,
+ IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ "hd3ss3220", &client->dev);
+ if (ret)
+ goto err_unreg_port;
+ }
+
+ ret = i2c_smbus_read_byte_data(client, HD3SS3220_REG_DEV_REV);
+ if (ret < 0)
+ goto err_unreg_port;
+
+ fwnode_handle_put(connector);
+
+ dev_info(&client->dev, "probed revision=0x%x\n", ret);
+
+ return 0;
+err_unreg_port:
+ typec_unregister_port(hd3ss3220->port);
+err_put_role:
+ usb_role_switch_put(hd3ss3220->role_sw);
+err_put_fwnode:
+ fwnode_handle_put(connector);
+
+ return ret;
+}
+
+static void hd3ss3220_remove(struct i2c_client *client)
+{
+ struct hd3ss3220 *hd3ss3220 = i2c_get_clientdata(client);
+
+ typec_unregister_port(hd3ss3220->port);
+ usb_role_switch_put(hd3ss3220->role_sw);
+}
+
+static const struct of_device_id dev_ids[] = {
+ { .compatible = "ti,hd3ss3220"},
+ {}
+};
+MODULE_DEVICE_TABLE(of, dev_ids);
+
+static struct i2c_driver hd3ss3220_driver = {
+ .driver = {
+ .name = "hd3ss3220",
+ .of_match_table = of_match_ptr(dev_ids),
+ },
+ .probe = hd3ss3220_probe,
+ .remove = hd3ss3220_remove,
+};
+
+module_i2c_driver(hd3ss3220_driver);
+
+MODULE_AUTHOR("Biju Das <biju.das@bp.renesas.com>");
+MODULE_DESCRIPTION("TI HD3SS3220 DRP Port Controller Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c
new file mode 100644
index 000000000..941735c73
--- /dev/null
+++ b/drivers/usb/typec/mux.c
@@ -0,0 +1,518 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Type-C Multiplexer/DeMultiplexer Switch support
+ *
+ * Copyright (C) 2018 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ * Hans de Goede <hdegoede@redhat.com>
+ */
+
+#include <linux/device.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+
+#include "class.h"
+#include "mux.h"
+
+#define TYPEC_MUX_MAX_DEVS 3
+
+struct typec_switch {
+ struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS];
+ unsigned int num_sw_devs;
+};
+
+static int switch_fwnode_match(struct device *dev, const void *fwnode)
+{
+ if (!is_typec_switch_dev(dev))
+ return 0;
+
+ return device_match_fwnode(dev, fwnode);
+}
+
+static void *typec_switch_match(struct fwnode_handle *fwnode, const char *id,
+ void *data)
+{
+ struct device *dev;
+
+ /*
+ * Device graph (OF graph) does not give any means to identify the
+ * device type or the device class of the remote port parent that @fwnode
+ * represents, so in order to identify the type or the class of @fwnode
+ * an additional device property is needed. With typec switches the
+ * property is named "orientation-switch" (@id). The value of the device
+ * property is ignored.
+ */
+ if (id && !fwnode_property_present(fwnode, id))
+ return NULL;
+
+ /*
+ * At this point we are sure that @fwnode is a typec switch in all
+ * cases. If the switch hasn't yet been registered for some reason, the
+ * function "defers probe" for now.
+ */
+ dev = class_find_device(&typec_mux_class, NULL, fwnode,
+ switch_fwnode_match);
+
+ return dev ? to_typec_switch_dev(dev) : ERR_PTR(-EPROBE_DEFER);
+}
+
+/**
+ * fwnode_typec_switch_get - Find USB Type-C orientation switch
+ * @fwnode: The caller device node
+ *
+ * Finds a switch linked with @dev. Returns a reference to the switch on
+ * success, NULL if no matching connection was found, or
+ * ERR_PTR(-EPROBE_DEFER) when a connection was found but the switch
+ * has not been enumerated yet.
+ */
+struct typec_switch *fwnode_typec_switch_get(struct fwnode_handle *fwnode)
+{
+ struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS];
+ struct typec_switch *sw;
+ int count;
+ int err;
+ int i;
+
+ sw = kzalloc(sizeof(*sw), GFP_KERNEL);
+ if (!sw)
+ return ERR_PTR(-ENOMEM);
+
+ count = fwnode_connection_find_matches(fwnode, "orientation-switch", NULL,
+ typec_switch_match,
+ (void **)sw_devs,
+ ARRAY_SIZE(sw_devs));
+ if (count <= 0) {
+ kfree(sw);
+ return NULL;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (IS_ERR(sw_devs[i])) {
+ err = PTR_ERR(sw_devs[i]);
+ goto put_sw_devs;
+ }
+ }
+
+ for (i = 0; i < count; i++) {
+ WARN_ON(!try_module_get(sw_devs[i]->dev.parent->driver->owner));
+ sw->sw_devs[i] = sw_devs[i];
+ }
+
+ sw->num_sw_devs = count;
+
+ return sw;
+
+put_sw_devs:
+ for (i = 0; i < count; i++) {
+ if (!IS_ERR(sw_devs[i]))
+ put_device(&sw_devs[i]->dev);
+ }
+
+ kfree(sw);
+
+ return ERR_PTR(err);
+}
+EXPORT_SYMBOL_GPL(fwnode_typec_switch_get);
+
+/**
+ * typec_switch_put - Release USB Type-C orientation switch
+ * @sw: USB Type-C orientation switch
+ *
+ * Decrement reference count for @sw.
+ */
+void typec_switch_put(struct typec_switch *sw)
+{
+ struct typec_switch_dev *sw_dev;
+ unsigned int i;
+
+ if (IS_ERR_OR_NULL(sw))
+ return;
+
+ for (i = 0; i < sw->num_sw_devs; i++) {
+ sw_dev = sw->sw_devs[i];
+
+ module_put(sw_dev->dev.parent->driver->owner);
+ put_device(&sw_dev->dev);
+ }
+ kfree(sw);
+}
+EXPORT_SYMBOL_GPL(typec_switch_put);
+
+static void typec_switch_release(struct device *dev)
+{
+ kfree(to_typec_switch_dev(dev));
+}
+
+const struct device_type typec_switch_dev_type = {
+ .name = "orientation_switch",
+ .release = typec_switch_release,
+};
+
+/**
+ * typec_switch_register - Register USB Type-C orientation switch
+ * @parent: Parent device
+ * @desc: Orientation switch description
+ *
+ * This function registers a switch that can be used for routing the correct
+ * data pairs depending on the cable plug orientation from the USB Type-C
+ * connector to the USB controllers. USB Type-C plugs can be inserted
+ * right-side-up or upside-down.
+ */
+struct typec_switch_dev *
+typec_switch_register(struct device *parent,
+ const struct typec_switch_desc *desc)
+{
+ struct typec_switch_dev *sw_dev;
+ int ret;
+
+ if (!desc || !desc->set)
+ return ERR_PTR(-EINVAL);
+
+ sw_dev = kzalloc(sizeof(*sw_dev), GFP_KERNEL);
+ if (!sw_dev)
+ return ERR_PTR(-ENOMEM);
+
+ sw_dev->set = desc->set;
+
+ device_initialize(&sw_dev->dev);
+ sw_dev->dev.parent = parent;
+ sw_dev->dev.fwnode = desc->fwnode;
+ sw_dev->dev.class = &typec_mux_class;
+ sw_dev->dev.type = &typec_switch_dev_type;
+ sw_dev->dev.driver_data = desc->drvdata;
+ ret = dev_set_name(&sw_dev->dev, "%s-switch", desc->name ? desc->name : dev_name(parent));
+ if (ret) {
+ put_device(&sw_dev->dev);
+ return ERR_PTR(ret);
+ }
+
+ ret = device_add(&sw_dev->dev);
+ if (ret) {
+ dev_err(parent, "failed to register switch (%d)\n", ret);
+ put_device(&sw_dev->dev);
+ return ERR_PTR(ret);
+ }
+
+ return sw_dev;
+}
+EXPORT_SYMBOL_GPL(typec_switch_register);
+
+int typec_switch_set(struct typec_switch *sw,
+ enum typec_orientation orientation)
+{
+ struct typec_switch_dev *sw_dev;
+ unsigned int i;
+ int ret;
+
+ if (IS_ERR_OR_NULL(sw))
+ return 0;
+
+ for (i = 0; i < sw->num_sw_devs; i++) {
+ sw_dev = sw->sw_devs[i];
+
+ ret = sw_dev->set(sw_dev, orientation);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_switch_set);
+
+/**
+ * typec_switch_unregister - Unregister USB Type-C orientation switch
+ * @sw_dev: USB Type-C orientation switch
+ *
+ * Unregister switch that was registered with typec_switch_register().
+ */
+void typec_switch_unregister(struct typec_switch_dev *sw_dev)
+{
+ if (!IS_ERR_OR_NULL(sw_dev))
+ device_unregister(&sw_dev->dev);
+}
+EXPORT_SYMBOL_GPL(typec_switch_unregister);
+
+void typec_switch_set_drvdata(struct typec_switch_dev *sw_dev, void *data)
+{
+ dev_set_drvdata(&sw_dev->dev, data);
+}
+EXPORT_SYMBOL_GPL(typec_switch_set_drvdata);
+
+void *typec_switch_get_drvdata(struct typec_switch_dev *sw_dev)
+{
+ return dev_get_drvdata(&sw_dev->dev);
+}
+EXPORT_SYMBOL_GPL(typec_switch_get_drvdata);
+
+/* ------------------------------------------------------------------------- */
+
+struct typec_mux {
+ struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS];
+ unsigned int num_mux_devs;
+};
+
+static int mux_fwnode_match(struct device *dev, const void *fwnode)
+{
+ if (!is_typec_mux_dev(dev))
+ return 0;
+
+ return device_match_fwnode(dev, fwnode);
+}
+
+static void *typec_mux_match(struct fwnode_handle *fwnode, const char *id,
+ void *data)
+{
+ const struct typec_altmode_desc *desc = data;
+ struct device *dev;
+ bool match;
+ int nval;
+ u16 *val;
+ int ret;
+ int i;
+
+ /*
+ * Check has the identifier already been "consumed". If it
+ * has, no need to do any extra connection identification.
+ */
+ match = !id;
+ if (match)
+ goto find_mux;
+
+ if (!desc) {
+ /*
+ * Accessory Mode muxes & muxes which explicitly specify
+ * the required identifier can avoid SVID matching.
+ */
+ match = fwnode_property_present(fwnode, "accessory") ||
+ fwnode_property_present(fwnode, id);
+ if (match)
+ goto find_mux;
+ return NULL;
+ }
+
+ /* Alternate Mode muxes */
+ nval = fwnode_property_count_u16(fwnode, "svid");
+ if (nval <= 0)
+ return NULL;
+
+ val = kcalloc(nval, sizeof(*val), GFP_KERNEL);
+ if (!val)
+ return ERR_PTR(-ENOMEM);
+
+ ret = fwnode_property_read_u16_array(fwnode, "svid", val, nval);
+ if (ret < 0) {
+ kfree(val);
+ return ERR_PTR(ret);
+ }
+
+ for (i = 0; i < nval; i++) {
+ match = val[i] == desc->svid;
+ if (match) {
+ kfree(val);
+ goto find_mux;
+ }
+ }
+ kfree(val);
+ return NULL;
+
+find_mux:
+ dev = class_find_device(&typec_mux_class, NULL, fwnode,
+ mux_fwnode_match);
+
+ return dev ? to_typec_mux_dev(dev) : ERR_PTR(-EPROBE_DEFER);
+}
+
+/**
+ * fwnode_typec_mux_get - Find USB Type-C Multiplexer
+ * @fwnode: The caller device node
+ * @desc: Alt Mode description
+ *
+ * Finds a mux linked to the caller. This function is primarily meant for the
+ * Type-C drivers. Returns a reference to the mux on success, NULL if no
+ * matching connection was found, or ERR_PTR(-EPROBE_DEFER) when a connection
+ * was found but the mux has not been enumerated yet.
+ */
+struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode,
+ const struct typec_altmode_desc *desc)
+{
+ struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS];
+ struct typec_mux *mux;
+ int count;
+ int err;
+ int i;
+
+ mux = kzalloc(sizeof(*mux), GFP_KERNEL);
+ if (!mux)
+ return ERR_PTR(-ENOMEM);
+
+ count = fwnode_connection_find_matches(fwnode, "mode-switch",
+ (void *)desc, typec_mux_match,
+ (void **)mux_devs,
+ ARRAY_SIZE(mux_devs));
+ if (count <= 0) {
+ kfree(mux);
+ return NULL;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (IS_ERR(mux_devs[i])) {
+ err = PTR_ERR(mux_devs[i]);
+ goto put_mux_devs;
+ }
+ }
+
+ for (i = 0; i < count; i++) {
+ WARN_ON(!try_module_get(mux_devs[i]->dev.parent->driver->owner));
+ mux->mux_devs[i] = mux_devs[i];
+ }
+
+ mux->num_mux_devs = count;
+
+ return mux;
+
+put_mux_devs:
+ for (i = 0; i < count; i++) {
+ if (!IS_ERR(mux_devs[i]))
+ put_device(&mux_devs[i]->dev);
+ }
+
+ kfree(mux);
+
+ return ERR_PTR(err);
+}
+EXPORT_SYMBOL_GPL(fwnode_typec_mux_get);
+
+/**
+ * typec_mux_put - Release handle to a Multiplexer
+ * @mux: USB Type-C Connector Multiplexer/DeMultiplexer
+ *
+ * Decrements reference count for @mux.
+ */
+void typec_mux_put(struct typec_mux *mux)
+{
+ struct typec_mux_dev *mux_dev;
+ unsigned int i;
+
+ if (IS_ERR_OR_NULL(mux))
+ return;
+
+ for (i = 0; i < mux->num_mux_devs; i++) {
+ mux_dev = mux->mux_devs[i];
+ module_put(mux_dev->dev.parent->driver->owner);
+ put_device(&mux_dev->dev);
+ }
+ kfree(mux);
+}
+EXPORT_SYMBOL_GPL(typec_mux_put);
+
+int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state)
+{
+ struct typec_mux_dev *mux_dev;
+ unsigned int i;
+ int ret;
+
+ if (IS_ERR_OR_NULL(mux))
+ return 0;
+
+ for (i = 0; i < mux->num_mux_devs; i++) {
+ mux_dev = mux->mux_devs[i];
+
+ ret = mux_dev->set(mux_dev, state);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_mux_set);
+
+static void typec_mux_release(struct device *dev)
+{
+ kfree(to_typec_mux_dev(dev));
+}
+
+const struct device_type typec_mux_dev_type = {
+ .name = "mode_switch",
+ .release = typec_mux_release,
+};
+
+/**
+ * typec_mux_register - Register Multiplexer routing USB Type-C pins
+ * @parent: Parent device
+ * @desc: Multiplexer description
+ *
+ * USB Type-C connectors can be used for alternate modes of operation besides
+ * USB when Accessory/Alternate Modes are supported. With some of those modes,
+ * the pins on the connector need to be reconfigured. This function registers
+ * multiplexer switches routing the pins on the connector.
+ */
+struct typec_mux_dev *
+typec_mux_register(struct device *parent, const struct typec_mux_desc *desc)
+{
+ struct typec_mux_dev *mux_dev;
+ int ret;
+
+ if (!desc || !desc->set)
+ return ERR_PTR(-EINVAL);
+
+ mux_dev = kzalloc(sizeof(*mux_dev), GFP_KERNEL);
+ if (!mux_dev)
+ return ERR_PTR(-ENOMEM);
+
+ mux_dev->set = desc->set;
+
+ device_initialize(&mux_dev->dev);
+ mux_dev->dev.parent = parent;
+ mux_dev->dev.fwnode = desc->fwnode;
+ mux_dev->dev.class = &typec_mux_class;
+ mux_dev->dev.type = &typec_mux_dev_type;
+ mux_dev->dev.driver_data = desc->drvdata;
+ ret = dev_set_name(&mux_dev->dev, "%s-mux", desc->name ? desc->name : dev_name(parent));
+ if (ret) {
+ put_device(&mux_dev->dev);
+ return ERR_PTR(ret);
+ }
+
+ ret = device_add(&mux_dev->dev);
+ if (ret) {
+ dev_err(parent, "failed to register mux (%d)\n", ret);
+ put_device(&mux_dev->dev);
+ return ERR_PTR(ret);
+ }
+
+ return mux_dev;
+}
+EXPORT_SYMBOL_GPL(typec_mux_register);
+
+/**
+ * typec_mux_unregister - Unregister Multiplexer Switch
+ * @mux_dev: USB Type-C Connector Multiplexer/DeMultiplexer
+ *
+ * Unregister mux that was registered with typec_mux_register().
+ */
+void typec_mux_unregister(struct typec_mux_dev *mux_dev)
+{
+ if (!IS_ERR_OR_NULL(mux_dev))
+ device_unregister(&mux_dev->dev);
+}
+EXPORT_SYMBOL_GPL(typec_mux_unregister);
+
+void typec_mux_set_drvdata(struct typec_mux_dev *mux_dev, void *data)
+{
+ dev_set_drvdata(&mux_dev->dev, data);
+}
+EXPORT_SYMBOL_GPL(typec_mux_set_drvdata);
+
+void *typec_mux_get_drvdata(struct typec_mux_dev *mux_dev)
+{
+ return dev_get_drvdata(&mux_dev->dev);
+}
+EXPORT_SYMBOL_GPL(typec_mux_get_drvdata);
+
+struct class typec_mux_class = {
+ .name = "typec_mux",
+ .owner = THIS_MODULE,
+};
diff --git a/drivers/usb/typec/mux.h b/drivers/usb/typec/mux.h
new file mode 100644
index 000000000..58f0f28b6
--- /dev/null
+++ b/drivers/usb/typec/mux.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_TYPEC_MUX__
+#define __USB_TYPEC_MUX__
+
+#include <linux/usb/typec_mux.h>
+
+struct typec_switch_dev {
+ struct device dev;
+ typec_switch_set_fn_t set;
+};
+
+struct typec_mux_dev {
+ struct device dev;
+ typec_mux_set_fn_t set;
+};
+
+#define to_typec_switch_dev(_dev_) container_of(_dev_, struct typec_switch_dev, dev)
+#define to_typec_mux_dev(_dev_) container_of(_dev_, struct typec_mux_dev, dev)
+
+extern const struct device_type typec_switch_dev_type;
+extern const struct device_type typec_mux_dev_type;
+
+#define is_typec_switch_dev(dev) ((dev)->type == &typec_switch_dev_type)
+#define is_typec_mux_dev(dev) ((dev)->type == &typec_mux_dev_type)
+
+#endif /* __USB_TYPEC_MUX__ */
diff --git a/drivers/usb/typec/mux/Kconfig b/drivers/usb/typec/mux/Kconfig
new file mode 100644
index 000000000..5eb2c17d7
--- /dev/null
+++ b/drivers/usb/typec/mux/Kconfig
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: GPL-2.0
+
+menu "USB Type-C Multiplexer/DeMultiplexer Switch support"
+
+config TYPEC_MUX_FSA4480
+ tristate "ON Semi FSA4480 Analog Audio Switch driver"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Driver for the ON Semiconductor FSA4480 Analog Audio Switch, which
+ provides support for muxing analog audio and sideband signals on a
+ common USB Type-C connector.
+ If compiled as a module, the module will be named fsa4480.
+
+config TYPEC_MUX_PI3USB30532
+ tristate "Pericom PI3USB30532 Type-C cross switch driver"
+ depends on I2C
+ help
+ Say Y or M if your system has a Pericom PI3USB30532 Type-C cross
+ switch / mux chip found on some devices with a Type-C port.
+
+config TYPEC_MUX_INTEL_PMC
+ tristate "Intel PMC mux control"
+ depends on ACPI
+ depends on INTEL_SCU_IPC
+ select USB_ROLE_SWITCH
+ help
+ Driver for USB muxes controlled by Intel PMC FW. Intel PMC FW can
+ control the USB role switch and also the multiplexer/demultiplexer
+ switches used with USB Type-C Alternate Modes.
+
+endmenu
diff --git a/drivers/usb/typec/mux/Makefile b/drivers/usb/typec/mux/Makefile
new file mode 100644
index 000000000..e52a56c16
--- /dev/null
+++ b/drivers/usb/typec/mux/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_TYPEC_MUX_FSA4480) += fsa4480.o
+obj-$(CONFIG_TYPEC_MUX_PI3USB30532) += pi3usb30532.o
+obj-$(CONFIG_TYPEC_MUX_INTEL_PMC) += intel_pmc_mux.o
diff --git a/drivers/usb/typec/mux/fsa4480.c b/drivers/usb/typec/mux/fsa4480.c
new file mode 100644
index 000000000..d6495e533
--- /dev/null
+++ b/drivers/usb/typec/mux/fsa4480.c
@@ -0,0 +1,216 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2021-2022 Linaro Ltd.
+ * Copyright (C) 2018-2020 The Linux Foundation
+ */
+
+#include <linux/bits.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+
+#define FSA4480_SWITCH_ENABLE 0x04
+#define FSA4480_SWITCH_SELECT 0x05
+#define FSA4480_SWITCH_STATUS1 0x07
+#define FSA4480_SLOW_L 0x08
+#define FSA4480_SLOW_R 0x09
+#define FSA4480_SLOW_MIC 0x0a
+#define FSA4480_SLOW_SENSE 0x0b
+#define FSA4480_SLOW_GND 0x0c
+#define FSA4480_DELAY_L_R 0x0d
+#define FSA4480_DELAY_L_MIC 0x0e
+#define FSA4480_DELAY_L_SENSE 0x0f
+#define FSA4480_DELAY_L_AGND 0x10
+#define FSA4480_RESET 0x1e
+#define FSA4480_MAX_REGISTER 0x1f
+
+#define FSA4480_ENABLE_DEVICE BIT(7)
+#define FSA4480_ENABLE_SBU GENMASK(6, 5)
+#define FSA4480_ENABLE_USB GENMASK(4, 3)
+
+#define FSA4480_SEL_SBU_REVERSE GENMASK(6, 5)
+#define FSA4480_SEL_USB GENMASK(4, 3)
+
+struct fsa4480 {
+ struct i2c_client *client;
+
+ /* used to serialize concurrent change requests */
+ struct mutex lock;
+
+ struct typec_switch_dev *sw;
+ struct typec_mux_dev *mux;
+
+ struct regmap *regmap;
+
+ u8 cur_enable;
+ u8 cur_select;
+};
+
+static const struct regmap_config fsa4480_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = FSA4480_MAX_REGISTER,
+ /* Accesses only done under fsa4480->lock */
+ .disable_locking = true,
+};
+
+static int fsa4480_switch_set(struct typec_switch_dev *sw,
+ enum typec_orientation orientation)
+{
+ struct fsa4480 *fsa = typec_switch_get_drvdata(sw);
+ u8 new_sel;
+
+ mutex_lock(&fsa->lock);
+ new_sel = FSA4480_SEL_USB;
+ if (orientation == TYPEC_ORIENTATION_REVERSE)
+ new_sel |= FSA4480_SEL_SBU_REVERSE;
+
+ if (new_sel == fsa->cur_select)
+ goto out_unlock;
+
+ if (fsa->cur_enable & FSA4480_ENABLE_SBU) {
+ /* Disable SBU output while re-configuring the switch */
+ regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE,
+ fsa->cur_enable & ~FSA4480_ENABLE_SBU);
+
+ /* 35us to allow the SBU switch to turn off */
+ usleep_range(35, 1000);
+ }
+
+ regmap_write(fsa->regmap, FSA4480_SWITCH_SELECT, new_sel);
+ fsa->cur_select = new_sel;
+
+ if (fsa->cur_enable & FSA4480_ENABLE_SBU) {
+ regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, fsa->cur_enable);
+
+ /* 15us to allow the SBU switch to turn on again */
+ usleep_range(15, 1000);
+ }
+
+out_unlock:
+ mutex_unlock(&fsa->lock);
+
+ return 0;
+}
+
+static int fsa4480_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state)
+{
+ struct fsa4480 *fsa = typec_mux_get_drvdata(mux);
+ u8 new_enable;
+
+ mutex_lock(&fsa->lock);
+
+ new_enable = FSA4480_ENABLE_DEVICE | FSA4480_ENABLE_USB;
+ if (state->mode >= TYPEC_DP_STATE_A)
+ new_enable |= FSA4480_ENABLE_SBU;
+
+ if (new_enable == fsa->cur_enable)
+ goto out_unlock;
+
+ regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, new_enable);
+ fsa->cur_enable = new_enable;
+
+ if (new_enable & FSA4480_ENABLE_SBU) {
+ /* 15us to allow the SBU switch to turn off */
+ usleep_range(15, 1000);
+ }
+
+out_unlock:
+ mutex_unlock(&fsa->lock);
+
+ return 0;
+}
+
+static int fsa4480_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct typec_switch_desc sw_desc = { };
+ struct typec_mux_desc mux_desc = { };
+ struct fsa4480 *fsa;
+
+ fsa = devm_kzalloc(dev, sizeof(*fsa), GFP_KERNEL);
+ if (!fsa)
+ return -ENOMEM;
+
+ fsa->client = client;
+ mutex_init(&fsa->lock);
+
+ fsa->regmap = devm_regmap_init_i2c(client, &fsa4480_regmap_config);
+ if (IS_ERR(fsa->regmap))
+ return dev_err_probe(dev, PTR_ERR(fsa->regmap), "failed to initialize regmap\n");
+
+ fsa->cur_enable = FSA4480_ENABLE_DEVICE | FSA4480_ENABLE_USB;
+ fsa->cur_select = FSA4480_SEL_USB;
+
+ /* set default settings */
+ regmap_write(fsa->regmap, FSA4480_SLOW_L, 0x00);
+ regmap_write(fsa->regmap, FSA4480_SLOW_R, 0x00);
+ regmap_write(fsa->regmap, FSA4480_SLOW_MIC, 0x00);
+ regmap_write(fsa->regmap, FSA4480_SLOW_SENSE, 0x00);
+ regmap_write(fsa->regmap, FSA4480_SLOW_GND, 0x00);
+ regmap_write(fsa->regmap, FSA4480_DELAY_L_R, 0x00);
+ regmap_write(fsa->regmap, FSA4480_DELAY_L_MIC, 0x00);
+ regmap_write(fsa->regmap, FSA4480_DELAY_L_SENSE, 0x00);
+ regmap_write(fsa->regmap, FSA4480_DELAY_L_AGND, 0x09);
+ regmap_write(fsa->regmap, FSA4480_SWITCH_SELECT, fsa->cur_select);
+ regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, fsa->cur_enable);
+
+ sw_desc.drvdata = fsa;
+ sw_desc.fwnode = dev_fwnode(dev);
+ sw_desc.set = fsa4480_switch_set;
+
+ fsa->sw = typec_switch_register(dev, &sw_desc);
+ if (IS_ERR(fsa->sw))
+ return dev_err_probe(dev, PTR_ERR(fsa->sw), "failed to register typec switch\n");
+
+ mux_desc.drvdata = fsa;
+ mux_desc.fwnode = dev_fwnode(dev);
+ mux_desc.set = fsa4480_mux_set;
+
+ fsa->mux = typec_mux_register(dev, &mux_desc);
+ if (IS_ERR(fsa->mux)) {
+ typec_switch_unregister(fsa->sw);
+ return dev_err_probe(dev, PTR_ERR(fsa->mux), "failed to register typec mux\n");
+ }
+
+ i2c_set_clientdata(client, fsa);
+ return 0;
+}
+
+static void fsa4480_remove(struct i2c_client *client)
+{
+ struct fsa4480 *fsa = i2c_get_clientdata(client);
+
+ typec_mux_unregister(fsa->mux);
+ typec_switch_unregister(fsa->sw);
+}
+
+static const struct i2c_device_id fsa4480_table[] = {
+ { "fsa4480" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, fsa4480_table);
+
+static const struct of_device_id fsa4480_of_table[] = {
+ { .compatible = "fcs,fsa4480" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, fsa4480_of_table);
+
+static struct i2c_driver fsa4480_driver = {
+ .driver = {
+ .name = "fsa4480",
+ .of_match_table = fsa4480_of_table,
+ },
+ .probe_new = fsa4480_probe,
+ .remove = fsa4480_remove,
+ .id_table = fsa4480_table,
+};
+module_i2c_driver(fsa4480_driver);
+
+MODULE_DESCRIPTION("ON Semiconductor FSA4480 driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/usb/typec/mux/intel_pmc_mux.c b/drivers/usb/typec/mux/intel_pmc_mux.c
new file mode 100644
index 000000000..87e2c9130
--- /dev/null
+++ b/drivers/usb/typec/mux/intel_pmc_mux.c
@@ -0,0 +1,745 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for Intel PMC USB mux control
+ *
+ * Copyright (C) 2020 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/acpi.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec_mux.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_tbt.h>
+
+#include <asm/intel_scu_ipc.h>
+
+#define PMC_USBC_CMD 0xa7
+
+/* Response status bits */
+#define PMC_USB_RESP_STATUS_FAILURE BIT(0)
+#define PMC_USB_RESP_STATUS_FATAL BIT(1)
+
+/* "Usage" OOB Message field values */
+enum {
+ PMC_USB_CONNECT,
+ PMC_USB_DISCONNECT,
+ PMC_USB_SAFE_MODE,
+ PMC_USB_ALT_MODE,
+ PMC_USB_DP_HPD,
+};
+
+#define PMC_USB_MSG_USB2_PORT_SHIFT 0
+#define PMC_USB_MSG_USB3_PORT_SHIFT 4
+#define PMC_USB_MSG_UFP_SHIFT 4
+#define PMC_USB_MSG_ORI_HSL_SHIFT 5
+#define PMC_USB_MSG_ORI_AUX_SHIFT 6
+
+/* Alt Mode Request */
+struct altmode_req {
+ u8 usage;
+ u8 mode_type;
+ u8 mode_id;
+ u8 reserved;
+ u32 mode_data;
+} __packed;
+
+#define PMC_USB_MODE_TYPE_SHIFT 4
+
+enum {
+ PMC_USB_MODE_TYPE_USB,
+ PMC_USB_MODE_TYPE_DP,
+ PMC_USB_MODE_TYPE_TBT,
+};
+
+/* Common Mode Data bits */
+#define PMC_USB_ALTMODE_ACTIVE_CABLE BIT(2)
+
+#define PMC_USB_ALTMODE_ORI_SHIFT 1
+#define PMC_USB_ALTMODE_UFP_SHIFT 3
+
+/* DP specific Mode Data bits */
+#define PMC_USB_ALTMODE_DP_MODE_SHIFT 8
+
+/* TBT specific Mode Data bits */
+#define PMC_USB_ALTMODE_TBT_TYPE BIT(17)
+#define PMC_USB_ALTMODE_CABLE_TYPE BIT(18)
+#define PMC_USB_ALTMODE_ACTIVE_LINK BIT(20)
+#define PMC_USB_ALTMODE_FORCE_LSR BIT(23)
+#define PMC_USB_ALTMODE_CABLE_SPD(_s_) (((_s_) & GENMASK(2, 0)) << 25)
+#define PMC_USB_ALTMODE_CABLE_USB31 1
+#define PMC_USB_ALTMODE_CABLE_10GPS 2
+#define PMC_USB_ALTMODE_CABLE_20GPS 3
+#define PMC_USB_ALTMODE_TBT_GEN(_g_) (((_g_) & GENMASK(1, 0)) << 28)
+
+/* Display HPD Request bits */
+#define PMC_USB_DP_HPD_LVL BIT(4)
+#define PMC_USB_DP_HPD_IRQ BIT(5)
+
+/*
+ * Input Output Manager (IOM) PORT STATUS
+ */
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_MASK GENMASK(9, 6)
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_SHIFT 6
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_USB 0x03
+/* activity type: Safe Mode */
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_SAFE_MODE 0x04
+/* activity type: Display Port */
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_DP 0x05
+/* activity type: Display Port Multi Function Device */
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_DP_MFD 0x06
+/* activity type: Thunderbolt */
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_TBT 0x07
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_ALT_MODE_USB 0x0c
+#define IOM_PORT_STATUS_ACTIVITY_TYPE_ALT_MODE_TBT_USB 0x0d
+/* Upstream Facing Port Information */
+#define IOM_PORT_STATUS_UFP BIT(10)
+/* Display Port Hot Plug Detect status */
+#define IOM_PORT_STATUS_DHPD_HPD_STATUS_MASK GENMASK(13, 12)
+#define IOM_PORT_STATUS_DHPD_HPD_STATUS_SHIFT 12
+#define IOM_PORT_STATUS_DHPD_HPD_STATUS_ASSERT 0x01
+#define IOM_PORT_STATUS_DHPD_HPD_SOURCE_TBT BIT(14)
+#define IOM_PORT_STATUS_CONNECTED BIT(31)
+
+#define IOM_PORT_ACTIVITY_IS(_status_, _type_) \
+ ((((_status_) & IOM_PORT_STATUS_ACTIVITY_TYPE_MASK) >> \
+ IOM_PORT_STATUS_ACTIVITY_TYPE_SHIFT) == \
+ (IOM_PORT_STATUS_ACTIVITY_TYPE_##_type_))
+
+#define IOM_PORT_HPD_ASSERTED(_status_) \
+ ((((_status_) & IOM_PORT_STATUS_DHPD_HPD_STATUS_MASK) >> \
+ IOM_PORT_STATUS_DHPD_HPD_STATUS_SHIFT) & \
+ IOM_PORT_STATUS_DHPD_HPD_STATUS_ASSERT)
+
+struct pmc_usb;
+
+struct pmc_usb_port {
+ int num;
+ u32 iom_status;
+ struct pmc_usb *pmc;
+ struct typec_mux_dev *typec_mux;
+ struct typec_switch_dev *typec_sw;
+ struct usb_role_switch *usb_sw;
+
+ enum typec_orientation orientation;
+ enum usb_role role;
+
+ u8 usb2_port;
+ u8 usb3_port;
+
+ enum typec_orientation sbu_orientation;
+ enum typec_orientation hsl_orientation;
+};
+
+struct pmc_usb {
+ u8 num_ports;
+ struct device *dev;
+ struct intel_scu_ipc_dev *ipc;
+ struct pmc_usb_port *port;
+ struct acpi_device *iom_adev;
+ void __iomem *iom_base;
+ u32 iom_port_status_offset;
+};
+
+static void update_port_status(struct pmc_usb_port *port)
+{
+ u8 port_num;
+
+ /* SoC expects the USB Type-C port numbers to start with 0 */
+ port_num = port->usb3_port - 1;
+
+ port->iom_status = readl(port->pmc->iom_base +
+ port->pmc->iom_port_status_offset +
+ port_num * sizeof(u32));
+}
+
+static int sbu_orientation(struct pmc_usb_port *port)
+{
+ if (port->sbu_orientation)
+ return port->sbu_orientation - 1;
+
+ return port->orientation - 1;
+}
+
+static int hsl_orientation(struct pmc_usb_port *port)
+{
+ if (port->hsl_orientation)
+ return port->hsl_orientation - 1;
+
+ return port->orientation - 1;
+}
+
+static int pmc_usb_send_command(struct intel_scu_ipc_dev *ipc, u8 *msg, u32 len)
+{
+ u8 response[4];
+ u8 status_res;
+ int ret;
+
+ /*
+ * Error bit will always be 0 with the USBC command.
+ * Status can be checked from the response message if the
+ * function intel_scu_ipc_dev_command succeeds.
+ */
+ ret = intel_scu_ipc_dev_command(ipc, PMC_USBC_CMD, 0, msg,
+ len, response, sizeof(response));
+
+ if (ret)
+ return ret;
+
+ status_res = (msg[0] & 0xf) < PMC_USB_SAFE_MODE ?
+ response[2] : response[1];
+
+ if (status_res & PMC_USB_RESP_STATUS_FAILURE) {
+ if (status_res & PMC_USB_RESP_STATUS_FATAL)
+ return -EIO;
+
+ return -EBUSY;
+ }
+
+ return 0;
+}
+
+static int pmc_usb_command(struct pmc_usb_port *port, u8 *msg, u32 len)
+{
+ int retry_count = 3;
+ int ret;
+
+ /*
+ * If PMC is busy then retry the command once again
+ */
+ while (retry_count--) {
+ ret = pmc_usb_send_command(port->pmc->ipc, msg, len);
+ if (ret != -EBUSY)
+ break;
+ }
+
+ return ret;
+}
+
+static int
+pmc_usb_mux_dp_hpd(struct pmc_usb_port *port, struct typec_displayport_data *dp)
+{
+ u8 msg[2] = { };
+ int ret;
+
+ msg[0] = PMC_USB_DP_HPD;
+ msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+
+ /* Configure HPD first if HPD,IRQ comes together */
+ if (!IOM_PORT_HPD_ASSERTED(port->iom_status) &&
+ dp->status & DP_STATUS_IRQ_HPD &&
+ dp->status & DP_STATUS_HPD_STATE) {
+ msg[1] = PMC_USB_DP_HPD_LVL;
+ ret = pmc_usb_command(port, msg, sizeof(msg));
+ if (ret)
+ return ret;
+ }
+
+ if (dp->status & DP_STATUS_IRQ_HPD)
+ msg[1] = PMC_USB_DP_HPD_IRQ;
+
+ if (dp->status & DP_STATUS_HPD_STATE)
+ msg[1] |= PMC_USB_DP_HPD_LVL;
+
+ return pmc_usb_command(port, msg, sizeof(msg));
+}
+
+static int
+pmc_usb_mux_dp(struct pmc_usb_port *port, struct typec_mux_state *state)
+{
+ struct typec_displayport_data *data = state->data;
+ struct altmode_req req = { };
+ int ret;
+
+ if (IOM_PORT_ACTIVITY_IS(port->iom_status, DP) ||
+ IOM_PORT_ACTIVITY_IS(port->iom_status, DP_MFD)) {
+ if (IOM_PORT_HPD_ASSERTED(port->iom_status) &&
+ (!(data->status & DP_STATUS_IRQ_HPD) &&
+ data->status & DP_STATUS_HPD_STATE))
+ return 0;
+
+ return pmc_usb_mux_dp_hpd(port, state->data);
+ }
+
+ req.usage = PMC_USB_ALT_MODE;
+ req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+ req.mode_type = PMC_USB_MODE_TYPE_DP << PMC_USB_MODE_TYPE_SHIFT;
+
+ req.mode_data = (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT;
+ req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT;
+
+ req.mode_data |= (state->mode - TYPEC_STATE_MODAL) <<
+ PMC_USB_ALTMODE_DP_MODE_SHIFT;
+
+ ret = pmc_usb_command(port, (void *)&req, sizeof(req));
+ if (ret)
+ return ret;
+
+ if (data->status & (DP_STATUS_IRQ_HPD | DP_STATUS_HPD_STATE))
+ return pmc_usb_mux_dp_hpd(port, state->data);
+
+ return 0;
+}
+
+static int
+pmc_usb_mux_tbt(struct pmc_usb_port *port, struct typec_mux_state *state)
+{
+ struct typec_thunderbolt_data *data = state->data;
+ u8 cable_rounded = TBT_CABLE_ROUNDED_SUPPORT(data->cable_mode);
+ u8 cable_speed = TBT_CABLE_SPEED(data->cable_mode);
+ struct altmode_req req = { };
+
+ if (IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) ||
+ IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB))
+ return 0;
+
+ req.usage = PMC_USB_ALT_MODE;
+ req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+ req.mode_type = PMC_USB_MODE_TYPE_TBT << PMC_USB_MODE_TYPE_SHIFT;
+
+ req.mode_data = (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT;
+ req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT;
+
+ if (TBT_ADAPTER(data->device_mode) == TBT_ADAPTER_TBT3)
+ req.mode_data |= PMC_USB_ALTMODE_TBT_TYPE;
+
+ if (data->cable_mode & TBT_CABLE_OPTICAL)
+ req.mode_data |= PMC_USB_ALTMODE_CABLE_TYPE;
+
+ if (data->cable_mode & TBT_CABLE_LINK_TRAINING)
+ req.mode_data |= PMC_USB_ALTMODE_ACTIVE_LINK;
+
+ if (data->enter_vdo & TBT_ENTER_MODE_ACTIVE_CABLE)
+ req.mode_data |= PMC_USB_ALTMODE_ACTIVE_CABLE;
+
+ req.mode_data |= PMC_USB_ALTMODE_CABLE_SPD(cable_speed);
+
+ req.mode_data |= PMC_USB_ALTMODE_TBT_GEN(cable_rounded);
+
+ return pmc_usb_command(port, (void *)&req, sizeof(req));
+}
+
+static int
+pmc_usb_mux_usb4(struct pmc_usb_port *port, struct typec_mux_state *state)
+{
+ struct enter_usb_data *data = state->data;
+ struct altmode_req req = { };
+ u8 cable_speed;
+
+ if (IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) ||
+ IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB))
+ return 0;
+
+ req.usage = PMC_USB_ALT_MODE;
+ req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+ req.mode_type = PMC_USB_MODE_TYPE_TBT << PMC_USB_MODE_TYPE_SHIFT;
+
+ /* USB4 Mode */
+ req.mode_data = PMC_USB_ALTMODE_FORCE_LSR;
+
+ if (data->active_link_training)
+ req.mode_data |= PMC_USB_ALTMODE_ACTIVE_LINK;
+
+ req.mode_data |= (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT;
+ req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT;
+
+ switch ((data->eudo & EUDO_CABLE_TYPE_MASK) >> EUDO_CABLE_TYPE_SHIFT) {
+ case EUDO_CABLE_TYPE_PASSIVE:
+ break;
+ case EUDO_CABLE_TYPE_OPTICAL:
+ req.mode_data |= PMC_USB_ALTMODE_CABLE_TYPE;
+ fallthrough;
+ default:
+ req.mode_data |= PMC_USB_ALTMODE_ACTIVE_CABLE;
+
+ /* Configure data rate to rounded in the case of Active TBT3
+ * and USB4 cables.
+ */
+ req.mode_data |= PMC_USB_ALTMODE_TBT_GEN(1);
+ break;
+ }
+
+ cable_speed = (data->eudo & EUDO_CABLE_SPEED_MASK) >> EUDO_CABLE_SPEED_SHIFT;
+ req.mode_data |= PMC_USB_ALTMODE_CABLE_SPD(cable_speed);
+
+ return pmc_usb_command(port, (void *)&req, sizeof(req));
+}
+
+static int pmc_usb_mux_safe_state(struct pmc_usb_port *port,
+ struct typec_mux_state *state)
+{
+ u8 msg;
+
+ if (IOM_PORT_ACTIVITY_IS(port->iom_status, SAFE_MODE))
+ return 0;
+
+ if ((IOM_PORT_ACTIVITY_IS(port->iom_status, DP) ||
+ IOM_PORT_ACTIVITY_IS(port->iom_status, DP_MFD)) &&
+ state->alt && state->alt->svid == USB_TYPEC_DP_SID)
+ return 0;
+
+ if ((IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) ||
+ IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB)) &&
+ state->alt && state->alt->svid == USB_TYPEC_TBT_SID)
+ return 0;
+
+ msg = PMC_USB_SAFE_MODE;
+ msg |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+
+ return pmc_usb_command(port, &msg, sizeof(msg));
+}
+
+static int pmc_usb_disconnect(struct pmc_usb_port *port)
+{
+ struct typec_displayport_data data = { };
+ u8 msg[2];
+
+ if (!(port->iom_status & IOM_PORT_STATUS_CONNECTED))
+ return 0;
+
+ /* Clear DisplayPort HPD if it's still asserted. */
+ if (IOM_PORT_HPD_ASSERTED(port->iom_status))
+ pmc_usb_mux_dp_hpd(port, &data);
+
+ msg[0] = PMC_USB_DISCONNECT;
+ msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+
+ msg[1] = port->usb2_port << PMC_USB_MSG_USB2_PORT_SHIFT;
+
+ return pmc_usb_command(port, msg, sizeof(msg));
+}
+
+static int pmc_usb_connect(struct pmc_usb_port *port, enum usb_role role)
+{
+ u8 ufp = role == USB_ROLE_DEVICE ? 1 : 0;
+ u8 msg[2];
+ int ret;
+
+ if (port->orientation == TYPEC_ORIENTATION_NONE)
+ return -EINVAL;
+
+ if (port->iom_status & IOM_PORT_STATUS_CONNECTED) {
+ if (port->role == role || port->role == USB_ROLE_NONE)
+ return 0;
+
+ /* Role swap */
+ ret = pmc_usb_disconnect(port);
+ if (ret)
+ return ret;
+ }
+
+ msg[0] = PMC_USB_CONNECT;
+ msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT;
+
+ msg[1] = port->usb2_port << PMC_USB_MSG_USB2_PORT_SHIFT;
+ msg[1] |= ufp << PMC_USB_MSG_UFP_SHIFT;
+ msg[1] |= hsl_orientation(port) << PMC_USB_MSG_ORI_HSL_SHIFT;
+ msg[1] |= sbu_orientation(port) << PMC_USB_MSG_ORI_AUX_SHIFT;
+
+ return pmc_usb_command(port, msg, sizeof(msg));
+}
+
+static int
+pmc_usb_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state)
+{
+ struct pmc_usb_port *port = typec_mux_get_drvdata(mux);
+
+ update_port_status(port);
+
+ if (port->orientation == TYPEC_ORIENTATION_NONE || port->role == USB_ROLE_NONE)
+ return 0;
+
+ if (state->mode == TYPEC_STATE_SAFE)
+ return pmc_usb_mux_safe_state(port, state);
+ if (state->mode == TYPEC_STATE_USB)
+ return pmc_usb_connect(port, port->role);
+
+ if (state->alt) {
+ switch (state->alt->svid) {
+ case USB_TYPEC_TBT_SID:
+ return pmc_usb_mux_tbt(port, state);
+ case USB_TYPEC_DP_SID:
+ return pmc_usb_mux_dp(port, state);
+ }
+ } else {
+ switch (state->mode) {
+ case TYPEC_MODE_USB2:
+ /* REVISIT: Try with usb3_port set to 0? */
+ break;
+ case TYPEC_MODE_USB3:
+ return pmc_usb_connect(port, port->role);
+ case TYPEC_MODE_USB4:
+ return pmc_usb_mux_usb4(port, state);
+ }
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int pmc_usb_set_orientation(struct typec_switch_dev *sw,
+ enum typec_orientation orientation)
+{
+ struct pmc_usb_port *port = typec_switch_get_drvdata(sw);
+
+ update_port_status(port);
+
+ port->orientation = orientation;
+
+ return 0;
+}
+
+static int pmc_usb_set_role(struct usb_role_switch *sw, enum usb_role role)
+{
+ struct pmc_usb_port *port = usb_role_switch_get_drvdata(sw);
+ int ret;
+
+ update_port_status(port);
+
+ if (role == USB_ROLE_NONE)
+ ret = pmc_usb_disconnect(port);
+ else
+ ret = pmc_usb_connect(port, role);
+
+ port->role = role;
+
+ return ret;
+}
+
+static int pmc_usb_register_port(struct pmc_usb *pmc, int index,
+ struct fwnode_handle *fwnode)
+{
+ struct pmc_usb_port *port = &pmc->port[index];
+ struct usb_role_switch_desc desc = { };
+ struct typec_switch_desc sw_desc = { };
+ struct typec_mux_desc mux_desc = { };
+ const char *str;
+ int ret;
+
+ ret = fwnode_property_read_u8(fwnode, "usb2-port-number", &port->usb2_port);
+ if (ret)
+ return ret;
+
+ ret = fwnode_property_read_u8(fwnode, "usb3-port-number", &port->usb3_port);
+ if (ret)
+ return ret;
+
+ ret = fwnode_property_read_string(fwnode, "sbu-orientation", &str);
+ if (!ret)
+ port->sbu_orientation = typec_find_orientation(str);
+
+ ret = fwnode_property_read_string(fwnode, "hsl-orientation", &str);
+ if (!ret)
+ port->hsl_orientation = typec_find_orientation(str);
+
+ port->num = index;
+ port->pmc = pmc;
+
+ sw_desc.fwnode = fwnode;
+ sw_desc.drvdata = port;
+ sw_desc.name = fwnode_get_name(fwnode);
+ sw_desc.set = pmc_usb_set_orientation;
+
+ port->typec_sw = typec_switch_register(pmc->dev, &sw_desc);
+ if (IS_ERR(port->typec_sw))
+ return PTR_ERR(port->typec_sw);
+
+ mux_desc.fwnode = fwnode;
+ mux_desc.drvdata = port;
+ mux_desc.name = fwnode_get_name(fwnode);
+ mux_desc.set = pmc_usb_mux_set;
+
+ port->typec_mux = typec_mux_register(pmc->dev, &mux_desc);
+ if (IS_ERR(port->typec_mux)) {
+ ret = PTR_ERR(port->typec_mux);
+ goto err_unregister_switch;
+ }
+
+ desc.fwnode = fwnode;
+ desc.driver_data = port;
+ desc.name = fwnode_get_name(fwnode);
+ desc.set = pmc_usb_set_role;
+
+ port->usb_sw = usb_role_switch_register(pmc->dev, &desc);
+ if (IS_ERR(port->usb_sw)) {
+ ret = PTR_ERR(port->usb_sw);
+ goto err_unregister_mux;
+ }
+
+ return 0;
+
+err_unregister_mux:
+ typec_mux_unregister(port->typec_mux);
+
+err_unregister_switch:
+ typec_switch_unregister(port->typec_sw);
+
+ return ret;
+}
+
+/* IOM ACPI IDs and IOM_PORT_STATUS_OFFSET */
+static const struct acpi_device_id iom_acpi_ids[] = {
+ /* TigerLake */
+ { "INTC1072", 0x560, },
+
+ /* AlderLake */
+ { "INTC1079", 0x160, },
+
+ /* Meteor Lake */
+ { "INTC107A", 0x160, },
+ {}
+};
+
+static int pmc_usb_probe_iom(struct pmc_usb *pmc)
+{
+ struct list_head resource_list;
+ struct resource_entry *rentry;
+ static const struct acpi_device_id *dev_id;
+ struct acpi_device *adev = NULL;
+ int ret;
+
+ for (dev_id = &iom_acpi_ids[0]; dev_id->id[0]; dev_id++) {
+ if (acpi_dev_present(dev_id->id, NULL, -1)) {
+ pmc->iom_port_status_offset = (u32)dev_id->driver_data;
+ adev = acpi_dev_get_first_match_dev(dev_id->id, NULL, -1);
+ break;
+ }
+ }
+
+ if (!adev)
+ return -ENODEV;
+
+ INIT_LIST_HEAD(&resource_list);
+ ret = acpi_dev_get_memory_resources(adev, &resource_list);
+ if (ret < 0) {
+ acpi_dev_put(adev);
+ return ret;
+ }
+
+ rentry = list_first_entry_or_null(&resource_list, struct resource_entry, node);
+ if (rentry)
+ pmc->iom_base = devm_ioremap_resource(pmc->dev, rentry->res);
+
+ acpi_dev_free_resource_list(&resource_list);
+
+ if (!pmc->iom_base) {
+ acpi_dev_put(adev);
+ return -ENOMEM;
+ }
+
+ if (IS_ERR(pmc->iom_base)) {
+ acpi_dev_put(adev);
+ return PTR_ERR(pmc->iom_base);
+ }
+
+ pmc->iom_adev = adev;
+
+ return 0;
+}
+
+static int pmc_usb_probe(struct platform_device *pdev)
+{
+ struct fwnode_handle *fwnode = NULL;
+ struct pmc_usb *pmc;
+ int i = 0;
+ int ret;
+
+ pmc = devm_kzalloc(&pdev->dev, sizeof(*pmc), GFP_KERNEL);
+ if (!pmc)
+ return -ENOMEM;
+
+ device_for_each_child_node(&pdev->dev, fwnode)
+ pmc->num_ports++;
+
+ /* The IOM microcontroller has a limitation of max 4 ports. */
+ if (pmc->num_ports > 4) {
+ dev_err(&pdev->dev, "driver limited to 4 ports\n");
+ return -ERANGE;
+ }
+
+ pmc->port = devm_kcalloc(&pdev->dev, pmc->num_ports,
+ sizeof(struct pmc_usb_port), GFP_KERNEL);
+ if (!pmc->port)
+ return -ENOMEM;
+
+ pmc->ipc = devm_intel_scu_ipc_dev_get(&pdev->dev);
+ if (!pmc->ipc)
+ return -ENODEV;
+
+ pmc->dev = &pdev->dev;
+
+ ret = pmc_usb_probe_iom(pmc);
+ if (ret)
+ return ret;
+
+ /*
+ * For every physical USB connector (USB2 and USB3 combo) there is a
+ * child ACPI device node under the PMC mux ACPI device object.
+ */
+ for (i = 0; i < pmc->num_ports; i++) {
+ fwnode = device_get_next_child_node(pmc->dev, fwnode);
+ if (!fwnode)
+ break;
+
+ ret = pmc_usb_register_port(pmc, i, fwnode);
+ if (ret) {
+ fwnode_handle_put(fwnode);
+ goto err_remove_ports;
+ }
+ }
+
+ platform_set_drvdata(pdev, pmc);
+
+ return 0;
+
+err_remove_ports:
+ for (i = 0; i < pmc->num_ports; i++) {
+ typec_switch_unregister(pmc->port[i].typec_sw);
+ typec_mux_unregister(pmc->port[i].typec_mux);
+ usb_role_switch_unregister(pmc->port[i].usb_sw);
+ }
+
+ acpi_dev_put(pmc->iom_adev);
+
+ return ret;
+}
+
+static int pmc_usb_remove(struct platform_device *pdev)
+{
+ struct pmc_usb *pmc = platform_get_drvdata(pdev);
+ int i;
+
+ for (i = 0; i < pmc->num_ports; i++) {
+ typec_switch_unregister(pmc->port[i].typec_sw);
+ typec_mux_unregister(pmc->port[i].typec_mux);
+ usb_role_switch_unregister(pmc->port[i].usb_sw);
+ }
+
+ acpi_dev_put(pmc->iom_adev);
+
+ return 0;
+}
+
+static const struct acpi_device_id pmc_usb_acpi_ids[] = {
+ { "INTC105C", },
+ { }
+};
+MODULE_DEVICE_TABLE(acpi, pmc_usb_acpi_ids);
+
+static struct platform_driver pmc_usb_driver = {
+ .driver = {
+ .name = "intel_pmc_usb",
+ .acpi_match_table = ACPI_PTR(pmc_usb_acpi_ids),
+ },
+ .probe = pmc_usb_probe,
+ .remove = pmc_usb_remove,
+};
+
+module_platform_driver(pmc_usb_driver);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("Intel PMC USB mux control");
diff --git a/drivers/usb/typec/mux/pi3usb30532.c b/drivers/usb/typec/mux/pi3usb30532.c
new file mode 100644
index 000000000..1cd388b55
--- /dev/null
+++ b/drivers/usb/typec/mux/pi3usb30532.c
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Pericom PI3USB30532 Type-C cross switch / mux driver
+ *
+ * Copyright (c) 2017-2018 Hans de Goede <hdegoede@redhat.com>
+ */
+
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+
+#define PI3USB30532_CONF 0x00
+
+#define PI3USB30532_CONF_OPEN 0x00
+#define PI3USB30532_CONF_SWAP 0x01
+#define PI3USB30532_CONF_4LANE_DP 0x02
+#define PI3USB30532_CONF_USB3 0x04
+#define PI3USB30532_CONF_USB3_AND_2LANE_DP 0x06
+
+struct pi3usb30532 {
+ struct i2c_client *client;
+ struct mutex lock; /* protects the cached conf register */
+ struct typec_switch_dev *sw;
+ struct typec_mux_dev *mux;
+ u8 conf;
+};
+
+static int pi3usb30532_set_conf(struct pi3usb30532 *pi, u8 new_conf)
+{
+ int ret = 0;
+
+ if (pi->conf == new_conf)
+ return 0;
+
+ ret = i2c_smbus_write_byte_data(pi->client, PI3USB30532_CONF, new_conf);
+ if (ret) {
+ dev_err(&pi->client->dev, "Error writing conf: %d\n", ret);
+ return ret;
+ }
+
+ pi->conf = new_conf;
+ return 0;
+}
+
+static int pi3usb30532_sw_set(struct typec_switch_dev *sw,
+ enum typec_orientation orientation)
+{
+ struct pi3usb30532 *pi = typec_switch_get_drvdata(sw);
+ u8 new_conf;
+ int ret;
+
+ mutex_lock(&pi->lock);
+ new_conf = pi->conf;
+
+ switch (orientation) {
+ case TYPEC_ORIENTATION_NONE:
+ new_conf = PI3USB30532_CONF_OPEN;
+ break;
+ case TYPEC_ORIENTATION_NORMAL:
+ new_conf &= ~PI3USB30532_CONF_SWAP;
+ break;
+ case TYPEC_ORIENTATION_REVERSE:
+ new_conf |= PI3USB30532_CONF_SWAP;
+ break;
+ }
+
+ ret = pi3usb30532_set_conf(pi, new_conf);
+ mutex_unlock(&pi->lock);
+
+ return ret;
+}
+
+static int
+pi3usb30532_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state)
+{
+ struct pi3usb30532 *pi = typec_mux_get_drvdata(mux);
+ u8 new_conf;
+ int ret;
+
+ mutex_lock(&pi->lock);
+ new_conf = pi->conf;
+
+ switch (state->mode) {
+ case TYPEC_STATE_SAFE:
+ new_conf = (new_conf & PI3USB30532_CONF_SWAP) |
+ PI3USB30532_CONF_OPEN;
+ break;
+ case TYPEC_STATE_USB:
+ new_conf = (new_conf & PI3USB30532_CONF_SWAP) |
+ PI3USB30532_CONF_USB3;
+ break;
+ case TYPEC_DP_STATE_C:
+ case TYPEC_DP_STATE_E:
+ new_conf = (new_conf & PI3USB30532_CONF_SWAP) |
+ PI3USB30532_CONF_4LANE_DP;
+ break;
+ case TYPEC_DP_STATE_D:
+ new_conf = (new_conf & PI3USB30532_CONF_SWAP) |
+ PI3USB30532_CONF_USB3_AND_2LANE_DP;
+ break;
+ default:
+ break;
+ }
+
+ ret = pi3usb30532_set_conf(pi, new_conf);
+ mutex_unlock(&pi->lock);
+
+ return ret;
+}
+
+static int pi3usb30532_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct typec_switch_desc sw_desc = { };
+ struct typec_mux_desc mux_desc = { };
+ struct pi3usb30532 *pi;
+ int ret;
+
+ pi = devm_kzalloc(dev, sizeof(*pi), GFP_KERNEL);
+ if (!pi)
+ return -ENOMEM;
+
+ pi->client = client;
+ mutex_init(&pi->lock);
+
+ ret = i2c_smbus_read_byte_data(client, PI3USB30532_CONF);
+ if (ret < 0) {
+ dev_err(dev, "Error reading config register %d\n", ret);
+ return ret;
+ }
+ pi->conf = ret;
+
+ sw_desc.drvdata = pi;
+ sw_desc.fwnode = dev->fwnode;
+ sw_desc.set = pi3usb30532_sw_set;
+
+ pi->sw = typec_switch_register(dev, &sw_desc);
+ if (IS_ERR(pi->sw)) {
+ dev_err(dev, "Error registering typec switch: %ld\n",
+ PTR_ERR(pi->sw));
+ return PTR_ERR(pi->sw);
+ }
+
+ mux_desc.drvdata = pi;
+ mux_desc.fwnode = dev->fwnode;
+ mux_desc.set = pi3usb30532_mux_set;
+
+ pi->mux = typec_mux_register(dev, &mux_desc);
+ if (IS_ERR(pi->mux)) {
+ typec_switch_unregister(pi->sw);
+ dev_err(dev, "Error registering typec mux: %ld\n",
+ PTR_ERR(pi->mux));
+ return PTR_ERR(pi->mux);
+ }
+
+ i2c_set_clientdata(client, pi);
+ return 0;
+}
+
+static void pi3usb30532_remove(struct i2c_client *client)
+{
+ struct pi3usb30532 *pi = i2c_get_clientdata(client);
+
+ typec_mux_unregister(pi->mux);
+ typec_switch_unregister(pi->sw);
+}
+
+static const struct i2c_device_id pi3usb30532_table[] = {
+ { "pi3usb30532" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, pi3usb30532_table);
+
+static struct i2c_driver pi3usb30532_driver = {
+ .driver = {
+ .name = "pi3usb30532",
+ },
+ .probe_new = pi3usb30532_probe,
+ .remove = pi3usb30532_remove,
+ .id_table = pi3usb30532_table,
+};
+
+module_i2c_driver(pi3usb30532_driver);
+
+MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>");
+MODULE_DESCRIPTION("Pericom PI3USB30532 Type-C mux driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/typec/pd.c b/drivers/usb/typec/pd.c
new file mode 100644
index 000000000..fd2477161
--- /dev/null
+++ b/drivers/usb/typec/pd.c
@@ -0,0 +1,707 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Power Delivery sysfs entries
+ *
+ * Copyright (C) 2022, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/slab.h>
+#include <linux/usb/pd.h>
+
+#include "pd.h"
+
+static DEFINE_IDA(pd_ida);
+
+static struct class pd_class = {
+ .name = "usb_power_delivery",
+ .owner = THIS_MODULE,
+};
+
+#define to_pdo(o) container_of(o, struct pdo, dev)
+
+struct pdo {
+ struct device dev;
+ int object_position;
+ u32 pdo;
+};
+
+static void pdo_release(struct device *dev)
+{
+ kfree(to_pdo(dev));
+}
+
+/* -------------------------------------------------------------------------- */
+/* Fixed Supply */
+
+static ssize_t
+dual_role_power_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_DUAL_ROLE));
+}
+static DEVICE_ATTR_RO(dual_role_power);
+
+static ssize_t
+usb_suspend_supported_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_SUSPEND));
+}
+static DEVICE_ATTR_RO(usb_suspend_supported);
+
+static ssize_t
+unconstrained_power_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_EXTPOWER));
+}
+static DEVICE_ATTR_RO(unconstrained_power);
+
+static ssize_t
+usb_communication_capable_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_USB_COMM));
+}
+static DEVICE_ATTR_RO(usb_communication_capable);
+
+static ssize_t
+dual_role_data_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_DATA_SWAP));
+}
+static DEVICE_ATTR_RO(dual_role_data);
+
+static ssize_t
+unchunked_extended_messages_supported_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & PDO_FIXED_UNCHUNK_EXT));
+}
+static DEVICE_ATTR_RO(unchunked_extended_messages_supported);
+
+/*
+ * REVISIT: Peak Current requires access also to the RDO.
+static ssize_t
+peak_current_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ ...
+}
+*/
+
+static ssize_t
+fast_role_swap_current_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", (to_pdo(dev)->pdo >> PDO_FIXED_FRS_CURR_SHIFT) & 3);
+}
+static DEVICE_ATTR_RO(fast_role_swap_current);
+
+static ssize_t voltage_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umV\n", pdo_fixed_voltage(to_pdo(dev)->pdo));
+}
+static DEVICE_ATTR_RO(voltage);
+
+/* Shared with Variable supplies, both source and sink */
+static ssize_t current_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umA\n", pdo_max_current(to_pdo(dev)->pdo));
+}
+
+/* Shared with Variable type supplies */
+static struct device_attribute maximum_current_attr = {
+ .attr = {
+ .name = "maximum_current",
+ .mode = 0444,
+ },
+ .show = current_show,
+};
+
+static struct device_attribute operational_current_attr = {
+ .attr = {
+ .name = "operational_current",
+ .mode = 0444,
+ },
+ .show = current_show,
+};
+
+static struct attribute *source_fixed_supply_attrs[] = {
+ &dev_attr_dual_role_power.attr,
+ &dev_attr_usb_suspend_supported.attr,
+ &dev_attr_unconstrained_power.attr,
+ &dev_attr_usb_communication_capable.attr,
+ &dev_attr_dual_role_data.attr,
+ &dev_attr_unchunked_extended_messages_supported.attr,
+ /*&dev_attr_peak_current.attr,*/
+ &dev_attr_voltage.attr,
+ &maximum_current_attr.attr,
+ NULL
+};
+
+static umode_t fixed_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ if (to_pdo(kobj_to_dev(kobj))->object_position &&
+ /*attr != &dev_attr_peak_current.attr &&*/
+ attr != &dev_attr_voltage.attr &&
+ attr != &maximum_current_attr.attr &&
+ attr != &operational_current_attr.attr)
+ return 0;
+
+ return attr->mode;
+}
+
+static const struct attribute_group source_fixed_supply_group = {
+ .is_visible = fixed_attr_is_visible,
+ .attrs = source_fixed_supply_attrs,
+};
+__ATTRIBUTE_GROUPS(source_fixed_supply);
+
+static struct device_type source_fixed_supply_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = source_fixed_supply_groups,
+};
+
+static struct attribute *sink_fixed_supply_attrs[] = {
+ &dev_attr_dual_role_power.attr,
+ &dev_attr_unconstrained_power.attr,
+ &dev_attr_usb_communication_capable.attr,
+ &dev_attr_dual_role_data.attr,
+ &dev_attr_unchunked_extended_messages_supported.attr,
+ &dev_attr_fast_role_swap_current.attr,
+ &dev_attr_voltage.attr,
+ &operational_current_attr.attr,
+ NULL
+};
+
+static const struct attribute_group sink_fixed_supply_group = {
+ .is_visible = fixed_attr_is_visible,
+ .attrs = sink_fixed_supply_attrs,
+};
+__ATTRIBUTE_GROUPS(sink_fixed_supply);
+
+static struct device_type sink_fixed_supply_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = sink_fixed_supply_groups,
+};
+
+/* -------------------------------------------------------------------------- */
+/* Variable Supply */
+
+static ssize_t
+maximum_voltage_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umV\n", pdo_max_voltage(to_pdo(dev)->pdo));
+}
+static DEVICE_ATTR_RO(maximum_voltage);
+
+static ssize_t
+minimum_voltage_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umV\n", pdo_min_voltage(to_pdo(dev)->pdo));
+}
+static DEVICE_ATTR_RO(minimum_voltage);
+
+static struct attribute *source_variable_supply_attrs[] = {
+ &dev_attr_maximum_voltage.attr,
+ &dev_attr_minimum_voltage.attr,
+ &maximum_current_attr.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(source_variable_supply);
+
+static struct device_type source_variable_supply_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = source_variable_supply_groups,
+};
+
+static struct attribute *sink_variable_supply_attrs[] = {
+ &dev_attr_maximum_voltage.attr,
+ &dev_attr_minimum_voltage.attr,
+ &operational_current_attr.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(sink_variable_supply);
+
+static struct device_type sink_variable_supply_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = sink_variable_supply_groups,
+};
+
+/* -------------------------------------------------------------------------- */
+/* Battery */
+
+static ssize_t
+maximum_power_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umW\n", pdo_max_power(to_pdo(dev)->pdo));
+}
+static DEVICE_ATTR_RO(maximum_power);
+
+static ssize_t
+operational_power_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umW\n", pdo_max_power(to_pdo(dev)->pdo));
+}
+static DEVICE_ATTR_RO(operational_power);
+
+static struct attribute *source_battery_attrs[] = {
+ &dev_attr_maximum_voltage.attr,
+ &dev_attr_minimum_voltage.attr,
+ &dev_attr_maximum_power.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(source_battery);
+
+static struct device_type source_battery_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = source_battery_groups,
+};
+
+static struct attribute *sink_battery_attrs[] = {
+ &dev_attr_maximum_voltage.attr,
+ &dev_attr_minimum_voltage.attr,
+ &dev_attr_operational_power.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(sink_battery);
+
+static struct device_type sink_battery_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = sink_battery_groups,
+};
+
+/* -------------------------------------------------------------------------- */
+/* Standard Power Range (SPR) Programmable Power Supply (PPS) */
+
+static ssize_t
+pps_power_limited_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", !!(to_pdo(dev)->pdo & BIT(27)));
+}
+static DEVICE_ATTR_RO(pps_power_limited);
+
+static ssize_t
+pps_max_voltage_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umV\n", pdo_pps_apdo_max_voltage(to_pdo(dev)->pdo));
+}
+
+static ssize_t
+pps_min_voltage_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umV\n", pdo_pps_apdo_min_voltage(to_pdo(dev)->pdo));
+}
+
+static ssize_t
+pps_max_current_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%umA\n", pdo_pps_apdo_max_current(to_pdo(dev)->pdo));
+}
+
+static struct device_attribute pps_max_voltage_attr = {
+ .attr = {
+ .name = "maximum_voltage",
+ .mode = 0444,
+ },
+ .show = pps_max_voltage_show,
+};
+
+static struct device_attribute pps_min_voltage_attr = {
+ .attr = {
+ .name = "minimum_voltage",
+ .mode = 0444,
+ },
+ .show = pps_min_voltage_show,
+};
+
+static struct device_attribute pps_max_current_attr = {
+ .attr = {
+ .name = "maximum_current",
+ .mode = 0444,
+ },
+ .show = pps_max_current_show,
+};
+
+static struct attribute *source_pps_attrs[] = {
+ &dev_attr_pps_power_limited.attr,
+ &pps_max_voltage_attr.attr,
+ &pps_min_voltage_attr.attr,
+ &pps_max_current_attr.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(source_pps);
+
+static struct device_type source_pps_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = source_pps_groups,
+};
+
+static struct attribute *sink_pps_attrs[] = {
+ &pps_max_voltage_attr.attr,
+ &pps_min_voltage_attr.attr,
+ &pps_max_current_attr.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(sink_pps);
+
+static struct device_type sink_pps_type = {
+ .name = "pdo",
+ .release = pdo_release,
+ .groups = sink_pps_groups,
+};
+
+/* -------------------------------------------------------------------------- */
+
+static const char * const supply_name[] = {
+ [PDO_TYPE_FIXED] = "fixed_supply",
+ [PDO_TYPE_BATT] = "battery",
+ [PDO_TYPE_VAR] = "variable_supply",
+};
+
+static const char * const apdo_supply_name[] = {
+ [APDO_TYPE_PPS] = "programmable_supply",
+};
+
+static struct device_type *source_type[] = {
+ [PDO_TYPE_FIXED] = &source_fixed_supply_type,
+ [PDO_TYPE_BATT] = &source_battery_type,
+ [PDO_TYPE_VAR] = &source_variable_supply_type,
+};
+
+static struct device_type *source_apdo_type[] = {
+ [APDO_TYPE_PPS] = &source_pps_type,
+};
+
+static struct device_type *sink_type[] = {
+ [PDO_TYPE_FIXED] = &sink_fixed_supply_type,
+ [PDO_TYPE_BATT] = &sink_battery_type,
+ [PDO_TYPE_VAR] = &sink_variable_supply_type,
+};
+
+static struct device_type *sink_apdo_type[] = {
+ [APDO_TYPE_PPS] = &sink_pps_type,
+};
+
+/* REVISIT: Export when EPR_*_Capabilities need to be supported. */
+static int add_pdo(struct usb_power_delivery_capabilities *cap, u32 pdo, int position)
+{
+ struct device_type *type;
+ const char *name;
+ struct pdo *p;
+ int ret;
+
+ p = kzalloc(sizeof(*p), GFP_KERNEL);
+ if (!p)
+ return -ENOMEM;
+
+ p->pdo = pdo;
+ p->object_position = position;
+
+ if (pdo_type(pdo) == PDO_TYPE_APDO) {
+ /* FIXME: Only PPS supported for now! Skipping others. */
+ if (pdo_apdo_type(pdo) > APDO_TYPE_PPS) {
+ dev_warn(&cap->dev, "Unknown APDO type. PDO 0x%08x\n", pdo);
+ kfree(p);
+ return 0;
+ }
+
+ if (is_source(cap->role))
+ type = source_apdo_type[pdo_apdo_type(pdo)];
+ else
+ type = sink_apdo_type[pdo_apdo_type(pdo)];
+
+ name = apdo_supply_name[pdo_apdo_type(pdo)];
+ } else {
+ if (is_source(cap->role))
+ type = source_type[pdo_type(pdo)];
+ else
+ type = sink_type[pdo_type(pdo)];
+
+ name = supply_name[pdo_type(pdo)];
+ }
+
+ p->dev.parent = &cap->dev;
+ p->dev.type = type;
+ dev_set_name(&p->dev, "%u:%s", position + 1, name);
+
+ ret = device_register(&p->dev);
+ if (ret) {
+ put_device(&p->dev);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int remove_pdo(struct device *dev, void *data)
+{
+ device_unregister(dev);
+ return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+static const char * const cap_name[] = {
+ [TYPEC_SINK] = "sink-capabilities",
+ [TYPEC_SOURCE] = "source-capabilities",
+};
+
+static void pd_capabilities_release(struct device *dev)
+{
+ kfree(to_usb_power_delivery_capabilities(dev));
+}
+
+static struct device_type pd_capabilities_type = {
+ .name = "capabilities",
+ .release = pd_capabilities_release,
+};
+
+/**
+ * usb_power_delivery_register_capabilities - Register a set of capabilities.
+ * @pd: The USB PD instance that the capabilities belong to.
+ * @desc: Description of the Capablities Message.
+ *
+ * This function registers a Capabilities Message described in @desc. The
+ * capabilities will have their own sub-directory under @pd in sysfs.
+ *
+ * The function returns pointer to struct usb_power_delivery_capabilities, or
+ * ERR_PRT(errno).
+ */
+struct usb_power_delivery_capabilities *
+usb_power_delivery_register_capabilities(struct usb_power_delivery *pd,
+ struct usb_power_delivery_capabilities_desc *desc)
+{
+ struct usb_power_delivery_capabilities *cap;
+ int ret;
+ int i;
+
+ cap = kzalloc(sizeof(*cap), GFP_KERNEL);
+ if (!cap)
+ return ERR_PTR(-ENOMEM);
+
+ cap->pd = pd;
+ cap->role = desc->role;
+
+ cap->dev.parent = &pd->dev;
+ cap->dev.type = &pd_capabilities_type;
+ dev_set_name(&cap->dev, "%s", cap_name[cap->role]);
+
+ ret = device_register(&cap->dev);
+ if (ret) {
+ put_device(&cap->dev);
+ return ERR_PTR(ret);
+ }
+
+ for (i = 0; i < PDO_MAX_OBJECTS && desc->pdo[i]; i++) {
+ ret = add_pdo(cap, desc->pdo[i], i);
+ if (ret) {
+ usb_power_delivery_unregister_capabilities(cap);
+ return ERR_PTR(ret);
+ }
+ }
+
+ return cap;
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_register_capabilities);
+
+/**
+ * usb_power_delivery_unregister_capabilities - Unregister a set of capabilities
+ * @cap: The capabilities
+ */
+void usb_power_delivery_unregister_capabilities(struct usb_power_delivery_capabilities *cap)
+{
+ if (!cap)
+ return;
+
+ device_for_each_child(&cap->dev, NULL, remove_pdo);
+ device_unregister(&cap->dev);
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_unregister_capabilities);
+
+/* -------------------------------------------------------------------------- */
+
+static ssize_t revision_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct usb_power_delivery *pd = to_usb_power_delivery(dev);
+
+ return sysfs_emit(buf, "%u.%u\n", (pd->revision >> 8) & 0xff, (pd->revision >> 4) & 0xf);
+}
+static DEVICE_ATTR_RO(revision);
+
+static ssize_t version_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct usb_power_delivery *pd = to_usb_power_delivery(dev);
+
+ return sysfs_emit(buf, "%u.%u\n", (pd->version >> 8) & 0xff, (pd->version >> 4) & 0xf);
+}
+static DEVICE_ATTR_RO(version);
+
+static struct attribute *pd_attrs[] = {
+ &dev_attr_revision.attr,
+ &dev_attr_version.attr,
+ NULL
+};
+
+static umode_t pd_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ struct usb_power_delivery *pd = to_usb_power_delivery(kobj_to_dev(kobj));
+
+ if (attr == &dev_attr_version.attr && !pd->version)
+ return 0;
+
+ return attr->mode;
+}
+
+static const struct attribute_group pd_group = {
+ .is_visible = pd_attr_is_visible,
+ .attrs = pd_attrs,
+};
+__ATTRIBUTE_GROUPS(pd);
+
+static void pd_release(struct device *dev)
+{
+ struct usb_power_delivery *pd = to_usb_power_delivery(dev);
+
+ ida_simple_remove(&pd_ida, pd->id);
+ kfree(pd);
+}
+
+static struct device_type pd_type = {
+ .name = "usb_power_delivery",
+ .release = pd_release,
+ .groups = pd_groups,
+};
+
+struct usb_power_delivery *usb_power_delivery_find(const char *name)
+{
+ struct device *dev;
+
+ dev = class_find_device_by_name(&pd_class, name);
+
+ return dev ? to_usb_power_delivery(dev) : NULL;
+}
+
+/**
+ * usb_power_delivery_register - Register USB Power Delivery Support.
+ * @parent: Parent device.
+ * @desc: Description of the USB PD contract.
+ *
+ * This routine can be used to register USB Power Delivery capabilities that a
+ * device or devices can support. These capabilities represent all the
+ * capabilities that can be negotiated with a partner, so not only the Power
+ * Capabilities that are negotiated using the USB PD Capabilities Message.
+ *
+ * The USB Power Delivery Support object that this routine generates can be used
+ * as the parent object for all the actual USB Power Delivery Messages and
+ * objects that can be negotiated with the partner.
+ *
+ * Returns handle to struct usb_power_delivery or ERR_PTR.
+ */
+struct usb_power_delivery *
+usb_power_delivery_register(struct device *parent, struct usb_power_delivery_desc *desc)
+{
+ struct usb_power_delivery *pd;
+ int ret;
+
+ pd = kzalloc(sizeof(*pd), GFP_KERNEL);
+ if (!pd)
+ return ERR_PTR(-ENOMEM);
+
+ ret = ida_simple_get(&pd_ida, 0, 0, GFP_KERNEL);
+ if (ret < 0) {
+ kfree(pd);
+ return ERR_PTR(ret);
+ }
+
+ pd->id = ret;
+ pd->revision = desc->revision;
+ pd->version = desc->version;
+
+ pd->dev.parent = parent;
+ pd->dev.type = &pd_type;
+ pd->dev.class = &pd_class;
+ dev_set_name(&pd->dev, "pd%d", pd->id);
+
+ ret = device_register(&pd->dev);
+ if (ret) {
+ put_device(&pd->dev);
+ return ERR_PTR(ret);
+ }
+
+ return pd;
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_register);
+
+/**
+ * usb_power_delivery_unregister - Unregister USB Power Delivery Support.
+ * @pd: The USB PD contract.
+ */
+void usb_power_delivery_unregister(struct usb_power_delivery *pd)
+{
+ if (IS_ERR_OR_NULL(pd))
+ return;
+
+ device_unregister(&pd->dev);
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_unregister);
+
+/**
+ * usb_power_delivery_link_device - Link device to its USB PD object.
+ * @pd: The USB PD instance.
+ * @dev: The device.
+ *
+ * This function can be used to create a symlink named "usb_power_delivery" for
+ * @dev that points to @pd.
+ */
+int usb_power_delivery_link_device(struct usb_power_delivery *pd, struct device *dev)
+{
+ int ret;
+
+ if (IS_ERR_OR_NULL(pd) || !dev)
+ return 0;
+
+ ret = sysfs_create_link(&dev->kobj, &pd->dev.kobj, "usb_power_delivery");
+ if (ret)
+ return ret;
+
+ get_device(&pd->dev);
+ get_device(dev);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_link_device);
+
+/**
+ * usb_power_delivery_unlink_device - Unlink device from its USB PD object.
+ * @pd: The USB PD instance.
+ * @dev: The device.
+ *
+ * Remove the symlink that was previously created with pd_link_device().
+ */
+void usb_power_delivery_unlink_device(struct usb_power_delivery *pd, struct device *dev)
+{
+ if (IS_ERR_OR_NULL(pd) || !dev)
+ return;
+
+ sysfs_remove_link(&dev->kobj, "usb_power_delivery");
+ put_device(&pd->dev);
+ put_device(dev);
+}
+EXPORT_SYMBOL_GPL(usb_power_delivery_unlink_device);
+
+/* -------------------------------------------------------------------------- */
+
+int __init usb_power_delivery_init(void)
+{
+ return class_register(&pd_class);
+}
+
+void __exit usb_power_delivery_exit(void)
+{
+ ida_destroy(&pd_ida);
+ class_unregister(&pd_class);
+}
diff --git a/drivers/usb/typec/pd.h b/drivers/usb/typec/pd.h
new file mode 100644
index 000000000..049a1aad4
--- /dev/null
+++ b/drivers/usb/typec/pd.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_POWER_DELIVERY__
+#define __USB_POWER_DELIVERY__
+
+#include <linux/device.h>
+#include <linux/usb/typec.h>
+
+struct usb_power_delivery {
+ struct device dev;
+ int id;
+ u16 revision;
+ u16 version;
+};
+
+struct usb_power_delivery_capabilities {
+ struct device dev;
+ struct usb_power_delivery *pd;
+ enum typec_role role;
+};
+
+#define to_usb_power_delivery_capabilities(o) container_of(o, struct usb_power_delivery_capabilities, dev)
+#define to_usb_power_delivery(o) container_of(o, struct usb_power_delivery, dev)
+
+struct usb_power_delivery *usb_power_delivery_find(const char *name);
+
+int usb_power_delivery_init(void);
+void usb_power_delivery_exit(void);
+
+#endif /* __USB_POWER_DELIVERY__ */
diff --git a/drivers/usb/typec/port-mapper.c b/drivers/usb/typec/port-mapper.c
new file mode 100644
index 000000000..a929e000d
--- /dev/null
+++ b/drivers/usb/typec/port-mapper.c
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Type-C Connector Class Port Mapping Utility
+ *
+ * Copyright (C) 2021, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/acpi.h>
+#include <linux/component.h>
+
+#include "class.h"
+
+static int typec_aggregate_bind(struct device *dev)
+{
+ return component_bind_all(dev, NULL);
+}
+
+static void typec_aggregate_unbind(struct device *dev)
+{
+ component_unbind_all(dev, NULL);
+}
+
+static const struct component_master_ops typec_aggregate_ops = {
+ .bind = typec_aggregate_bind,
+ .unbind = typec_aggregate_unbind,
+};
+
+struct each_port_arg {
+ struct typec_port *port;
+ struct component_match *match;
+};
+
+static int typec_port_compare(struct device *dev, void *fwnode)
+{
+ return device_match_fwnode(dev, fwnode);
+}
+
+static int typec_port_match(struct device *dev, void *data)
+{
+ struct acpi_device *adev = to_acpi_device(dev);
+ struct each_port_arg *arg = data;
+ struct acpi_device *con_adev;
+
+ con_adev = ACPI_COMPANION(&arg->port->dev);
+ if (con_adev == adev)
+ return 0;
+
+ if (con_adev->pld_crc == adev->pld_crc)
+ component_match_add(&arg->port->dev, &arg->match, typec_port_compare,
+ acpi_fwnode_handle(adev));
+ return 0;
+}
+
+int typec_link_ports(struct typec_port *con)
+{
+ struct each_port_arg arg = { .port = con, .match = NULL };
+
+ if (!has_acpi_companion(&con->dev))
+ return 0;
+
+ acpi_bus_for_each_dev(typec_port_match, &arg);
+ if (!arg.match)
+ return 0;
+
+ /*
+ * REVISIT: Now each connector can have only a single component master.
+ * So far only the USB ports connected to the USB Type-C connector share
+ * the _PLD with it, but if there one day is something else (like maybe
+ * the DisplayPort ACPI device object) that also shares the _PLD with
+ * the connector, every one of those needs to have its own component
+ * master, because each different type of component needs to be bind to
+ * the connector independently of the other components. That requires
+ * improvements to the component framework. Right now you can only have
+ * one master per device.
+ */
+ return component_master_add_with_match(&con->dev, &typec_aggregate_ops, arg.match);
+}
+
+void typec_unlink_ports(struct typec_port *con)
+{
+ if (has_acpi_companion(&con->dev))
+ component_master_del(&con->dev, &typec_aggregate_ops);
+}
diff --git a/drivers/usb/typec/qcom-pmic-typec.c b/drivers/usb/typec/qcom-pmic-typec.c
new file mode 100644
index 000000000..432ea62f1
--- /dev/null
+++ b/drivers/usb/typec/qcom-pmic-typec.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2020, The Linux Foundation. All rights reserved.
+ */
+
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec_mux.h>
+
+#define TYPEC_MISC_STATUS 0xb
+#define CC_ATTACHED BIT(0)
+#define CC_ORIENTATION BIT(1)
+#define SNK_SRC_MODE BIT(6)
+#define TYPEC_MODE_CFG 0x44
+#define TYPEC_DISABLE_CMD BIT(0)
+#define EN_SNK_ONLY BIT(1)
+#define EN_SRC_ONLY BIT(2)
+#define TYPEC_VCONN_CONTROL 0x46
+#define VCONN_EN_SRC BIT(0)
+#define VCONN_EN_VAL BIT(1)
+#define TYPEC_EXIT_STATE_CFG 0x50
+#define SEL_SRC_UPPER_REF BIT(2)
+#define TYPEC_INTR_EN_CFG_1 0x5e
+#define TYPEC_INTR_EN_CFG_1_MASK GENMASK(7, 0)
+
+struct qcom_pmic_typec {
+ struct device *dev;
+ struct regmap *regmap;
+ u32 base;
+
+ struct typec_port *port;
+ struct usb_role_switch *role_sw;
+
+ struct regulator *vbus_reg;
+ bool vbus_enabled;
+};
+
+static void qcom_pmic_typec_enable_vbus_regulator(struct qcom_pmic_typec
+ *qcom_usb, bool enable)
+{
+ int ret;
+
+ if (enable == qcom_usb->vbus_enabled)
+ return;
+
+ if (enable) {
+ ret = regulator_enable(qcom_usb->vbus_reg);
+ if (ret)
+ return;
+ } else {
+ ret = regulator_disable(qcom_usb->vbus_reg);
+ if (ret)
+ return;
+ }
+ qcom_usb->vbus_enabled = enable;
+}
+
+static void qcom_pmic_typec_check_connection(struct qcom_pmic_typec *qcom_usb)
+{
+ enum typec_orientation orientation;
+ enum usb_role role;
+ unsigned int stat;
+ bool enable_vbus;
+
+ regmap_read(qcom_usb->regmap, qcom_usb->base + TYPEC_MISC_STATUS,
+ &stat);
+
+ if (stat & CC_ATTACHED) {
+ orientation = (stat & CC_ORIENTATION) ?
+ TYPEC_ORIENTATION_REVERSE :
+ TYPEC_ORIENTATION_NORMAL;
+ typec_set_orientation(qcom_usb->port, orientation);
+
+ role = (stat & SNK_SRC_MODE) ? USB_ROLE_HOST : USB_ROLE_DEVICE;
+ if (role == USB_ROLE_HOST)
+ enable_vbus = true;
+ else
+ enable_vbus = false;
+ } else {
+ role = USB_ROLE_NONE;
+ enable_vbus = false;
+ }
+
+ qcom_pmic_typec_enable_vbus_regulator(qcom_usb, enable_vbus);
+ usb_role_switch_set_role(qcom_usb->role_sw, role);
+}
+
+static irqreturn_t qcom_pmic_typec_interrupt(int irq, void *_qcom_usb)
+{
+ struct qcom_pmic_typec *qcom_usb = _qcom_usb;
+
+ qcom_pmic_typec_check_connection(qcom_usb);
+ return IRQ_HANDLED;
+}
+
+static void qcom_pmic_typec_typec_hw_init(struct qcom_pmic_typec *qcom_usb,
+ enum typec_port_type type)
+{
+ u8 mode = 0;
+
+ regmap_update_bits(qcom_usb->regmap,
+ qcom_usb->base + TYPEC_INTR_EN_CFG_1,
+ TYPEC_INTR_EN_CFG_1_MASK, 0);
+
+ if (type == TYPEC_PORT_SRC)
+ mode = EN_SRC_ONLY;
+ else if (type == TYPEC_PORT_SNK)
+ mode = EN_SNK_ONLY;
+
+ regmap_update_bits(qcom_usb->regmap, qcom_usb->base + TYPEC_MODE_CFG,
+ EN_SNK_ONLY | EN_SRC_ONLY, mode);
+
+ regmap_update_bits(qcom_usb->regmap,
+ qcom_usb->base + TYPEC_VCONN_CONTROL,
+ VCONN_EN_SRC | VCONN_EN_VAL, VCONN_EN_SRC);
+ regmap_update_bits(qcom_usb->regmap,
+ qcom_usb->base + TYPEC_EXIT_STATE_CFG,
+ SEL_SRC_UPPER_REF, SEL_SRC_UPPER_REF);
+}
+
+static int qcom_pmic_typec_probe(struct platform_device *pdev)
+{
+ struct qcom_pmic_typec *qcom_usb;
+ struct device *dev = &pdev->dev;
+ struct fwnode_handle *fwnode;
+ struct typec_capability cap;
+ const char *buf;
+ int ret, irq, role;
+ u32 reg;
+
+ ret = device_property_read_u32(dev, "reg", &reg);
+ if (ret < 0) {
+ dev_err(dev, "missing base address\n");
+ return ret;
+ }
+
+ qcom_usb = devm_kzalloc(dev, sizeof(*qcom_usb), GFP_KERNEL);
+ if (!qcom_usb)
+ return -ENOMEM;
+
+ qcom_usb->dev = dev;
+ qcom_usb->base = reg;
+
+ qcom_usb->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!qcom_usb->regmap) {
+ dev_err(dev, "Failed to get regmap\n");
+ return -EINVAL;
+ }
+
+ qcom_usb->vbus_reg = devm_regulator_get(qcom_usb->dev, "usb_vbus");
+ if (IS_ERR(qcom_usb->vbus_reg))
+ return PTR_ERR(qcom_usb->vbus_reg);
+
+ fwnode = device_get_named_child_node(dev, "connector");
+ if (!fwnode)
+ return -EINVAL;
+
+ ret = fwnode_property_read_string(fwnode, "power-role", &buf);
+ if (!ret) {
+ role = typec_find_port_power_role(buf);
+ if (role < 0)
+ role = TYPEC_PORT_SNK;
+ } else {
+ role = TYPEC_PORT_SNK;
+ }
+ cap.type = role;
+
+ ret = fwnode_property_read_string(fwnode, "data-role", &buf);
+ if (!ret) {
+ role = typec_find_port_data_role(buf);
+ if (role < 0)
+ role = TYPEC_PORT_UFP;
+ } else {
+ role = TYPEC_PORT_UFP;
+ }
+ cap.data = role;
+
+ cap.prefer_role = TYPEC_NO_PREFERRED_ROLE;
+ cap.fwnode = fwnode;
+ qcom_usb->port = typec_register_port(dev, &cap);
+ if (IS_ERR(qcom_usb->port)) {
+ ret = PTR_ERR(qcom_usb->port);
+ dev_err(dev, "Failed to register type c port %d\n", ret);
+ goto err_put_node;
+ }
+ fwnode_handle_put(fwnode);
+
+ qcom_usb->role_sw = fwnode_usb_role_switch_get(dev_fwnode(qcom_usb->dev));
+ if (IS_ERR(qcom_usb->role_sw)) {
+ ret = dev_err_probe(dev, PTR_ERR(qcom_usb->role_sw),
+ "failed to get role switch\n");
+ goto err_typec_port;
+ }
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ goto err_usb_role_sw;
+
+ ret = devm_request_threaded_irq(qcom_usb->dev, irq, NULL,
+ qcom_pmic_typec_interrupt, IRQF_ONESHOT,
+ "qcom-pmic-typec", qcom_usb);
+ if (ret) {
+ dev_err(&pdev->dev, "Could not request IRQ\n");
+ goto err_usb_role_sw;
+ }
+
+ platform_set_drvdata(pdev, qcom_usb);
+ qcom_pmic_typec_typec_hw_init(qcom_usb, cap.type);
+ qcom_pmic_typec_check_connection(qcom_usb);
+
+ return 0;
+
+err_usb_role_sw:
+ usb_role_switch_put(qcom_usb->role_sw);
+err_typec_port:
+ typec_unregister_port(qcom_usb->port);
+err_put_node:
+ fwnode_handle_put(fwnode);
+
+ return ret;
+}
+
+static int qcom_pmic_typec_remove(struct platform_device *pdev)
+{
+ struct qcom_pmic_typec *qcom_usb = platform_get_drvdata(pdev);
+
+ usb_role_switch_set_role(qcom_usb->role_sw, USB_ROLE_NONE);
+ qcom_pmic_typec_enable_vbus_regulator(qcom_usb, 0);
+
+ typec_unregister_port(qcom_usb->port);
+ usb_role_switch_put(qcom_usb->role_sw);
+
+ return 0;
+}
+
+static const struct of_device_id qcom_pmic_typec_table[] = {
+ { .compatible = "qcom,pm8150b-usb-typec" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, qcom_pmic_typec_table);
+
+static struct platform_driver qcom_pmic_typec = {
+ .driver = {
+ .name = "qcom,pmic-typec",
+ .of_match_table = qcom_pmic_typec_table,
+ },
+ .probe = qcom_pmic_typec_probe,
+ .remove = qcom_pmic_typec_remove,
+};
+module_platform_driver(qcom_pmic_typec);
+
+MODULE_DESCRIPTION("QCOM PMIC USB type C driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/usb/typec/retimer.c b/drivers/usb/typec/retimer.c
new file mode 100644
index 000000000..ee94dbbe4
--- /dev/null
+++ b/drivers/usb/typec/retimer.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2022 Google LLC
+ *
+ * USB Type-C Retimer support.
+ * Author: Prashant Malani <pmalani@chromium.org>
+ *
+ */
+
+#include <linux/device.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+
+#include "class.h"
+#include "retimer.h"
+
+static bool dev_name_ends_with(struct device *dev, const char *suffix)
+{
+ const char *name = dev_name(dev);
+ const int name_len = strlen(name);
+ const int suffix_len = strlen(suffix);
+
+ if (suffix_len > name_len)
+ return false;
+
+ return strcmp(name + (name_len - suffix_len), suffix) == 0;
+}
+
+static int retimer_fwnode_match(struct device *dev, const void *fwnode)
+{
+ return device_match_fwnode(dev, fwnode) && dev_name_ends_with(dev, "-retimer");
+}
+
+static void *typec_retimer_match(struct fwnode_handle *fwnode, const char *id, void *data)
+{
+ struct device *dev;
+
+ if (id && !fwnode_property_present(fwnode, id))
+ return NULL;
+
+ dev = class_find_device(&retimer_class, NULL, fwnode,
+ retimer_fwnode_match);
+
+ return dev ? to_typec_retimer(dev) : ERR_PTR(-EPROBE_DEFER);
+}
+
+/**
+ * fwnode_typec_retimer_get - Find USB Type-C retimer.
+ * @fwnode: The caller device node.
+ *
+ * Finds a retimer linked to the caller. This function is primarily meant for the
+ * Type-C drivers. Returns a reference to the retimer on success, NULL if no
+ * matching connection was found, or ERR_PTR(-EPROBE_DEFER) when a connection
+ * was found but the retimer has not been enumerated yet.
+ */
+struct typec_retimer *fwnode_typec_retimer_get(struct fwnode_handle *fwnode)
+{
+ struct typec_retimer *retimer;
+
+ retimer = fwnode_connection_find_match(fwnode, "retimer-switch", NULL, typec_retimer_match);
+ if (!IS_ERR_OR_NULL(retimer))
+ WARN_ON(!try_module_get(retimer->dev.parent->driver->owner));
+
+ return retimer;
+}
+EXPORT_SYMBOL_GPL(fwnode_typec_retimer_get);
+
+/**
+ * typec_retimer_put - Release handle to a retimer.
+ * @retimer: USB Type-C Connector Retimer.
+ *
+ * Decrements reference count for @retimer.
+ */
+void typec_retimer_put(struct typec_retimer *retimer)
+{
+ if (!IS_ERR_OR_NULL(retimer)) {
+ module_put(retimer->dev.parent->driver->owner);
+ put_device(&retimer->dev);
+ }
+}
+EXPORT_SYMBOL_GPL(typec_retimer_put);
+
+int typec_retimer_set(struct typec_retimer *retimer, struct typec_retimer_state *state)
+{
+ if (IS_ERR_OR_NULL(retimer))
+ return 0;
+
+ return retimer->set(retimer, state);
+}
+EXPORT_SYMBOL_GPL(typec_retimer_set);
+
+static void typec_retimer_release(struct device *dev)
+{
+ kfree(to_typec_retimer(dev));
+}
+
+static const struct device_type typec_retimer_dev_type = {
+ .name = "typec_retimer",
+ .release = typec_retimer_release,
+};
+
+/**
+ * typec_retimer_register - Register a retimer device.
+ * @parent: Parent device.
+ * @desc: Retimer description.
+ *
+ * Some USB Type-C connectors have their physical lines routed through retimers before they
+ * reach muxes or host controllers. In some cases (for example: using alternate modes)
+ * these retimers need to be reconfigured appropriately. This function registers retimer
+ * switches which route and potentially modify the signals on the Type C physical lines
+ * enroute to the host controllers.
+ */
+struct typec_retimer *
+typec_retimer_register(struct device *parent, const struct typec_retimer_desc *desc)
+{
+ struct typec_retimer *retimer;
+ int ret;
+
+ if (!desc || !desc->set)
+ return ERR_PTR(-EINVAL);
+
+ retimer = kzalloc(sizeof(*retimer), GFP_KERNEL);
+ if (!retimer)
+ return ERR_PTR(-ENOMEM);
+
+ retimer->set = desc->set;
+
+ device_initialize(&retimer->dev);
+ retimer->dev.parent = parent;
+ retimer->dev.fwnode = desc->fwnode;
+ retimer->dev.class = &retimer_class;
+ retimer->dev.type = &typec_retimer_dev_type;
+ retimer->dev.driver_data = desc->drvdata;
+ dev_set_name(&retimer->dev, "%s-retimer",
+ desc->name ? desc->name : dev_name(parent));
+
+ ret = device_add(&retimer->dev);
+ if (ret) {
+ dev_err(parent, "failed to register retimer (%d)\n", ret);
+ put_device(&retimer->dev);
+ return ERR_PTR(ret);
+ }
+
+ return retimer;
+}
+EXPORT_SYMBOL_GPL(typec_retimer_register);
+
+/**
+ * typec_retimer_unregister - Unregister retimer device.
+ * @retimer: USB Type-C Connector retimer.
+ *
+ * Unregister retimer that was registered with typec_retimer_register().
+ */
+void typec_retimer_unregister(struct typec_retimer *retimer)
+{
+ if (!IS_ERR_OR_NULL(retimer))
+ device_unregister(&retimer->dev);
+}
+EXPORT_SYMBOL_GPL(typec_retimer_unregister);
+
+void *typec_retimer_get_drvdata(struct typec_retimer *retimer)
+{
+ return dev_get_drvdata(&retimer->dev);
+}
+EXPORT_SYMBOL_GPL(typec_retimer_get_drvdata);
+
+struct class retimer_class = {
+ .name = "retimer",
+ .owner = THIS_MODULE,
+};
diff --git a/drivers/usb/typec/retimer.h b/drivers/usb/typec/retimer.h
new file mode 100644
index 000000000..fa15951d4
--- /dev/null
+++ b/drivers/usb/typec/retimer.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_TYPEC_RETIMER__
+#define __USB_TYPEC_RETIMER__
+
+#include <linux/usb/typec_retimer.h>
+
+struct typec_retimer {
+ struct device dev;
+ typec_retimer_set_fn_t set;
+};
+
+#define to_typec_retimer(_dev_) container_of(_dev_, struct typec_retimer, dev)
+
+#endif /* __USB_TYPEC_RETIMER__ */
diff --git a/drivers/usb/typec/rt1719.c b/drivers/usb/typec/rt1719.c
new file mode 100644
index 000000000..ea8b700b0
--- /dev/null
+++ b/drivers/usb/typec/rt1719.c
@@ -0,0 +1,959 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/bitfield.h>
+#include <linux/completion.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/regmap.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec.h>
+
+#define RT1719_REG_TXCTRL1 0x03
+#define RT1719_REG_TXCTRL2 0x04
+#define RT1719_REG_POLICYINFO 0x0E
+#define RT1719_REG_SRCPDO1 0x11
+#define RT1719_REG_MASKS 0x2D
+#define RT1719_REG_EVENTS 0x33
+#define RT1719_REG_STATS 0x37
+#define RT1719_REG_PSELINFO 0x3C
+#define RT1719_REG_USBSETINFO 0x3E
+#define RT1719_REG_VENID 0x82
+
+#define RT1719_UNIQUE_PID 0x1719
+#define RT1719_REQDRSWAP_MASK BIT(7)
+#define RT1719_EVALMODE_MASK BIT(4)
+#define RT1719_REQSRCPDO_MASK GENMASK(2, 0)
+#define RT1719_TXSPDOREQ_MASK BIT(7)
+#define RT1719_INT_DRSW_ACCEPT BIT(23)
+#define RT1719_INT_RX_SRCCAP BIT(21)
+#define RT1719_INT_VBUS_DCT BIT(6)
+#define RT1719_INT_VBUS_PRESENT BIT(5)
+#define RT1719_INT_PE_SNK_RDY BIT(2)
+#define RT1719_CC1_STAT GENMASK(9, 8)
+#define RT1719_CC2_STAT GENMASK(11, 10)
+#define RT1719_POLARITY_MASK BIT(23)
+#define RT1719_DATAROLE_MASK BIT(22)
+#define RT1719_PDSPECREV_MASK GENMASK(21, 20)
+#define RT1719_SPDOSEL_MASK GENMASK(18, 16)
+#define RT1719_SPDONUM_MASK GENMASK(15, 13)
+#define RT1719_ATTACH_VBUS BIT(12)
+#define RT1719_ATTACH_DBG BIT(10)
+#define RT1719_ATTACH_SNK BIT(9)
+#define RT1719_ATTACHDEV_MASK (RT1719_ATTACH_VBUS | RT1719_ATTACH_DBG | \
+ RT1719_ATTACH_SNK)
+#define RT1719_PE_EXP_CONTRACT BIT(2)
+#define RT1719_PSEL_SUPPORT BIT(15)
+#define RT1719_TBLSEL_MASK BIT(6)
+#define RT1719_LATPSEL_MASK GENMASK(5, 0)
+#define RT1719_USBINFO_MASK GENMASK(1, 0)
+#define RT1719_USB_DFPUFP 3
+#define RT1719_MAX_SRCPDO 7
+
+enum {
+ SNK_PWR_OPEN = 0,
+ SNK_PWR_DEF,
+ SNK_PWR_1P5A,
+ SNK_PWR_3A
+};
+
+enum {
+ USBPD_SPECREV_1_0 = 0,
+ USBPD_SPECREV_2_0,
+ USBPD_SPECREV_3_0
+};
+
+enum rt1719_snkcap {
+ RT1719_SNKCAP_5V = 0,
+ RT1719_SNKCAP_9V,
+ RT1719_SNKCAP_12V,
+ RT1719_SNKCAP_15V,
+ RT1719_SNKCAP_20V,
+ RT1719_MAX_SNKCAP
+};
+
+struct rt1719_psel_cap {
+ u8 lomask;
+ u8 himask;
+ u32 milliwatt;
+ u32 milliamp;
+};
+
+struct rt1719_data {
+ struct device *dev;
+ struct regmap *regmap;
+ struct typec_port *port;
+ struct usb_role_switch *role_sw;
+ struct power_supply *psy;
+ struct typec_partner *partner;
+ struct power_supply_desc psy_desc;
+ struct usb_pd_identity partner_ident;
+ struct typec_partner_desc partner_desc;
+ struct completion req_completion;
+ enum power_supply_usb_type usb_type;
+ bool attached;
+ bool pd_capable;
+ bool drswap_support;
+ u32 voltage;
+ u32 req_voltage;
+ u32 max_current;
+ u32 op_current;
+ u32 spdos[RT1719_MAX_SRCPDO];
+ u16 snkcaps[RT1719_MAX_SNKCAP];
+ int spdo_num;
+ int spdo_sel;
+ u32 conn_info;
+ u16 conn_stat;
+};
+
+static const enum power_supply_usb_type rt1719_psy_usb_types[] = {
+ POWER_SUPPLY_USB_TYPE_C,
+ POWER_SUPPLY_USB_TYPE_PD,
+ POWER_SUPPLY_USB_TYPE_PD_PPS
+};
+
+static const enum power_supply_property rt1719_psy_properties[] = {
+ POWER_SUPPLY_PROP_ONLINE,
+ POWER_SUPPLY_PROP_USB_TYPE,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_CURRENT_MAX,
+ POWER_SUPPLY_PROP_CURRENT_NOW
+};
+
+static int rt1719_read16(struct rt1719_data *data, unsigned int reg, u16 *val)
+{
+ __le16 regval;
+ int ret;
+
+ ret = regmap_raw_read(data->regmap, reg, &regval, sizeof(regval));
+ if (ret)
+ return ret;
+
+ *val = le16_to_cpu(regval);
+ return 0;
+}
+
+static int rt1719_read32(struct rt1719_data *data, unsigned int reg, u32 *val)
+{
+ __le32 regval;
+ int ret;
+
+ ret = regmap_raw_read(data->regmap, reg, &regval, sizeof(regval));
+ if (ret)
+ return ret;
+
+ *val = le32_to_cpu(regval);
+ return 0;
+}
+
+static int rt1719_write32(struct rt1719_data *data, unsigned int reg, u32 val)
+{
+ __le32 regval = cpu_to_le32(val);
+
+ return regmap_raw_write(data->regmap, reg, &regval, sizeof(regval));
+}
+
+static enum typec_pwr_opmode rt1719_get_pwr_opmode(u32 conn, u16 stat)
+{
+ u16 cc1, cc2, cc_stat;
+
+ cc1 = FIELD_GET(RT1719_CC1_STAT, stat);
+ cc2 = FIELD_GET(RT1719_CC2_STAT, stat);
+
+ if (conn & RT1719_ATTACH_SNK) {
+ if (conn & RT1719_POLARITY_MASK)
+ cc_stat = cc2;
+ else
+ cc_stat = cc1;
+
+ switch (cc_stat) {
+ case SNK_PWR_3A:
+ return TYPEC_PWR_MODE_3_0A;
+ case SNK_PWR_1P5A:
+ return TYPEC_PWR_MODE_1_5A;
+ }
+ } else if (conn & RT1719_ATTACH_DBG) {
+ if ((cc1 == SNK_PWR_1P5A && cc2 == SNK_PWR_DEF) ||
+ (cc1 == SNK_PWR_DEF && cc2 == SNK_PWR_1P5A))
+ return TYPEC_PWR_MODE_1_5A;
+ else if ((cc1 == SNK_PWR_3A && cc2 == SNK_PWR_DEF) ||
+ (cc1 == SNK_PWR_DEF && cc2 == SNK_PWR_3A))
+ return TYPEC_PWR_MODE_3_0A;
+ }
+
+ return TYPEC_PWR_MODE_USB;
+}
+
+static enum typec_data_role rt1719_get_data_role(u32 conn)
+{
+ if (conn & RT1719_DATAROLE_MASK)
+ return TYPEC_HOST;
+ return TYPEC_DEVICE;
+}
+
+static void rt1719_set_data_role(struct rt1719_data *data,
+ enum typec_data_role data_role,
+ bool attached)
+{
+ enum usb_role usb_role = USB_ROLE_NONE;
+
+ if (attached) {
+ if (data_role == TYPEC_HOST)
+ usb_role = USB_ROLE_HOST;
+ else
+ usb_role = USB_ROLE_DEVICE;
+ }
+
+ usb_role_switch_set_role(data->role_sw, usb_role);
+ typec_set_data_role(data->port, data_role);
+}
+
+static void rt1719_update_data_role(struct rt1719_data *data)
+{
+ if (!data->attached)
+ return;
+
+ rt1719_set_data_role(data, rt1719_get_data_role(data->conn_info), true);
+}
+
+static void rt1719_register_partner(struct rt1719_data *data)
+{
+ u16 spec_rev = 0;
+
+ if (data->pd_capable) {
+ u32 rev;
+
+ rev = FIELD_GET(RT1719_PDSPECREV_MASK, data->conn_info);
+ switch (rev) {
+ case USBPD_SPECREV_3_0:
+ spec_rev = 0x0300;
+ break;
+ case USBPD_SPECREV_2_0:
+ spec_rev = 0x0200;
+ break;
+ default:
+ spec_rev = 0x0100;
+ break;
+ }
+ }
+
+ /* Just to prevent multiple times attach */
+ if (data->partner)
+ typec_unregister_partner(data->partner);
+
+ memset(&data->partner_ident, 0, sizeof(data->partner_ident));
+ data->partner_desc.usb_pd = data->pd_capable;
+ data->partner_desc.pd_revision = spec_rev;
+
+ if (data->conn_info & RT1719_ATTACH_DBG)
+ data->partner_desc.accessory = TYPEC_ACCESSORY_DEBUG;
+ else
+ data->partner_desc.accessory = TYPEC_ACCESSORY_NONE;
+
+ data->partner = typec_register_partner(data->port, &data->partner_desc);
+}
+
+static void rt1719_attach(struct rt1719_data *data)
+{
+ enum typec_pwr_opmode pwr_opmode;
+ enum typec_data_role data_role;
+ u32 volt = 5000, curr = 500;
+
+ if (!(data->conn_info & RT1719_ATTACHDEV_MASK))
+ return;
+
+ pwr_opmode = rt1719_get_pwr_opmode(data->conn_info, data->conn_stat);
+ data_role = rt1719_get_data_role(data->conn_info);
+
+ typec_set_pwr_opmode(data->port, pwr_opmode);
+ rt1719_set_data_role(data, data_role, true);
+
+ if (data->conn_info & RT1719_ATTACH_SNK)
+ rt1719_register_partner(data);
+
+ if (pwr_opmode == TYPEC_PWR_MODE_3_0A)
+ curr = 3000;
+ else if (pwr_opmode == TYPEC_PWR_MODE_1_5A)
+ curr = 1500;
+
+ data->voltage = volt * 1000;
+ data->max_current = data->op_current = curr * 1000;
+ data->attached = true;
+
+ power_supply_changed(data->psy);
+}
+
+static void rt1719_detach(struct rt1719_data *data)
+{
+ if (!data->attached || (data->conn_info & RT1719_ATTACHDEV_MASK))
+ return;
+
+ typec_unregister_partner(data->partner);
+ data->partner = NULL;
+
+ typec_set_pwr_opmode(data->port, TYPEC_PWR_MODE_USB);
+ rt1719_set_data_role(data, TYPEC_DEVICE, false);
+
+ memset32(data->spdos, 0, RT1719_MAX_SRCPDO);
+ data->spdo_num = 0;
+ data->voltage = data->max_current = data->op_current = 0;
+ data->attached = data->pd_capable = false;
+
+ data->usb_type = POWER_SUPPLY_USB_TYPE_C;
+
+ power_supply_changed(data->psy);
+}
+
+static void rt1719_update_operating_status(struct rt1719_data *data)
+{
+ enum power_supply_usb_type usb_type = POWER_SUPPLY_USB_TYPE_PD;
+ u32 voltage, max_current, op_current;
+ int i, snk_sel;
+
+ for (i = 0; i < data->spdo_num; i++) {
+ u32 pdo = data->spdos[i];
+ enum pd_pdo_type type = pdo_type(pdo);
+
+ if (type == PDO_TYPE_APDO) {
+ usb_type = POWER_SUPPLY_USB_TYPE_PD_PPS;
+ break;
+ }
+ }
+
+ data->spdo_sel = FIELD_GET(RT1719_SPDOSEL_MASK, data->conn_info);
+ if (data->spdo_sel <= 0)
+ return;
+
+ data->usb_type = usb_type;
+
+ voltage = pdo_fixed_voltage(data->spdos[data->spdo_sel - 1]);
+ max_current = pdo_max_current(data->spdos[data->spdo_sel - 1]);
+
+ switch (voltage) {
+ case 5000:
+ snk_sel = RT1719_SNKCAP_5V;
+ break;
+ case 9000:
+ snk_sel = RT1719_SNKCAP_9V;
+ break;
+ case 12000:
+ snk_sel = RT1719_SNKCAP_12V;
+ break;
+ case 15000:
+ snk_sel = RT1719_SNKCAP_15V;
+ break;
+ case 20000:
+ snk_sel = RT1719_SNKCAP_20V;
+ break;
+ default:
+ return;
+ }
+
+ op_current = min(max_current, pdo_max_current(data->snkcaps[snk_sel]));
+
+ /* covert mV/mA to uV/uA */
+ data->voltage = voltage * 1000;
+ data->max_current = max_current * 1000;
+ data->op_current = op_current * 1000;
+
+ power_supply_changed(data->psy);
+}
+
+static void rt1719_update_pwr_opmode(struct rt1719_data *data)
+{
+ if (!data->attached)
+ return;
+
+ if (!data->pd_capable) {
+ data->pd_capable = true;
+
+ typec_set_pwr_opmode(data->port, TYPEC_PWR_MODE_PD);
+ rt1719_register_partner(data);
+ }
+
+ rt1719_update_operating_status(data);
+}
+
+static void rt1719_update_source_pdos(struct rt1719_data *data)
+{
+ int spdo_num = FIELD_GET(RT1719_SPDONUM_MASK, data->conn_info);
+ __le32 src_pdos[RT1719_MAX_SRCPDO] = { };
+ int i, ret;
+
+ if (!data->attached)
+ return;
+
+ ret = regmap_raw_read(data->regmap, RT1719_REG_SRCPDO1, src_pdos,
+ sizeof(__le32) * spdo_num);
+ if (ret)
+ return;
+
+ data->spdo_num = spdo_num;
+ for (i = 0; i < spdo_num; i++)
+ data->spdos[i] = le32_to_cpu(src_pdos[i]);
+}
+
+static int rt1719_dr_set(struct typec_port *port, enum typec_data_role role)
+{
+ struct rt1719_data *data = typec_get_drvdata(port);
+ enum typec_data_role cur_role;
+ int ret;
+
+ if (!data->attached || !data->pd_capable || !data->drswap_support)
+ return -EOPNOTSUPP;
+
+ if (data->spdo_num > 0 && !(data->spdos[0] & PDO_FIXED_DATA_SWAP))
+ return -EINVAL;
+
+ cur_role = rt1719_get_data_role(data->conn_info);
+ if (cur_role == role)
+ return 0;
+
+ ret = regmap_update_bits(data->regmap, RT1719_REG_TXCTRL1,
+ RT1719_REQDRSWAP_MASK, RT1719_REQDRSWAP_MASK);
+ if (ret)
+ return ret;
+
+ reinit_completion(&data->req_completion);
+ ret = wait_for_completion_timeout(&data->req_completion,
+ msecs_to_jiffies(400));
+ if (ret == 0)
+ return -ETIMEDOUT;
+
+ cur_role = rt1719_get_data_role(data->conn_info);
+ if (cur_role != role)
+ return -EAGAIN;
+
+ rt1719_set_data_role(data, role, true);
+ return 0;
+}
+
+static const struct typec_operations rt1719_port_ops = {
+ .dr_set = rt1719_dr_set,
+};
+
+static int rt1719_usbpd_request_voltage(struct rt1719_data *data)
+{
+ u32 src_voltage;
+ int snk_sel, src_sel = -1;
+ int i, ret;
+
+ if (!data->attached || !data->pd_capable || data->spdo_sel <= 0)
+ return -EINVAL;
+
+ src_voltage = pdo_fixed_voltage(data->spdos[data->spdo_sel - 1]);
+ if (src_voltage == data->req_voltage)
+ return 0;
+
+ switch (data->req_voltage) {
+ case 5000:
+ snk_sel = RT1719_SNKCAP_5V;
+ break;
+ case 9000:
+ snk_sel = RT1719_SNKCAP_9V;
+ break;
+ case 12000:
+ snk_sel = RT1719_SNKCAP_12V;
+ break;
+ case 15000:
+ snk_sel = RT1719_SNKCAP_15V;
+ break;
+ case 20000:
+ snk_sel = RT1719_SNKCAP_20V;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!(data->snkcaps[snk_sel] & RT1719_PSEL_SUPPORT))
+ return -EINVAL;
+
+ for (i = 0; i < data->spdo_num; i++) {
+ enum pd_pdo_type type = pdo_type(data->spdos[i]);
+
+ if (type != PDO_TYPE_FIXED)
+ continue;
+
+ src_voltage = pdo_fixed_voltage(data->spdos[i]);
+ if (src_voltage == data->req_voltage) {
+ src_sel = i;
+ break;
+ }
+ }
+
+ if (src_sel == -1)
+ return -EOPNOTSUPP;
+
+ ret = regmap_update_bits(data->regmap, RT1719_REG_TXCTRL1,
+ RT1719_EVALMODE_MASK | RT1719_REQSRCPDO_MASK,
+ RT1719_EVALMODE_MASK | (src_sel + 1));
+ ret |= regmap_update_bits(data->regmap, RT1719_REG_TXCTRL2,
+ RT1719_TXSPDOREQ_MASK, RT1719_TXSPDOREQ_MASK);
+ if (ret)
+ return ret;
+
+ reinit_completion(&data->req_completion);
+ ret = wait_for_completion_timeout(&data->req_completion,
+ msecs_to_jiffies(400));
+ if (!ret)
+ return -ETIMEDOUT;
+
+ return 0;
+}
+
+static int rt1719_psy_set_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ const union power_supply_propval *val)
+{
+ struct rt1719_data *data = power_supply_get_drvdata(psy);
+
+ if (psp == POWER_SUPPLY_PROP_VOLTAGE_NOW) {
+ data->req_voltage = val->intval / 1000;
+ return rt1719_usbpd_request_voltage(data);
+ }
+
+ return -EINVAL;
+}
+
+static int rt1719_psy_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct rt1719_data *data = power_supply_get_drvdata(psy);
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_ONLINE:
+ val->intval = data->attached ? 1 : 0;
+ break;
+ case POWER_SUPPLY_PROP_USB_TYPE:
+ val->intval = data->usb_type;
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ val->intval = data->voltage;
+ break;
+ case POWER_SUPPLY_PROP_CURRENT_MAX:
+ val->intval = data->max_current;
+ break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ val->intval = data->op_current;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static int rt1719_psy_property_is_writeable(struct power_supply *psy,
+ enum power_supply_property psp)
+{
+ if (psp == POWER_SUPPLY_PROP_VOLTAGE_NOW)
+ return 1;
+ return 0;
+}
+
+static int devm_rt1719_psy_register(struct rt1719_data *data)
+{
+ struct power_supply_config psy_cfg = { };
+ char *psy_name;
+
+ psy_cfg.fwnode = dev_fwnode(data->dev);
+ psy_cfg.drv_data = data;
+
+ psy_name = devm_kasprintf(data->dev, GFP_KERNEL, "rt1719-source-psy-%s",
+ dev_name(data->dev));
+ if (!psy_name)
+ return -ENOMEM;
+
+ data->psy_desc.name = psy_name;
+ data->psy_desc.type = POWER_SUPPLY_TYPE_USB;
+ data->psy_desc.usb_types = rt1719_psy_usb_types;
+ data->psy_desc.num_usb_types = ARRAY_SIZE(rt1719_psy_usb_types);
+ data->psy_desc.properties = rt1719_psy_properties;
+ data->psy_desc.num_properties = ARRAY_SIZE(rt1719_psy_properties);
+ data->psy_desc.get_property = rt1719_psy_get_property;
+ data->psy_desc.set_property = rt1719_psy_set_property;
+ data->psy_desc.property_is_writeable = rt1719_psy_property_is_writeable;
+
+ data->usb_type = POWER_SUPPLY_USB_TYPE_C;
+
+ data->psy = devm_power_supply_register(data->dev, &data->psy_desc,
+ &psy_cfg);
+
+ return PTR_ERR_OR_ZERO(data->psy);
+}
+
+static irqreturn_t rt1719_irq_handler(int irq, void *priv)
+{
+ struct rt1719_data *data = priv;
+ u32 events, conn_info;
+ u16 conn_stat;
+ int ret;
+
+ ret = rt1719_read32(data, RT1719_REG_EVENTS, &events);
+ ret |= rt1719_read32(data, RT1719_REG_POLICYINFO, &conn_info);
+ ret |= rt1719_read16(data, RT1719_REG_STATS, &conn_stat);
+ if (ret)
+ return IRQ_NONE;
+
+ data->conn_info = conn_info;
+ data->conn_stat = conn_stat;
+
+ events &= (RT1719_INT_DRSW_ACCEPT | RT1719_INT_RX_SRCCAP |
+ RT1719_INT_VBUS_PRESENT | RT1719_INT_VBUS_DCT |
+ RT1719_INT_PE_SNK_RDY);
+
+ if (events & RT1719_INT_DRSW_ACCEPT)
+ rt1719_update_data_role(data);
+
+ if (events & RT1719_INT_VBUS_PRESENT)
+ rt1719_attach(data);
+
+ if (events & RT1719_INT_VBUS_DCT)
+ rt1719_detach(data);
+
+ if (events & RT1719_INT_RX_SRCCAP)
+ rt1719_update_source_pdos(data);
+
+ if (events & RT1719_INT_PE_SNK_RDY) {
+ complete(&data->req_completion);
+ rt1719_update_pwr_opmode(data);
+ }
+
+ /* Write 1 to clear already handled events */
+ rt1719_write32(data, RT1719_REG_EVENTS, events);
+
+ return IRQ_HANDLED;
+}
+
+static int rt1719_irq_init(struct rt1719_data *data)
+{
+ struct i2c_client *i2c = to_i2c_client(data->dev);
+ u32 irq_enable;
+ int ret;
+
+ irq_enable = RT1719_INT_DRSW_ACCEPT | RT1719_INT_RX_SRCCAP |
+ RT1719_INT_VBUS_DCT | RT1719_INT_VBUS_PRESENT |
+ RT1719_INT_PE_SNK_RDY;
+
+ ret = rt1719_write32(data, RT1719_REG_MASKS, irq_enable);
+ if (ret) {
+ dev_err(&i2c->dev, "Failed to config irq enable\n");
+ return ret;
+ }
+
+ return devm_request_threaded_irq(&i2c->dev, i2c->irq, NULL,
+ rt1719_irq_handler, IRQF_ONESHOT,
+ dev_name(&i2c->dev), data);
+}
+
+static int rt1719_init_attach_state(struct rt1719_data *data)
+{
+ u32 conn_info, irq_clear;
+ u16 conn_stat;
+ int ret;
+
+ irq_clear = RT1719_INT_DRSW_ACCEPT | RT1719_INT_RX_SRCCAP |
+ RT1719_INT_VBUS_DCT | RT1719_INT_VBUS_PRESENT |
+ RT1719_INT_PE_SNK_RDY;
+
+ ret = rt1719_read32(data, RT1719_REG_POLICYINFO, &conn_info);
+ ret |= rt1719_read16(data, RT1719_REG_STATS, &conn_stat);
+ ret |= rt1719_write32(data, RT1719_REG_EVENTS, irq_clear);
+ if (ret)
+ return ret;
+
+ data->conn_info = conn_info;
+ data->conn_stat = conn_stat;
+
+ if (conn_info & RT1719_ATTACHDEV_MASK)
+ rt1719_attach(data);
+
+ if (conn_info & RT1719_PE_EXP_CONTRACT) {
+ rt1719_update_source_pdos(data);
+ rt1719_update_pwr_opmode(data);
+ }
+
+ return 0;
+}
+
+#define RT1719_PSEL_CAPINFO(_lomask, _milliwatt, _himask, _milliamp) { \
+ .lomask = _lomask, \
+ .milliwatt = _milliwatt, \
+ .himask = _himask, \
+ .milliamp = _milliamp, \
+}
+
+static const struct rt1719_psel_cap rt1719_psel_caps[] = {
+ RT1719_PSEL_CAPINFO(0x18, 75000, 0x10, 5000),
+ RT1719_PSEL_CAPINFO(0x18, 60000, 0x10, 4500),
+ RT1719_PSEL_CAPINFO(0x18, 45000, 0x10, 4000),
+ RT1719_PSEL_CAPINFO(0x18, 30000, 0x10, 3500),
+ RT1719_PSEL_CAPINFO(0x18, 25000, 0x10, 3000),
+ RT1719_PSEL_CAPINFO(0x18, 20000, 0x10, 2500),
+ RT1719_PSEL_CAPINFO(0x18, 15000, 0x10, 2000),
+ RT1719_PSEL_CAPINFO(0x18, 10000, 0x10, 1000),
+ RT1719_PSEL_CAPINFO(0x1C, 60000, 0x1F, 5000),
+ RT1719_PSEL_CAPINFO(0x1C, 45000, 0x1F, 4500),
+ RT1719_PSEL_CAPINFO(0x1C, 30000, 0x1F, 4000),
+ RT1719_PSEL_CAPINFO(0x1C, 24000, 0x1F, 3500),
+ RT1719_PSEL_CAPINFO(0x1C, 15000, 0x1F, 3000),
+ RT1719_PSEL_CAPINFO(0x1C, 10000, 0x1F, 2500),
+ RT1719_PSEL_CAPINFO(0x0C, 60000, 0x1F, 2000),
+ RT1719_PSEL_CAPINFO(0x0C, 45000, 0x1F, 1000),
+ RT1719_PSEL_CAPINFO(0x0C, 36000, 0x08, 5000),
+ RT1719_PSEL_CAPINFO(0x0C, 30000, 0x08, 4500),
+ RT1719_PSEL_CAPINFO(0x0C, 24000, 0x08, 4000),
+ RT1719_PSEL_CAPINFO(0x0C, 15000, 0x08, 3500),
+ RT1719_PSEL_CAPINFO(0x0C, 10000, 0x08, 3000),
+ RT1719_PSEL_CAPINFO(0x1E, 45000, 0x08, 2500),
+ RT1719_PSEL_CAPINFO(0x1E, 36000, 0x08, 2000),
+ RT1719_PSEL_CAPINFO(0x1E, 27000, 0x08, 1500),
+ RT1719_PSEL_CAPINFO(0x1E, 20000, 0x08, 1000),
+ RT1719_PSEL_CAPINFO(0x1E, 15000, 0x0F, 5000),
+ RT1719_PSEL_CAPINFO(0x1E, 9000, 0x0F, 4500),
+ RT1719_PSEL_CAPINFO(0x0E, 45000, 0x0F, 4000),
+ RT1719_PSEL_CAPINFO(0x0E, 36000, 0x0F, 3500),
+ RT1719_PSEL_CAPINFO(0x0E, 27000, 0x0F, 3000),
+ RT1719_PSEL_CAPINFO(0x0E, 20000, 0x0F, 2500),
+ RT1719_PSEL_CAPINFO(0x0E, 15000, 0x0F, 2000),
+ RT1719_PSEL_CAPINFO(0x0E, 9000, 0x0F, 1500),
+ RT1719_PSEL_CAPINFO(0x06, 45000, 0x0F, 1000),
+ RT1719_PSEL_CAPINFO(0x06, 36000, 0x0F, 500),
+ RT1719_PSEL_CAPINFO(0x06, 27000, 0x04, 5000),
+ RT1719_PSEL_CAPINFO(0x06, 24000, 0x04, 4500),
+ RT1719_PSEL_CAPINFO(0x06, 18000, 0x04, 4000),
+ RT1719_PSEL_CAPINFO(0x06, 12000, 0x04, 3500),
+ RT1719_PSEL_CAPINFO(0x06, 9000, 0x04, 3000),
+ RT1719_PSEL_CAPINFO(0x1F, 25000, 0x04, 2500),
+ RT1719_PSEL_CAPINFO(0x1F, 20000, 0x04, 2000),
+ RT1719_PSEL_CAPINFO(0x1F, 15000, 0x04, 1500),
+ RT1719_PSEL_CAPINFO(0x1F, 10000, 0x04, 1000),
+ RT1719_PSEL_CAPINFO(0x1F, 7500, 0x07, 5000),
+ RT1719_PSEL_CAPINFO(0x0F, 25000, 0x07, 4500),
+ RT1719_PSEL_CAPINFO(0x0F, 20000, 0x07, 4000),
+ RT1719_PSEL_CAPINFO(0x0F, 15000, 0x07, 3500),
+ RT1719_PSEL_CAPINFO(0x0F, 10000, 0x07, 3000),
+ RT1719_PSEL_CAPINFO(0x0F, 7500, 0x07, 2500),
+ RT1719_PSEL_CAPINFO(0x07, 25000, 0x07, 2000),
+ RT1719_PSEL_CAPINFO(0x07, 20000, 0x07, 1500),
+ RT1719_PSEL_CAPINFO(0x07, 15000, 0x07, 1000),
+ RT1719_PSEL_CAPINFO(0x07, 10000, 0x07, 500),
+ RT1719_PSEL_CAPINFO(0x07, 7500, 0x03, 5000),
+ RT1719_PSEL_CAPINFO(0x03, 25000, 0x03, 4500),
+ RT1719_PSEL_CAPINFO(0x03, 20000, 0x03, 4000),
+ RT1719_PSEL_CAPINFO(0x03, 15000, 0x03, 3500),
+ RT1719_PSEL_CAPINFO(0x03, 10000, 0x03, 3000),
+ RT1719_PSEL_CAPINFO(0x03, 7500, 0x03, 2500),
+ RT1719_PSEL_CAPINFO(0x01, 15000, 0x03, 2000),
+ RT1719_PSEL_CAPINFO(0x01, 10000, 0x03, 1500),
+ RT1719_PSEL_CAPINFO(0x01, 7500, 0x03, 1000),
+ RT1719_PSEL_CAPINFO(0x01, 2500, 0x03, 500)
+};
+
+static u16 rt1719_gen_snkcap_by_current(const struct rt1719_psel_cap *psel_cap,
+ enum rt1719_snkcap capsel)
+{
+ u16 cap = RT1719_PSEL_SUPPORT;
+
+ if (!(psel_cap->himask & BIT(capsel)))
+ return 0;
+
+ cap |= psel_cap->milliamp / 10;
+ return cap;
+}
+
+static u16 rt1719_gen_snkcap_by_watt(const struct rt1719_psel_cap *psel_cap,
+ enum rt1719_snkcap capsel)
+{
+ u32 volt_div[RT1719_MAX_SNKCAP] = { 5, 9, 12, 15, 20 };
+ u16 cap = RT1719_PSEL_SUPPORT;
+
+ if (!(psel_cap->lomask & BIT(capsel)))
+ return 0;
+
+ cap |= min(psel_cap->milliwatt / volt_div[capsel], (u32)5000) / 10;
+ return cap;
+}
+
+static u16 rt1719_gen_snkcap(unsigned int pselinfo, enum rt1719_snkcap capsel)
+{
+ int psel = FIELD_GET(RT1719_LATPSEL_MASK, pselinfo);
+ const struct rt1719_psel_cap *psel_cap;
+ bool by_current = false;
+
+ if (pselinfo & RT1719_TBLSEL_MASK)
+ by_current = true;
+
+ psel_cap = rt1719_psel_caps + psel;
+ if (by_current)
+ return rt1719_gen_snkcap_by_current(psel_cap, capsel);
+
+ return rt1719_gen_snkcap_by_watt(psel_cap, capsel);
+}
+
+static int rt1719_get_caps(struct rt1719_data *data)
+{
+ unsigned int pselinfo, usbinfo;
+ int i, ret;
+
+ ret = regmap_read(data->regmap, RT1719_REG_PSELINFO, &pselinfo);
+ ret |= regmap_read(data->regmap, RT1719_REG_USBSETINFO, &usbinfo);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < RT1719_MAX_SNKCAP; i++)
+ data->snkcaps[i] = rt1719_gen_snkcap(pselinfo, i);
+
+ usbinfo = FIELD_GET(RT1719_USBINFO_MASK, usbinfo);
+ if (usbinfo == RT1719_USB_DFPUFP)
+ data->drswap_support = true;
+
+ return 0;
+}
+
+static int rt1719_check_exist(struct rt1719_data *data)
+{
+ u16 pid;
+ int ret;
+
+ ret = rt1719_read16(data, RT1719_REG_VENID, &pid);
+ if (ret)
+ return ret;
+
+ if (pid != RT1719_UNIQUE_PID) {
+ dev_err(data->dev, "Incorrect PID 0x%04x\n", pid);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+static const struct regmap_config rt1719_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0xff,
+};
+
+static int rt1719_probe(struct i2c_client *i2c)
+{
+ struct rt1719_data *data;
+ struct fwnode_handle *fwnode;
+ struct typec_capability typec_cap = { };
+ int ret;
+
+ data = devm_kzalloc(&i2c->dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ data->dev = &i2c->dev;
+ init_completion(&data->req_completion);
+
+ data->regmap = devm_regmap_init_i2c(i2c, &rt1719_regmap_config);
+ if (IS_ERR(data->regmap)) {
+ ret = PTR_ERR(data->regmap);
+ dev_err(&i2c->dev, "Failed to init regmap (%d)\n", ret);
+ return ret;
+ }
+
+ ret = rt1719_check_exist(data);
+ if (ret)
+ return ret;
+
+ ret = rt1719_get_caps(data);
+ if (ret)
+ return ret;
+
+ fwnode = device_get_named_child_node(&i2c->dev, "connector");
+ if (!fwnode)
+ return -ENODEV;
+
+ data->role_sw = fwnode_usb_role_switch_get(fwnode);
+ if (IS_ERR(data->role_sw)) {
+ ret = PTR_ERR(data->role_sw);
+ dev_err(&i2c->dev, "Failed to get usb role switch (%d)\n", ret);
+ goto err_fwnode_put;
+ }
+
+ ret = devm_rt1719_psy_register(data);
+ if (ret) {
+ dev_err(&i2c->dev, "Failed to register psy (%d)\n", ret);
+ goto err_role_put;
+ }
+
+ typec_cap.revision = USB_TYPEC_REV_1_2;
+ typec_cap.pd_revision = 0x300; /* USB-PD spec release 3.0 */
+ typec_cap.type = TYPEC_PORT_SNK;
+ typec_cap.data = TYPEC_PORT_DRD;
+ typec_cap.ops = &rt1719_port_ops;
+ typec_cap.fwnode = fwnode;
+ typec_cap.driver_data = data;
+ typec_cap.accessory[0] = TYPEC_ACCESSORY_DEBUG;
+
+ data->partner_desc.identity = &data->partner_ident;
+
+ data->port = typec_register_port(&i2c->dev, &typec_cap);
+ if (IS_ERR(data->port)) {
+ ret = PTR_ERR(data->port);
+ dev_err(&i2c->dev, "Failed to register typec port (%d)\n", ret);
+ goto err_role_put;
+ }
+
+ ret = rt1719_init_attach_state(data);
+ if (ret) {
+ dev_err(&i2c->dev, "Failed to init attach state (%d)\n", ret);
+ goto err_role_put;
+ }
+
+ ret = rt1719_irq_init(data);
+ if (ret) {
+ dev_err(&i2c->dev, "Failed to init irq\n");
+ goto err_role_put;
+ }
+
+ fwnode_handle_put(fwnode);
+
+ i2c_set_clientdata(i2c, data);
+
+ return 0;
+
+err_role_put:
+ usb_role_switch_put(data->role_sw);
+err_fwnode_put:
+ fwnode_handle_put(fwnode);
+
+ return ret;
+}
+
+static void rt1719_remove(struct i2c_client *i2c)
+{
+ struct rt1719_data *data = i2c_get_clientdata(i2c);
+
+ typec_unregister_port(data->port);
+ usb_role_switch_put(data->role_sw);
+}
+
+static const struct of_device_id __maybe_unused rt1719_device_table[] = {
+ { .compatible = "richtek,rt1719", },
+ { }
+};
+MODULE_DEVICE_TABLE(of, rt1719_device_table);
+
+static struct i2c_driver rt1719_driver = {
+ .driver = {
+ .name = "rt1719",
+ .of_match_table = rt1719_device_table,
+ },
+ .probe_new = rt1719_probe,
+ .remove = rt1719_remove,
+};
+module_i2c_driver(rt1719_driver);
+
+MODULE_AUTHOR("ChiYuan Huang <cy_huang@richtek.com>");
+MODULE_DESCRIPTION("Richtek RT1719 Sink Only USBPD Controller Driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/usb/typec/stusb160x.c b/drivers/usb/typec/stusb160x.c
new file mode 100644
index 000000000..494b37115
--- /dev/null
+++ b/drivers/usb/typec/stusb160x.c
@@ -0,0 +1,880 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * STMicroelectronics STUSB160x Type-C controller family driver
+ *
+ * Copyright (C) 2020, STMicroelectronics
+ * Author(s): Amelie Delaunay <amelie.delaunay@st.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec.h>
+
+#define STUSB160X_ALERT_STATUS 0x0B /* RC */
+#define STUSB160X_ALERT_STATUS_MASK_CTRL 0x0C /* RW */
+#define STUSB160X_CC_CONNECTION_STATUS_TRANS 0x0D /* RC */
+#define STUSB160X_CC_CONNECTION_STATUS 0x0E /* RO */
+#define STUSB160X_MONITORING_STATUS_TRANS 0x0F /* RC */
+#define STUSB160X_MONITORING_STATUS 0x10 /* RO */
+#define STUSB160X_CC_OPERATION_STATUS 0x11 /* RO */
+#define STUSB160X_HW_FAULT_STATUS_TRANS 0x12 /* RC */
+#define STUSB160X_HW_FAULT_STATUS 0x13 /* RO */
+#define STUSB160X_CC_CAPABILITY_CTRL 0x18 /* RW */
+#define STUSB160X_CC_VCONN_SWITCH_CTRL 0x1E /* RW */
+#define STUSB160X_VCONN_MONITORING_CTRL 0x20 /* RW */
+#define STUSB160X_VBUS_MONITORING_RANGE_CTRL 0x22 /* RW */
+#define STUSB160X_RESET_CTRL 0x23 /* RW */
+#define STUSB160X_VBUS_DISCHARGE_TIME_CTRL 0x25 /* RW */
+#define STUSB160X_VBUS_DISCHARGE_STATUS 0x26 /* RO */
+#define STUSB160X_VBUS_ENABLE_STATUS 0x27 /* RO */
+#define STUSB160X_CC_POWER_MODE_CTRL 0x28 /* RW */
+#define STUSB160X_VBUS_MONITORING_CTRL 0x2E /* RW */
+#define STUSB1600_REG_MAX 0x2F /* RO - Reserved */
+
+/* STUSB160X_ALERT_STATUS/STUSB160X_ALERT_STATUS_MASK_CTRL bitfields */
+#define STUSB160X_HW_FAULT BIT(4)
+#define STUSB160X_MONITORING BIT(5)
+#define STUSB160X_CC_CONNECTION BIT(6)
+#define STUSB160X_ALL_ALERTS GENMASK(6, 4)
+
+/* STUSB160X_CC_CONNECTION_STATUS_TRANS bitfields */
+#define STUSB160X_CC_ATTACH_TRANS BIT(0)
+
+/* STUSB160X_CC_CONNECTION_STATUS bitfields */
+#define STUSB160X_CC_ATTACH BIT(0)
+#define STUSB160X_CC_VCONN_SUPPLY BIT(1)
+#define STUSB160X_CC_DATA_ROLE(s) (!!((s) & BIT(2)))
+#define STUSB160X_CC_POWER_ROLE(s) (!!((s) & BIT(3)))
+#define STUSB160X_CC_ATTACHED_MODE GENMASK(7, 5)
+
+/* STUSB160X_MONITORING_STATUS_TRANS bitfields */
+#define STUSB160X_VCONN_PRESENCE_TRANS BIT(0)
+#define STUSB160X_VBUS_PRESENCE_TRANS BIT(1)
+#define STUSB160X_VBUS_VSAFE0V_TRANS BIT(2)
+#define STUSB160X_VBUS_VALID_TRANS BIT(3)
+
+/* STUSB160X_MONITORING_STATUS bitfields */
+#define STUSB160X_VCONN_PRESENCE BIT(0)
+#define STUSB160X_VBUS_PRESENCE BIT(1)
+#define STUSB160X_VBUS_VSAFE0V BIT(2)
+#define STUSB160X_VBUS_VALID BIT(3)
+
+/* STUSB160X_CC_OPERATION_STATUS bitfields */
+#define STUSB160X_TYPEC_FSM_STATE GENMASK(4, 0)
+#define STUSB160X_SINK_POWER_STATE GENMASK(6, 5)
+#define STUSB160X_CC_ATTACHED BIT(7)
+
+/* STUSB160X_HW_FAULT_STATUS_TRANS bitfields */
+#define STUSB160X_VCONN_SW_OVP_FAULT_TRANS BIT(0)
+#define STUSB160X_VCONN_SW_OCP_FAULT_TRANS BIT(1)
+#define STUSB160X_VCONN_SW_RVP_FAULT_TRANS BIT(2)
+#define STUSB160X_VPU_VALID_TRANS BIT(4)
+#define STUSB160X_VPU_OVP_FAULT_TRANS BIT(5)
+#define STUSB160X_THERMAL_FAULT BIT(7)
+
+/* STUSB160X_HW_FAULT_STATUS bitfields */
+#define STUSB160X_VCONN_SW_OVP_FAULT_CC2 BIT(0)
+#define STUSB160X_VCONN_SW_OVP_FAULT_CC1 BIT(1)
+#define STUSB160X_VCONN_SW_OCP_FAULT_CC2 BIT(2)
+#define STUSB160X_VCONN_SW_OCP_FAULT_CC1 BIT(3)
+#define STUSB160X_VCONN_SW_RVP_FAULT_CC2 BIT(4)
+#define STUSB160X_VCONN_SW_RVP_FAULT_CC1 BIT(5)
+#define STUSB160X_VPU_VALID BIT(6)
+#define STUSB160X_VPU_OVP_FAULT BIT(7)
+
+/* STUSB160X_CC_CAPABILITY_CTRL bitfields */
+#define STUSB160X_CC_VCONN_SUPPLY_EN BIT(0)
+#define STUSB160X_CC_VCONN_DISCHARGE_EN BIT(4)
+#define STUSB160X_CC_CURRENT_ADVERTISED GENMASK(7, 6)
+
+/* STUSB160X_VCONN_SWITCH_CTRL bitfields */
+#define STUSB160X_CC_VCONN_SWITCH_ILIM GENMASK(3, 0)
+
+/* STUSB160X_VCONN_MONITORING_CTRL bitfields */
+#define STUSB160X_VCONN_UVLO_THRESHOLD BIT(6)
+#define STUSB160X_VCONN_MONITORING_EN BIT(7)
+
+/* STUSB160X_VBUS_MONITORING_RANGE_CTRL bitfields */
+#define STUSB160X_SHIFT_LOW_VBUS_LIMIT GENMASK(3, 0)
+#define STUSB160X_SHIFT_HIGH_VBUS_LIMIT GENMASK(7, 4)
+
+/* STUSB160X_RESET_CTRL bitfields */
+#define STUSB160X_SW_RESET_EN BIT(0)
+
+/* STUSB160X_VBUS_DISCHARGE_TIME_CTRL bitfields */
+#define STUSBXX02_VBUS_DISCHARGE_TIME_TO_PDO GENMASK(3, 0)
+#define STUSB160X_VBUS_DISCHARGE_TIME_TO_0V GENMASK(7, 4)
+
+/* STUSB160X_VBUS_DISCHARGE_STATUS bitfields */
+#define STUSB160X_VBUS_DISCHARGE_EN BIT(7)
+
+/* STUSB160X_VBUS_ENABLE_STATUS bitfields */
+#define STUSB160X_VBUS_SOURCE_EN BIT(0)
+#define STUSB160X_VBUS_SINK_EN BIT(1)
+
+/* STUSB160X_CC_POWER_MODE_CTRL bitfields */
+#define STUSB160X_CC_POWER_MODE GENMASK(2, 0)
+
+/* STUSB160X_VBUS_MONITORING_CTRL bitfields */
+#define STUSB160X_VDD_UVLO_DISABLE BIT(0)
+#define STUSB160X_VBUS_VSAFE0V_THRESHOLD GENMASK(2, 1)
+#define STUSB160X_VBUS_RANGE_DISABLE BIT(4)
+#define STUSB160X_VDD_OVLO_DISABLE BIT(6)
+
+enum stusb160x_pwr_mode {
+ SOURCE_WITH_ACCESSORY,
+ SINK_WITH_ACCESSORY,
+ SINK_WITHOUT_ACCESSORY,
+ DUAL_WITH_ACCESSORY,
+ DUAL_WITH_ACCESSORY_AND_TRY_SRC,
+ DUAL_WITH_ACCESSORY_AND_TRY_SNK,
+};
+
+enum stusb160x_attached_mode {
+ NO_DEVICE_ATTACHED,
+ SINK_ATTACHED,
+ SOURCE_ATTACHED,
+ DEBUG_ACCESSORY_ATTACHED,
+ AUDIO_ACCESSORY_ATTACHED,
+};
+
+struct stusb160x {
+ struct device *dev;
+ struct regmap *regmap;
+ struct regulator *vdd_supply;
+ struct regulator *vsys_supply;
+ struct regulator *vconn_supply;
+ struct regulator *main_supply;
+
+ struct typec_port *port;
+ struct typec_capability capability;
+ struct typec_partner *partner;
+
+ enum typec_port_type port_type;
+ enum typec_pwr_opmode pwr_opmode;
+ bool vbus_on;
+
+ struct usb_role_switch *role_sw;
+};
+
+static bool stusb160x_reg_writeable(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case STUSB160X_ALERT_STATUS_MASK_CTRL:
+ case STUSB160X_CC_CAPABILITY_CTRL:
+ case STUSB160X_CC_VCONN_SWITCH_CTRL:
+ case STUSB160X_VCONN_MONITORING_CTRL:
+ case STUSB160X_VBUS_MONITORING_RANGE_CTRL:
+ case STUSB160X_RESET_CTRL:
+ case STUSB160X_VBUS_DISCHARGE_TIME_CTRL:
+ case STUSB160X_CC_POWER_MODE_CTRL:
+ case STUSB160X_VBUS_MONITORING_CTRL:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool stusb160x_reg_readable(struct device *dev, unsigned int reg)
+{
+ if (reg <= 0x0A ||
+ (reg >= 0x14 && reg <= 0x17) ||
+ (reg >= 0x19 && reg <= 0x1D) ||
+ (reg >= 0x29 && reg <= 0x2D) ||
+ (reg == 0x1F || reg == 0x21 || reg == 0x24 || reg == 0x2F))
+ return false;
+ else
+ return true;
+}
+
+static bool stusb160x_reg_volatile(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case STUSB160X_ALERT_STATUS:
+ case STUSB160X_CC_CONNECTION_STATUS_TRANS:
+ case STUSB160X_CC_CONNECTION_STATUS:
+ case STUSB160X_MONITORING_STATUS_TRANS:
+ case STUSB160X_MONITORING_STATUS:
+ case STUSB160X_CC_OPERATION_STATUS:
+ case STUSB160X_HW_FAULT_STATUS_TRANS:
+ case STUSB160X_HW_FAULT_STATUS:
+ case STUSB160X_VBUS_DISCHARGE_STATUS:
+ case STUSB160X_VBUS_ENABLE_STATUS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool stusb160x_reg_precious(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case STUSB160X_ALERT_STATUS:
+ case STUSB160X_CC_CONNECTION_STATUS_TRANS:
+ case STUSB160X_MONITORING_STATUS_TRANS:
+ case STUSB160X_HW_FAULT_STATUS_TRANS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config stusb1600_regmap_config = {
+ .reg_bits = 8,
+ .reg_stride = 1,
+ .val_bits = 8,
+ .max_register = STUSB1600_REG_MAX,
+ .writeable_reg = stusb160x_reg_writeable,
+ .readable_reg = stusb160x_reg_readable,
+ .volatile_reg = stusb160x_reg_volatile,
+ .precious_reg = stusb160x_reg_precious,
+ .cache_type = REGCACHE_RBTREE,
+};
+
+static bool stusb160x_get_vconn(struct stusb160x *chip)
+{
+ u32 val;
+ int ret;
+
+ ret = regmap_read(chip->regmap, STUSB160X_CC_CAPABILITY_CTRL, &val);
+ if (ret) {
+ dev_err(chip->dev, "Unable to get Vconn status: %d\n", ret);
+ return false;
+ }
+
+ return !!FIELD_GET(STUSB160X_CC_VCONN_SUPPLY_EN, val);
+}
+
+static int stusb160x_set_vconn(struct stusb160x *chip, bool on)
+{
+ int ret;
+
+ /* Manage VCONN input supply */
+ if (chip->vconn_supply) {
+ if (on) {
+ ret = regulator_enable(chip->vconn_supply);
+ if (ret) {
+ dev_err(chip->dev,
+ "failed to enable vconn supply: %d\n",
+ ret);
+ return ret;
+ }
+ } else {
+ regulator_disable(chip->vconn_supply);
+ }
+ }
+
+ /* Manage VCONN monitoring and power path */
+ ret = regmap_update_bits(chip->regmap, STUSB160X_VCONN_MONITORING_CTRL,
+ STUSB160X_VCONN_MONITORING_EN,
+ on ? STUSB160X_VCONN_MONITORING_EN : 0);
+ if (ret)
+ goto vconn_reg_disable;
+
+ return 0;
+
+vconn_reg_disable:
+ if (chip->vconn_supply && on)
+ regulator_disable(chip->vconn_supply);
+
+ return ret;
+}
+
+static enum typec_pwr_opmode stusb160x_get_pwr_opmode(struct stusb160x *chip)
+{
+ u32 val;
+ int ret;
+
+ ret = regmap_read(chip->regmap, STUSB160X_CC_CAPABILITY_CTRL, &val);
+ if (ret) {
+ dev_err(chip->dev, "Unable to get pwr opmode: %d\n", ret);
+ return TYPEC_PWR_MODE_USB;
+ }
+
+ return FIELD_GET(STUSB160X_CC_CURRENT_ADVERTISED, val);
+}
+
+static enum typec_accessory stusb160x_get_accessory(u32 status)
+{
+ enum stusb160x_attached_mode mode;
+
+ mode = FIELD_GET(STUSB160X_CC_ATTACHED_MODE, status);
+
+ switch (mode) {
+ case DEBUG_ACCESSORY_ATTACHED:
+ return TYPEC_ACCESSORY_DEBUG;
+ case AUDIO_ACCESSORY_ATTACHED:
+ return TYPEC_ACCESSORY_AUDIO;
+ default:
+ return TYPEC_ACCESSORY_NONE;
+ }
+}
+
+static enum typec_role stusb160x_get_vconn_role(u32 status)
+{
+ if (FIELD_GET(STUSB160X_CC_VCONN_SUPPLY, status))
+ return TYPEC_SOURCE;
+
+ return TYPEC_SINK;
+}
+
+static void stusb160x_set_data_role(struct stusb160x *chip,
+ enum typec_data_role data_role,
+ bool attached)
+{
+ enum usb_role usb_role = USB_ROLE_NONE;
+
+ if (attached) {
+ if (data_role == TYPEC_HOST)
+ usb_role = USB_ROLE_HOST;
+ else
+ usb_role = USB_ROLE_DEVICE;
+ }
+
+ usb_role_switch_set_role(chip->role_sw, usb_role);
+ typec_set_data_role(chip->port, data_role);
+}
+
+static int stusb160x_attach(struct stusb160x *chip, u32 status)
+{
+ struct typec_partner_desc desc;
+ int ret;
+
+ if ((STUSB160X_CC_POWER_ROLE(status) == TYPEC_SOURCE) &&
+ chip->vdd_supply) {
+ ret = regulator_enable(chip->vdd_supply);
+ if (ret) {
+ dev_err(chip->dev,
+ "Failed to enable Vbus supply: %d\n", ret);
+ return ret;
+ }
+ chip->vbus_on = true;
+ }
+
+ desc.usb_pd = false;
+ desc.accessory = stusb160x_get_accessory(status);
+ desc.identity = NULL;
+
+ chip->partner = typec_register_partner(chip->port, &desc);
+ if (IS_ERR(chip->partner)) {
+ ret = PTR_ERR(chip->partner);
+ goto vbus_disable;
+ }
+
+ typec_set_pwr_role(chip->port, STUSB160X_CC_POWER_ROLE(status));
+ typec_set_pwr_opmode(chip->port, stusb160x_get_pwr_opmode(chip));
+ typec_set_vconn_role(chip->port, stusb160x_get_vconn_role(status));
+ stusb160x_set_data_role(chip, STUSB160X_CC_DATA_ROLE(status), true);
+
+ return 0;
+
+vbus_disable:
+ if (chip->vbus_on) {
+ regulator_disable(chip->vdd_supply);
+ chip->vbus_on = false;
+ }
+
+ return ret;
+}
+
+static void stusb160x_detach(struct stusb160x *chip, u32 status)
+{
+ typec_unregister_partner(chip->partner);
+ chip->partner = NULL;
+
+ typec_set_pwr_role(chip->port, STUSB160X_CC_POWER_ROLE(status));
+ typec_set_pwr_opmode(chip->port, TYPEC_PWR_MODE_USB);
+ typec_set_vconn_role(chip->port, stusb160x_get_vconn_role(status));
+ stusb160x_set_data_role(chip, STUSB160X_CC_DATA_ROLE(status), false);
+
+ if (chip->vbus_on) {
+ regulator_disable(chip->vdd_supply);
+ chip->vbus_on = false;
+ }
+}
+
+static irqreturn_t stusb160x_irq_handler(int irq, void *data)
+{
+ struct stusb160x *chip = data;
+ u32 pending, trans, status;
+ int ret;
+
+ ret = regmap_read(chip->regmap, STUSB160X_ALERT_STATUS, &pending);
+ if (ret)
+ goto err;
+
+ if (pending & STUSB160X_CC_CONNECTION) {
+ ret = regmap_read(chip->regmap,
+ STUSB160X_CC_CONNECTION_STATUS_TRANS, &trans);
+ if (ret)
+ goto err;
+ ret = regmap_read(chip->regmap,
+ STUSB160X_CC_CONNECTION_STATUS, &status);
+ if (ret)
+ goto err;
+
+ if (trans & STUSB160X_CC_ATTACH_TRANS) {
+ if (status & STUSB160X_CC_ATTACH) {
+ ret = stusb160x_attach(chip, status);
+ if (ret)
+ goto err;
+ } else {
+ stusb160x_detach(chip, status);
+ }
+ }
+ }
+err:
+ return IRQ_HANDLED;
+}
+
+static int stusb160x_irq_init(struct stusb160x *chip, int irq)
+{
+ u32 status;
+ int ret;
+
+ ret = regmap_read(chip->regmap,
+ STUSB160X_CC_CONNECTION_STATUS, &status);
+ if (ret)
+ return ret;
+
+ if (status & STUSB160X_CC_ATTACH) {
+ ret = stusb160x_attach(chip, status);
+ if (ret)
+ dev_err(chip->dev, "attach failed: %d\n", ret);
+ }
+
+ ret = devm_request_threaded_irq(chip->dev, irq, NULL,
+ stusb160x_irq_handler, IRQF_ONESHOT,
+ dev_name(chip->dev), chip);
+ if (ret)
+ goto partner_unregister;
+
+ /* Unmask CC_CONNECTION events */
+ ret = regmap_write_bits(chip->regmap, STUSB160X_ALERT_STATUS_MASK_CTRL,
+ STUSB160X_CC_CONNECTION, 0);
+ if (ret)
+ goto partner_unregister;
+
+ return 0;
+
+partner_unregister:
+ if (chip->partner) {
+ typec_unregister_partner(chip->partner);
+ chip->partner = NULL;
+ }
+
+ return ret;
+}
+
+static int stusb160x_chip_init(struct stusb160x *chip)
+{
+ u32 val;
+ int ret;
+
+ /* Change the default Type-C power mode */
+ if (chip->port_type == TYPEC_PORT_SRC)
+ ret = regmap_update_bits(chip->regmap,
+ STUSB160X_CC_POWER_MODE_CTRL,
+ STUSB160X_CC_POWER_MODE,
+ SOURCE_WITH_ACCESSORY);
+ else if (chip->port_type == TYPEC_PORT_SNK)
+ ret = regmap_update_bits(chip->regmap,
+ STUSB160X_CC_POWER_MODE_CTRL,
+ STUSB160X_CC_POWER_MODE,
+ SINK_WITH_ACCESSORY);
+ else /* (chip->port_type == TYPEC_PORT_DRP) */
+ ret = regmap_update_bits(chip->regmap,
+ STUSB160X_CC_POWER_MODE_CTRL,
+ STUSB160X_CC_POWER_MODE,
+ DUAL_WITH_ACCESSORY);
+ if (ret)
+ return ret;
+
+ if (chip->port_type == TYPEC_PORT_SNK)
+ goto skip_src;
+
+ /* Change the default Type-C Source power operation mode capability */
+ ret = regmap_update_bits(chip->regmap, STUSB160X_CC_CAPABILITY_CTRL,
+ STUSB160X_CC_CURRENT_ADVERTISED,
+ FIELD_PREP(STUSB160X_CC_CURRENT_ADVERTISED,
+ chip->pwr_opmode));
+ if (ret)
+ return ret;
+
+ /* Manage Type-C Source Vconn supply */
+ if (stusb160x_get_vconn(chip)) {
+ ret = stusb160x_set_vconn(chip, true);
+ if (ret)
+ return ret;
+ }
+
+skip_src:
+ /* Mask all events interrupts - to be unmasked with interrupt support */
+ ret = regmap_update_bits(chip->regmap, STUSB160X_ALERT_STATUS_MASK_CTRL,
+ STUSB160X_ALL_ALERTS, STUSB160X_ALL_ALERTS);
+ if (ret)
+ return ret;
+
+ /* Read status at least once to clear any stale interrupts */
+ regmap_read(chip->regmap, STUSB160X_ALERT_STATUS, &val);
+ regmap_read(chip->regmap, STUSB160X_CC_CONNECTION_STATUS_TRANS, &val);
+ regmap_read(chip->regmap, STUSB160X_MONITORING_STATUS_TRANS, &val);
+ regmap_read(chip->regmap, STUSB160X_HW_FAULT_STATUS_TRANS, &val);
+
+ return 0;
+}
+
+static int stusb160x_get_fw_caps(struct stusb160x *chip,
+ struct fwnode_handle *fwnode)
+{
+ const char *cap_str;
+ int ret;
+
+ chip->capability.fwnode = fwnode;
+
+ /*
+ * Supported port type can be configured through device tree
+ * else it is read from chip registers in stusb160x_get_caps.
+ */
+ ret = fwnode_property_read_string(fwnode, "power-role", &cap_str);
+ if (!ret) {
+ ret = typec_find_port_power_role(cap_str);
+ if (ret < 0)
+ return ret;
+ chip->port_type = ret;
+ }
+ chip->capability.type = chip->port_type;
+
+ /* Skip DRP/Source capabilities in case of Sink only */
+ if (chip->port_type == TYPEC_PORT_SNK)
+ return 0;
+
+ if (chip->port_type == TYPEC_PORT_DRP)
+ chip->capability.prefer_role = TYPEC_SINK;
+
+ /*
+ * Supported power operation mode can be configured through device tree
+ * else it is read from chip registers in stusb160x_get_caps.
+ */
+ ret = fwnode_property_read_string(fwnode, "typec-power-opmode", &cap_str);
+ if (!ret) {
+ ret = typec_find_pwr_opmode(cap_str);
+ /* Power delivery not yet supported */
+ if (ret < 0 || ret == TYPEC_PWR_MODE_PD) {
+ dev_err(chip->dev, "bad power operation mode: %d\n", ret);
+ return -EINVAL;
+ }
+ chip->pwr_opmode = ret;
+ }
+
+ return 0;
+}
+
+static int stusb160x_get_caps(struct stusb160x *chip)
+{
+ enum typec_port_type *type = &chip->capability.type;
+ enum typec_port_data *data = &chip->capability.data;
+ enum typec_accessory *accessory = chip->capability.accessory;
+ u32 val;
+ int ret;
+
+ chip->capability.revision = USB_TYPEC_REV_1_2;
+
+ ret = regmap_read(chip->regmap, STUSB160X_CC_POWER_MODE_CTRL, &val);
+ if (ret)
+ return ret;
+
+ switch (FIELD_GET(STUSB160X_CC_POWER_MODE, val)) {
+ case SOURCE_WITH_ACCESSORY:
+ *type = TYPEC_PORT_SRC;
+ *data = TYPEC_PORT_DFP;
+ *accessory++ = TYPEC_ACCESSORY_AUDIO;
+ *accessory++ = TYPEC_ACCESSORY_DEBUG;
+ break;
+ case SINK_WITH_ACCESSORY:
+ *type = TYPEC_PORT_SNK;
+ *data = TYPEC_PORT_UFP;
+ *accessory++ = TYPEC_ACCESSORY_AUDIO;
+ *accessory++ = TYPEC_ACCESSORY_DEBUG;
+ break;
+ case SINK_WITHOUT_ACCESSORY:
+ *type = TYPEC_PORT_SNK;
+ *data = TYPEC_PORT_UFP;
+ break;
+ case DUAL_WITH_ACCESSORY:
+ case DUAL_WITH_ACCESSORY_AND_TRY_SRC:
+ case DUAL_WITH_ACCESSORY_AND_TRY_SNK:
+ *type = TYPEC_PORT_DRP;
+ *data = TYPEC_PORT_DRD;
+ *accessory++ = TYPEC_ACCESSORY_AUDIO;
+ *accessory++ = TYPEC_ACCESSORY_DEBUG;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ chip->port_type = *type;
+ chip->pwr_opmode = stusb160x_get_pwr_opmode(chip);
+
+ return 0;
+}
+
+static const struct of_device_id stusb160x_of_match[] = {
+ { .compatible = "st,stusb1600", .data = &stusb1600_regmap_config},
+ {},
+};
+MODULE_DEVICE_TABLE(of, stusb160x_of_match);
+
+static int stusb160x_probe(struct i2c_client *client)
+{
+ struct stusb160x *chip;
+ const struct of_device_id *match;
+ struct regmap_config *regmap_config;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ chip = devm_kzalloc(&client->dev, sizeof(struct stusb160x), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, chip);
+
+ match = i2c_of_match_device(stusb160x_of_match, client);
+ regmap_config = (struct regmap_config *)match->data;
+ chip->regmap = devm_regmap_init_i2c(client, regmap_config);
+ if (IS_ERR(chip->regmap)) {
+ ret = PTR_ERR(chip->regmap);
+ dev_err(&client->dev,
+ "Failed to allocate register map:%d\n", ret);
+ return ret;
+ }
+
+ chip->dev = &client->dev;
+
+ chip->vsys_supply = devm_regulator_get_optional(chip->dev, "vsys");
+ if (IS_ERR(chip->vsys_supply)) {
+ ret = PTR_ERR(chip->vsys_supply);
+ if (ret != -ENODEV)
+ return ret;
+ chip->vsys_supply = NULL;
+ }
+
+ chip->vdd_supply = devm_regulator_get_optional(chip->dev, "vdd");
+ if (IS_ERR(chip->vdd_supply)) {
+ ret = PTR_ERR(chip->vdd_supply);
+ if (ret != -ENODEV)
+ return ret;
+ chip->vdd_supply = NULL;
+ }
+
+ chip->vconn_supply = devm_regulator_get_optional(chip->dev, "vconn");
+ if (IS_ERR(chip->vconn_supply)) {
+ ret = PTR_ERR(chip->vconn_supply);
+ if (ret != -ENODEV)
+ return ret;
+ chip->vconn_supply = NULL;
+ }
+
+ fwnode = device_get_named_child_node(chip->dev, "connector");
+ if (!fwnode)
+ return -ENODEV;
+
+ /*
+ * 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);
+
+ /*
+ * When both VDD and VSYS power supplies are present, the low power
+ * supply VSYS is selected when VSYS voltage is above 3.1 V.
+ * Otherwise VDD is selected.
+ */
+ if (chip->vdd_supply &&
+ (!chip->vsys_supply ||
+ (regulator_get_voltage(chip->vsys_supply) <= 3100000)))
+ chip->main_supply = chip->vdd_supply;
+ else
+ chip->main_supply = chip->vsys_supply;
+
+ if (chip->main_supply) {
+ ret = regulator_enable(chip->main_supply);
+ if (ret) {
+ dev_err(chip->dev,
+ "Failed to enable main supply: %d\n", ret);
+ goto fwnode_put;
+ }
+ }
+
+ /* Get configuration from chip */
+ ret = stusb160x_get_caps(chip);
+ if (ret) {
+ dev_err(chip->dev, "Failed to get port caps: %d\n", ret);
+ goto main_reg_disable;
+ }
+
+ /* Get optional re-configuration from device tree */
+ ret = stusb160x_get_fw_caps(chip, fwnode);
+ if (ret) {
+ dev_err(chip->dev, "Failed to get connector caps: %d\n", ret);
+ goto main_reg_disable;
+ }
+
+ ret = stusb160x_chip_init(chip);
+ if (ret) {
+ dev_err(chip->dev, "Failed to init port: %d\n", ret);
+ goto main_reg_disable;
+ }
+
+ chip->port = typec_register_port(chip->dev, &chip->capability);
+ if (IS_ERR(chip->port)) {
+ ret = PTR_ERR(chip->port);
+ goto all_reg_disable;
+ }
+
+ /*
+ * Default power operation mode initialization: will be updated upon
+ * attach/detach interrupt
+ */
+ typec_set_pwr_opmode(chip->port, chip->pwr_opmode);
+
+ if (client->irq) {
+ chip->role_sw = fwnode_usb_role_switch_get(fwnode);
+ if (IS_ERR(chip->role_sw)) {
+ ret = dev_err_probe(chip->dev, PTR_ERR(chip->role_sw),
+ "Failed to get usb role switch\n");
+ goto port_unregister;
+ }
+
+ ret = stusb160x_irq_init(chip, client->irq);
+ if (ret)
+ goto role_sw_put;
+ } else {
+ /*
+ * If Source or Dual power role, need to enable VDD supply
+ * providing Vbus if present. In case of interrupt support,
+ * VDD supply will be dynamically managed upon attach/detach
+ * interrupt.
+ */
+ if (chip->port_type != TYPEC_PORT_SNK && chip->vdd_supply) {
+ ret = regulator_enable(chip->vdd_supply);
+ if (ret) {
+ dev_err(chip->dev,
+ "Failed to enable VDD supply: %d\n",
+ ret);
+ goto port_unregister;
+ }
+ chip->vbus_on = true;
+ }
+ }
+
+ fwnode_handle_put(fwnode);
+
+ return 0;
+
+role_sw_put:
+ if (chip->role_sw)
+ usb_role_switch_put(chip->role_sw);
+port_unregister:
+ typec_unregister_port(chip->port);
+all_reg_disable:
+ if (stusb160x_get_vconn(chip))
+ stusb160x_set_vconn(chip, false);
+main_reg_disable:
+ if (chip->main_supply)
+ regulator_disable(chip->main_supply);
+fwnode_put:
+ fwnode_handle_put(fwnode);
+
+ return ret;
+}
+
+static void stusb160x_remove(struct i2c_client *client)
+{
+ struct stusb160x *chip = i2c_get_clientdata(client);
+
+ if (chip->partner) {
+ typec_unregister_partner(chip->partner);
+ chip->partner = NULL;
+ }
+
+ if (chip->vbus_on)
+ regulator_disable(chip->vdd_supply);
+
+ if (chip->role_sw)
+ usb_role_switch_put(chip->role_sw);
+
+ typec_unregister_port(chip->port);
+
+ if (stusb160x_get_vconn(chip))
+ stusb160x_set_vconn(chip, false);
+
+ if (chip->main_supply)
+ regulator_disable(chip->main_supply);
+}
+
+static int __maybe_unused stusb160x_suspend(struct device *dev)
+{
+ struct stusb160x *chip = dev_get_drvdata(dev);
+
+ /* Mask interrupts */
+ return regmap_update_bits(chip->regmap,
+ STUSB160X_ALERT_STATUS_MASK_CTRL,
+ STUSB160X_ALL_ALERTS, STUSB160X_ALL_ALERTS);
+}
+
+static int __maybe_unused stusb160x_resume(struct device *dev)
+{
+ struct stusb160x *chip = dev_get_drvdata(dev);
+ u32 status;
+ int ret;
+
+ ret = regcache_sync(chip->regmap);
+ if (ret)
+ return ret;
+
+ /* Check if attach/detach occurred during low power */
+ ret = regmap_read(chip->regmap,
+ STUSB160X_CC_CONNECTION_STATUS, &status);
+ if (ret)
+ return ret;
+
+ if (chip->partner && !(status & STUSB160X_CC_ATTACH))
+ stusb160x_detach(chip, status);
+
+ if (!chip->partner && (status & STUSB160X_CC_ATTACH)) {
+ ret = stusb160x_attach(chip, status);
+ if (ret)
+ dev_err(chip->dev, "attach failed: %d\n", ret);
+ }
+
+ /* Unmask interrupts */
+ return regmap_write_bits(chip->regmap, STUSB160X_ALERT_STATUS_MASK_CTRL,
+ STUSB160X_CC_CONNECTION, 0);
+}
+
+static SIMPLE_DEV_PM_OPS(stusb160x_pm_ops, stusb160x_suspend, stusb160x_resume);
+
+static struct i2c_driver stusb160x_driver = {
+ .driver = {
+ .name = "stusb160x",
+ .pm = &stusb160x_pm_ops,
+ .of_match_table = stusb160x_of_match,
+ },
+ .probe_new = stusb160x_probe,
+ .remove = stusb160x_remove,
+};
+module_i2c_driver(stusb160x_driver);
+
+MODULE_AUTHOR("Amelie Delaunay <amelie.delaunay@st.com>");
+MODULE_DESCRIPTION("STMicroelectronics STUSB160x Type-C controller driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/usb/typec/tcpm/Kconfig b/drivers/usb/typec/tcpm/Kconfig
new file mode 100644
index 000000000..e6b88ca4a
--- /dev/null
+++ b/drivers/usb/typec/tcpm/Kconfig
@@ -0,0 +1,79 @@
+# 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
+
+endif # TYPEC_TCPM
diff --git a/drivers/usb/typec/tcpm/Makefile b/drivers/usb/typec/tcpm/Makefile
new file mode 100644
index 000000000..906d9dced
--- /dev/null
+++ b/drivers/usb/typec/tcpm/Makefile
@@ -0,0 +1,10 @@
+# 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
diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c
new file mode 100644
index 000000000..721b2a548
--- /dev/null
+++ b/drivers/usb/typec/tcpm/fusb302.c
@@ -0,0 +1,1848 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2016-2017 Google, Inc
+ *
+ * Fairchild FUSB302 Type-C Chip Driver
+ */
+
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/extcon.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of_device.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/proc_fs.h>
+#include <linux/regulator/consumer.h>
+#include <linux/sched/clock.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/usb.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/tcpm.h>
+#include <linux/usb/pd.h>
+#include <linux/workqueue.h>
+
+#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 = (struct fusb302_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,
+ const struct i2c_device_id *id)
+{
+ 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[] = {
+ {.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 <yueyao.zhu@gmail.com>");
+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 000000000..edc0e4b0f
--- /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/tcpci.c b/drivers/usb/typec/tcpm/tcpci.c
new file mode 100644
index 000000000..816945913
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpci.c
@@ -0,0 +1,895 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2015-2017 Google, Inc
+ *
+ * USB Type-C Port Controller Interface.
+ */
+
+#include <linux/delay.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/tcpm.h>
+#include <linux/usb/typec.h>
+
+#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;
+
+ 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, &reg);
+ if (ret < 0)
+ return ret;
+
+ vconn_pres = !!(reg & TCPC_POWER_STATUS_VCONN_PRES);
+ if (vconn_pres) {
+ ret = regmap_read(tcpci->regmap, TCPC_TCPC_CTRL, &reg);
+ 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, &reg);
+ 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, &reg);
+ 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, &reg);
+ 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 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, &reg);
+ 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, &reg);
+ 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, &reg);
+ 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;
+ }
+ 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_HANDLED;
+}
+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->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,
+ const struct i2c_device_id *i2c_id)
+{
+ 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_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.c b/drivers/usb/typec/tcpm/tcpci_maxim.c
new file mode 100644
index 000000000..03f89e6f1
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpci_maxim.c
@@ -0,0 +1,530 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2020, Google LLC
+ *
+ * MAXIM TCPCI based TCPC driver
+ */
+
+#include <linux/interrupt.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/tcpm.h>
+#include <linux/usb/typec.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
+
+struct max_tcpci_chip {
+ struct tcpci_data data;
+ struct tcpci *tcpci;
+ struct device *dev;
+ struct i2c_client *client;
+ struct tcpm_port *port;
+};
+
+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 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 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 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 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));
+}
+
+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, &reg_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 *)&reg_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)
+ 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 int max_tcpci_probe(struct i2c_client *client, const struct i2c_device_id *i2c_id)
+{
+ 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;
+
+ 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 <badhri@google.com>");
+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 000000000..1b7c31278
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpci_mt6360.c
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2020 MediaTek Inc.
+ *
+ * Author: ChiYuan Huang <cy_huang@richtek.com>
+ */
+
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/tcpm.h>
+
+#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_read16(struct regmap *regmap,
+ unsigned int reg, u16 *val)
+{
+ return regmap_raw_read(regmap, reg, val, sizeof(u16));
+}
+
+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 int 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);
+ return 0;
+}
+
+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 = mt6360_tcpc_remove,
+};
+module_platform_driver(mt6360_tcpc_driver);
+
+MODULE_AUTHOR("ChiYuan Huang <cy_huang@richtek.com>");
+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 000000000..c5bb201a5
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpci_mt6370.c
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2022 Richtek Technology Corp.
+ *
+ * Author: ChiYuan Huang <cy_huang@richtek.com>
+ */
+
+#include <linux/bits.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm_wakeup.h>
+#include <linux/pm_wakeirq.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/tcpm.h>
+
+#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 dev_err_probe(dev, irq, "Failed to get irq\n");
+
+ /* 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 int mt6370_tcpc_remove(struct platform_device *pdev)
+{
+ dev_pm_clear_wake_irq(&pdev->dev);
+ device_init_wakeup(&pdev->dev, false);
+
+ return 0;
+}
+
+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 = mt6370_tcpc_remove,
+};
+module_platform_driver(mt6370_tcpc_driver);
+
+MODULE_AUTHOR("ChiYuan Huang <cy_huang@richtek.com>");
+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 000000000..7b217c712
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpci_rt1711h.c
@@ -0,0 +1,424 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2018, Richtek Technology Corporation
+ *
+ * Richtek RT1711H Type-C Chip Driver
+ */
+
+#include <linux/bits.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/gpio/consumer.h>
+#include <linux/usb/tcpci.h>
+#include <linux/usb/tcpm.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+
+#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,
+ const struct i2c_device_id *i2c_id)
+{
+ 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 <shufan_lee@richtek.com>");
+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 000000000..bf615dc80
--- /dev/null
+++ b/drivers/usb/typec/tcpm/tcpm.c
@@ -0,0 +1,6669 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2015-2017 Google, Inc
+ *
+ * USB Power Delivery protocol stack.
+ */
+
+#include <linux/completion.h>
+#include <linux/debugfs.h>
+#include <linux/device.h>
+#include <linux/hrtimer.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/power_supply.h>
+#include <linux/proc_fs.h>
+#include <linux/property.h>
+#include <linux/sched/clock.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/pd_ado.h>
+#include <linux/usb/pd_bdo.h>
+#include <linux/usb/pd_ext_sdb.h>
+#include <linux/usb/pd_vdo.h>
+#include <linux/usb/role.h>
+#include <linux/usb/tcpm.h>
+#include <linux/usb/typec_altmode.h>
+
+#include <uapi/linux/sched/types.h>
+
+#define FOREACH_STATE(S) \
+ S(INVALID_STATE), \
+ S(TOGGLING), \
+ 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 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;
+#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))
+ 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 = (struct tcpm_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) {
+ typec_altmode_update_active(pdev, true);
+ *adev_action = ADEV_QUEUE_VDM_SEND_EXIT_MODE_ON_FAIL;
+ }
+ return 0;
+ case CMD_EXIT_MODE:
+ if (adev && pdev) {
+ typec_altmode_update_active(pdev, false);
+ /* 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;
+}
+
+#define min_pps_apdo_current(x, y) \
+ min(pdo_pps_apdo_max_current(x), pdo_pps_apdo_max_current(y))
+
+static unsigned int tcpm_pd_select_pps_apdo(struct tcpm_port *port)
+{
+ unsigned int i, j, max_mw = 0, max_mv = 0;
+ unsigned int min_src_mv, max_src_mv, src_ma, src_mw;
+ unsigned int min_snk_mv, max_snk_mv;
+ unsigned int max_op_mv;
+ u32 pdo, src, snk;
+ unsigned int src_pdo = 0, snk_pdo = 0;
+
+ /*
+ * Select the source PPS APDO providing the most power while staying
+ * within the board's limits. We skip the first PDO as this is always
+ * 5V 3A.
+ */
+ 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;
+ }
+
+ min_src_mv = pdo_pps_apdo_min_voltage(pdo);
+ max_src_mv = pdo_pps_apdo_max_voltage(pdo);
+ src_ma = pdo_pps_apdo_max_current(pdo);
+ src_mw = (src_ma * max_src_mv) / 1000;
+
+ /*
+ * Now search through the sink PDOs to find a matching
+ * PPS APDO. Again skip the first sink PDO as this will
+ * always be 5V 3A.
+ */
+ for (j = 1; j < port->nr_snk_pdo; j++) {
+ pdo = port->snk_pdo[j];
+
+ switch (pdo_type(pdo)) {
+ case PDO_TYPE_APDO:
+ if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
+ tcpm_log(port,
+ "Not PPS APDO (sink), ignoring");
+ continue;
+ }
+
+ min_snk_mv =
+ pdo_pps_apdo_min_voltage(pdo);
+ max_snk_mv =
+ pdo_pps_apdo_max_voltage(pdo);
+ break;
+ default:
+ tcpm_log(port,
+ "Not APDO type (sink), ignoring");
+ continue;
+ }
+
+ if (min_src_mv <= max_snk_mv &&
+ max_src_mv >= min_snk_mv) {
+ max_op_mv = min(max_src_mv, max_snk_mv);
+ src_mw = (max_op_mv * src_ma) / 1000;
+ /* Prefer higher voltages if available */
+ if ((src_mw == max_mw &&
+ max_op_mv > max_mv) ||
+ src_mw > max_mw) {
+ src_pdo = i;
+ snk_pdo = j;
+ max_mw = src_mw;
+ max_mv = max_op_mv;
+ }
+ }
+ }
+
+ break;
+ default:
+ tcpm_log(port, "Not APDO type (source), ignoring");
+ continue;
+ }
+ }
+
+ if (src_pdo) {
+ src = port->source_caps[src_pdo];
+ snk = port->snk_pdo[snk_pdo];
+
+ port->pps_data.req_min_volt = max(pdo_pps_apdo_min_voltage(src),
+ pdo_pps_apdo_min_voltage(snk));
+ port->pps_data.req_max_volt = min(pdo_pps_apdo_max_voltage(src),
+ pdo_pps_apdo_max_voltage(snk));
+ port->pps_data.req_max_curr = min_pps_apdo_current(src, snk);
+ port->pps_data.req_out_volt = min(port->pps_data.req_max_volt,
+ max(port->pps_data.req_min_volt,
+ port->pps_data.req_out_volt));
+ 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;
+ enum pd_pdo_type type;
+ unsigned int src_pdo_index;
+ u32 pdo;
+
+ src_pdo_index = tcpm_pd_select_pps_apdo(port);
+ if (!src_pdo_index)
+ return -EOPNOTSUPP;
+
+ pdo = port->source_caps[src_pdo_index];
+ type = pdo_type(pdo);
+
+ switch (type) {
+ case PDO_TYPE_APDO:
+ if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
+ tcpm_log(port, "Invalid APDO selected!");
+ return -EINVAL;
+ }
+ 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;
+ break;
+ default:
+ tcpm_log(port, "Invalid PDO selected!");
+ return -EINVAL;
+ }
+
+ 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;
+
+ 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;
+
+ port->enter_state = port->state;
+ switch (port->state) {
+ case TOGGLING:
+ break;
+ /* SRC states */
+ case SRC_UNATTACHED:
+ if (!port->non_pd_role_swap)
+ tcpm_swap_complete(port, -ENOTCONN);
+ tcpm_src_detach(port);
+ 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 (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);
+ tcpm_set_charge(port, true);
+ 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);
+ tcpm_set_charge(port, true);
+ }
+ 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, TYPEC_CC_OPEN);
+ 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 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);
+ }
+
+ 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);
+
+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;
+ }
+
+ if (req_out_volt < port->pps_data.min_volt ||
+ req_out_volt > port->pps_data.max_volt) {
+ ret = -EINVAL;
+ 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_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;
+ 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:
+ if (val->intval < port->pps_data.min_volt * 1000 ||
+ val->intval > port->pps_data.max_volt * 1000)
+ ret = -EINVAL;
+ else
+ 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 (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 <groeck@chromium.org>");
+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 000000000..20917d85d
--- /dev/null
+++ b/drivers/usb/typec/tcpm/wcove.c
@@ -0,0 +1,704 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * typec_wcove.c - WhiskeyCove PMIC USB Type-C PHY driver
+ *
+ * Copyright (C) 2017 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/acpi.h>
+#include <linux/module.h>
+#include <linux/usb/tcpm.h>
+#include <linux/interrupt.h>
+#include <linux/platform_device.h>
+#include <linux/mfd/intel_soc_pmic.h>
+
+/* 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 int 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);
+
+ return 0;
+}
+
+static struct platform_driver wcove_typec_driver = {
+ .driver = {
+ .name = "bxt_wcove_usbc",
+ },
+ .probe = wcove_typec_probe,
+ .remove = 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");
diff --git a/drivers/usb/typec/tipd/Kconfig b/drivers/usb/typec/tipd/Kconfig
new file mode 100644
index 000000000..b82715293
--- /dev/null
+++ b/drivers/usb/typec/tipd/Kconfig
@@ -0,0 +1,12 @@
+config TYPEC_TPS6598X
+ tristate "TI TPS6598x USB Power Delivery controller driver"
+ depends on I2C
+ select POWER_SUPPLY
+ select REGMAP_I2C
+ select USB_ROLE_SWITCH
+ help
+ Say Y or M here if your system has TI TPS65982 or TPS65983 USB Power
+ Delivery controller.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called tps6598x.ko.
diff --git a/drivers/usb/typec/tipd/Makefile b/drivers/usb/typec/tipd/Makefile
new file mode 100644
index 000000000..aa439f80a
--- /dev/null
+++ b/drivers/usb/typec/tipd/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS_trace.o := -I$(src)
+
+obj-$(CONFIG_TYPEC_TPS6598X) += tps6598x.o
+tps6598x-y := core.o
+tps6598x-$(CONFIG_TRACING) += trace.o
diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c
new file mode 100644
index 000000000..195c9c16f
--- /dev/null
+++ b/drivers/usb/typec/tipd/core.c
@@ -0,0 +1,896 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for TI TPS6598x USB Power Delivery controller family
+ *
+ * Copyright (C) 2017, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/i2c.h>
+#include <linux/acpi.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/power_supply.h>
+#include <linux/regmap.h>
+#include <linux/interrupt.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/role.h>
+
+#include "tps6598x.h"
+#include "trace.h"
+
+/* Register offsets */
+#define TPS_REG_VID 0x00
+#define TPS_REG_MODE 0x03
+#define TPS_REG_CMD1 0x08
+#define TPS_REG_DATA1 0x09
+#define TPS_REG_INT_EVENT1 0x14
+#define TPS_REG_INT_EVENT2 0x15
+#define TPS_REG_INT_MASK1 0x16
+#define TPS_REG_INT_MASK2 0x17
+#define TPS_REG_INT_CLEAR1 0x18
+#define TPS_REG_INT_CLEAR2 0x19
+#define TPS_REG_SYSTEM_POWER_STATE 0x20
+#define TPS_REG_STATUS 0x1a
+#define TPS_REG_SYSTEM_CONF 0x28
+#define TPS_REG_CTRL_CONF 0x29
+#define TPS_REG_POWER_STATUS 0x3f
+#define TPS_REG_RX_IDENTITY_SOP 0x48
+#define TPS_REG_DATA_STATUS 0x5f
+
+/* TPS_REG_SYSTEM_CONF bits */
+#define TPS_SYSCONF_PORTINFO(c) ((c) & 7)
+
+enum {
+ TPS_PORTINFO_SINK,
+ TPS_PORTINFO_SINK_ACCESSORY,
+ TPS_PORTINFO_DRP_UFP,
+ TPS_PORTINFO_DRP_UFP_DRD,
+ TPS_PORTINFO_DRP_DFP,
+ TPS_PORTINFO_DRP_DFP_DRD,
+ TPS_PORTINFO_SOURCE,
+};
+
+/* TPS_REG_RX_IDENTITY_SOP */
+struct tps6598x_rx_identity_reg {
+ u8 status;
+ struct usb_pd_identity identity;
+} __packed;
+
+/* Standard Task return codes */
+#define TPS_TASK_TIMEOUT 1
+#define TPS_TASK_REJECTED 3
+
+enum {
+ TPS_MODE_APP,
+ TPS_MODE_BOOT,
+ TPS_MODE_BIST,
+ TPS_MODE_DISC,
+};
+
+static const char *const modes[] = {
+ [TPS_MODE_APP] = "APP ",
+ [TPS_MODE_BOOT] = "BOOT",
+ [TPS_MODE_BIST] = "BIST",
+ [TPS_MODE_DISC] = "DISC",
+};
+
+/* Unrecognized commands will be replaced with "!CMD" */
+#define INVALID_CMD(_cmd_) (_cmd_ == 0x444d4321)
+
+struct tps6598x {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex lock; /* device lock */
+ u8 i2c_protocol:1;
+
+ struct typec_port *port;
+ struct typec_partner *partner;
+ struct usb_pd_identity partner_identity;
+ struct usb_role_switch *role_sw;
+ struct typec_capability typec_cap;
+
+ struct power_supply *psy;
+ struct power_supply_desc psy_desc;
+ enum power_supply_usb_type usb_type;
+
+ u16 pwr_status;
+};
+
+static enum power_supply_property tps6598x_psy_props[] = {
+ POWER_SUPPLY_PROP_USB_TYPE,
+ POWER_SUPPLY_PROP_ONLINE,
+};
+
+static enum power_supply_usb_type tps6598x_psy_usb_types[] = {
+ POWER_SUPPLY_USB_TYPE_C,
+ POWER_SUPPLY_USB_TYPE_PD,
+};
+
+static const char *tps6598x_psy_name_prefix = "tps6598x-source-psy-";
+
+/*
+ * Max data bytes for Data1, Data2, and other registers. See ch 1.3.2:
+ * https://www.ti.com/lit/ug/slvuan1a/slvuan1a.pdf
+ */
+#define TPS_MAX_LEN 64
+
+static int
+tps6598x_block_read(struct tps6598x *tps, u8 reg, void *val, size_t len)
+{
+ u8 data[TPS_MAX_LEN + 1];
+ int ret;
+
+ if (len + 1 > sizeof(data))
+ return -EINVAL;
+
+ if (!tps->i2c_protocol)
+ return regmap_raw_read(tps->regmap, reg, val, len);
+
+ ret = regmap_raw_read(tps->regmap, reg, data, len + 1);
+ if (ret)
+ return ret;
+
+ if (data[0] < len)
+ return -EIO;
+
+ memcpy(val, &data[1], len);
+ return 0;
+}
+
+static int tps6598x_block_write(struct tps6598x *tps, u8 reg,
+ const void *val, size_t len)
+{
+ u8 data[TPS_MAX_LEN + 1];
+
+ if (len + 1 > sizeof(data))
+ return -EINVAL;
+
+ if (!tps->i2c_protocol)
+ return regmap_raw_write(tps->regmap, reg, val, len);
+
+ data[0] = len;
+ memcpy(&data[1], val, len);
+
+ return regmap_raw_write(tps->regmap, reg, data, len + 1);
+}
+
+static inline int tps6598x_read8(struct tps6598x *tps, u8 reg, u8 *val)
+{
+ return tps6598x_block_read(tps, reg, val, sizeof(u8));
+}
+
+static inline int tps6598x_read16(struct tps6598x *tps, u8 reg, u16 *val)
+{
+ return tps6598x_block_read(tps, reg, val, sizeof(u16));
+}
+
+static inline int tps6598x_read32(struct tps6598x *tps, u8 reg, u32 *val)
+{
+ return tps6598x_block_read(tps, reg, val, sizeof(u32));
+}
+
+static inline int tps6598x_read64(struct tps6598x *tps, u8 reg, u64 *val)
+{
+ return tps6598x_block_read(tps, reg, val, sizeof(u64));
+}
+
+static inline int tps6598x_write16(struct tps6598x *tps, u8 reg, u16 val)
+{
+ return tps6598x_block_write(tps, reg, &val, sizeof(u16));
+}
+
+static inline int tps6598x_write32(struct tps6598x *tps, u8 reg, u32 val)
+{
+ return tps6598x_block_write(tps, reg, &val, sizeof(u32));
+}
+
+static inline int tps6598x_write64(struct tps6598x *tps, u8 reg, u64 val)
+{
+ return tps6598x_block_write(tps, reg, &val, sizeof(u64));
+}
+
+static inline int
+tps6598x_write_4cc(struct tps6598x *tps, u8 reg, const char *val)
+{
+ return tps6598x_block_write(tps, reg, val, 4);
+}
+
+static int tps6598x_read_partner_identity(struct tps6598x *tps)
+{
+ struct tps6598x_rx_identity_reg id;
+ int ret;
+
+ ret = tps6598x_block_read(tps, TPS_REG_RX_IDENTITY_SOP,
+ &id, sizeof(id));
+ if (ret)
+ return ret;
+
+ tps->partner_identity = id.identity;
+
+ return 0;
+}
+
+static void tps6598x_set_data_role(struct tps6598x *tps,
+ enum typec_data_role role, bool connected)
+{
+ enum usb_role role_val;
+
+ if (role == TYPEC_HOST)
+ role_val = USB_ROLE_HOST;
+ else
+ role_val = USB_ROLE_DEVICE;
+
+ if (!connected)
+ role_val = USB_ROLE_NONE;
+
+ usb_role_switch_set_role(tps->role_sw, role_val);
+ typec_set_data_role(tps->port, role);
+}
+
+static int tps6598x_connect(struct tps6598x *tps, u32 status)
+{
+ struct typec_partner_desc desc;
+ enum typec_pwr_opmode mode;
+ int ret;
+
+ if (tps->partner)
+ return 0;
+
+ mode = TPS_POWER_STATUS_PWROPMODE(tps->pwr_status);
+
+ desc.usb_pd = mode == TYPEC_PWR_MODE_PD;
+ desc.accessory = TYPEC_ACCESSORY_NONE; /* XXX: handle accessories */
+ desc.identity = NULL;
+
+ if (desc.usb_pd) {
+ ret = tps6598x_read_partner_identity(tps);
+ if (ret)
+ return ret;
+ desc.identity = &tps->partner_identity;
+ }
+
+ typec_set_pwr_opmode(tps->port, mode);
+ typec_set_pwr_role(tps->port, TPS_STATUS_TO_TYPEC_PORTROLE(status));
+ typec_set_vconn_role(tps->port, TPS_STATUS_TO_TYPEC_VCONN(status));
+ if (TPS_STATUS_TO_UPSIDE_DOWN(status))
+ typec_set_orientation(tps->port, TYPEC_ORIENTATION_REVERSE);
+ else
+ typec_set_orientation(tps->port, TYPEC_ORIENTATION_NORMAL);
+ tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), true);
+
+ tps->partner = typec_register_partner(tps->port, &desc);
+ if (IS_ERR(tps->partner))
+ return PTR_ERR(tps->partner);
+
+ if (desc.identity)
+ typec_partner_set_identity(tps->partner);
+
+ power_supply_changed(tps->psy);
+
+ return 0;
+}
+
+static void tps6598x_disconnect(struct tps6598x *tps, u32 status)
+{
+ if (!IS_ERR(tps->partner))
+ typec_unregister_partner(tps->partner);
+ tps->partner = NULL;
+ typec_set_pwr_opmode(tps->port, TYPEC_PWR_MODE_USB);
+ typec_set_pwr_role(tps->port, TPS_STATUS_TO_TYPEC_PORTROLE(status));
+ typec_set_vconn_role(tps->port, TPS_STATUS_TO_TYPEC_VCONN(status));
+ typec_set_orientation(tps->port, TYPEC_ORIENTATION_NONE);
+ tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), false);
+
+ power_supply_changed(tps->psy);
+}
+
+static int tps6598x_exec_cmd(struct tps6598x *tps, const char *cmd,
+ size_t in_len, u8 *in_data,
+ size_t out_len, u8 *out_data)
+{
+ unsigned long timeout;
+ u32 val;
+ int ret;
+
+ ret = tps6598x_read32(tps, TPS_REG_CMD1, &val);
+ if (ret)
+ return ret;
+ if (val && !INVALID_CMD(val))
+ return -EBUSY;
+
+ if (in_len) {
+ ret = tps6598x_block_write(tps, TPS_REG_DATA1,
+ in_data, in_len);
+ if (ret)
+ return ret;
+ }
+
+ ret = tps6598x_write_4cc(tps, TPS_REG_CMD1, cmd);
+ if (ret < 0)
+ return ret;
+
+ /* XXX: Using 1s for now, but it may not be enough for every command. */
+ timeout = jiffies + msecs_to_jiffies(1000);
+
+ do {
+ ret = tps6598x_read32(tps, TPS_REG_CMD1, &val);
+ if (ret)
+ return ret;
+ if (INVALID_CMD(val))
+ return -EINVAL;
+
+ if (time_is_before_jiffies(timeout))
+ return -ETIMEDOUT;
+ } while (val);
+
+ if (out_len) {
+ ret = tps6598x_block_read(tps, TPS_REG_DATA1,
+ out_data, out_len);
+ if (ret)
+ return ret;
+ val = out_data[0];
+ } else {
+ ret = tps6598x_block_read(tps, TPS_REG_DATA1, &val, sizeof(u8));
+ if (ret)
+ return ret;
+ }
+
+ switch (val) {
+ case TPS_TASK_TIMEOUT:
+ return -ETIMEDOUT;
+ case TPS_TASK_REJECTED:
+ return -EPERM;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int tps6598x_dr_set(struct typec_port *port, enum typec_data_role role)
+{
+ const char *cmd = (role == TYPEC_DEVICE) ? "SWUF" : "SWDF";
+ struct tps6598x *tps = typec_get_drvdata(port);
+ u32 status;
+ int ret;
+
+ mutex_lock(&tps->lock);
+
+ ret = tps6598x_exec_cmd(tps, cmd, 0, NULL, 0, NULL);
+ if (ret)
+ goto out_unlock;
+
+ ret = tps6598x_read32(tps, TPS_REG_STATUS, &status);
+ if (ret)
+ goto out_unlock;
+
+ if (role != TPS_STATUS_TO_TYPEC_DATAROLE(status)) {
+ ret = -EPROTO;
+ goto out_unlock;
+ }
+
+ tps6598x_set_data_role(tps, role, true);
+
+out_unlock:
+ mutex_unlock(&tps->lock);
+
+ return ret;
+}
+
+static int tps6598x_pr_set(struct typec_port *port, enum typec_role role)
+{
+ const char *cmd = (role == TYPEC_SINK) ? "SWSk" : "SWSr";
+ struct tps6598x *tps = typec_get_drvdata(port);
+ u32 status;
+ int ret;
+
+ mutex_lock(&tps->lock);
+
+ ret = tps6598x_exec_cmd(tps, cmd, 0, NULL, 0, NULL);
+ if (ret)
+ goto out_unlock;
+
+ ret = tps6598x_read32(tps, TPS_REG_STATUS, &status);
+ if (ret)
+ goto out_unlock;
+
+ if (role != TPS_STATUS_TO_TYPEC_PORTROLE(status)) {
+ ret = -EPROTO;
+ goto out_unlock;
+ }
+
+ typec_set_pwr_role(tps->port, role);
+
+out_unlock:
+ mutex_unlock(&tps->lock);
+
+ return ret;
+}
+
+static const struct typec_operations tps6598x_ops = {
+ .dr_set = tps6598x_dr_set,
+ .pr_set = tps6598x_pr_set,
+};
+
+static bool tps6598x_read_status(struct tps6598x *tps, u32 *status)
+{
+ int ret;
+
+ ret = tps6598x_read32(tps, TPS_REG_STATUS, status);
+ if (ret) {
+ dev_err(tps->dev, "%s: failed to read status\n", __func__);
+ return false;
+ }
+ trace_tps6598x_status(*status);
+
+ return true;
+}
+
+static bool tps6598x_read_data_status(struct tps6598x *tps)
+{
+ u32 data_status;
+ int ret;
+
+ ret = tps6598x_read32(tps, TPS_REG_DATA_STATUS, &data_status);
+ if (ret < 0) {
+ dev_err(tps->dev, "failed to read data status: %d\n", ret);
+ return false;
+ }
+ trace_tps6598x_data_status(data_status);
+
+ return true;
+}
+
+static bool tps6598x_read_power_status(struct tps6598x *tps)
+{
+ u16 pwr_status;
+ int ret;
+
+ ret = tps6598x_read16(tps, TPS_REG_POWER_STATUS, &pwr_status);
+ if (ret < 0) {
+ dev_err(tps->dev, "failed to read power status: %d\n", ret);
+ return false;
+ }
+ tps->pwr_status = pwr_status;
+ trace_tps6598x_power_status(pwr_status);
+
+ return true;
+}
+
+static void tps6598x_handle_plug_event(struct tps6598x *tps, u32 status)
+{
+ int ret;
+
+ if (status & TPS_STATUS_PLUG_PRESENT) {
+ ret = tps6598x_connect(tps, status);
+ if (ret)
+ dev_err(tps->dev, "failed to register partner\n");
+ } else {
+ tps6598x_disconnect(tps, status);
+ }
+}
+
+static irqreturn_t cd321x_interrupt(int irq, void *data)
+{
+ struct tps6598x *tps = data;
+ u64 event = 0;
+ u32 status;
+ int ret;
+
+ mutex_lock(&tps->lock);
+
+ ret = tps6598x_read64(tps, TPS_REG_INT_EVENT1, &event);
+ if (ret) {
+ dev_err(tps->dev, "%s: failed to read events\n", __func__);
+ goto err_unlock;
+ }
+ trace_cd321x_irq(event);
+
+ if (!event)
+ goto err_unlock;
+
+ if (!tps6598x_read_status(tps, &status))
+ goto err_clear_ints;
+
+ if (event & APPLE_CD_REG_INT_POWER_STATUS_UPDATE)
+ if (!tps6598x_read_power_status(tps))
+ goto err_clear_ints;
+
+ if (event & APPLE_CD_REG_INT_DATA_STATUS_UPDATE)
+ if (!tps6598x_read_data_status(tps))
+ goto err_clear_ints;
+
+ /* Handle plug insert or removal */
+ if (event & APPLE_CD_REG_INT_PLUG_EVENT)
+ tps6598x_handle_plug_event(tps, status);
+
+err_clear_ints:
+ tps6598x_write64(tps, TPS_REG_INT_CLEAR1, event);
+
+err_unlock:
+ mutex_unlock(&tps->lock);
+
+ if (event)
+ return IRQ_HANDLED;
+ return IRQ_NONE;
+}
+
+static irqreturn_t tps6598x_interrupt(int irq, void *data)
+{
+ struct tps6598x *tps = data;
+ u64 event1 = 0;
+ u64 event2 = 0;
+ u32 status;
+ int ret;
+
+ mutex_lock(&tps->lock);
+
+ ret = tps6598x_read64(tps, TPS_REG_INT_EVENT1, &event1);
+ ret |= tps6598x_read64(tps, TPS_REG_INT_EVENT2, &event2);
+ if (ret) {
+ dev_err(tps->dev, "%s: failed to read events\n", __func__);
+ goto err_unlock;
+ }
+ trace_tps6598x_irq(event1, event2);
+
+ if (!(event1 | event2))
+ goto err_unlock;
+
+ if (!tps6598x_read_status(tps, &status))
+ goto err_clear_ints;
+
+ if ((event1 | event2) & TPS_REG_INT_POWER_STATUS_UPDATE)
+ if (!tps6598x_read_power_status(tps))
+ goto err_clear_ints;
+
+ if ((event1 | event2) & TPS_REG_INT_DATA_STATUS_UPDATE)
+ if (!tps6598x_read_data_status(tps))
+ goto err_clear_ints;
+
+ /* Handle plug insert or removal */
+ if ((event1 | event2) & TPS_REG_INT_PLUG_EVENT)
+ tps6598x_handle_plug_event(tps, status);
+
+err_clear_ints:
+ tps6598x_write64(tps, TPS_REG_INT_CLEAR1, event1);
+ tps6598x_write64(tps, TPS_REG_INT_CLEAR2, event2);
+
+err_unlock:
+ mutex_unlock(&tps->lock);
+
+ if (event1 | event2)
+ return IRQ_HANDLED;
+ return IRQ_NONE;
+}
+
+static int tps6598x_check_mode(struct tps6598x *tps)
+{
+ char mode[5] = { };
+ int ret;
+
+ ret = tps6598x_read32(tps, TPS_REG_MODE, (void *)mode);
+ if (ret)
+ return ret;
+
+ switch (match_string(modes, ARRAY_SIZE(modes), mode)) {
+ case TPS_MODE_APP:
+ return 0;
+ case TPS_MODE_BOOT:
+ dev_warn(tps->dev, "dead-battery condition\n");
+ return 0;
+ case TPS_MODE_BIST:
+ case TPS_MODE_DISC:
+ default:
+ dev_err(tps->dev, "controller in unsupported mode \"%s\"\n",
+ mode);
+ break;
+ }
+
+ return -ENODEV;
+}
+
+static const struct regmap_config tps6598x_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x7F,
+};
+
+static int tps6598x_psy_get_online(struct tps6598x *tps,
+ union power_supply_propval *val)
+{
+ if (TPS_POWER_STATUS_CONNECTION(tps->pwr_status) &&
+ TPS_POWER_STATUS_SOURCESINK(tps->pwr_status)) {
+ val->intval = 1;
+ } else {
+ val->intval = 0;
+ }
+ return 0;
+}
+
+static int tps6598x_psy_get_prop(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct tps6598x *tps = power_supply_get_drvdata(psy);
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_USB_TYPE:
+ if (TPS_POWER_STATUS_PWROPMODE(tps->pwr_status) == TYPEC_PWR_MODE_PD)
+ val->intval = POWER_SUPPLY_USB_TYPE_PD;
+ else
+ val->intval = POWER_SUPPLY_USB_TYPE_C;
+ break;
+ case POWER_SUPPLY_PROP_ONLINE:
+ ret = tps6598x_psy_get_online(tps, val);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static int cd321x_switch_power_state(struct tps6598x *tps, u8 target_state)
+{
+ u8 state;
+ int ret;
+
+ ret = tps6598x_read8(tps, TPS_REG_SYSTEM_POWER_STATE, &state);
+ if (ret)
+ return ret;
+
+ if (state == target_state)
+ return 0;
+
+ ret = tps6598x_exec_cmd(tps, "SSPS", sizeof(u8), &target_state, 0, NULL);
+ if (ret)
+ return ret;
+
+ ret = tps6598x_read8(tps, TPS_REG_SYSTEM_POWER_STATE, &state);
+ if (ret)
+ return ret;
+
+ if (state != target_state)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int devm_tps6598_psy_register(struct tps6598x *tps)
+{
+ struct power_supply_config psy_cfg = {};
+ const char *port_dev_name = dev_name(tps->dev);
+ char *psy_name;
+
+ psy_cfg.drv_data = tps;
+ psy_cfg.fwnode = dev_fwnode(tps->dev);
+
+ psy_name = devm_kasprintf(tps->dev, GFP_KERNEL, "%s%s", tps6598x_psy_name_prefix,
+ port_dev_name);
+ if (!psy_name)
+ return -ENOMEM;
+
+ tps->psy_desc.name = psy_name;
+ tps->psy_desc.type = POWER_SUPPLY_TYPE_USB;
+ tps->psy_desc.usb_types = tps6598x_psy_usb_types;
+ tps->psy_desc.num_usb_types = ARRAY_SIZE(tps6598x_psy_usb_types);
+ tps->psy_desc.properties = tps6598x_psy_props;
+ tps->psy_desc.num_properties = ARRAY_SIZE(tps6598x_psy_props);
+ tps->psy_desc.get_property = tps6598x_psy_get_prop;
+
+ tps->usb_type = POWER_SUPPLY_USB_TYPE_C;
+
+ tps->psy = devm_power_supply_register(tps->dev, &tps->psy_desc,
+ &psy_cfg);
+ return PTR_ERR_OR_ZERO(tps->psy);
+}
+
+static int tps6598x_probe(struct i2c_client *client)
+{
+ irq_handler_t irq_handler = tps6598x_interrupt;
+ struct device_node *np = client->dev.of_node;
+ struct typec_capability typec_cap = { };
+ struct tps6598x *tps;
+ struct fwnode_handle *fwnode;
+ u32 status;
+ u32 conf;
+ u32 vid;
+ int ret;
+ u64 mask1;
+
+ tps = devm_kzalloc(&client->dev, sizeof(*tps), GFP_KERNEL);
+ if (!tps)
+ return -ENOMEM;
+
+ mutex_init(&tps->lock);
+ tps->dev = &client->dev;
+
+ tps->regmap = devm_regmap_init_i2c(client, &tps6598x_regmap_config);
+ if (IS_ERR(tps->regmap))
+ return PTR_ERR(tps->regmap);
+
+ ret = tps6598x_read32(tps, TPS_REG_VID, &vid);
+ if (ret < 0 || !vid)
+ return -ENODEV;
+
+ /*
+ * Checking can the adapter handle SMBus protocol. If it can not, the
+ * driver needs to take care of block reads separately.
+ */
+ if (i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+ tps->i2c_protocol = true;
+
+ if (np && of_device_is_compatible(np, "apple,cd321x")) {
+ /* Switch CD321X chips to the correct system power state */
+ ret = cd321x_switch_power_state(tps, TPS_SYSTEM_POWER_STATE_S0);
+ if (ret)
+ return ret;
+
+ /* CD321X chips have all interrupts masked initially */
+ mask1 = APPLE_CD_REG_INT_POWER_STATUS_UPDATE |
+ APPLE_CD_REG_INT_DATA_STATUS_UPDATE |
+ APPLE_CD_REG_INT_PLUG_EVENT;
+
+ irq_handler = cd321x_interrupt;
+ } else {
+ /* Enable power status, data status and plug event interrupts */
+ mask1 = TPS_REG_INT_POWER_STATUS_UPDATE |
+ TPS_REG_INT_DATA_STATUS_UPDATE |
+ TPS_REG_INT_PLUG_EVENT;
+ }
+
+ /* Make sure the controller has application firmware running */
+ ret = tps6598x_check_mode(tps);
+ if (ret)
+ return ret;
+
+ ret = tps6598x_write64(tps, TPS_REG_INT_MASK1, mask1);
+ if (ret)
+ return ret;
+
+ ret = tps6598x_read32(tps, TPS_REG_STATUS, &status);
+ if (ret < 0)
+ goto err_clear_mask;
+ trace_tps6598x_status(status);
+
+ ret = tps6598x_read32(tps, TPS_REG_SYSTEM_CONF, &conf);
+ if (ret < 0)
+ goto err_clear_mask;
+
+ /*
+ * This fwnode has a "compatible" property, but is never populated as a
+ * struct device. Instead we simply parse it to read the properties.
+ * This 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.
+ */
+ fwnode = device_get_named_child_node(&client->dev, "connector");
+ if (fwnode)
+ fw_devlink_purge_absent_suppliers(fwnode);
+
+ tps->role_sw = fwnode_usb_role_switch_get(fwnode);
+ if (IS_ERR(tps->role_sw)) {
+ ret = PTR_ERR(tps->role_sw);
+ goto err_fwnode_put;
+ }
+
+ typec_cap.revision = USB_TYPEC_REV_1_2;
+ typec_cap.pd_revision = 0x200;
+ typec_cap.prefer_role = TYPEC_NO_PREFERRED_ROLE;
+ typec_cap.driver_data = tps;
+ typec_cap.ops = &tps6598x_ops;
+ typec_cap.fwnode = fwnode;
+
+ switch (TPS_SYSCONF_PORTINFO(conf)) {
+ case TPS_PORTINFO_SINK_ACCESSORY:
+ case TPS_PORTINFO_SINK:
+ typec_cap.type = TYPEC_PORT_SNK;
+ typec_cap.data = TYPEC_PORT_UFP;
+ break;
+ case TPS_PORTINFO_DRP_UFP_DRD:
+ case TPS_PORTINFO_DRP_DFP_DRD:
+ typec_cap.type = TYPEC_PORT_DRP;
+ typec_cap.data = TYPEC_PORT_DRD;
+ break;
+ case TPS_PORTINFO_DRP_UFP:
+ typec_cap.type = TYPEC_PORT_DRP;
+ typec_cap.data = TYPEC_PORT_UFP;
+ break;
+ case TPS_PORTINFO_DRP_DFP:
+ typec_cap.type = TYPEC_PORT_DRP;
+ typec_cap.data = TYPEC_PORT_DFP;
+ break;
+ case TPS_PORTINFO_SOURCE:
+ typec_cap.type = TYPEC_PORT_SRC;
+ typec_cap.data = TYPEC_PORT_DFP;
+ break;
+ default:
+ ret = -ENODEV;
+ goto err_role_put;
+ }
+
+ ret = devm_tps6598_psy_register(tps);
+ if (ret)
+ goto err_role_put;
+
+ tps->port = typec_register_port(&client->dev, &typec_cap);
+ if (IS_ERR(tps->port)) {
+ ret = PTR_ERR(tps->port);
+ goto err_role_put;
+ }
+
+ if (status & TPS_STATUS_PLUG_PRESENT) {
+ ret = tps6598x_read16(tps, TPS_REG_POWER_STATUS, &tps->pwr_status);
+ if (ret < 0) {
+ dev_err(tps->dev, "failed to read power status: %d\n", ret);
+ goto err_unregister_port;
+ }
+ ret = tps6598x_connect(tps, status);
+ if (ret)
+ dev_err(&client->dev, "failed to register partner\n");
+ }
+
+ ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ irq_handler,
+ IRQF_SHARED | IRQF_ONESHOT,
+ dev_name(&client->dev), tps);
+ if (ret) {
+ tps6598x_disconnect(tps, 0);
+ goto err_unregister_port;
+ }
+
+ i2c_set_clientdata(client, tps);
+ fwnode_handle_put(fwnode);
+
+ return 0;
+
+err_unregister_port:
+ typec_unregister_port(tps->port);
+err_role_put:
+ usb_role_switch_put(tps->role_sw);
+err_fwnode_put:
+ fwnode_handle_put(fwnode);
+err_clear_mask:
+ tps6598x_write64(tps, TPS_REG_INT_MASK1, 0);
+ return ret;
+}
+
+static void tps6598x_remove(struct i2c_client *client)
+{
+ struct tps6598x *tps = i2c_get_clientdata(client);
+
+ tps6598x_disconnect(tps, 0);
+ typec_unregister_port(tps->port);
+ usb_role_switch_put(tps->role_sw);
+}
+
+static const struct of_device_id tps6598x_of_match[] = {
+ { .compatible = "ti,tps6598x", },
+ { .compatible = "apple,cd321x", },
+ {}
+};
+MODULE_DEVICE_TABLE(of, tps6598x_of_match);
+
+static const struct i2c_device_id tps6598x_id[] = {
+ { "tps6598x" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, tps6598x_id);
+
+static struct i2c_driver tps6598x_i2c_driver = {
+ .driver = {
+ .name = "tps6598x",
+ .of_match_table = tps6598x_of_match,
+ },
+ .probe_new = tps6598x_probe,
+ .remove = tps6598x_remove,
+ .id_table = tps6598x_id,
+};
+module_i2c_driver(tps6598x_i2c_driver);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("TI TPS6598x USB Power Delivery Controller Driver");
diff --git a/drivers/usb/typec/tipd/tps6598x.h b/drivers/usb/typec/tipd/tps6598x.h
new file mode 100644
index 000000000..527857549
--- /dev/null
+++ b/drivers/usb/typec/tipd/tps6598x.h
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Driver for TI TPS6598x USB Power Delivery controller family
+ *
+ * Copyright (C) 2017, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+
+#ifndef __TPS6598X_H__
+#define __TPS6598X_H__
+
+#define TPS_FIELD_GET(_mask, _reg) ((typeof(_mask))(((_reg) & (_mask)) >> __bf_shf(_mask)))
+
+/* TPS_REG_STATUS bits */
+#define TPS_STATUS_PLUG_PRESENT BIT(0)
+#define TPS_STATUS_PLUG_UPSIDE_DOWN BIT(4)
+#define TPS_STATUS_TO_UPSIDE_DOWN(s) (!!((s) & TPS_STATUS_PLUG_UPSIDE_DOWN))
+#define TPS_STATUS_PORTROLE BIT(5)
+#define TPS_STATUS_TO_TYPEC_PORTROLE(s) (!!((s) & TPS_STATUS_PORTROLE))
+#define TPS_STATUS_DATAROLE BIT(6)
+#define TPS_STATUS_TO_TYPEC_DATAROLE(s) (!!((s) & TPS_STATUS_DATAROLE))
+#define TPS_STATUS_VCONN BIT(7)
+#define TPS_STATUS_TO_TYPEC_VCONN(s) (!!((s) & TPS_STATUS_VCONN))
+#define TPS_STATUS_OVERCURRENT BIT(16)
+#define TPS_STATUS_GOTO_MIN_ACTIVE BIT(26)
+#define TPS_STATUS_BIST BIT(27)
+#define TPS_STATUS_HIGH_VOLAGE_WARNING BIT(28)
+#define TPS_STATUS_HIGH_LOW_VOLTAGE_WARNING BIT(29)
+
+#define TPS_STATUS_CONN_STATE_MASK GENMASK(3, 1)
+#define TPS_STATUS_CONN_STATE(x) TPS_FIELD_GET(TPS_STATUS_CONN_STATE_MASK, (x))
+#define TPS_STATUS_PP_5V0_SWITCH_MASK GENMASK(9, 8)
+#define TPS_STATUS_PP_5V0_SWITCH(x) TPS_FIELD_GET(TPS_STATUS_PP_5V0_SWITCH_MASK, (x))
+#define TPS_STATUS_PP_HV_SWITCH_MASK GENMASK(11, 10)
+#define TPS_STATUS_PP_HV_SWITCH(x) TPS_FIELD_GET(TPS_STATUS_PP_HV_SWITCH_MASK, (x))
+#define TPS_STATUS_PP_EXT_SWITCH_MASK GENMASK(13, 12)
+#define TPS_STATUS_PP_EXT_SWITCH(x) TPS_FIELD_GET(TPS_STATUS_PP_EXT_SWITCH_MASK, (x))
+#define TPS_STATUS_PP_CABLE_SWITCH_MASK GENMASK(15, 14)
+#define TPS_STATUS_PP_CABLE_SWITCH(x) TPS_FIELD_GET(TPS_STATUS_PP_CABLE_SWITCH_MASK, (x))
+#define TPS_STATUS_POWER_SOURCE_MASK GENMASK(19, 18)
+#define TPS_STATUS_POWER_SOURCE(x) TPS_FIELD_GET(TPS_STATUS_POWER_SOURCE_MASK, (x))
+#define TPS_STATUS_VBUS_STATUS_MASK GENMASK(21, 20)
+#define TPS_STATUS_VBUS_STATUS(x) TPS_FIELD_GET(TPS_STATUS_VBUS_STATUS_MASK, (x))
+#define TPS_STATUS_USB_HOST_PRESENT_MASK GENMASK(23, 22)
+#define TPS_STATUS_USB_HOST_PRESENT(x) TPS_FIELD_GET(TPS_STATUS_USB_HOST_PRESENT_MASK, (x))
+#define TPS_STATUS_LEGACY_MASK GENMASK(25, 24)
+#define TPS_STATUS_LEGACY(x) TPS_FIELD_GET(TPS_STATUS_LEGACY_MASK, (x))
+
+#define TPS_STATUS_CONN_STATE_NO_CONN 0
+#define TPS_STATUS_CONN_STATE_DISABLED 1
+#define TPS_STATUS_CONN_STATE_AUDIO_CONN 2
+#define TPS_STATUS_CONN_STATE_DEBUG_CONN 3
+#define TPS_STATUS_CONN_STATE_NO_CONN_R_A 4
+#define TPS_STATUS_CONN_STATE_RESERVED 5
+#define TPS_STATUS_CONN_STATE_CONN_NO_R_A 6
+#define TPS_STATUS_CONN_STATE_CONN_WITH_R_A 7
+
+#define TPS_STATUS_PP_SWITCH_STATE_DISABLED 0
+#define TPS_STATUS_PP_SWITCH_STATE_FAULT 1
+#define TPS_STATUS_PP_SWITCH_STATE_OUT 2
+#define TPS_STATUS_PP_SWITCH_STATE_IN 3
+
+#define TPS_STATUS_POWER_SOURCE_UNKNOWN 0
+#define TPS_STATUS_POWER_SOURCE_VIN_3P3 1
+#define TPS_STATUS_POWER_SOURCE_DEAD_BAT 2
+#define TPS_STATUS_POWER_SOURCE_VBUS 3
+
+#define TPS_STATUS_VBUS_STATUS_VSAFE0V 0
+#define TPS_STATUS_VBUS_STATUS_VSAFE5V 1
+#define TPS_STATUS_VBUS_STATUS_PD 2
+#define TPS_STATUS_VBUS_STATUS_FAULT 3
+
+#define TPS_STATUS_USB_HOST_PRESENT_NO 0
+#define TPS_STATUS_USB_HOST_PRESENT_PD_NO_USB 1
+#define TPS_STATUS_USB_HOST_PRESENT_NO_PD 2
+#define TPS_STATUS_USB_HOST_PRESENT_PD_USB 3
+
+#define TPS_STATUS_LEGACY_NO 0
+#define TPS_STATUS_LEGACY_SINK 1
+#define TPS_STATUS_LEGACY_SOURCE 2
+
+/* TPS_REG_INT_* bits */
+#define TPS_REG_INT_USER_VID_ALT_MODE_OTHER_VDM BIT_ULL(27+32)
+#define TPS_REG_INT_USER_VID_ALT_MODE_ATTN_VDM BIT_ULL(26+32)
+#define TPS_REG_INT_USER_VID_ALT_MODE_EXIT BIT_ULL(25+32)
+#define TPS_REG_INT_USER_VID_ALT_MODE_ENTERED BIT_ULL(24+32)
+#define TPS_REG_INT_EXIT_MODES_COMPLETE BIT_ULL(20+32)
+#define TPS_REG_INT_DISCOVER_MODES_COMPLETE BIT_ULL(19+32)
+#define TPS_REG_INT_VDM_MSG_SENT BIT_ULL(18+32)
+#define TPS_REG_INT_VDM_ENTERED_MODE BIT_ULL(17+32)
+#define TPS_REG_INT_ERROR_UNABLE_TO_SOURCE BIT_ULL(14+32)
+#define TPS_REG_INT_SRC_TRANSITION BIT_ULL(10+32)
+#define TPS_REG_INT_ERROR_DISCHARGE_FAILED BIT_ULL(9+32)
+#define TPS_REG_INT_ERROR_MESSAGE_DATA BIT_ULL(7+32)
+#define TPS_REG_INT_ERROR_PROTOCOL_ERROR BIT_ULL(6+32)
+#define TPS_REG_INT_ERROR_MISSING_GET_CAP_MESSAGE BIT_ULL(4+32)
+#define TPS_REG_INT_ERROR_POWER_EVENT_OCCURRED BIT_ULL(3+32)
+#define TPS_REG_INT_ERROR_CAN_PROVIDE_PWR_LATER BIT_ULL(2+32)
+#define TPS_REG_INT_ERROR_CANNOT_PROVIDE_PWR BIT_ULL(1+32)
+#define TPS_REG_INT_ERROR_DEVICE_INCOMPATIBLE BIT_ULL(0+32)
+#define TPS_REG_INT_CMD2_COMPLETE BIT(31)
+#define TPS_REG_INT_CMD1_COMPLETE BIT(30)
+#define TPS_REG_INT_ADC_HIGH_THRESHOLD BIT(29)
+#define TPS_REG_INT_ADC_LOW_THRESHOLD BIT(28)
+#define TPS_REG_INT_PD_STATUS_UPDATE BIT(27)
+#define TPS_REG_INT_STATUS_UPDATE BIT(26)
+#define TPS_REG_INT_DATA_STATUS_UPDATE BIT(25)
+#define TPS_REG_INT_POWER_STATUS_UPDATE BIT(24)
+#define TPS_REG_INT_PP_SWITCH_CHANGED BIT(23)
+#define TPS_REG_INT_HIGH_VOLTAGE_WARNING BIT(22)
+#define TPS_REG_INT_USB_HOST_PRESENT_NO_LONGER BIT(21)
+#define TPS_REG_INT_USB_HOST_PRESENT BIT(20)
+#define TPS_REG_INT_GOTO_MIN_RECEIVED BIT(19)
+#define TPS_REG_INT_PR_SWAP_REQUESTED BIT(17)
+#define TPS_REG_INT_SINK_CAP_MESSAGE_READY BIT(15)
+#define TPS_REG_INT_SOURCE_CAP_MESSAGE_READY BIT(14)
+#define TPS_REG_INT_NEW_CONTRACT_AS_PROVIDER BIT(13)
+#define TPS_REG_INT_NEW_CONTRACT_AS_CONSUMER BIT(12)
+#define TPS_REG_INT_VDM_RECEIVED BIT(11)
+#define TPS_REG_INT_ATTENTION_RECEIVED BIT(10)
+#define TPS_REG_INT_OVERCURRENT BIT(9)
+#define TPS_REG_INT_BIST BIT(8)
+#define TPS_REG_INT_RDO_RECEIVED_FROM_SINK BIT(7)
+#define TPS_REG_INT_DR_SWAP_COMPLETE BIT(5)
+#define TPS_REG_INT_PR_SWAP_COMPLETE BIT(4)
+#define TPS_REG_INT_PLUG_EVENT BIT(3)
+#define TPS_REG_INT_HARD_RESET BIT(1)
+#define TPS_REG_INT_PD_SOFT_RESET BIT(0)
+
+/* Apple-specific TPS_REG_INT_* bits */
+#define APPLE_CD_REG_INT_DATA_STATUS_UPDATE BIT(10)
+#define APPLE_CD_REG_INT_POWER_STATUS_UPDATE BIT(9)
+#define APPLE_CD_REG_INT_STATUS_UPDATE BIT(8)
+#define APPLE_CD_REG_INT_PLUG_EVENT BIT(1)
+
+/* TPS_REG_SYSTEM_POWER_STATE states */
+#define TPS_SYSTEM_POWER_STATE_S0 0x00
+#define TPS_SYSTEM_POWER_STATE_S3 0x03
+#define TPS_SYSTEM_POWER_STATE_S4 0x04
+#define TPS_SYSTEM_POWER_STATE_S5 0x05
+
+/* TPS_REG_POWER_STATUS bits */
+#define TPS_POWER_STATUS_CONNECTION(x) TPS_FIELD_GET(BIT(0), (x))
+#define TPS_POWER_STATUS_SOURCESINK(x) TPS_FIELD_GET(BIT(1), (x))
+#define TPS_POWER_STATUS_BC12_DET(x) TPS_FIELD_GET(BIT(2), (x))
+
+#define TPS_POWER_STATUS_TYPEC_CURRENT_MASK GENMASK(3, 2)
+#define TPS_POWER_STATUS_PWROPMODE(p) TPS_FIELD_GET(TPS_POWER_STATUS_TYPEC_CURRENT_MASK, (p))
+#define TPS_POWER_STATUS_BC12_STATUS_MASK GENMASK(6, 5)
+#define TPS_POWER_STATUS_BC12_STATUS(p) TPS_FIELD_GET(TPS_POWER_STATUS_BC12_STATUS_MASK, (p))
+
+#define TPS_POWER_STATUS_TYPEC_CURRENT_USB 0
+#define TPS_POWER_STATUS_TYPEC_CURRENT_1A5 1
+#define TPS_POWER_STATUS_TYPEC_CURRENT_3A0 2
+#define TPS_POWER_STATUS_TYPEC_CURRENT_PD 3
+
+#define TPS_POWER_STATUS_BC12_STATUS_SDP 0
+#define TPS_POWER_STATUS_BC12_STATUS_CDP 2
+#define TPS_POWER_STATUS_BC12_STATUS_DCP 3
+
+/* TPS_REG_DATA_STATUS bits */
+#define TPS_DATA_STATUS_DATA_CONNECTION BIT(0)
+#define TPS_DATA_STATUS_UPSIDE_DOWN BIT(1)
+#define TPS_DATA_STATUS_ACTIVE_CABLE BIT(2)
+#define TPS_DATA_STATUS_USB2_CONNECTION BIT(4)
+#define TPS_DATA_STATUS_USB3_CONNECTION BIT(5)
+#define TPS_DATA_STATUS_USB3_GEN2 BIT(6)
+#define TPS_DATA_STATUS_USB_DATA_ROLE BIT(7)
+#define TPS_DATA_STATUS_DP_CONNECTION BIT(8)
+#define TPS_DATA_STATUS_DP_SINK BIT(9)
+#define TPS_DATA_STATUS_TBT_CONNECTION BIT(16)
+#define TPS_DATA_STATUS_TBT_TYPE BIT(17)
+#define TPS_DATA_STATUS_OPTICAL_CABLE BIT(18)
+#define TPS_DATA_STATUS_ACTIVE_LINK_TRAIN BIT(20)
+#define TPS_DATA_STATUS_FORCE_LSX BIT(23)
+#define TPS_DATA_STATUS_POWER_MISMATCH BIT(24)
+
+#define TPS_DATA_STATUS_DP_PIN_ASSIGNMENT_MASK GENMASK(11, 10)
+#define TPS_DATA_STATUS_DP_PIN_ASSIGNMENT(x) \
+ TPS_FIELD_GET(TPS_DATA_STATUS_DP_PIN_ASSIGNMENT_MASK, (x))
+#define TPS_DATA_STATUS_TBT_CABLE_SPEED_MASK GENMASK(27, 25)
+#define TPS_DATA_STATUS_TBT_CABLE_SPEED \
+ TPS_FIELD_GET(TPS_DATA_STATUS_TBT_CABLE_SPEED_MASK, (x))
+#define TPS_DATA_STATUS_TBT_CABLE_GEN_MASK GENMASK(29, 28)
+#define TPS_DATA_STATUS_TBT_CABLE_GEN \
+ TPS_FIELD_GET(TPS_DATA_STATUS_TBT_CABLE_GEN_MASK, (x))
+
+/* Map data status to DP spec assignments */
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT(x) \
+ ((TPS_DATA_STATUS_DP_PIN_ASSIGNMENT(x) << 1) | \
+ TPS_FIELD_GET(TPS_DATA_STATUS_USB3_CONNECTION, (x)))
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_E 0
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_F BIT(0)
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_C BIT(1)
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_D (BIT(1) | BIT(0))
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_A BIT(2)
+#define TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_B (BIT(2) | BIT(1))
+
+#endif /* __TPS6598X_H__ */
diff --git a/drivers/usb/typec/tipd/trace.c b/drivers/usb/typec/tipd/trace.c
new file mode 100644
index 000000000..016e68048
--- /dev/null
+++ b/drivers/usb/typec/tipd/trace.c
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TI TPS6598x USB Power Delivery Controller Trace Support
+ *
+ * Copyright (C) 2021, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+#define CREATE_TRACE_POINTS
+#include "trace.h"
diff --git a/drivers/usb/typec/tipd/trace.h b/drivers/usb/typec/tipd/trace.h
new file mode 100644
index 000000000..12cad1bde
--- /dev/null
+++ b/drivers/usb/typec/tipd/trace.h
@@ -0,0 +1,306 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Driver for TI TPS6598x USB Power Delivery controller family
+ *
+ * Copyright (C) 2020 Purism SPC
+ * Author: Guido Günther <agx@sigxcpu.org>
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM tps6598x
+
+#if !defined(_TPS6598X_TRACE_H_) || defined(TRACE_HEADER_MULTI_READ)
+#define _TPS6598X_TRACE_H_
+
+#include "tps6598x.h"
+
+#include <linux/stringify.h>
+#include <linux/types.h>
+#include <linux/tracepoint.h>
+
+#define show_irq_flags(flags) \
+ __print_flags_u64(flags, "|", \
+ { TPS_REG_INT_PD_SOFT_RESET, "PD_SOFT_RESET" }, \
+ { TPS_REG_INT_HARD_RESET, "HARD_RESET" }, \
+ { TPS_REG_INT_PLUG_EVENT, "PLUG_EVENT" }, \
+ { TPS_REG_INT_PR_SWAP_COMPLETE, "PR_SWAP_COMPLETE" }, \
+ { TPS_REG_INT_DR_SWAP_COMPLETE, "DR_SWAP_COMPLETE" }, \
+ { TPS_REG_INT_RDO_RECEIVED_FROM_SINK, "RDO_RECEIVED_FROM_SINK" }, \
+ { TPS_REG_INT_BIST, "BIST" }, \
+ { TPS_REG_INT_OVERCURRENT, "OVERCURRENT" }, \
+ { TPS_REG_INT_ATTENTION_RECEIVED, "ATTENTION_RECEIVED" }, \
+ { TPS_REG_INT_VDM_RECEIVED, "VDM_RECEIVED" }, \
+ { TPS_REG_INT_NEW_CONTRACT_AS_CONSUMER, "NEW_CONTRACT_AS_CONSUMER" }, \
+ { TPS_REG_INT_NEW_CONTRACT_AS_PROVIDER, "NEW_CONTRACT_AS_PROVIDER" }, \
+ { TPS_REG_INT_SOURCE_CAP_MESSAGE_READY, "SOURCE_CAP_MESSAGE_READY" }, \
+ { TPS_REG_INT_SINK_CAP_MESSAGE_READY, "SINK_CAP_MESSAGE_READY" }, \
+ { TPS_REG_INT_PR_SWAP_REQUESTED, "PR_SWAP_REQUESTED" }, \
+ { TPS_REG_INT_GOTO_MIN_RECEIVED, "GOTO_MIN_RECEIVED" }, \
+ { TPS_REG_INT_USB_HOST_PRESENT, "USB_HOST_PRESENT" }, \
+ { TPS_REG_INT_USB_HOST_PRESENT_NO_LONGER, "USB_HOST_PRESENT_NO_LONGER" }, \
+ { TPS_REG_INT_HIGH_VOLTAGE_WARNING, "HIGH_VOLTAGE_WARNING" }, \
+ { TPS_REG_INT_PP_SWITCH_CHANGED, "PP_SWITCH_CHANGED" }, \
+ { TPS_REG_INT_POWER_STATUS_UPDATE, "POWER_STATUS_UPDATE" }, \
+ { TPS_REG_INT_DATA_STATUS_UPDATE, "DATA_STATUS_UPDATE" }, \
+ { TPS_REG_INT_STATUS_UPDATE, "STATUS_UPDATE" }, \
+ { TPS_REG_INT_PD_STATUS_UPDATE, "PD_STATUS_UPDATE" }, \
+ { TPS_REG_INT_ADC_LOW_THRESHOLD, "ADC_LOW_THRESHOLD" }, \
+ { TPS_REG_INT_ADC_HIGH_THRESHOLD, "ADC_HIGH_THRESHOLD" }, \
+ { TPS_REG_INT_CMD1_COMPLETE, "CMD1_COMPLETE" }, \
+ { TPS_REG_INT_CMD2_COMPLETE, "CMD2_COMPLETE" }, \
+ { TPS_REG_INT_ERROR_DEVICE_INCOMPATIBLE, "ERROR_DEVICE_INCOMPATIBLE" }, \
+ { TPS_REG_INT_ERROR_CANNOT_PROVIDE_PWR, "ERROR_CANNOT_PROVIDE_PWR" }, \
+ { TPS_REG_INT_ERROR_CAN_PROVIDE_PWR_LATER, "ERROR_CAN_PROVIDE_PWR_LATER" }, \
+ { TPS_REG_INT_ERROR_POWER_EVENT_OCCURRED, "ERROR_POWER_EVENT_OCCURRED" }, \
+ { TPS_REG_INT_ERROR_MISSING_GET_CAP_MESSAGE, "ERROR_MISSING_GET_CAP_MESSAGE" }, \
+ { TPS_REG_INT_ERROR_PROTOCOL_ERROR, "ERROR_PROTOCOL_ERROR" }, \
+ { TPS_REG_INT_ERROR_MESSAGE_DATA, "ERROR_MESSAGE_DATA" }, \
+ { TPS_REG_INT_ERROR_DISCHARGE_FAILED, "ERROR_DISCHARGE_FAILED" }, \
+ { TPS_REG_INT_SRC_TRANSITION, "SRC_TRANSITION" }, \
+ { TPS_REG_INT_ERROR_UNABLE_TO_SOURCE, "ERROR_UNABLE_TO_SOURCE" }, \
+ { TPS_REG_INT_VDM_ENTERED_MODE, "VDM_ENTERED_MODE" }, \
+ { TPS_REG_INT_VDM_MSG_SENT, "VDM_MSG_SENT" }, \
+ { TPS_REG_INT_DISCOVER_MODES_COMPLETE, "DISCOVER_MODES_COMPLETE" }, \
+ { TPS_REG_INT_EXIT_MODES_COMPLETE, "EXIT_MODES_COMPLETE" }, \
+ { TPS_REG_INT_USER_VID_ALT_MODE_ENTERED, "USER_VID_ALT_MODE_ENTERED" }, \
+ { TPS_REG_INT_USER_VID_ALT_MODE_EXIT, "USER_VID_ALT_MODE_EXIT" }, \
+ { TPS_REG_INT_USER_VID_ALT_MODE_ATTN_VDM, "USER_VID_ALT_MODE_ATTN_VDM" }, \
+ { TPS_REG_INT_USER_VID_ALT_MODE_OTHER_VDM, "USER_VID_ALT_MODE_OTHER_VDM" })
+
+#define show_cd321x_irq_flags(flags) \
+ __print_flags_u64(flags, "|", \
+ { APPLE_CD_REG_INT_PLUG_EVENT, "PLUG_EVENT" }, \
+ { APPLE_CD_REG_INT_POWER_STATUS_UPDATE, "POWER_STATUS_UPDATE" }, \
+ { APPLE_CD_REG_INT_DATA_STATUS_UPDATE, "DATA_STATUS_UPDATE" }, \
+ { APPLE_CD_REG_INT_STATUS_UPDATE, "STATUS_UPDATE" })
+
+#define TPS6598X_STATUS_FLAGS_MASK (GENMASK(31, 0) ^ (TPS_STATUS_CONN_STATE_MASK | \
+ TPS_STATUS_PP_5V0_SWITCH_MASK | \
+ TPS_STATUS_PP_HV_SWITCH_MASK | \
+ TPS_STATUS_PP_EXT_SWITCH_MASK | \
+ TPS_STATUS_PP_CABLE_SWITCH_MASK | \
+ TPS_STATUS_POWER_SOURCE_MASK | \
+ TPS_STATUS_VBUS_STATUS_MASK | \
+ TPS_STATUS_USB_HOST_PRESENT_MASK | \
+ TPS_STATUS_LEGACY_MASK))
+
+#define show_status_conn_state(status) \
+ __print_symbolic(TPS_STATUS_CONN_STATE((status)), \
+ { TPS_STATUS_CONN_STATE_CONN_WITH_R_A, "conn-Ra" }, \
+ { TPS_STATUS_CONN_STATE_CONN_NO_R_A, "conn-no-Ra" }, \
+ { TPS_STATUS_CONN_STATE_NO_CONN_R_A, "no-conn-Ra" }, \
+ { TPS_STATUS_CONN_STATE_DEBUG_CONN, "debug" }, \
+ { TPS_STATUS_CONN_STATE_AUDIO_CONN, "audio" }, \
+ { TPS_STATUS_CONN_STATE_DISABLED, "disabled" }, \
+ { TPS_STATUS_CONN_STATE_NO_CONN, "no-conn" })
+
+#define show_status_pp_switch_state(status) \
+ __print_symbolic(status, \
+ { TPS_STATUS_PP_SWITCH_STATE_IN, "in" }, \
+ { TPS_STATUS_PP_SWITCH_STATE_OUT, "out" }, \
+ { TPS_STATUS_PP_SWITCH_STATE_FAULT, "fault" }, \
+ { TPS_STATUS_PP_SWITCH_STATE_DISABLED, "off" })
+
+#define show_status_power_sources(status) \
+ __print_symbolic(TPS_STATUS_POWER_SOURCE(status), \
+ { TPS_STATUS_POWER_SOURCE_VBUS, "vbus" }, \
+ { TPS_STATUS_POWER_SOURCE_VIN_3P3, "vin-3p3" }, \
+ { TPS_STATUS_POWER_SOURCE_DEAD_BAT, "dead-battery" }, \
+ { TPS_STATUS_POWER_SOURCE_UNKNOWN, "unknown" })
+
+#define show_status_vbus_status(status) \
+ __print_symbolic(TPS_STATUS_VBUS_STATUS(status), \
+ { TPS_STATUS_VBUS_STATUS_VSAFE0V, "vSafe0V" }, \
+ { TPS_STATUS_VBUS_STATUS_VSAFE5V, "vSafe5V" }, \
+ { TPS_STATUS_VBUS_STATUS_PD, "pd" }, \
+ { TPS_STATUS_VBUS_STATUS_FAULT, "fault" })
+
+#define show_status_usb_host_present(status) \
+ __print_symbolic(TPS_STATUS_USB_HOST_PRESENT(status), \
+ { TPS_STATUS_USB_HOST_PRESENT_PD_USB, "pd-usb" }, \
+ { TPS_STATUS_USB_HOST_PRESENT_NO_PD, "no-pd" }, \
+ { TPS_STATUS_USB_HOST_PRESENT_PD_NO_USB, "pd-no-usb" }, \
+ { TPS_STATUS_USB_HOST_PRESENT_NO, "no" })
+
+#define show_status_legacy(status) \
+ __print_symbolic(TPS_STATUS_LEGACY(status), \
+ { TPS_STATUS_LEGACY_SOURCE, "source" }, \
+ { TPS_STATUS_LEGACY_SINK, "sink" }, \
+ { TPS_STATUS_LEGACY_NO, "no" })
+
+#define show_status_flags(flags) \
+ __print_flags((flags & TPS6598X_STATUS_FLAGS_MASK), "|", \
+ { TPS_STATUS_PLUG_PRESENT, "PLUG_PRESENT" }, \
+ { TPS_STATUS_PLUG_UPSIDE_DOWN, "UPSIDE_DOWN" }, \
+ { TPS_STATUS_PORTROLE, "PORTROLE" }, \
+ { TPS_STATUS_DATAROLE, "DATAROLE" }, \
+ { TPS_STATUS_VCONN, "VCONN" }, \
+ { TPS_STATUS_OVERCURRENT, "OVERCURRENT" }, \
+ { TPS_STATUS_GOTO_MIN_ACTIVE, "GOTO_MIN_ACTIVE" }, \
+ { TPS_STATUS_BIST, "BIST" }, \
+ { TPS_STATUS_HIGH_VOLAGE_WARNING, "HIGH_VOLAGE_WARNING" }, \
+ { TPS_STATUS_HIGH_LOW_VOLTAGE_WARNING, "HIGH_LOW_VOLTAGE_WARNING" })
+
+#define show_power_status_source_sink(power_status) \
+ __print_symbolic(TPS_POWER_STATUS_SOURCESINK(power_status), \
+ { 1, "sink" }, \
+ { 0, "source" })
+
+#define show_power_status_typec_status(power_status) \
+ __print_symbolic(TPS_POWER_STATUS_PWROPMODE(power_status), \
+ { TPS_POWER_STATUS_TYPEC_CURRENT_PD, "pd" }, \
+ { TPS_POWER_STATUS_TYPEC_CURRENT_3A0, "3.0A" }, \
+ { TPS_POWER_STATUS_TYPEC_CURRENT_1A5, "1.5A" }, \
+ { TPS_POWER_STATUS_TYPEC_CURRENT_USB, "usb" })
+
+#define show_power_status_bc12_status(power_status) \
+ __print_symbolic(TPS_POWER_STATUS_BC12_STATUS(power_status), \
+ { TPS_POWER_STATUS_BC12_STATUS_DCP, "dcp" }, \
+ { TPS_POWER_STATUS_BC12_STATUS_CDP, "cdp" }, \
+ { TPS_POWER_STATUS_BC12_STATUS_SDP, "sdp" })
+
+#define TPS_DATA_STATUS_FLAGS_MASK (GENMASK(31, 0) ^ (TPS_DATA_STATUS_DP_PIN_ASSIGNMENT_MASK | \
+ TPS_DATA_STATUS_TBT_CABLE_SPEED_MASK | \
+ TPS_DATA_STATUS_TBT_CABLE_GEN_MASK))
+
+#define show_data_status_flags(data_status) \
+ __print_flags(data_status & TPS_DATA_STATUS_FLAGS_MASK, "|", \
+ { TPS_DATA_STATUS_DATA_CONNECTION, "DATA_CONNECTION" }, \
+ { TPS_DATA_STATUS_UPSIDE_DOWN, "DATA_UPSIDE_DOWN" }, \
+ { TPS_DATA_STATUS_ACTIVE_CABLE, "ACTIVE_CABLE" }, \
+ { TPS_DATA_STATUS_USB2_CONNECTION, "USB2_CONNECTION" }, \
+ { TPS_DATA_STATUS_USB3_CONNECTION, "USB3_CONNECTION" }, \
+ { TPS_DATA_STATUS_USB3_GEN2, "USB3_GEN2" }, \
+ { TPS_DATA_STATUS_USB_DATA_ROLE, "USB_DATA_ROLE" }, \
+ { TPS_DATA_STATUS_DP_CONNECTION, "DP_CONNECTION" }, \
+ { TPS_DATA_STATUS_DP_SINK, "DP_SINK" }, \
+ { TPS_DATA_STATUS_TBT_CONNECTION, "TBT_CONNECTION" }, \
+ { TPS_DATA_STATUS_TBT_TYPE, "TBT_TYPE" }, \
+ { TPS_DATA_STATUS_OPTICAL_CABLE, "OPTICAL_CABLE" }, \
+ { TPS_DATA_STATUS_ACTIVE_LINK_TRAIN, "ACTIVE_LINK_TRAIN" }, \
+ { TPS_DATA_STATUS_FORCE_LSX, "FORCE_LSX" }, \
+ { TPS_DATA_STATUS_POWER_MISMATCH, "POWER_MISMATCH" })
+
+#define show_data_status_dp_pin_assignment(data_status) \
+ __print_symbolic(TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT(data_status), \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_E, "E" }, \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_F, "F" }, \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_C, "C" }, \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_D, "D" }, \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_A, "A" }, \
+ { TPS_DATA_STATUS_DP_SPEC_PIN_ASSIGNMENT_B, "B" })
+
+#define maybe_show_data_status_dp_pin_assignment(data_status) \
+ (data_status & TPS_DATA_STATUS_DP_CONNECTION ? \
+ show_data_status_dp_pin_assignment(data_status) : "")
+
+TRACE_EVENT(tps6598x_irq,
+ TP_PROTO(u64 event1,
+ u64 event2),
+ TP_ARGS(event1, event2),
+
+ TP_STRUCT__entry(
+ __field(u64, event1)
+ __field(u64, event2)
+ ),
+
+ TP_fast_assign(
+ __entry->event1 = event1;
+ __entry->event2 = event2;
+ ),
+
+ TP_printk("event1=%s, event2=%s",
+ show_irq_flags(__entry->event1),
+ show_irq_flags(__entry->event2))
+);
+
+TRACE_EVENT(cd321x_irq,
+ TP_PROTO(u64 event),
+ TP_ARGS(event),
+
+ TP_STRUCT__entry(
+ __field(u64, event)
+ ),
+
+ TP_fast_assign(
+ __entry->event = event;
+ ),
+
+ TP_printk("event=%s",
+ show_cd321x_irq_flags(__entry->event))
+);
+
+TRACE_EVENT(tps6598x_status,
+ TP_PROTO(u32 status),
+ TP_ARGS(status),
+
+ TP_STRUCT__entry(
+ __field(u32, status)
+ ),
+
+ TP_fast_assign(
+ __entry->status = status;
+ ),
+
+ TP_printk("conn: %s, pp_5v0: %s, pp_hv: %s, pp_ext: %s, pp_cable: %s, "
+ "pwr-src: %s, vbus: %s, usb-host: %s, legacy: %s, flags: %s",
+ show_status_conn_state(__entry->status),
+ show_status_pp_switch_state(TPS_STATUS_PP_5V0_SWITCH(__entry->status)),
+ show_status_pp_switch_state(TPS_STATUS_PP_HV_SWITCH(__entry->status)),
+ show_status_pp_switch_state(TPS_STATUS_PP_EXT_SWITCH(__entry->status)),
+ show_status_pp_switch_state(TPS_STATUS_PP_CABLE_SWITCH(__entry->status)),
+ show_status_power_sources(__entry->status),
+ show_status_vbus_status(__entry->status),
+ show_status_usb_host_present(__entry->status),
+ show_status_legacy(__entry->status),
+ show_status_flags(__entry->status)
+ )
+);
+
+TRACE_EVENT(tps6598x_power_status,
+ TP_PROTO(u16 power_status),
+ TP_ARGS(power_status),
+
+ TP_STRUCT__entry(
+ __field(u16, power_status)
+ ),
+
+ TP_fast_assign(
+ __entry->power_status = power_status;
+ ),
+
+ TP_printk("conn: %d, pwr-role: %s, typec: %s, bc: %s",
+ !!TPS_POWER_STATUS_CONNECTION(__entry->power_status),
+ show_power_status_source_sink(__entry->power_status),
+ show_power_status_typec_status(__entry->power_status),
+ show_power_status_bc12_status(__entry->power_status)
+ )
+);
+
+TRACE_EVENT(tps6598x_data_status,
+ TP_PROTO(u32 data_status),
+ TP_ARGS(data_status),
+
+ TP_STRUCT__entry(
+ __field(u32, data_status)
+ ),
+
+ TP_fast_assign(
+ __entry->data_status = data_status;
+ ),
+
+ TP_printk("%s%s%s",
+ show_data_status_flags(__entry->data_status),
+ __entry->data_status & TPS_DATA_STATUS_DP_CONNECTION ? ", DP pinout " : "",
+ maybe_show_data_status_dp_pin_assignment(__entry->data_status)
+ )
+);
+
+#endif /* _TPS6598X_TRACE_H_ */
+
+/* This part must be outside protection */
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_FILE trace
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH .
+#include <trace/define_trace.h>
diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig
new file mode 100644
index 000000000..8f9c4b9f3
--- /dev/null
+++ b/drivers/usb/typec/ucsi/Kconfig
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config TYPEC_UCSI
+ tristate "USB Type-C Connector System Software Interface driver"
+ depends on !CPU_BIG_ENDIAN
+ depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+ help
+ USB Type-C Connector System Software Interface (UCSI) is a
+ specification for an interface that allows the operating system to
+ control the USB Type-C ports. On UCSI system the USB Type-C ports
+ function autonomously by default, but in order to get the status of
+ the ports and support basic operations like role swapping, the driver
+ is required. UCSI is available on most of the new Intel based systems
+ that are equipped with Embedded Controller and USB Type-C ports.
+
+ UCSI specification does not define the interface method, so depending
+ on the platform, ACPI, PCI, I2C, etc. may be used. Therefore this
+ driver only provides the core part, and separate drivers are needed
+ for every supported interface method.
+
+ The UCSI specification can be downloaded from:
+ https://www.intel.com/content/www/us/en/io/universal-serial-bus/usb-type-c-ucsi-spec.html
+
+ To compile the driver as a module, choose M here: the module will be
+ called typec_ucsi.
+
+if TYPEC_UCSI
+
+config UCSI_CCG
+ tristate "UCSI Interface Driver for Cypress CCGx"
+ depends on I2C
+ help
+ This driver enables UCSI support on platforms that expose a
+ Cypress CCGx Type-C controller over I2C interface.
+
+ To compile the driver as a module, choose M here: the module will be
+ called ucsi_ccg.
+
+config UCSI_ACPI
+ tristate "UCSI ACPI Interface Driver"
+ depends on ACPI
+ help
+ This driver enables UCSI support on platforms that expose UCSI
+ interface as ACPI device. On new Intel Atom based platforms starting
+ from Broxton SoCs and Core platforms stating from Skylake, UCSI is an
+ ACPI enumerated device.
+
+ To compile the driver as a module, choose M here: the module will be
+ called ucsi_acpi
+
+config UCSI_STM32G0
+ tristate "UCSI Interface Driver for STM32G0"
+ depends on I2C
+ help
+ This driver enables UCSI support on platforms that expose a STM32G0
+ Type-C controller over I2C interface.
+
+ To compile the driver as a module, choose M here: the module will be
+ called ucsi_stm32g0.
+
+endif
diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile
new file mode 100644
index 000000000..480d533d7
--- /dev/null
+++ b/drivers/usb/typec/ucsi/Makefile
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS_trace.o := -I$(src)
+
+obj-$(CONFIG_TYPEC_UCSI) += typec_ucsi.o
+
+typec_ucsi-y := ucsi.o
+
+typec_ucsi-$(CONFIG_TRACING) += trace.o
+
+ifneq ($(CONFIG_POWER_SUPPLY),)
+ typec_ucsi-y += psy.o
+endif
+
+ifneq ($(CONFIG_TYPEC_DP_ALTMODE),)
+ typec_ucsi-y += displayport.o
+endif
+
+obj-$(CONFIG_UCSI_ACPI) += ucsi_acpi.o
+obj-$(CONFIG_UCSI_CCG) += ucsi_ccg.o
+obj-$(CONFIG_UCSI_STM32G0) += ucsi_stm32g0.o
diff --git a/drivers/usb/typec/ucsi/displayport.c b/drivers/usb/typec/ucsi/displayport.c
new file mode 100644
index 000000000..73cd5bf35
--- /dev/null
+++ b/drivers/usb/typec/ucsi/displayport.c
@@ -0,0 +1,344 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI DisplayPort Alternate Mode Support
+ *
+ * Copyright (C) 2018, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/pd_vdo.h>
+
+#include "ucsi.h"
+
+#define UCSI_CMD_SET_NEW_CAM(_con_num_, _enter_, _cam_, _am_) \
+ (UCSI_SET_NEW_CAM | ((_con_num_) << 16) | ((_enter_) << 23) | \
+ ((_cam_) << 24) | ((u64)(_am_) << 32))
+
+struct ucsi_dp {
+ struct typec_displayport_data data;
+ struct ucsi_connector *con;
+ struct typec_altmode *alt;
+ struct work_struct work;
+ int offset;
+
+ bool override;
+ bool initialized;
+
+ u32 header;
+ u32 *vdo_data;
+ u8 vdo_size;
+};
+
+/*
+ * Note. Alternate mode control is optional feature in UCSI. It means that even
+ * if the system supports alternate modes, the OS may not be aware of them.
+ *
+ * In most cases however, the OS will be able to see the supported alternate
+ * modes, but it may still not be able to configure them, not even enter or exit
+ * them. That is because UCSI defines alt mode details and alt mode "overriding"
+ * as separate options.
+ *
+ * In case alt mode details are supported, but overriding is not, the driver
+ * will still display the supported pin assignments and configuration, but any
+ * changes the user attempts to do will lead into failure with return value of
+ * -EOPNOTSUPP.
+ */
+
+static int ucsi_displayport_enter(struct typec_altmode *alt, u32 *vdo)
+{
+ struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+ struct ucsi *ucsi = dp->con->ucsi;
+ int svdm_version;
+ u64 command;
+ u8 cur = 0;
+ int ret;
+
+ mutex_lock(&dp->con->lock);
+
+ if (!dp->override && dp->initialized) {
+ const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+ dev_warn(&p->dev,
+ "firmware doesn't support alternate mode overriding\n");
+ ret = -EOPNOTSUPP;
+ goto err_unlock;
+ }
+
+ command = UCSI_GET_CURRENT_CAM | UCSI_CONNECTOR_NUMBER(dp->con->num);
+ ret = ucsi_send_command(ucsi, command, &cur, sizeof(cur));
+ if (ret < 0) {
+ if (ucsi->version > 0x0100)
+ goto err_unlock;
+ cur = 0xff;
+ }
+
+ if (cur != 0xff) {
+ ret = dp->con->port_altmode[cur] == alt ? 0 : -EBUSY;
+ goto err_unlock;
+ }
+
+ /*
+ * We can't send the New CAM command yet to the PPM as it needs the
+ * configuration value as well. Pretending that we have now entered the
+ * mode, and letting the alt mode driver continue.
+ */
+
+ svdm_version = typec_altmode_get_svdm_version(alt);
+ if (svdm_version < 0) {
+ ret = svdm_version;
+ goto err_unlock;
+ }
+
+ dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_ENTER_MODE);
+ dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+ dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+
+ dp->vdo_data = NULL;
+ dp->vdo_size = 1;
+
+ schedule_work(&dp->work);
+ ret = 0;
+err_unlock:
+ mutex_unlock(&dp->con->lock);
+
+ return ret;
+}
+
+static int ucsi_displayport_exit(struct typec_altmode *alt)
+{
+ struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+ int svdm_version;
+ u64 command;
+ int ret = 0;
+
+ mutex_lock(&dp->con->lock);
+
+ if (!dp->override) {
+ const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+ dev_warn(&p->dev,
+ "firmware doesn't support alternate mode overriding\n");
+ ret = -EOPNOTSUPP;
+ goto out_unlock;
+ }
+
+ command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 0, dp->offset, 0);
+ ret = ucsi_send_command(dp->con->ucsi, command, NULL, 0);
+ if (ret < 0)
+ goto out_unlock;
+
+ svdm_version = typec_altmode_get_svdm_version(alt);
+ if (svdm_version < 0) {
+ ret = svdm_version;
+ goto out_unlock;
+ }
+
+ dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_EXIT_MODE);
+ dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+ dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+
+ dp->vdo_data = NULL;
+ dp->vdo_size = 1;
+
+ schedule_work(&dp->work);
+
+out_unlock:
+ mutex_unlock(&dp->con->lock);
+
+ return ret;
+}
+
+/*
+ * We do not actually have access to the Status Update VDO, so we have to guess
+ * things.
+ */
+static int ucsi_displayport_status_update(struct ucsi_dp *dp)
+{
+ u32 cap = dp->alt->vdo;
+
+ dp->data.status = DP_STATUS_ENABLED;
+
+ /*
+ * If pin assignement D is supported, claiming always
+ * that Multi-function is preferred.
+ */
+ if (DP_CAP_CAPABILITY(cap) & DP_CAP_UFP_D) {
+ dp->data.status |= DP_STATUS_CON_UFP_D;
+
+ if (DP_CAP_UFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
+ dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
+ } else {
+ dp->data.status |= DP_STATUS_CON_DFP_D;
+
+ if (DP_CAP_DFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
+ dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
+ }
+
+ dp->vdo_data = &dp->data.status;
+ dp->vdo_size = 2;
+
+ return 0;
+}
+
+static int ucsi_displayport_configure(struct ucsi_dp *dp)
+{
+ u32 pins = DP_CONF_GET_PIN_ASSIGN(dp->data.conf);
+ u64 command;
+
+ if (!dp->override)
+ return 0;
+
+ command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 1, dp->offset, pins);
+
+ return ucsi_send_command(dp->con->ucsi, command, NULL, 0);
+}
+
+static int ucsi_displayport_vdm(struct typec_altmode *alt,
+ u32 header, const u32 *data, int count)
+{
+ struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+ int cmd_type = PD_VDO_CMDT(header);
+ int cmd = PD_VDO_CMD(header);
+ int svdm_version;
+
+ mutex_lock(&dp->con->lock);
+
+ if (!dp->override && dp->initialized) {
+ const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+ dev_warn(&p->dev,
+ "firmware doesn't support alternate mode overriding\n");
+ mutex_unlock(&dp->con->lock);
+ return -EOPNOTSUPP;
+ }
+
+ svdm_version = typec_altmode_get_svdm_version(alt);
+ if (svdm_version < 0) {
+ mutex_unlock(&dp->con->lock);
+ return svdm_version;
+ }
+
+ switch (cmd_type) {
+ case CMDT_INIT:
+ if (PD_VDO_SVDM_VER(header) < svdm_version) {
+ typec_partner_set_svdm_version(dp->con->partner, PD_VDO_SVDM_VER(header));
+ svdm_version = PD_VDO_SVDM_VER(header);
+ }
+
+ dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, cmd);
+ dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+
+ switch (cmd) {
+ case DP_CMD_STATUS_UPDATE:
+ if (ucsi_displayport_status_update(dp))
+ dp->header |= VDO_CMDT(CMDT_RSP_NAK);
+ else
+ dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+ break;
+ case DP_CMD_CONFIGURE:
+ dp->data.conf = *data;
+ if (ucsi_displayport_configure(dp)) {
+ dp->header |= VDO_CMDT(CMDT_RSP_NAK);
+ } else {
+ dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+ if (dp->initialized)
+ ucsi_altmode_update_active(dp->con);
+ else
+ dp->initialized = true;
+ }
+ break;
+ default:
+ dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+ break;
+ }
+
+ schedule_work(&dp->work);
+ break;
+ default:
+ break;
+ }
+
+ mutex_unlock(&dp->con->lock);
+
+ return 0;
+}
+
+static const struct typec_altmode_ops ucsi_displayport_ops = {
+ .enter = ucsi_displayport_enter,
+ .exit = ucsi_displayport_exit,
+ .vdm = ucsi_displayport_vdm,
+};
+
+static void ucsi_displayport_work(struct work_struct *work)
+{
+ struct ucsi_dp *dp = container_of(work, struct ucsi_dp, work);
+ int ret;
+
+ mutex_lock(&dp->con->lock);
+
+ ret = typec_altmode_vdm(dp->alt, dp->header,
+ dp->vdo_data, dp->vdo_size);
+ if (ret)
+ dev_err(&dp->alt->dev, "VDM 0x%x failed\n", dp->header);
+
+ dp->vdo_data = NULL;
+ dp->vdo_size = 0;
+ dp->header = 0;
+
+ mutex_unlock(&dp->con->lock);
+}
+
+void ucsi_displayport_remove_partner(struct typec_altmode *alt)
+{
+ struct ucsi_dp *dp;
+
+ if (!alt)
+ return;
+
+ dp = typec_altmode_get_drvdata(alt);
+ if (!dp)
+ return;
+
+ dp->data.conf = 0;
+ dp->data.status = 0;
+ dp->initialized = false;
+}
+
+struct typec_altmode *ucsi_register_displayport(struct ucsi_connector *con,
+ bool override, int offset,
+ struct typec_altmode_desc *desc)
+{
+ u8 all_assignments = BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) |
+ BIT(DP_PIN_ASSIGN_E);
+ struct typec_altmode *alt;
+ struct ucsi_dp *dp;
+
+ /* We can't rely on the firmware with the capabilities. */
+ desc->vdo |= DP_CAP_DP_SIGNALING | DP_CAP_RECEPTACLE;
+
+ /* Claiming that we support all pin assignments */
+ desc->vdo |= all_assignments << 8;
+ desc->vdo |= all_assignments << 16;
+
+ alt = typec_port_register_altmode(con->port, desc);
+ if (IS_ERR(alt))
+ return alt;
+
+ dp = devm_kzalloc(&alt->dev, sizeof(*dp), GFP_KERNEL);
+ if (!dp) {
+ typec_unregister_altmode(alt);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ INIT_WORK(&dp->work, ucsi_displayport_work);
+ dp->override = override;
+ dp->offset = offset;
+ dp->con = con;
+ dp->alt = alt;
+
+ alt->ops = &ucsi_displayport_ops;
+ typec_altmode_set_drvdata(alt, dp);
+
+ return alt;
+}
diff --git a/drivers/usb/typec/ucsi/psy.c b/drivers/usb/typec/ucsi/psy.c
new file mode 100644
index 000000000..b35c6e079
--- /dev/null
+++ b/drivers/usb/typec/ucsi/psy.c
@@ -0,0 +1,273 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Power Supply for UCSI
+ *
+ * Copyright (C) 2020, Intel Corporation
+ * Author: K V, Abhilash <abhilash.k.v@intel.com>
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/property.h>
+#include <linux/usb/pd.h>
+
+#include "ucsi.h"
+
+/* Power Supply access to expose source power information */
+enum ucsi_psy_online_states {
+ UCSI_PSY_OFFLINE = 0,
+ UCSI_PSY_FIXED_ONLINE,
+ UCSI_PSY_PROG_ONLINE,
+};
+
+static enum power_supply_property ucsi_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,
+ POWER_SUPPLY_PROP_SCOPE,
+};
+
+static int ucsi_psy_get_scope(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u8 scope = POWER_SUPPLY_SCOPE_UNKNOWN;
+ struct device *dev = con->ucsi->dev;
+
+ device_property_read_u8(dev, "scope", &scope);
+ if (scope == POWER_SUPPLY_SCOPE_UNKNOWN) {
+ u32 mask = UCSI_CAP_ATTR_POWER_AC_SUPPLY |
+ UCSI_CAP_ATTR_BATTERY_CHARGING;
+
+ if (con->ucsi->cap.attributes & mask)
+ scope = POWER_SUPPLY_SCOPE_SYSTEM;
+ else
+ scope = POWER_SUPPLY_SCOPE_DEVICE;
+ }
+ val->intval = scope;
+ return 0;
+}
+
+static int ucsi_psy_get_online(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ val->intval = UCSI_PSY_OFFLINE;
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED &&
+ (con->status.flags & UCSI_CONSTAT_PWR_DIR) == TYPEC_SINK)
+ val->intval = UCSI_PSY_FIXED_ONLINE;
+ return 0;
+}
+
+static int ucsi_psy_get_voltage_min(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u32 pdo;
+
+ switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ pdo = con->src_pdos[0];
+ val->intval = pdo_fixed_voltage(pdo) * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ case UCSI_CONSTAT_PWR_OPMODE_BC:
+ case UCSI_CONSTAT_PWR_OPMODE_DEFAULT:
+ val->intval = UCSI_TYPEC_VSAFE5V * 1000;
+ break;
+ default:
+ val->intval = 0;
+ break;
+ }
+ return 0;
+}
+
+static int ucsi_psy_get_voltage_max(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u32 pdo;
+
+ switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ if (con->num_pdos > 0) {
+ pdo = con->src_pdos[con->num_pdos - 1];
+ val->intval = pdo_fixed_voltage(pdo) * 1000;
+ } else {
+ val->intval = 0;
+ }
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ case UCSI_CONSTAT_PWR_OPMODE_BC:
+ case UCSI_CONSTAT_PWR_OPMODE_DEFAULT:
+ val->intval = UCSI_TYPEC_VSAFE5V * 1000;
+ break;
+ default:
+ val->intval = 0;
+ break;
+ }
+ return 0;
+}
+
+static int ucsi_psy_get_voltage_now(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ int index;
+ u32 pdo;
+
+ switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ index = rdo_index(con->rdo);
+ if (index > 0) {
+ pdo = con->src_pdos[index - 1];
+ val->intval = pdo_fixed_voltage(pdo) * 1000;
+ } else {
+ val->intval = 0;
+ }
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ case UCSI_CONSTAT_PWR_OPMODE_BC:
+ case UCSI_CONSTAT_PWR_OPMODE_DEFAULT:
+ val->intval = UCSI_TYPEC_VSAFE5V * 1000;
+ break;
+ default:
+ val->intval = 0;
+ break;
+ }
+ return 0;
+}
+
+static int ucsi_psy_get_current_max(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u32 pdo;
+
+ switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ if (con->num_pdos > 0) {
+ pdo = con->src_pdos[con->num_pdos - 1];
+ val->intval = pdo_max_current(pdo) * 1000;
+ } else {
+ val->intval = 0;
+ }
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ val->intval = UCSI_TYPEC_1_5_CURRENT * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ val->intval = UCSI_TYPEC_3_0_CURRENT * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_BC:
+ case UCSI_CONSTAT_PWR_OPMODE_DEFAULT:
+ /* UCSI can't tell b/w DCP/CDP or USB2/3x1/3x2 SDP chargers */
+ default:
+ val->intval = 0;
+ break;
+ }
+ return 0;
+}
+
+static int ucsi_psy_get_current_now(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u16 flags = con->status.flags;
+
+ if (UCSI_CONSTAT_PWR_OPMODE(flags) == UCSI_CONSTAT_PWR_OPMODE_PD)
+ val->intval = rdo_op_current(con->rdo) * 1000;
+ else
+ val->intval = 0;
+ return 0;
+}
+
+static int ucsi_psy_get_usb_type(struct ucsi_connector *con,
+ union power_supply_propval *val)
+{
+ u16 flags = con->status.flags;
+
+ val->intval = POWER_SUPPLY_USB_TYPE_C;
+ if (flags & UCSI_CONSTAT_CONNECTED &&
+ UCSI_CONSTAT_PWR_OPMODE(flags) == UCSI_CONSTAT_PWR_OPMODE_PD)
+ val->intval = POWER_SUPPLY_USB_TYPE_PD;
+
+ return 0;
+}
+
+static int ucsi_psy_get_prop(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct ucsi_connector *con = power_supply_get_drvdata(psy);
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_USB_TYPE:
+ return ucsi_psy_get_usb_type(con, val);
+ case POWER_SUPPLY_PROP_ONLINE:
+ return ucsi_psy_get_online(con, val);
+ case POWER_SUPPLY_PROP_VOLTAGE_MIN:
+ return ucsi_psy_get_voltage_min(con, val);
+ case POWER_SUPPLY_PROP_VOLTAGE_MAX:
+ return ucsi_psy_get_voltage_max(con, val);
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ return ucsi_psy_get_voltage_now(con, val);
+ case POWER_SUPPLY_PROP_CURRENT_MAX:
+ return ucsi_psy_get_current_max(con, val);
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ return ucsi_psy_get_current_now(con, val);
+ case POWER_SUPPLY_PROP_SCOPE:
+ return ucsi_psy_get_scope(con, val);
+ default:
+ return -EINVAL;
+ }
+}
+
+static enum power_supply_usb_type ucsi_psy_usb_types[] = {
+ POWER_SUPPLY_USB_TYPE_C,
+ POWER_SUPPLY_USB_TYPE_PD,
+ POWER_SUPPLY_USB_TYPE_PD_PPS,
+};
+
+int ucsi_register_port_psy(struct ucsi_connector *con)
+{
+ struct power_supply_config psy_cfg = {};
+ struct device *dev = con->ucsi->dev;
+ char *psy_name;
+
+ psy_cfg.drv_data = con;
+ psy_cfg.fwnode = dev_fwnode(dev);
+
+ psy_name = devm_kasprintf(dev, GFP_KERNEL, "ucsi-source-psy-%s%d",
+ dev_name(dev), con->num);
+ if (!psy_name)
+ return -ENOMEM;
+
+ con->psy_desc.name = psy_name;
+ con->psy_desc.type = POWER_SUPPLY_TYPE_USB;
+ con->psy_desc.usb_types = ucsi_psy_usb_types;
+ con->psy_desc.num_usb_types = ARRAY_SIZE(ucsi_psy_usb_types);
+ con->psy_desc.properties = ucsi_psy_props;
+ con->psy_desc.num_properties = ARRAY_SIZE(ucsi_psy_props);
+ con->psy_desc.get_property = ucsi_psy_get_prop;
+
+ con->psy = power_supply_register(dev, &con->psy_desc, &psy_cfg);
+
+ return PTR_ERR_OR_ZERO(con->psy);
+}
+
+void ucsi_unregister_port_psy(struct ucsi_connector *con)
+{
+ if (IS_ERR_OR_NULL(con->psy))
+ return;
+
+ power_supply_unregister(con->psy);
+ con->psy = NULL;
+}
+
+void ucsi_port_psy_changed(struct ucsi_connector *con)
+{
+ if (IS_ERR_OR_NULL(con->psy))
+ return;
+
+ power_supply_changed(con->psy);
+}
diff --git a/drivers/usb/typec/ucsi/trace.c b/drivers/usb/typec/ucsi/trace.c
new file mode 100644
index 000000000..cb62ad835
--- /dev/null
+++ b/drivers/usb/typec/ucsi/trace.c
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0
+#define CREATE_TRACE_POINTS
+#include "ucsi.h"
+#include "trace.h"
+
+static const char * const ucsi_cmd_strs[] = {
+ [0] = "Unknown command",
+ [UCSI_PPM_RESET] = "PPM_RESET",
+ [UCSI_CANCEL] = "CANCEL",
+ [UCSI_CONNECTOR_RESET] = "CONNECTOR_RESET",
+ [UCSI_ACK_CC_CI] = "ACK_CC_CI",
+ [UCSI_SET_NOTIFICATION_ENABLE] = "SET_NOTIFICATION_ENABLE",
+ [UCSI_GET_CAPABILITY] = "GET_CAPABILITY",
+ [UCSI_GET_CONNECTOR_CAPABILITY] = "GET_CONNECTOR_CAPABILITY",
+ [UCSI_SET_UOM] = "SET_UOM",
+ [UCSI_SET_UOR] = "SET_UOR",
+ [UCSI_SET_PDM] = "SET_PDM",
+ [UCSI_SET_PDR] = "SET_PDR",
+ [UCSI_GET_ALTERNATE_MODES] = "GET_ALTERNATE_MODES",
+ [UCSI_GET_CAM_SUPPORTED] = "GET_CAM_SUPPORTED",
+ [UCSI_GET_CURRENT_CAM] = "GET_CURRENT_CAM",
+ [UCSI_SET_NEW_CAM] = "SET_NEW_CAM",
+ [UCSI_GET_PDOS] = "GET_PDOS",
+ [UCSI_GET_CABLE_PROPERTY] = "GET_CABLE_PROPERTY",
+ [UCSI_GET_CONNECTOR_STATUS] = "GET_CONNECTOR_STATUS",
+ [UCSI_GET_ERROR_STATUS] = "GET_ERROR_STATUS",
+};
+
+const char *ucsi_cmd_str(u64 raw_cmd)
+{
+ u8 cmd = raw_cmd & GENMASK(7, 0);
+
+ return ucsi_cmd_strs[(cmd >= ARRAY_SIZE(ucsi_cmd_strs)) ? 0 : cmd];
+}
+
+const char *ucsi_cci_str(u32 cci)
+{
+ if (UCSI_CCI_CONNECTOR(cci)) {
+ if (cci & UCSI_CCI_ACK_COMPLETE)
+ return "Event pending (ACK completed)";
+ if (cci & UCSI_CCI_COMMAND_COMPLETE)
+ return "Event pending (command completed)";
+ return "Connector Change";
+ }
+ if (cci & UCSI_CCI_ACK_COMPLETE)
+ return "ACK completed";
+ if (cci & UCSI_CCI_COMMAND_COMPLETE)
+ return "Command completed";
+
+ return "";
+}
+
+static const char * const ucsi_recipient_strs[] = {
+ [UCSI_RECIPIENT_CON] = "port",
+ [UCSI_RECIPIENT_SOP] = "partner",
+ [UCSI_RECIPIENT_SOP_P] = "plug (prime)",
+ [UCSI_RECIPIENT_SOP_PP] = "plug (double prime)",
+};
+
+const char *ucsi_recipient_str(u8 recipient)
+{
+ return ucsi_recipient_strs[recipient];
+}
diff --git a/drivers/usb/typec/ucsi/trace.h b/drivers/usb/typec/ucsi/trace.h
new file mode 100644
index 000000000..a0d3a934d
--- /dev/null
+++ b/drivers/usb/typec/ucsi/trace.h
@@ -0,0 +1,120 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM ucsi
+
+#if !defined(__UCSI_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
+#define __UCSI_TRACE_H
+
+#include <linux/tracepoint.h>
+#include <linux/usb/typec_altmode.h>
+
+const char *ucsi_cmd_str(u64 raw_cmd);
+const char *ucsi_cci_str(u32 cci);
+const char *ucsi_recipient_str(u8 recipient);
+
+DECLARE_EVENT_CLASS(ucsi_log_command,
+ TP_PROTO(u64 command, int ret),
+ TP_ARGS(command, ret),
+ TP_STRUCT__entry(
+ __field(u64, ctrl)
+ __field(int, ret)
+ ),
+ TP_fast_assign(
+ __entry->ctrl = command;
+ __entry->ret = ret;
+ ),
+ TP_printk("%s -> %s (err=%d)", ucsi_cmd_str(__entry->ctrl),
+ __entry->ret < 0 ? "FAIL" : "OK",
+ __entry->ret < 0 ? __entry->ret : 0)
+);
+
+DEFINE_EVENT(ucsi_log_command, ucsi_run_command,
+ TP_PROTO(u64 command, int ret),
+ TP_ARGS(command, ret)
+);
+
+DEFINE_EVENT(ucsi_log_command, ucsi_reset_ppm,
+ TP_PROTO(u64 command, int ret),
+ TP_ARGS(command, ret)
+);
+
+DECLARE_EVENT_CLASS(ucsi_log_connector_status,
+ TP_PROTO(int port, struct ucsi_connector_status *status),
+ TP_ARGS(port, status),
+ TP_STRUCT__entry(
+ __field(int, port)
+ __field(u16, change)
+ __field(u8, opmode)
+ __field(u8, connected)
+ __field(u8, pwr_dir)
+ __field(u8, partner_flags)
+ __field(u8, partner_type)
+ __field(u32, request_data_obj)
+ __field(u8, bc_status)
+ ),
+ TP_fast_assign(
+ __entry->port = port - 1;
+ __entry->change = status->change;
+ __entry->opmode = UCSI_CONSTAT_PWR_OPMODE(status->flags);
+ __entry->connected = !!(status->flags & UCSI_CONSTAT_CONNECTED);
+ __entry->pwr_dir = !!(status->flags & UCSI_CONSTAT_PWR_DIR);
+ __entry->partner_flags = UCSI_CONSTAT_PARTNER_FLAGS(status->flags);
+ __entry->partner_type = UCSI_CONSTAT_PARTNER_TYPE(status->flags);
+ __entry->request_data_obj = status->request_data_obj;
+ __entry->bc_status = UCSI_CONSTAT_BC_STATUS(status->pwr_status);
+ ),
+ TP_printk("port%d status: change=%04x, opmode=%x, connected=%d, "
+ "sourcing=%d, partner_flags=%x, partner_type=%x, "
+ "request_data_obj=%08x, BC status=%x", __entry->port,
+ __entry->change, __entry->opmode, __entry->connected,
+ __entry->pwr_dir, __entry->partner_flags, __entry->partner_type,
+ __entry->request_data_obj, __entry->bc_status)
+);
+
+DEFINE_EVENT(ucsi_log_connector_status, ucsi_connector_change,
+ TP_PROTO(int port, struct ucsi_connector_status *status),
+ TP_ARGS(port, status)
+);
+
+DEFINE_EVENT(ucsi_log_connector_status, ucsi_register_port,
+ TP_PROTO(int port, struct ucsi_connector_status *status),
+ TP_ARGS(port, status)
+);
+
+DECLARE_EVENT_CLASS(ucsi_log_register_altmode,
+ TP_PROTO(u8 recipient, struct typec_altmode *alt),
+ TP_ARGS(recipient, alt),
+ TP_STRUCT__entry(
+ __field(u8, recipient)
+ __field(u16, svid)
+ __field(u8, mode)
+ __field(u32, vdo)
+ ),
+ TP_fast_assign(
+ __entry->recipient = recipient;
+ __entry->svid = alt->svid;
+ __entry->mode = alt->mode;
+ __entry->vdo = alt->vdo;
+ ),
+ TP_printk("%s alt mode: svid %04x, mode %d vdo %x",
+ ucsi_recipient_str(__entry->recipient), __entry->svid,
+ __entry->mode, __entry->vdo)
+);
+
+DEFINE_EVENT(ucsi_log_register_altmode, ucsi_register_altmode,
+ TP_PROTO(u8 recipient, struct typec_altmode *alt),
+ TP_ARGS(recipient, alt)
+);
+
+#endif /* __UCSI_TRACE_H */
+
+/* This part must be outside protection */
+
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH .
+
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_FILE trace
+
+#include <trace/define_trace.h>
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
new file mode 100644
index 000000000..dc2dea376
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -0,0 +1,1460 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Type-C Connector System Software Interface driver
+ *
+ * Copyright (C) 2017, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/completion.h>
+#include <linux/property.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/delay.h>
+#include <linux/slab.h>
+#include <linux/usb/typec_dp.h>
+
+#include "ucsi.h"
+#include "trace.h"
+
+/*
+ * UCSI_TIMEOUT_MS - PPM communication timeout
+ *
+ * Ideally we could use MIN_TIME_TO_RESPOND_WITH_BUSY (which is defined in UCSI
+ * specification) here as reference, but unfortunately we can't. It is very
+ * difficult to estimate the time it takes for the system to process the command
+ * before it is actually passed to the PPM.
+ */
+#define UCSI_TIMEOUT_MS 5000
+
+/*
+ * UCSI_SWAP_TIMEOUT_MS - Timeout for role swap requests
+ *
+ * 5 seconds is close to the time it takes for CapsCounter to reach 0, so even
+ * if the PPM does not generate Connector Change events before that with
+ * partners that do not support USB Power Delivery, this should still work.
+ */
+#define UCSI_SWAP_TIMEOUT_MS 5000
+
+static int ucsi_acknowledge_command(struct ucsi *ucsi)
+{
+ u64 ctrl;
+
+ ctrl = UCSI_ACK_CC_CI;
+ ctrl |= UCSI_ACK_COMMAND_COMPLETE;
+
+ return ucsi->ops->sync_write(ucsi, UCSI_CONTROL, &ctrl, sizeof(ctrl));
+}
+
+static int ucsi_acknowledge_connector_change(struct ucsi *ucsi)
+{
+ u64 ctrl;
+
+ ctrl = UCSI_ACK_CC_CI;
+ ctrl |= UCSI_ACK_CONNECTOR_CHANGE;
+
+ return ucsi->ops->sync_write(ucsi, UCSI_CONTROL, &ctrl, sizeof(ctrl));
+}
+
+static int ucsi_exec_command(struct ucsi *ucsi, u64 command);
+
+static int ucsi_read_error(struct ucsi *ucsi)
+{
+ u16 error;
+ int ret;
+
+ /* Acknowledge the command that failed */
+ ret = ucsi_acknowledge_command(ucsi);
+ if (ret)
+ return ret;
+
+ ret = ucsi_exec_command(ucsi, UCSI_GET_ERROR_STATUS);
+ if (ret < 0)
+ return ret;
+
+ ret = ucsi->ops->read(ucsi, UCSI_MESSAGE_IN, &error, sizeof(error));
+ if (ret)
+ return ret;
+
+ ret = ucsi_acknowledge_command(ucsi);
+ if (ret)
+ return ret;
+
+ switch (error) {
+ case UCSI_ERROR_INCOMPATIBLE_PARTNER:
+ return -EOPNOTSUPP;
+ case UCSI_ERROR_CC_COMMUNICATION_ERR:
+ return -ECOMM;
+ case UCSI_ERROR_CONTRACT_NEGOTIATION_FAIL:
+ return -EPROTO;
+ case UCSI_ERROR_DEAD_BATTERY:
+ dev_warn(ucsi->dev, "Dead battery condition!\n");
+ return -EPERM;
+ case UCSI_ERROR_INVALID_CON_NUM:
+ case UCSI_ERROR_UNREGONIZED_CMD:
+ case UCSI_ERROR_INVALID_CMD_ARGUMENT:
+ dev_err(ucsi->dev, "possible UCSI driver bug %u\n", error);
+ return -EINVAL;
+ case UCSI_ERROR_OVERCURRENT:
+ dev_warn(ucsi->dev, "Overcurrent condition\n");
+ break;
+ case UCSI_ERROR_PARTNER_REJECTED_SWAP:
+ dev_warn(ucsi->dev, "Partner rejected swap\n");
+ break;
+ case UCSI_ERROR_HARD_RESET:
+ dev_warn(ucsi->dev, "Hard reset occurred\n");
+ break;
+ case UCSI_ERROR_PPM_POLICY_CONFLICT:
+ dev_warn(ucsi->dev, "PPM Policy conflict\n");
+ break;
+ case UCSI_ERROR_SWAP_REJECTED:
+ dev_warn(ucsi->dev, "Swap rejected\n");
+ break;
+ case UCSI_ERROR_UNDEFINED:
+ default:
+ dev_err(ucsi->dev, "unknown error %u\n", error);
+ break;
+ }
+
+ return -EIO;
+}
+
+static int ucsi_exec_command(struct ucsi *ucsi, u64 cmd)
+{
+ u32 cci;
+ int ret;
+
+ ret = ucsi->ops->sync_write(ucsi, UCSI_CONTROL, &cmd, sizeof(cmd));
+ if (ret)
+ return ret;
+
+ ret = ucsi->ops->read(ucsi, UCSI_CCI, &cci, sizeof(cci));
+ if (ret)
+ return ret;
+
+ if (cmd != UCSI_CANCEL && cci & UCSI_CCI_BUSY)
+ return ucsi_exec_command(ucsi, UCSI_CANCEL);
+
+ if (!(cci & UCSI_CCI_COMMAND_COMPLETE))
+ return -EIO;
+
+ if (cci & UCSI_CCI_NOT_SUPPORTED)
+ return -EOPNOTSUPP;
+
+ if (cci & UCSI_CCI_ERROR) {
+ if (cmd == UCSI_GET_ERROR_STATUS)
+ return -EIO;
+ return ucsi_read_error(ucsi);
+ }
+
+ if (cmd == UCSI_CANCEL && cci & UCSI_CCI_CANCEL_COMPLETE) {
+ ret = ucsi_acknowledge_command(ucsi);
+ return ret ? ret : -EBUSY;
+ }
+
+ return UCSI_CCI_LENGTH(cci);
+}
+
+int ucsi_send_command(struct ucsi *ucsi, u64 command,
+ void *data, size_t size)
+{
+ u8 length;
+ int ret;
+
+ mutex_lock(&ucsi->ppm_lock);
+
+ ret = ucsi_exec_command(ucsi, command);
+ if (ret < 0)
+ goto out;
+
+ length = ret;
+
+ if (data) {
+ ret = ucsi->ops->read(ucsi, UCSI_MESSAGE_IN, data, size);
+ if (ret)
+ goto out;
+ }
+
+ ret = ucsi_acknowledge_command(ucsi);
+ if (ret)
+ goto out;
+
+ ret = length;
+out:
+ mutex_unlock(&ucsi->ppm_lock);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(ucsi_send_command);
+
+/* -------------------------------------------------------------------------- */
+
+struct ucsi_work {
+ struct delayed_work work;
+ struct list_head node;
+ unsigned long delay;
+ unsigned int count;
+ struct ucsi_connector *con;
+ int (*cb)(struct ucsi_connector *);
+};
+
+static void ucsi_poll_worker(struct work_struct *work)
+{
+ struct ucsi_work *uwork = container_of(work, struct ucsi_work, work.work);
+ struct ucsi_connector *con = uwork->con;
+ int ret;
+
+ mutex_lock(&con->lock);
+
+ if (!con->partner) {
+ list_del(&uwork->node);
+ mutex_unlock(&con->lock);
+ kfree(uwork);
+ return;
+ }
+
+ ret = uwork->cb(con);
+
+ if (uwork->count-- && (ret == -EBUSY || ret == -ETIMEDOUT)) {
+ queue_delayed_work(con->wq, &uwork->work, uwork->delay);
+ } else {
+ list_del(&uwork->node);
+ kfree(uwork);
+ }
+
+ mutex_unlock(&con->lock);
+}
+
+static int ucsi_partner_task(struct ucsi_connector *con,
+ int (*cb)(struct ucsi_connector *),
+ int retries, unsigned long delay)
+{
+ struct ucsi_work *uwork;
+
+ if (!con->partner)
+ return 0;
+
+ uwork = kzalloc(sizeof(*uwork), GFP_KERNEL);
+ if (!uwork)
+ return -ENOMEM;
+
+ INIT_DELAYED_WORK(&uwork->work, ucsi_poll_worker);
+ uwork->count = retries;
+ uwork->delay = delay;
+ uwork->con = con;
+ uwork->cb = cb;
+
+ list_add_tail(&uwork->node, &con->partner_tasks);
+ queue_delayed_work(con->wq, &uwork->work, delay);
+
+ return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void ucsi_altmode_update_active(struct ucsi_connector *con)
+{
+ const struct typec_altmode *altmode = NULL;
+ u64 command;
+ int ret;
+ u8 cur;
+ int i;
+
+ command = UCSI_GET_CURRENT_CAM | UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(con->ucsi, command, &cur, sizeof(cur));
+ if (ret < 0) {
+ if (con->ucsi->version > 0x0100) {
+ dev_err(con->ucsi->dev,
+ "GET_CURRENT_CAM command failed\n");
+ return;
+ }
+ cur = 0xff;
+ }
+
+ if (cur < UCSI_MAX_ALTMODES)
+ altmode = typec_altmode_get_partner(con->port_altmode[cur]);
+
+ for (i = 0; con->partner_altmode[i]; i++)
+ typec_altmode_update_active(con->partner_altmode[i],
+ con->partner_altmode[i] == altmode);
+}
+
+static int ucsi_altmode_next_mode(struct typec_altmode **alt, u16 svid)
+{
+ u8 mode = 1;
+ int i;
+
+ for (i = 0; alt[i]; i++) {
+ if (i > MODE_DISCOVERY_MAX)
+ return -ERANGE;
+
+ if (alt[i]->svid == svid)
+ mode++;
+ }
+
+ return mode;
+}
+
+static int ucsi_next_altmode(struct typec_altmode **alt)
+{
+ int i = 0;
+
+ for (i = 0; i < UCSI_MAX_ALTMODES; i++)
+ if (!alt[i])
+ return i;
+
+ return -ENOENT;
+}
+
+static int ucsi_get_num_altmode(struct typec_altmode **alt)
+{
+ int i;
+
+ for (i = 0; i < UCSI_MAX_ALTMODES; i++)
+ if (!alt[i])
+ break;
+
+ return i;
+}
+
+static int ucsi_register_altmode(struct ucsi_connector *con,
+ struct typec_altmode_desc *desc,
+ u8 recipient)
+{
+ struct typec_altmode *alt;
+ bool override;
+ int ret;
+ int i;
+
+ override = !!(con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE);
+
+ switch (recipient) {
+ case UCSI_RECIPIENT_CON:
+ i = ucsi_next_altmode(con->port_altmode);
+ if (i < 0) {
+ ret = i;
+ goto err;
+ }
+
+ ret = ucsi_altmode_next_mode(con->port_altmode, desc->svid);
+ if (ret < 0)
+ return ret;
+
+ desc->mode = ret;
+
+ switch (desc->svid) {
+ case USB_TYPEC_DP_SID:
+ alt = ucsi_register_displayport(con, override, i, desc);
+ break;
+ case USB_TYPEC_NVIDIA_VLINK_SID:
+ if (desc->vdo == USB_TYPEC_NVIDIA_VLINK_DBG_VDO)
+ alt = typec_port_register_altmode(con->port,
+ desc);
+ else
+ alt = ucsi_register_displayport(con, override,
+ i, desc);
+ break;
+ default:
+ alt = typec_port_register_altmode(con->port, desc);
+ break;
+ }
+
+ if (IS_ERR(alt)) {
+ ret = PTR_ERR(alt);
+ goto err;
+ }
+
+ con->port_altmode[i] = alt;
+ break;
+ case UCSI_RECIPIENT_SOP:
+ i = ucsi_next_altmode(con->partner_altmode);
+ if (i < 0) {
+ ret = i;
+ goto err;
+ }
+
+ ret = ucsi_altmode_next_mode(con->partner_altmode, desc->svid);
+ if (ret < 0)
+ return ret;
+
+ desc->mode = ret;
+
+ alt = typec_partner_register_altmode(con->partner, desc);
+ if (IS_ERR(alt)) {
+ ret = PTR_ERR(alt);
+ goto err;
+ }
+
+ con->partner_altmode[i] = alt;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ trace_ucsi_register_altmode(recipient, alt);
+
+ return 0;
+
+err:
+ dev_err(con->ucsi->dev, "failed to registers svid 0x%04x mode %d\n",
+ desc->svid, desc->mode);
+
+ return ret;
+}
+
+static int
+ucsi_register_altmodes_nvidia(struct ucsi_connector *con, u8 recipient)
+{
+ int max_altmodes = UCSI_MAX_ALTMODES;
+ struct typec_altmode_desc desc;
+ struct ucsi_altmode alt;
+ struct ucsi_altmode orig[UCSI_MAX_ALTMODES];
+ struct ucsi_altmode updated[UCSI_MAX_ALTMODES];
+ struct ucsi *ucsi = con->ucsi;
+ bool multi_dp = false;
+ u64 command;
+ int ret;
+ int len;
+ int i;
+ int k = 0;
+
+ if (recipient == UCSI_RECIPIENT_CON)
+ max_altmodes = con->ucsi->cap.num_alt_modes;
+
+ memset(orig, 0, sizeof(orig));
+ memset(updated, 0, sizeof(updated));
+
+ /* First get all the alternate modes */
+ for (i = 0; i < max_altmodes; i++) {
+ memset(&alt, 0, sizeof(alt));
+ command = UCSI_GET_ALTERNATE_MODES;
+ command |= UCSI_GET_ALTMODE_RECIPIENT(recipient);
+ command |= UCSI_GET_ALTMODE_CONNECTOR_NUMBER(con->num);
+ command |= UCSI_GET_ALTMODE_OFFSET(i);
+ len = ucsi_send_command(con->ucsi, command, &alt, sizeof(alt));
+ /*
+ * We are collecting all altmodes first and then registering.
+ * Some type-C device will return zero length data beyond last
+ * alternate modes. We should not return if length is zero.
+ */
+ if (len < 0)
+ return len;
+
+ /* We got all altmodes, now break out and register them */
+ if (!len || !alt.svid)
+ break;
+
+ orig[k].mid = alt.mid;
+ orig[k].svid = alt.svid;
+ k++;
+ }
+ /*
+ * Update the original altmode table as some ppms may report
+ * multiple DP altmodes.
+ */
+ if (recipient == UCSI_RECIPIENT_CON)
+ multi_dp = ucsi->ops->update_altmodes(ucsi, orig, updated);
+
+ /* now register altmodes */
+ for (i = 0; i < max_altmodes; i++) {
+ memset(&desc, 0, sizeof(desc));
+ if (multi_dp && recipient == UCSI_RECIPIENT_CON) {
+ desc.svid = updated[i].svid;
+ desc.vdo = updated[i].mid;
+ } else {
+ desc.svid = orig[i].svid;
+ desc.vdo = orig[i].mid;
+ }
+ desc.roles = TYPEC_PORT_DRD;
+
+ if (!desc.svid)
+ return 0;
+
+ ret = ucsi_register_altmode(con, &desc, recipient);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
+{
+ int max_altmodes = UCSI_MAX_ALTMODES;
+ struct typec_altmode_desc desc;
+ struct ucsi_altmode alt[2];
+ u64 command;
+ int num;
+ int ret;
+ int len;
+ int j;
+ int i;
+
+ if (!(con->ucsi->cap.features & UCSI_CAP_ALT_MODE_DETAILS))
+ return 0;
+
+ if (recipient == UCSI_RECIPIENT_SOP && con->partner_altmode[0])
+ return 0;
+
+ if (con->ucsi->ops->update_altmodes)
+ return ucsi_register_altmodes_nvidia(con, recipient);
+
+ if (recipient == UCSI_RECIPIENT_CON)
+ max_altmodes = con->ucsi->cap.num_alt_modes;
+
+ for (i = 0; i < max_altmodes;) {
+ memset(alt, 0, sizeof(alt));
+ command = UCSI_GET_ALTERNATE_MODES;
+ command |= UCSI_GET_ALTMODE_RECIPIENT(recipient);
+ command |= UCSI_GET_ALTMODE_CONNECTOR_NUMBER(con->num);
+ command |= UCSI_GET_ALTMODE_OFFSET(i);
+ len = ucsi_send_command(con->ucsi, command, alt, sizeof(alt));
+ if (len == -EBUSY)
+ continue;
+ if (len <= 0)
+ return len;
+
+ /*
+ * This code is requesting one alt mode at a time, but some PPMs
+ * may still return two. If that happens both alt modes need be
+ * registered and the offset for the next alt mode has to be
+ * incremented.
+ */
+ num = len / sizeof(alt[0]);
+ i += num;
+
+ for (j = 0; j < num; j++) {
+ if (!alt[j].svid)
+ return 0;
+
+ memset(&desc, 0, sizeof(desc));
+ desc.vdo = alt[j].mid;
+ desc.svid = alt[j].svid;
+ desc.roles = TYPEC_PORT_DRD;
+
+ ret = ucsi_register_altmode(con, &desc, recipient);
+ if (ret)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static void ucsi_unregister_altmodes(struct ucsi_connector *con, u8 recipient)
+{
+ const struct typec_altmode *pdev;
+ struct typec_altmode **adev;
+ int i = 0;
+
+ switch (recipient) {
+ case UCSI_RECIPIENT_CON:
+ adev = con->port_altmode;
+ break;
+ case UCSI_RECIPIENT_SOP:
+ adev = con->partner_altmode;
+ break;
+ default:
+ return;
+ }
+
+ while (adev[i]) {
+ if (recipient == UCSI_RECIPIENT_SOP &&
+ (adev[i]->svid == USB_TYPEC_DP_SID ||
+ (adev[i]->svid == USB_TYPEC_NVIDIA_VLINK_SID &&
+ adev[i]->vdo != USB_TYPEC_NVIDIA_VLINK_DBG_VDO))) {
+ pdev = typec_altmode_get_partner(adev[i]);
+ ucsi_displayport_remove_partner((void *)pdev);
+ }
+ typec_unregister_altmode(adev[i]);
+ adev[i++] = NULL;
+ }
+}
+
+static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner,
+ u32 *pdos, int offset, int num_pdos)
+{
+ struct ucsi *ucsi = con->ucsi;
+ u64 command;
+ int ret;
+
+ command = UCSI_COMMAND(UCSI_GET_PDOS) | UCSI_CONNECTOR_NUMBER(con->num);
+ command |= UCSI_GET_PDOS_PARTNER_PDO(is_partner);
+ command |= UCSI_GET_PDOS_PDO_OFFSET(offset);
+ command |= UCSI_GET_PDOS_NUM_PDOS(num_pdos - 1);
+ command |= UCSI_GET_PDOS_SRC_PDOS;
+ ret = ucsi_send_command(ucsi, command, pdos + offset,
+ num_pdos * sizeof(u32));
+ if (ret < 0 && ret != -ETIMEDOUT)
+ dev_err(ucsi->dev, "UCSI_GET_PDOS failed (%d)\n", ret);
+
+ return ret;
+}
+
+static int ucsi_get_src_pdos(struct ucsi_connector *con)
+{
+ int ret;
+
+ /* UCSI max payload means only getting at most 4 PDOs at a time */
+ ret = ucsi_get_pdos(con, 1, con->src_pdos, 0, UCSI_MAX_PDOS);
+ if (ret < 0)
+ return ret;
+
+ con->num_pdos = ret / sizeof(u32); /* number of bytes to 32-bit PDOs */
+ if (con->num_pdos < UCSI_MAX_PDOS)
+ return 0;
+
+ /* get the remaining PDOs, if any */
+ ret = ucsi_get_pdos(con, 1, con->src_pdos, UCSI_MAX_PDOS,
+ PDO_MAX_OBJECTS - UCSI_MAX_PDOS);
+ if (ret < 0)
+ return ret;
+
+ con->num_pdos += ret / sizeof(u32);
+
+ ucsi_port_psy_changed(con);
+
+ return 0;
+}
+
+static int ucsi_check_altmodes(struct ucsi_connector *con)
+{
+ int ret, num_partner_am;
+
+ ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_SOP);
+ if (ret && ret != -ETIMEDOUT)
+ dev_err(con->ucsi->dev,
+ "con%d: failed to register partner alt modes (%d)\n",
+ con->num, ret);
+
+ /* Ignoring the errors in this case. */
+ if (con->partner_altmode[0]) {
+ num_partner_am = ucsi_get_num_altmode(con->partner_altmode);
+ if (num_partner_am > 0)
+ typec_partner_set_num_altmodes(con->partner, num_partner_am);
+ ucsi_altmode_update_active(con);
+ return 0;
+ }
+
+ return ret;
+}
+
+static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
+{
+ switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ con->rdo = con->status.request_data_obj;
+ typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_PD);
+ ucsi_partner_task(con, ucsi_get_src_pdos, 30, 0);
+ ucsi_partner_task(con, ucsi_check_altmodes, 30, 0);
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ con->rdo = 0;
+ typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_1_5A);
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ con->rdo = 0;
+ typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_3_0A);
+ break;
+ default:
+ con->rdo = 0;
+ typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_USB);
+ break;
+ }
+}
+
+static int ucsi_register_partner(struct ucsi_connector *con)
+{
+ u8 pwr_opmode = UCSI_CONSTAT_PWR_OPMODE(con->status.flags);
+ struct typec_partner_desc desc;
+ struct typec_partner *partner;
+
+ if (con->partner)
+ return 0;
+
+ memset(&desc, 0, sizeof(desc));
+
+ switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
+ case UCSI_CONSTAT_PARTNER_TYPE_DEBUG:
+ desc.accessory = TYPEC_ACCESSORY_DEBUG;
+ break;
+ case UCSI_CONSTAT_PARTNER_TYPE_AUDIO:
+ desc.accessory = TYPEC_ACCESSORY_AUDIO;
+ break;
+ default:
+ break;
+ }
+
+ desc.usb_pd = pwr_opmode == UCSI_CONSTAT_PWR_OPMODE_PD;
+
+ partner = typec_register_partner(con->port, &desc);
+ if (IS_ERR(partner)) {
+ dev_err(con->ucsi->dev,
+ "con%d: failed to register partner (%ld)\n", con->num,
+ PTR_ERR(partner));
+ return PTR_ERR(partner);
+ }
+
+ con->partner = partner;
+
+ return 0;
+}
+
+static void ucsi_unregister_partner(struct ucsi_connector *con)
+{
+ if (!con->partner)
+ return;
+
+ ucsi_unregister_altmodes(con, UCSI_RECIPIENT_SOP);
+ typec_unregister_partner(con->partner);
+ con->partner = NULL;
+}
+
+static void ucsi_partner_change(struct ucsi_connector *con)
+{
+ enum usb_role u_role = USB_ROLE_NONE;
+ int ret;
+
+ switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
+ case UCSI_CONSTAT_PARTNER_TYPE_UFP:
+ case UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP:
+ u_role = USB_ROLE_HOST;
+ fallthrough;
+ case UCSI_CONSTAT_PARTNER_TYPE_CABLE:
+ typec_set_data_role(con->port, TYPEC_HOST);
+ break;
+ case UCSI_CONSTAT_PARTNER_TYPE_DFP:
+ u_role = USB_ROLE_DEVICE;
+ typec_set_data_role(con->port, TYPEC_DEVICE);
+ break;
+ default:
+ break;
+ }
+
+ /* Only notify USB controller if partner supports USB data */
+ if (!(UCSI_CONSTAT_PARTNER_FLAGS(con->status.flags) & UCSI_CONSTAT_PARTNER_FLAG_USB))
+ u_role = USB_ROLE_NONE;
+
+ ret = usb_role_switch_set_role(con->usb_role_sw, u_role);
+ if (ret)
+ dev_err(con->ucsi->dev, "con:%d: failed to set usb role:%d\n",
+ con->num, u_role);
+}
+
+static int ucsi_check_connection(struct ucsi_connector *con)
+{
+ u8 prev_flags = con->status.flags;
+ u64 command;
+ int ret;
+
+ command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(con->ucsi, command, &con->status, sizeof(con->status));
+ if (ret < 0) {
+ dev_err(con->ucsi->dev, "GET_CONNECTOR_STATUS failed (%d)\n", ret);
+ return ret;
+ }
+
+ if (con->status.flags == prev_flags)
+ return 0;
+
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED) {
+ ucsi_register_partner(con);
+ ucsi_pwr_opmode_change(con);
+ ucsi_partner_change(con);
+ } else {
+ ucsi_partner_change(con);
+ ucsi_port_psy_changed(con);
+ ucsi_unregister_partner(con);
+ }
+
+ return 0;
+}
+
+static void ucsi_handle_connector_change(struct work_struct *work)
+{
+ struct ucsi_connector *con = container_of(work, struct ucsi_connector,
+ work);
+ struct ucsi *ucsi = con->ucsi;
+ enum typec_role role;
+ u64 command;
+ int ret;
+
+ mutex_lock(&con->lock);
+
+ command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(ucsi, command, &con->status, sizeof(con->status));
+ if (ret < 0) {
+ dev_err(ucsi->dev, "%s: GET_CONNECTOR_STATUS failed (%d)\n",
+ __func__, ret);
+ clear_bit(EVENT_PENDING, &con->ucsi->flags);
+ goto out_unlock;
+ }
+
+ trace_ucsi_connector_change(con->num, &con->status);
+
+ role = !!(con->status.flags & UCSI_CONSTAT_PWR_DIR);
+
+ if (con->status.change & UCSI_CONSTAT_POWER_DIR_CHANGE) {
+ typec_set_pwr_role(con->port, role);
+
+ /* Complete pending power role swap */
+ if (!completion_done(&con->complete))
+ complete(&con->complete);
+ }
+
+ if (con->status.change & UCSI_CONSTAT_CONNECT_CHANGE) {
+ typec_set_pwr_role(con->port, role);
+ ucsi_port_psy_changed(con);
+ ucsi_partner_change(con);
+
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED) {
+ ucsi_register_partner(con);
+ ucsi_partner_task(con, ucsi_check_connection, 1, HZ);
+ } else {
+ ucsi_unregister_partner(con);
+ }
+ }
+
+ if (con->status.change & UCSI_CONSTAT_POWER_OPMODE_CHANGE ||
+ con->status.change & UCSI_CONSTAT_POWER_LEVEL_CHANGE)
+ ucsi_pwr_opmode_change(con);
+
+ if (con->partner && con->status.change & UCSI_CONSTAT_PARTNER_CHANGE) {
+ ucsi_partner_change(con);
+
+ /* Complete pending data role swap */
+ if (!completion_done(&con->complete))
+ complete(&con->complete);
+ }
+
+ if (con->status.change & UCSI_CONSTAT_CAM_CHANGE)
+ ucsi_partner_task(con, ucsi_check_altmodes, 1, 0);
+
+ clear_bit(EVENT_PENDING, &con->ucsi->flags);
+
+ ret = ucsi_acknowledge_connector_change(ucsi);
+ if (ret)
+ dev_err(ucsi->dev, "%s: ACK failed (%d)", __func__, ret);
+
+out_unlock:
+ mutex_unlock(&con->lock);
+}
+
+/**
+ * ucsi_connector_change - Process Connector Change Event
+ * @ucsi: UCSI Interface
+ * @num: Connector number
+ */
+void ucsi_connector_change(struct ucsi *ucsi, u8 num)
+{
+ struct ucsi_connector *con = &ucsi->connector[num - 1];
+
+ if (!(ucsi->ntfy & UCSI_ENABLE_NTFY_CONNECTOR_CHANGE)) {
+ dev_dbg(ucsi->dev, "Bogus connector change event\n");
+ return;
+ }
+
+ if (!test_and_set_bit(EVENT_PENDING, &ucsi->flags))
+ schedule_work(&con->work);
+}
+EXPORT_SYMBOL_GPL(ucsi_connector_change);
+
+/* -------------------------------------------------------------------------- */
+
+static int ucsi_reset_connector(struct ucsi_connector *con, bool hard)
+{
+ u64 command;
+
+ command = UCSI_CONNECTOR_RESET | UCSI_CONNECTOR_NUMBER(con->num);
+ command |= hard ? UCSI_CONNECTOR_RESET_HARD : 0;
+
+ return ucsi_send_command(con->ucsi, command, NULL, 0);
+}
+
+static int ucsi_reset_ppm(struct ucsi *ucsi)
+{
+ u64 command = UCSI_PPM_RESET;
+ unsigned long tmo;
+ u32 cci;
+ int ret;
+
+ mutex_lock(&ucsi->ppm_lock);
+
+ ret = ucsi->ops->async_write(ucsi, UCSI_CONTROL, &command,
+ sizeof(command));
+ if (ret < 0)
+ goto out;
+
+ tmo = jiffies + msecs_to_jiffies(UCSI_TIMEOUT_MS);
+
+ do {
+ if (time_is_before_jiffies(tmo)) {
+ ret = -ETIMEDOUT;
+ goto out;
+ }
+
+ ret = ucsi->ops->read(ucsi, UCSI_CCI, &cci, sizeof(cci));
+ if (ret)
+ goto out;
+
+ /* If the PPM is still doing something else, reset it again. */
+ if (cci & ~UCSI_CCI_RESET_COMPLETE) {
+ ret = ucsi->ops->async_write(ucsi, UCSI_CONTROL,
+ &command,
+ sizeof(command));
+ if (ret < 0)
+ goto out;
+ }
+
+ msleep(20);
+ } while (!(cci & UCSI_CCI_RESET_COMPLETE));
+
+out:
+ mutex_unlock(&ucsi->ppm_lock);
+ return ret;
+}
+
+static int ucsi_role_cmd(struct ucsi_connector *con, u64 command)
+{
+ int ret;
+
+ ret = ucsi_send_command(con->ucsi, command, NULL, 0);
+ if (ret == -ETIMEDOUT) {
+ u64 c;
+
+ /* PPM most likely stopped responding. Resetting everything. */
+ ucsi_reset_ppm(con->ucsi);
+
+ c = UCSI_SET_NOTIFICATION_ENABLE | con->ucsi->ntfy;
+ ucsi_send_command(con->ucsi, c, NULL, 0);
+
+ ucsi_reset_connector(con, true);
+ }
+
+ return ret;
+}
+
+static int ucsi_dr_swap(struct typec_port *port, enum typec_data_role role)
+{
+ struct ucsi_connector *con = typec_get_drvdata(port);
+ u8 partner_type;
+ u64 command;
+ int ret = 0;
+
+ mutex_lock(&con->lock);
+
+ if (!con->partner) {
+ ret = -ENOTCONN;
+ goto out_unlock;
+ }
+
+ partner_type = UCSI_CONSTAT_PARTNER_TYPE(con->status.flags);
+ if ((partner_type == UCSI_CONSTAT_PARTNER_TYPE_DFP &&
+ role == TYPEC_DEVICE) ||
+ (partner_type == UCSI_CONSTAT_PARTNER_TYPE_UFP &&
+ role == TYPEC_HOST))
+ goto out_unlock;
+
+ reinit_completion(&con->complete);
+
+ command = UCSI_SET_UOR | UCSI_CONNECTOR_NUMBER(con->num);
+ command |= UCSI_SET_UOR_ROLE(role);
+ command |= UCSI_SET_UOR_ACCEPT_ROLE_SWAPS;
+ ret = ucsi_role_cmd(con, command);
+ if (ret < 0)
+ goto out_unlock;
+
+ mutex_unlock(&con->lock);
+
+ if (!wait_for_completion_timeout(&con->complete,
+ msecs_to_jiffies(UCSI_SWAP_TIMEOUT_MS)))
+ return -ETIMEDOUT;
+
+ return 0;
+
+out_unlock:
+ mutex_unlock(&con->lock);
+
+ return ret;
+}
+
+static int ucsi_pr_swap(struct typec_port *port, enum typec_role role)
+{
+ struct ucsi_connector *con = typec_get_drvdata(port);
+ enum typec_role cur_role;
+ u64 command;
+ int ret = 0;
+
+ mutex_lock(&con->lock);
+
+ if (!con->partner) {
+ ret = -ENOTCONN;
+ goto out_unlock;
+ }
+
+ cur_role = !!(con->status.flags & UCSI_CONSTAT_PWR_DIR);
+
+ if (cur_role == role)
+ goto out_unlock;
+
+ reinit_completion(&con->complete);
+
+ command = UCSI_SET_PDR | UCSI_CONNECTOR_NUMBER(con->num);
+ command |= UCSI_SET_PDR_ROLE(role);
+ command |= UCSI_SET_PDR_ACCEPT_ROLE_SWAPS;
+ ret = ucsi_role_cmd(con, command);
+ if (ret < 0)
+ goto out_unlock;
+
+ mutex_unlock(&con->lock);
+
+ if (!wait_for_completion_timeout(&con->complete,
+ msecs_to_jiffies(UCSI_SWAP_TIMEOUT_MS)))
+ return -ETIMEDOUT;
+
+ mutex_lock(&con->lock);
+
+ /* Something has gone wrong while swapping the role */
+ if (UCSI_CONSTAT_PWR_OPMODE(con->status.flags) !=
+ UCSI_CONSTAT_PWR_OPMODE_PD) {
+ ucsi_reset_connector(con, true);
+ ret = -EPROTO;
+ }
+
+out_unlock:
+ mutex_unlock(&con->lock);
+
+ return ret;
+}
+
+static const struct typec_operations ucsi_ops = {
+ .dr_set = ucsi_dr_swap,
+ .pr_set = ucsi_pr_swap
+};
+
+/* Caller must call fwnode_handle_put() after use */
+static struct fwnode_handle *ucsi_find_fwnode(struct ucsi_connector *con)
+{
+ struct fwnode_handle *fwnode;
+ int i = 1;
+
+ device_for_each_child_node(con->ucsi->dev, fwnode)
+ if (i++ == con->num)
+ return fwnode;
+ return NULL;
+}
+
+static int ucsi_register_port(struct ucsi *ucsi, struct ucsi_connector *con)
+{
+ struct typec_capability *cap = &con->typec_cap;
+ enum typec_accessory *accessory = cap->accessory;
+ enum usb_role u_role = USB_ROLE_NONE;
+ u64 command;
+ char *name;
+ int ret;
+
+ name = kasprintf(GFP_KERNEL, "%s-con%d", dev_name(ucsi->dev), con->num);
+ if (!name)
+ return -ENOMEM;
+
+ con->wq = create_singlethread_workqueue(name);
+ kfree(name);
+ if (!con->wq)
+ return -ENOMEM;
+
+ INIT_WORK(&con->work, ucsi_handle_connector_change);
+ init_completion(&con->complete);
+ mutex_init(&con->lock);
+ INIT_LIST_HEAD(&con->partner_tasks);
+ con->ucsi = ucsi;
+
+ cap->fwnode = ucsi_find_fwnode(con);
+ con->usb_role_sw = fwnode_usb_role_switch_get(cap->fwnode);
+ if (IS_ERR(con->usb_role_sw))
+ return dev_err_probe(ucsi->dev, PTR_ERR(con->usb_role_sw),
+ "con%d: failed to get usb role switch\n", con->num);
+
+ /* Delay other interactions with the con until registration is complete */
+ mutex_lock(&con->lock);
+
+ /* Get connector capability */
+ command = UCSI_GET_CONNECTOR_CAPABILITY;
+ command |= UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(ucsi, command, &con->cap, sizeof(con->cap));
+ if (ret < 0)
+ goto out_unlock;
+
+ if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DRP)
+ cap->data = TYPEC_PORT_DRD;
+ else if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DFP)
+ cap->data = TYPEC_PORT_DFP;
+ else if (con->cap.op_mode & UCSI_CONCAP_OPMODE_UFP)
+ cap->data = TYPEC_PORT_UFP;
+
+ if ((con->cap.flags & UCSI_CONCAP_FLAG_PROVIDER) &&
+ (con->cap.flags & UCSI_CONCAP_FLAG_CONSUMER))
+ cap->type = TYPEC_PORT_DRP;
+ else if (con->cap.flags & UCSI_CONCAP_FLAG_PROVIDER)
+ cap->type = TYPEC_PORT_SRC;
+ else if (con->cap.flags & UCSI_CONCAP_FLAG_CONSUMER)
+ cap->type = TYPEC_PORT_SNK;
+
+ cap->revision = ucsi->cap.typec_version;
+ cap->pd_revision = ucsi->cap.pd_version;
+ cap->svdm_version = SVDM_VER_2_0;
+ cap->prefer_role = TYPEC_NO_PREFERRED_ROLE;
+
+ if (con->cap.op_mode & UCSI_CONCAP_OPMODE_AUDIO_ACCESSORY)
+ *accessory++ = TYPEC_ACCESSORY_AUDIO;
+ if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DEBUG_ACCESSORY)
+ *accessory = TYPEC_ACCESSORY_DEBUG;
+
+ cap->driver_data = con;
+ cap->ops = &ucsi_ops;
+
+ ret = ucsi_register_port_psy(con);
+ if (ret)
+ goto out;
+
+ /* Register the connector */
+ con->port = typec_register_port(ucsi->dev, cap);
+ if (IS_ERR(con->port)) {
+ ret = PTR_ERR(con->port);
+ goto out;
+ }
+
+ /* Alternate modes */
+ ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_CON);
+ if (ret) {
+ dev_err(ucsi->dev, "con%d: failed to register alt modes\n",
+ con->num);
+ goto out;
+ }
+
+ /* Get the status */
+ command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(ucsi, command, &con->status, sizeof(con->status));
+ if (ret < 0) {
+ dev_err(ucsi->dev, "con%d: failed to get status\n", con->num);
+ ret = 0;
+ goto out;
+ }
+ ret = 0; /* ucsi_send_command() returns length on success */
+
+ switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
+ case UCSI_CONSTAT_PARTNER_TYPE_UFP:
+ case UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP:
+ u_role = USB_ROLE_HOST;
+ fallthrough;
+ case UCSI_CONSTAT_PARTNER_TYPE_CABLE:
+ typec_set_data_role(con->port, TYPEC_HOST);
+ break;
+ case UCSI_CONSTAT_PARTNER_TYPE_DFP:
+ u_role = USB_ROLE_DEVICE;
+ typec_set_data_role(con->port, TYPEC_DEVICE);
+ break;
+ default:
+ break;
+ }
+
+ /* Check if there is already something connected */
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED) {
+ typec_set_pwr_role(con->port,
+ !!(con->status.flags & UCSI_CONSTAT_PWR_DIR));
+ ucsi_pwr_opmode_change(con);
+ ucsi_register_partner(con);
+ ucsi_port_psy_changed(con);
+ }
+
+ /* Only notify USB controller if partner supports USB data */
+ if (!(UCSI_CONSTAT_PARTNER_FLAGS(con->status.flags) & UCSI_CONSTAT_PARTNER_FLAG_USB))
+ u_role = USB_ROLE_NONE;
+
+ ret = usb_role_switch_set_role(con->usb_role_sw, u_role);
+ if (ret) {
+ dev_err(ucsi->dev, "con:%d: failed to set usb role:%d\n",
+ con->num, u_role);
+ ret = 0;
+ }
+
+ if (con->partner &&
+ UCSI_CONSTAT_PWR_OPMODE(con->status.flags) ==
+ UCSI_CONSTAT_PWR_OPMODE_PD) {
+ ucsi_get_src_pdos(con);
+ ucsi_check_altmodes(con);
+ }
+
+ trace_ucsi_register_port(con->num, &con->status);
+
+out:
+ fwnode_handle_put(cap->fwnode);
+out_unlock:
+ mutex_unlock(&con->lock);
+
+ if (ret && con->wq) {
+ destroy_workqueue(con->wq);
+ con->wq = NULL;
+ }
+
+ return ret;
+}
+
+/**
+ * ucsi_init - Initialize UCSI interface
+ * @ucsi: UCSI to be initialized
+ *
+ * Registers all ports @ucsi has and enables all notification events.
+ */
+static int ucsi_init(struct ucsi *ucsi)
+{
+ struct ucsi_connector *con, *connector;
+ u64 command, ntfy;
+ int ret;
+ int i;
+
+ /* Reset the PPM */
+ ret = ucsi_reset_ppm(ucsi);
+ if (ret) {
+ dev_err(ucsi->dev, "failed to reset PPM!\n");
+ goto err;
+ }
+
+ /* Enable basic notifications */
+ ntfy = UCSI_ENABLE_NTFY_CMD_COMPLETE | UCSI_ENABLE_NTFY_ERROR;
+ command = UCSI_SET_NOTIFICATION_ENABLE | ntfy;
+ ret = ucsi_send_command(ucsi, command, NULL, 0);
+ if (ret < 0)
+ goto err_reset;
+
+ /* Get PPM capabilities */
+ command = UCSI_GET_CAPABILITY;
+ ret = ucsi_send_command(ucsi, command, &ucsi->cap, sizeof(ucsi->cap));
+ if (ret < 0)
+ goto err_reset;
+
+ if (!ucsi->cap.num_connectors) {
+ ret = -ENODEV;
+ goto err_reset;
+ }
+
+ /* Allocate the connectors. Released in ucsi_unregister() */
+ connector = kcalloc(ucsi->cap.num_connectors + 1, sizeof(*connector), GFP_KERNEL);
+ if (!connector) {
+ ret = -ENOMEM;
+ goto err_reset;
+ }
+
+ /* Register all connectors */
+ for (i = 0; i < ucsi->cap.num_connectors; i++) {
+ connector[i].num = i + 1;
+ ret = ucsi_register_port(ucsi, &connector[i]);
+ if (ret)
+ goto err_unregister;
+ }
+
+ /* Enable all notifications */
+ ntfy = UCSI_ENABLE_NTFY_ALL;
+ command = UCSI_SET_NOTIFICATION_ENABLE | ntfy;
+ ret = ucsi_send_command(ucsi, command, NULL, 0);
+ if (ret < 0)
+ goto err_unregister;
+
+ ucsi->connector = connector;
+ ucsi->ntfy = ntfy;
+ return 0;
+
+err_unregister:
+ for (con = connector; con->port; con++) {
+ ucsi_unregister_partner(con);
+ ucsi_unregister_altmodes(con, UCSI_RECIPIENT_CON);
+ ucsi_unregister_port_psy(con);
+ if (con->wq)
+ destroy_workqueue(con->wq);
+ typec_unregister_port(con->port);
+ con->port = NULL;
+ }
+ kfree(connector);
+err_reset:
+ memset(&ucsi->cap, 0, sizeof(ucsi->cap));
+ ucsi_reset_ppm(ucsi);
+err:
+ return ret;
+}
+
+static void ucsi_resume_work(struct work_struct *work)
+{
+ struct ucsi *ucsi = container_of(work, struct ucsi, resume_work);
+ struct ucsi_connector *con;
+ u64 command;
+ int ret;
+
+ /* Restore UCSI notification enable mask after system resume */
+ command = UCSI_SET_NOTIFICATION_ENABLE | ucsi->ntfy;
+ ret = ucsi_send_command(ucsi, command, NULL, 0);
+ if (ret < 0) {
+ dev_err(ucsi->dev, "failed to re-enable notifications (%d)\n", ret);
+ return;
+ }
+
+ for (con = ucsi->connector; con->port; con++) {
+ mutex_lock(&con->lock);
+ ucsi_partner_task(con, ucsi_check_connection, 1, 0);
+ mutex_unlock(&con->lock);
+ }
+}
+
+int ucsi_resume(struct ucsi *ucsi)
+{
+ if (ucsi->connector)
+ queue_work(system_long_wq, &ucsi->resume_work);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(ucsi_resume);
+
+static void ucsi_init_work(struct work_struct *work)
+{
+ struct ucsi *ucsi = container_of(work, struct ucsi, work.work);
+ int ret;
+
+ ret = ucsi_init(ucsi);
+ if (ret)
+ dev_err(ucsi->dev, "PPM init failed (%d)\n", ret);
+
+ if (ret == -EPROBE_DEFER) {
+ if (ucsi->work_count++ > UCSI_ROLE_SWITCH_WAIT_COUNT)
+ return;
+
+ queue_delayed_work(system_long_wq, &ucsi->work,
+ UCSI_ROLE_SWITCH_INTERVAL);
+ }
+}
+
+/**
+ * ucsi_get_drvdata - Return private driver data pointer
+ * @ucsi: UCSI interface
+ */
+void *ucsi_get_drvdata(struct ucsi *ucsi)
+{
+ return ucsi->driver_data;
+}
+EXPORT_SYMBOL_GPL(ucsi_get_drvdata);
+
+/**
+ * ucsi_set_drvdata - Assign private driver data pointer
+ * @ucsi: UCSI interface
+ * @data: Private data pointer
+ */
+void ucsi_set_drvdata(struct ucsi *ucsi, void *data)
+{
+ ucsi->driver_data = data;
+}
+EXPORT_SYMBOL_GPL(ucsi_set_drvdata);
+
+/**
+ * ucsi_create - Allocate UCSI instance
+ * @dev: Device interface to the PPM (Platform Policy Manager)
+ * @ops: I/O routines
+ */
+struct ucsi *ucsi_create(struct device *dev, const struct ucsi_operations *ops)
+{
+ struct ucsi *ucsi;
+
+ if (!ops || !ops->read || !ops->sync_write || !ops->async_write)
+ return ERR_PTR(-EINVAL);
+
+ ucsi = kzalloc(sizeof(*ucsi), GFP_KERNEL);
+ if (!ucsi)
+ return ERR_PTR(-ENOMEM);
+
+ INIT_WORK(&ucsi->resume_work, ucsi_resume_work);
+ INIT_DELAYED_WORK(&ucsi->work, ucsi_init_work);
+ mutex_init(&ucsi->ppm_lock);
+ ucsi->dev = dev;
+ ucsi->ops = ops;
+
+ return ucsi;
+}
+EXPORT_SYMBOL_GPL(ucsi_create);
+
+/**
+ * ucsi_destroy - Free UCSI instance
+ * @ucsi: UCSI instance to be freed
+ */
+void ucsi_destroy(struct ucsi *ucsi)
+{
+ kfree(ucsi);
+}
+EXPORT_SYMBOL_GPL(ucsi_destroy);
+
+/**
+ * ucsi_register - Register UCSI interface
+ * @ucsi: UCSI instance
+ */
+int ucsi_register(struct ucsi *ucsi)
+{
+ int ret;
+
+ ret = ucsi->ops->read(ucsi, UCSI_VERSION, &ucsi->version,
+ sizeof(ucsi->version));
+ if (ret)
+ return ret;
+
+ if (!ucsi->version)
+ return -ENODEV;
+
+ queue_delayed_work(system_long_wq, &ucsi->work, 0);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(ucsi_register);
+
+/**
+ * ucsi_unregister - Unregister UCSI interface
+ * @ucsi: UCSI interface to be unregistered
+ *
+ * Unregister UCSI interface that was created with ucsi_register().
+ */
+void ucsi_unregister(struct ucsi *ucsi)
+{
+ u64 cmd = UCSI_SET_NOTIFICATION_ENABLE;
+ int i;
+
+ /* Make sure that we are not in the middle of driver initialization */
+ cancel_delayed_work_sync(&ucsi->work);
+ cancel_work_sync(&ucsi->resume_work);
+
+ /* Disable notifications */
+ ucsi->ops->async_write(ucsi, UCSI_CONTROL, &cmd, sizeof(cmd));
+
+ if (!ucsi->connector)
+ return;
+
+ for (i = 0; i < ucsi->cap.num_connectors; i++) {
+ cancel_work_sync(&ucsi->connector[i].work);
+ ucsi_unregister_partner(&ucsi->connector[i]);
+ ucsi_unregister_altmodes(&ucsi->connector[i],
+ UCSI_RECIPIENT_CON);
+ ucsi_unregister_port_psy(&ucsi->connector[i]);
+
+ if (ucsi->connector[i].wq) {
+ struct ucsi_work *uwork;
+
+ mutex_lock(&ucsi->connector[i].lock);
+ /*
+ * queue delayed items immediately so they can execute
+ * and free themselves before the wq is destroyed
+ */
+ list_for_each_entry(uwork, &ucsi->connector[i].partner_tasks, node)
+ mod_delayed_work(ucsi->connector[i].wq, &uwork->work, 0);
+ mutex_unlock(&ucsi->connector[i].lock);
+ destroy_workqueue(ucsi->connector[i].wq);
+ }
+ typec_unregister_port(ucsi->connector[i].port);
+ }
+
+ kfree(ucsi->connector);
+}
+EXPORT_SYMBOL_GPL(ucsi_unregister);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("USB Type-C Connector System Software Interface driver");
diff --git a/drivers/usb/typec/ucsi/ucsi.h b/drivers/usb/typec/ucsi/ucsi.h
new file mode 100644
index 000000000..60ce9fb6e
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi.h
@@ -0,0 +1,390 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __DRIVER_USB_TYPEC_UCSI_H
+#define __DRIVER_USB_TYPEC_UCSI_H
+
+#include <linux/bitops.h>
+#include <linux/device.h>
+#include <linux/power_supply.h>
+#include <linux/types.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/role.h>
+
+/* -------------------------------------------------------------------------- */
+
+struct ucsi;
+struct ucsi_altmode;
+
+/* UCSI offsets (Bytes) */
+#define UCSI_VERSION 0
+#define UCSI_CCI 4
+#define UCSI_CONTROL 8
+#define UCSI_MESSAGE_IN 16
+#define UCSI_MESSAGE_OUT 32
+
+/* Command Status and Connector Change Indication (CCI) bits */
+#define UCSI_CCI_CONNECTOR(_c_) (((_c_) & GENMASK(7, 1)) >> 1)
+#define UCSI_CCI_LENGTH(_c_) (((_c_) & GENMASK(15, 8)) >> 8)
+#define UCSI_CCI_NOT_SUPPORTED BIT(25)
+#define UCSI_CCI_CANCEL_COMPLETE BIT(26)
+#define UCSI_CCI_RESET_COMPLETE BIT(27)
+#define UCSI_CCI_BUSY BIT(28)
+#define UCSI_CCI_ACK_COMPLETE BIT(29)
+#define UCSI_CCI_ERROR BIT(30)
+#define UCSI_CCI_COMMAND_COMPLETE BIT(31)
+
+/**
+ * struct ucsi_operations - UCSI I/O operations
+ * @read: Read operation
+ * @sync_write: Blocking write operation
+ * @async_write: Non-blocking write operation
+ * @update_altmodes: Squashes duplicate DP altmodes
+ *
+ * Read and write routines for UCSI interface. @sync_write must wait for the
+ * Command Completion Event from the PPM before returning, and @async_write must
+ * return immediately after sending the data to the PPM.
+ */
+struct ucsi_operations {
+ int (*read)(struct ucsi *ucsi, unsigned int offset,
+ void *val, size_t val_len);
+ int (*sync_write)(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len);
+ int (*async_write)(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len);
+ bool (*update_altmodes)(struct ucsi *ucsi, struct ucsi_altmode *orig,
+ struct ucsi_altmode *updated);
+};
+
+struct ucsi *ucsi_create(struct device *dev, const struct ucsi_operations *ops);
+void ucsi_destroy(struct ucsi *ucsi);
+int ucsi_register(struct ucsi *ucsi);
+void ucsi_unregister(struct ucsi *ucsi);
+void *ucsi_get_drvdata(struct ucsi *ucsi);
+void ucsi_set_drvdata(struct ucsi *ucsi, void *data);
+
+void ucsi_connector_change(struct ucsi *ucsi, u8 num);
+
+/* -------------------------------------------------------------------------- */
+
+/* Commands */
+#define UCSI_PPM_RESET 0x01
+#define UCSI_CANCEL 0x02
+#define UCSI_CONNECTOR_RESET 0x03
+#define UCSI_ACK_CC_CI 0x04
+#define UCSI_SET_NOTIFICATION_ENABLE 0x05
+#define UCSI_GET_CAPABILITY 0x06
+#define UCSI_GET_CONNECTOR_CAPABILITY 0x07
+#define UCSI_SET_UOM 0x08
+#define UCSI_SET_UOR 0x09
+#define UCSI_SET_PDM 0x0a
+#define UCSI_SET_PDR 0x0b
+#define UCSI_GET_ALTERNATE_MODES 0x0c
+#define UCSI_GET_CAM_SUPPORTED 0x0d
+#define UCSI_GET_CURRENT_CAM 0x0e
+#define UCSI_SET_NEW_CAM 0x0f
+#define UCSI_GET_PDOS 0x10
+#define UCSI_GET_CABLE_PROPERTY 0x11
+#define UCSI_GET_CONNECTOR_STATUS 0x12
+#define UCSI_GET_ERROR_STATUS 0x13
+
+#define UCSI_CONNECTOR_NUMBER(_num_) ((u64)(_num_) << 16)
+#define UCSI_COMMAND(_cmd_) ((_cmd_) & 0xff)
+
+/* CONNECTOR_RESET command bits */
+#define UCSI_CONNECTOR_RESET_HARD BIT(23) /* Deprecated in v1.1 */
+
+/* ACK_CC_CI bits */
+#define UCSI_ACK_CONNECTOR_CHANGE BIT(16)
+#define UCSI_ACK_COMMAND_COMPLETE BIT(17)
+
+/* SET_NOTIFICATION_ENABLE command bits */
+#define UCSI_ENABLE_NTFY_CMD_COMPLETE BIT(16)
+#define UCSI_ENABLE_NTFY_EXT_PWR_SRC_CHANGE BIT(17)
+#define UCSI_ENABLE_NTFY_PWR_OPMODE_CHANGE BIT(18)
+#define UCSI_ENABLE_NTFY_CAP_CHANGE BIT(21)
+#define UCSI_ENABLE_NTFY_PWR_LEVEL_CHANGE BIT(22)
+#define UCSI_ENABLE_NTFY_PD_RESET_COMPLETE BIT(23)
+#define UCSI_ENABLE_NTFY_CAM_CHANGE BIT(24)
+#define UCSI_ENABLE_NTFY_BAT_STATUS_CHANGE BIT(25)
+#define UCSI_ENABLE_NTFY_PARTNER_CHANGE BIT(27)
+#define UCSI_ENABLE_NTFY_PWR_DIR_CHANGE BIT(28)
+#define UCSI_ENABLE_NTFY_CONNECTOR_CHANGE BIT(30)
+#define UCSI_ENABLE_NTFY_ERROR BIT(31)
+#define UCSI_ENABLE_NTFY_ALL 0xdbe70000
+
+/* SET_UOR command bits */
+#define UCSI_SET_UOR_ROLE(_r_) (((_r_) == TYPEC_HOST ? 1 : 2) << 23)
+#define UCSI_SET_UOR_ACCEPT_ROLE_SWAPS BIT(25)
+
+/* SET_PDF command bits */
+#define UCSI_SET_PDR_ROLE(_r_) (((_r_) == TYPEC_SOURCE ? 1 : 2) << 23)
+#define UCSI_SET_PDR_ACCEPT_ROLE_SWAPS BIT(25)
+
+/* GET_ALTERNATE_MODES command bits */
+#define UCSI_ALTMODE_RECIPIENT(_r_) (((_r_) >> 16) & 0x7)
+#define UCSI_GET_ALTMODE_RECIPIENT(_r_) ((u64)(_r_) << 16)
+#define UCSI_RECIPIENT_CON 0
+#define UCSI_RECIPIENT_SOP 1
+#define UCSI_RECIPIENT_SOP_P 2
+#define UCSI_RECIPIENT_SOP_PP 3
+#define UCSI_GET_ALTMODE_CONNECTOR_NUMBER(_r_) ((u64)(_r_) << 24)
+#define UCSI_ALTMODE_OFFSET(_r_) (((_r_) >> 32) & 0xff)
+#define UCSI_GET_ALTMODE_OFFSET(_r_) ((u64)(_r_) << 32)
+#define UCSI_GET_ALTMODE_NUM_ALTMODES(_r_) ((u64)(_r_) << 40)
+
+/* GET_PDOS command bits */
+#define UCSI_GET_PDOS_PARTNER_PDO(_r_) ((u64)(_r_) << 23)
+#define UCSI_GET_PDOS_PDO_OFFSET(_r_) ((u64)(_r_) << 24)
+#define UCSI_GET_PDOS_NUM_PDOS(_r_) ((u64)(_r_) << 32)
+#define UCSI_MAX_PDOS (4)
+#define UCSI_GET_PDOS_SRC_PDOS ((u64)1 << 34)
+
+/* -------------------------------------------------------------------------- */
+
+/* Error information returned by PPM in response to GET_ERROR_STATUS command. */
+#define UCSI_ERROR_UNREGONIZED_CMD BIT(0)
+#define UCSI_ERROR_INVALID_CON_NUM BIT(1)
+#define UCSI_ERROR_INVALID_CMD_ARGUMENT BIT(2)
+#define UCSI_ERROR_INCOMPATIBLE_PARTNER BIT(3)
+#define UCSI_ERROR_CC_COMMUNICATION_ERR BIT(4)
+#define UCSI_ERROR_DEAD_BATTERY BIT(5)
+#define UCSI_ERROR_CONTRACT_NEGOTIATION_FAIL BIT(6)
+#define UCSI_ERROR_OVERCURRENT BIT(7)
+#define UCSI_ERROR_UNDEFINED BIT(8)
+#define UCSI_ERROR_PARTNER_REJECTED_SWAP BIT(9)
+#define UCSI_ERROR_HARD_RESET BIT(10)
+#define UCSI_ERROR_PPM_POLICY_CONFLICT BIT(11)
+#define UCSI_ERROR_SWAP_REJECTED BIT(12)
+
+#define UCSI_SET_NEW_CAM_ENTER(x) (((x) >> 23) & 0x1)
+#define UCSI_SET_NEW_CAM_GET_AM(x) (((x) >> 24) & 0xff)
+#define UCSI_SET_NEW_CAM_AM_MASK (0xff << 24)
+#define UCSI_SET_NEW_CAM_SET_AM(x) (((x) & 0xff) << 24)
+#define UCSI_CMD_CONNECTOR_MASK (0x7)
+
+/* Data structure filled by PPM in response to GET_CAPABILITY command. */
+struct ucsi_capability {
+ u32 attributes;
+#define UCSI_CAP_ATTR_DISABLE_STATE BIT(0)
+#define UCSI_CAP_ATTR_BATTERY_CHARGING BIT(1)
+#define UCSI_CAP_ATTR_USB_PD BIT(2)
+#define UCSI_CAP_ATTR_TYPEC_CURRENT BIT(6)
+#define UCSI_CAP_ATTR_POWER_AC_SUPPLY BIT(8)
+#define UCSI_CAP_ATTR_POWER_OTHER BIT(10)
+#define UCSI_CAP_ATTR_POWER_VBUS BIT(14)
+ u8 num_connectors;
+ u8 features;
+#define UCSI_CAP_SET_UOM BIT(0)
+#define UCSI_CAP_SET_PDM BIT(1)
+#define UCSI_CAP_ALT_MODE_DETAILS BIT(2)
+#define UCSI_CAP_ALT_MODE_OVERRIDE BIT(3)
+#define UCSI_CAP_PDO_DETAILS BIT(4)
+#define UCSI_CAP_CABLE_DETAILS BIT(5)
+#define UCSI_CAP_EXT_SUPPLY_NOTIFICATIONS BIT(6)
+#define UCSI_CAP_PD_RESET BIT(7)
+ u16 reserved_1;
+ u8 num_alt_modes;
+ u8 reserved_2;
+ u16 bc_version;
+ u16 pd_version;
+ u16 typec_version;
+} __packed;
+
+/* Data structure filled by PPM in response to GET_CONNECTOR_CAPABILITY cmd. */
+struct ucsi_connector_capability {
+ u8 op_mode;
+#define UCSI_CONCAP_OPMODE_DFP BIT(0)
+#define UCSI_CONCAP_OPMODE_UFP BIT(1)
+#define UCSI_CONCAP_OPMODE_DRP BIT(2)
+#define UCSI_CONCAP_OPMODE_AUDIO_ACCESSORY BIT(3)
+#define UCSI_CONCAP_OPMODE_DEBUG_ACCESSORY BIT(4)
+#define UCSI_CONCAP_OPMODE_USB2 BIT(5)
+#define UCSI_CONCAP_OPMODE_USB3 BIT(6)
+#define UCSI_CONCAP_OPMODE_ALT_MODE BIT(7)
+ u8 flags;
+#define UCSI_CONCAP_FLAG_PROVIDER BIT(0)
+#define UCSI_CONCAP_FLAG_CONSUMER BIT(1)
+} __packed;
+
+struct ucsi_altmode {
+ u16 svid;
+ u32 mid;
+} __packed;
+
+/* Data structure filled by PPM in response to GET_CABLE_PROPERTY command. */
+struct ucsi_cable_property {
+ u16 speed_supported;
+ u8 current_capability;
+ u8 flags;
+#define UCSI_CABLE_PROP_FLAG_VBUS_IN_CABLE BIT(0)
+#define UCSI_CABLE_PROP_FLAG_ACTIVE_CABLE BIT(1)
+#define UCSI_CABLE_PROP_FLAG_DIRECTIONALITY BIT(2)
+#define UCSI_CABLE_PROP_FLAG_PLUG_TYPE(_f_) ((_f_) & GENMASK(3, 0))
+#define UCSI_CABLE_PROPERTY_PLUG_TYPE_A 0
+#define UCSI_CABLE_PROPERTY_PLUG_TYPE_B 1
+#define UCSI_CABLE_PROPERTY_PLUG_TYPE_C 2
+#define UCSI_CABLE_PROPERTY_PLUG_OTHER 3
+#define UCSI_CABLE_PROP_MODE_SUPPORT BIT(5)
+ u8 latency;
+} __packed;
+
+/* Data structure filled by PPM in response to GET_CONNECTOR_STATUS command. */
+struct ucsi_connector_status {
+ u16 change;
+#define UCSI_CONSTAT_EXT_SUPPLY_CHANGE BIT(1)
+#define UCSI_CONSTAT_POWER_OPMODE_CHANGE BIT(2)
+#define UCSI_CONSTAT_PDOS_CHANGE BIT(5)
+#define UCSI_CONSTAT_POWER_LEVEL_CHANGE BIT(6)
+#define UCSI_CONSTAT_PD_RESET_COMPLETE BIT(7)
+#define UCSI_CONSTAT_CAM_CHANGE BIT(8)
+#define UCSI_CONSTAT_BC_CHANGE BIT(9)
+#define UCSI_CONSTAT_PARTNER_CHANGE BIT(11)
+#define UCSI_CONSTAT_POWER_DIR_CHANGE BIT(12)
+#define UCSI_CONSTAT_CONNECT_CHANGE BIT(14)
+#define UCSI_CONSTAT_ERROR BIT(15)
+ u16 flags;
+#define UCSI_CONSTAT_PWR_OPMODE(_f_) ((_f_) & GENMASK(2, 0))
+#define UCSI_CONSTAT_PWR_OPMODE_NONE 0
+#define UCSI_CONSTAT_PWR_OPMODE_DEFAULT 1
+#define UCSI_CONSTAT_PWR_OPMODE_BC 2
+#define UCSI_CONSTAT_PWR_OPMODE_PD 3
+#define UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5 4
+#define UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0 5
+#define UCSI_CONSTAT_CONNECTED BIT(3)
+#define UCSI_CONSTAT_PWR_DIR BIT(4)
+#define UCSI_CONSTAT_PARTNER_FLAGS(_f_) (((_f_) & GENMASK(12, 5)) >> 5)
+#define UCSI_CONSTAT_PARTNER_FLAG_USB 1
+#define UCSI_CONSTAT_PARTNER_FLAG_ALT_MODE 2
+#define UCSI_CONSTAT_PARTNER_TYPE(_f_) (((_f_) & GENMASK(15, 13)) >> 13)
+#define UCSI_CONSTAT_PARTNER_TYPE_DFP 1
+#define UCSI_CONSTAT_PARTNER_TYPE_UFP 2
+#define UCSI_CONSTAT_PARTNER_TYPE_CABLE 3 /* Powered Cable */
+#define UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP 4 /* Powered Cable */
+#define UCSI_CONSTAT_PARTNER_TYPE_DEBUG 5
+#define UCSI_CONSTAT_PARTNER_TYPE_AUDIO 6
+ u32 request_data_obj;
+ u8 pwr_status;
+#define UCSI_CONSTAT_BC_STATUS(_p_) ((_p_) & GENMASK(2, 0))
+#define UCSI_CONSTAT_BC_NOT_CHARGING 0
+#define UCSI_CONSTAT_BC_NOMINAL_CHARGING 1
+#define UCSI_CONSTAT_BC_SLOW_CHARGING 2
+#define UCSI_CONSTAT_BC_TRICKLE_CHARGING 3
+#define UCSI_CONSTAT_PROVIDER_CAP_LIMIT(_p_) (((_p_) & GENMASK(6, 3)) >> 3)
+#define UCSI_CONSTAT_CAP_PWR_LOWERED 0
+#define UCSI_CONSTAT_CAP_PWR_BUDGET_LIMIT 1
+} __packed;
+
+/* -------------------------------------------------------------------------- */
+
+struct ucsi {
+ u16 version;
+ struct device *dev;
+ struct driver_data *driver_data;
+
+ const struct ucsi_operations *ops;
+
+ struct ucsi_capability cap;
+ struct ucsi_connector *connector;
+
+ struct work_struct resume_work;
+ struct delayed_work work;
+ int work_count;
+#define UCSI_ROLE_SWITCH_RETRY_PER_HZ 10
+#define UCSI_ROLE_SWITCH_INTERVAL (HZ / UCSI_ROLE_SWITCH_RETRY_PER_HZ)
+#define UCSI_ROLE_SWITCH_WAIT_COUNT (10 * UCSI_ROLE_SWITCH_RETRY_PER_HZ)
+
+ /* PPM Communication lock */
+ struct mutex ppm_lock;
+
+ /* The latest "Notification Enable" bits (SET_NOTIFICATION_ENABLE) */
+ u64 ntfy;
+
+ /* PPM communication flags */
+ unsigned long flags;
+#define EVENT_PENDING 0
+#define COMMAND_PENDING 1
+#define ACK_PENDING 2
+};
+
+#define UCSI_MAX_SVID 5
+#define UCSI_MAX_ALTMODES (UCSI_MAX_SVID * 6)
+
+#define UCSI_TYPEC_VSAFE5V 5000
+#define UCSI_TYPEC_1_5_CURRENT 1500
+#define UCSI_TYPEC_3_0_CURRENT 3000
+
+struct ucsi_connector {
+ int num;
+
+ struct ucsi *ucsi;
+ struct mutex lock; /* port lock */
+ struct work_struct work;
+ struct completion complete;
+ struct workqueue_struct *wq;
+ struct list_head partner_tasks;
+
+ struct typec_port *port;
+ struct typec_partner *partner;
+
+ struct typec_altmode *port_altmode[UCSI_MAX_ALTMODES];
+ struct typec_altmode *partner_altmode[UCSI_MAX_ALTMODES];
+
+ struct typec_capability typec_cap;
+
+ struct ucsi_connector_status status;
+ struct ucsi_connector_capability cap;
+ struct power_supply *psy;
+ struct power_supply_desc psy_desc;
+ u32 rdo;
+ u32 src_pdos[PDO_MAX_OBJECTS];
+ int num_pdos;
+
+ struct usb_role_switch *usb_role_sw;
+};
+
+int ucsi_send_command(struct ucsi *ucsi, u64 command,
+ void *retval, size_t size);
+
+void ucsi_altmode_update_active(struct ucsi_connector *con);
+int ucsi_resume(struct ucsi *ucsi);
+
+#if IS_ENABLED(CONFIG_POWER_SUPPLY)
+int ucsi_register_port_psy(struct ucsi_connector *con);
+void ucsi_unregister_port_psy(struct ucsi_connector *con);
+void ucsi_port_psy_changed(struct ucsi_connector *con);
+#else
+static inline int ucsi_register_port_psy(struct ucsi_connector *con) { return 0; }
+static inline void ucsi_unregister_port_psy(struct ucsi_connector *con) { }
+static inline void ucsi_port_psy_changed(struct ucsi_connector *con) { }
+#endif /* CONFIG_POWER_SUPPLY */
+
+#if IS_ENABLED(CONFIG_TYPEC_DP_ALTMODE)
+struct typec_altmode *
+ucsi_register_displayport(struct ucsi_connector *con,
+ bool override, int offset,
+ struct typec_altmode_desc *desc);
+
+void ucsi_displayport_remove_partner(struct typec_altmode *adev);
+
+#else
+static inline struct typec_altmode *
+ucsi_register_displayport(struct ucsi_connector *con,
+ bool override, int offset,
+ struct typec_altmode_desc *desc)
+{
+ return NULL;
+}
+
+static inline void
+ucsi_displayport_remove_partner(struct typec_altmode *adev) { }
+#endif /* CONFIG_TYPEC_DP_ALTMODE */
+
+/*
+ * NVIDIA VirtualLink (svid 0x955) has two altmode. VirtualLink
+ * DP mode with vdo=0x1 and NVIDIA test mode with vdo=0x3
+ */
+#define USB_TYPEC_NVIDIA_VLINK_DP_VDO 0x1
+#define USB_TYPEC_NVIDIA_VLINK_DBG_VDO 0x3
+
+#endif /* __DRIVER_USB_TYPEC_UCSI_H */
diff --git a/drivers/usb/typec/ucsi/ucsi_acpi.c b/drivers/usb/typec/ucsi/ucsi_acpi.c
new file mode 100644
index 000000000..217355f1f
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi_acpi.c
@@ -0,0 +1,257 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI ACPI driver
+ *
+ * Copyright (C) 2017, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/platform_device.h>
+#include <linux/module.h>
+#include <linux/acpi.h>
+#include <linux/dmi.h>
+
+#include "ucsi.h"
+
+#define UCSI_DSM_UUID "6f8398c2-7ca4-11e4-ad36-631042b5008f"
+#define UCSI_DSM_FUNC_WRITE 1
+#define UCSI_DSM_FUNC_READ 2
+
+struct ucsi_acpi {
+ struct device *dev;
+ struct ucsi *ucsi;
+ void *base;
+ struct completion complete;
+ unsigned long flags;
+ guid_t guid;
+ u64 cmd;
+};
+
+static int ucsi_acpi_dsm(struct ucsi_acpi *ua, int func)
+{
+ union acpi_object *obj;
+
+ obj = acpi_evaluate_dsm(ACPI_HANDLE(ua->dev), &ua->guid, 1, func,
+ NULL);
+ if (!obj) {
+ dev_err(ua->dev, "%s: failed to evaluate _DSM %d\n",
+ __func__, func);
+ return -EIO;
+ }
+
+ ACPI_FREE(obj);
+ return 0;
+}
+
+static int ucsi_acpi_read(struct ucsi *ucsi, unsigned int offset,
+ void *val, size_t val_len)
+{
+ struct ucsi_acpi *ua = ucsi_get_drvdata(ucsi);
+ int ret;
+
+ ret = ucsi_acpi_dsm(ua, UCSI_DSM_FUNC_READ);
+ if (ret)
+ return ret;
+
+ memcpy(val, ua->base + offset, val_len);
+
+ return 0;
+}
+
+static int ucsi_acpi_async_write(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len)
+{
+ struct ucsi_acpi *ua = ucsi_get_drvdata(ucsi);
+
+ memcpy(ua->base + offset, val, val_len);
+ ua->cmd = *(u64 *)val;
+
+ return ucsi_acpi_dsm(ua, UCSI_DSM_FUNC_WRITE);
+}
+
+static int ucsi_acpi_sync_write(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len)
+{
+ struct ucsi_acpi *ua = ucsi_get_drvdata(ucsi);
+ int ret;
+
+ set_bit(COMMAND_PENDING, &ua->flags);
+
+ ret = ucsi_acpi_async_write(ucsi, offset, val, val_len);
+ if (ret)
+ goto out_clear_bit;
+
+ if (!wait_for_completion_timeout(&ua->complete, 5 * HZ))
+ ret = -ETIMEDOUT;
+
+out_clear_bit:
+ clear_bit(COMMAND_PENDING, &ua->flags);
+
+ return ret;
+}
+
+static const struct ucsi_operations ucsi_acpi_ops = {
+ .read = ucsi_acpi_read,
+ .sync_write = ucsi_acpi_sync_write,
+ .async_write = ucsi_acpi_async_write
+};
+
+static int
+ucsi_zenbook_read(struct ucsi *ucsi, unsigned int offset, void *val, size_t val_len)
+{
+ struct ucsi_acpi *ua = ucsi_get_drvdata(ucsi);
+ int ret;
+
+ if (offset == UCSI_VERSION || UCSI_COMMAND(ua->cmd) == UCSI_PPM_RESET) {
+ ret = ucsi_acpi_dsm(ua, UCSI_DSM_FUNC_READ);
+ if (ret)
+ return ret;
+ }
+
+ memcpy(val, ua->base + offset, val_len);
+
+ return 0;
+}
+
+static const struct ucsi_operations ucsi_zenbook_ops = {
+ .read = ucsi_zenbook_read,
+ .sync_write = ucsi_acpi_sync_write,
+ .async_write = ucsi_acpi_async_write
+};
+
+static const struct dmi_system_id zenbook_dmi_id[] = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "ZenBook UX325UA_UM325UA"),
+ },
+ },
+ { }
+};
+
+static void ucsi_acpi_notify(acpi_handle handle, u32 event, void *data)
+{
+ struct ucsi_acpi *ua = data;
+ u32 cci;
+ int ret;
+
+ ret = ua->ucsi->ops->read(ua->ucsi, UCSI_CCI, &cci, sizeof(cci));
+ if (ret)
+ return;
+
+ if (UCSI_CCI_CONNECTOR(cci))
+ ucsi_connector_change(ua->ucsi, UCSI_CCI_CONNECTOR(cci));
+
+ if (test_bit(COMMAND_PENDING, &ua->flags) &&
+ cci & (UCSI_CCI_ACK_COMPLETE | UCSI_CCI_COMMAND_COMPLETE))
+ complete(&ua->complete);
+}
+
+static int ucsi_acpi_probe(struct platform_device *pdev)
+{
+ struct acpi_device *adev = ACPI_COMPANION(&pdev->dev);
+ const struct ucsi_operations *ops = &ucsi_acpi_ops;
+ struct ucsi_acpi *ua;
+ struct resource *res;
+ acpi_status status;
+ int ret;
+
+ if (adev->dep_unmet)
+ return -EPROBE_DEFER;
+
+ ua = devm_kzalloc(&pdev->dev, sizeof(*ua), GFP_KERNEL);
+ if (!ua)
+ return -ENOMEM;
+
+ res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+ if (!res) {
+ dev_err(&pdev->dev, "missing memory resource\n");
+ return -ENODEV;
+ }
+
+ ua->base = devm_memremap(&pdev->dev, res->start, resource_size(res), MEMREMAP_WB);
+ if (IS_ERR(ua->base))
+ return PTR_ERR(ua->base);
+
+ ret = guid_parse(UCSI_DSM_UUID, &ua->guid);
+ if (ret)
+ return ret;
+
+ init_completion(&ua->complete);
+ ua->dev = &pdev->dev;
+
+ if (dmi_check_system(zenbook_dmi_id))
+ ops = &ucsi_zenbook_ops;
+
+ ua->ucsi = ucsi_create(&pdev->dev, ops);
+ if (IS_ERR(ua->ucsi))
+ return PTR_ERR(ua->ucsi);
+
+ ucsi_set_drvdata(ua->ucsi, ua);
+
+ status = acpi_install_notify_handler(ACPI_HANDLE(&pdev->dev),
+ ACPI_DEVICE_NOTIFY,
+ ucsi_acpi_notify, ua);
+ if (ACPI_FAILURE(status)) {
+ dev_err(&pdev->dev, "failed to install notify handler\n");
+ ucsi_destroy(ua->ucsi);
+ return -ENODEV;
+ }
+
+ ret = ucsi_register(ua->ucsi);
+ if (ret) {
+ acpi_remove_notify_handler(ACPI_HANDLE(&pdev->dev),
+ ACPI_DEVICE_NOTIFY,
+ ucsi_acpi_notify);
+ ucsi_destroy(ua->ucsi);
+ return ret;
+ }
+
+ platform_set_drvdata(pdev, ua);
+
+ return 0;
+}
+
+static int ucsi_acpi_remove(struct platform_device *pdev)
+{
+ struct ucsi_acpi *ua = platform_get_drvdata(pdev);
+
+ ucsi_unregister(ua->ucsi);
+ ucsi_destroy(ua->ucsi);
+
+ acpi_remove_notify_handler(ACPI_HANDLE(&pdev->dev), ACPI_DEVICE_NOTIFY,
+ ucsi_acpi_notify);
+
+ return 0;
+}
+
+static int ucsi_acpi_resume(struct device *dev)
+{
+ struct ucsi_acpi *ua = dev_get_drvdata(dev);
+
+ return ucsi_resume(ua->ucsi);
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(ucsi_acpi_pm_ops, NULL, ucsi_acpi_resume);
+
+static const struct acpi_device_id ucsi_acpi_match[] = {
+ { "PNP0CA0", 0 },
+ { },
+};
+MODULE_DEVICE_TABLE(acpi, ucsi_acpi_match);
+
+static struct platform_driver ucsi_acpi_platform_driver = {
+ .driver = {
+ .name = "ucsi_acpi",
+ .pm = pm_ptr(&ucsi_acpi_pm_ops),
+ .acpi_match_table = ACPI_PTR(ucsi_acpi_match),
+ },
+ .probe = ucsi_acpi_probe,
+ .remove = ucsi_acpi_remove,
+};
+
+module_platform_driver(ucsi_acpi_platform_driver);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("UCSI ACPI driver");
diff --git a/drivers/usb/typec/ucsi/ucsi_ccg.c b/drivers/usb/typec/ucsi/ucsi_ccg.c
new file mode 100644
index 000000000..835f1c437
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi_ccg.c
@@ -0,0 +1,1494 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI driver for Cypress CCGx Type-C controller
+ *
+ * Copyright (C) 2017-2018 NVIDIA Corporation. All rights reserved.
+ * Author: Ajay Gupta <ajayg@nvidia.com>
+ *
+ * Some code borrowed from drivers/usb/typec/ucsi/ucsi_acpi.c
+ */
+#include <linux/acpi.h>
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/pci.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/pm_runtime.h>
+#include <linux/usb/typec_dp.h>
+
+#include <asm/unaligned.h>
+#include "ucsi.h"
+
+enum enum_fw_mode {
+ BOOT, /* bootloader */
+ FW1, /* FW partition-1 (contains secondary fw) */
+ FW2, /* FW partition-2 (contains primary fw) */
+ FW_INVALID,
+};
+
+#define CCGX_RAB_DEVICE_MODE 0x0000
+#define CCGX_RAB_INTR_REG 0x0006
+#define DEV_INT BIT(0)
+#define PORT0_INT BIT(1)
+#define PORT1_INT BIT(2)
+#define UCSI_READ_INT BIT(7)
+#define CCGX_RAB_JUMP_TO_BOOT 0x0007
+#define TO_BOOT 'J'
+#define TO_ALT_FW 'A'
+#define CCGX_RAB_RESET_REQ 0x0008
+#define RESET_SIG 'R'
+#define CMD_RESET_I2C 0x0
+#define CMD_RESET_DEV 0x1
+#define CCGX_RAB_ENTER_FLASHING 0x000A
+#define FLASH_ENTER_SIG 'P'
+#define CCGX_RAB_VALIDATE_FW 0x000B
+#define CCGX_RAB_FLASH_ROW_RW 0x000C
+#define FLASH_SIG 'F'
+#define FLASH_RD_CMD 0x0
+#define FLASH_WR_CMD 0x1
+#define FLASH_FWCT1_WR_CMD 0x2
+#define FLASH_FWCT2_WR_CMD 0x3
+#define FLASH_FWCT_SIG_WR_CMD 0x4
+#define CCGX_RAB_READ_ALL_VER 0x0010
+#define CCGX_RAB_READ_FW2_VER 0x0020
+#define CCGX_RAB_UCSI_CONTROL 0x0039
+#define CCGX_RAB_UCSI_CONTROL_START BIT(0)
+#define CCGX_RAB_UCSI_CONTROL_STOP BIT(1)
+#define CCGX_RAB_UCSI_DATA_BLOCK(offset) (0xf000 | ((offset) & 0xff))
+#define REG_FLASH_RW_MEM 0x0200
+#define DEV_REG_IDX CCGX_RAB_DEVICE_MODE
+#define CCGX_RAB_PDPORT_ENABLE 0x002C
+#define PDPORT_1 BIT(0)
+#define PDPORT_2 BIT(1)
+#define CCGX_RAB_RESPONSE 0x007E
+#define ASYNC_EVENT BIT(7)
+
+/* CCGx events & async msg codes */
+#define RESET_COMPLETE 0x80
+#define EVENT_INDEX RESET_COMPLETE
+#define PORT_CONNECT_DET 0x84
+#define PORT_DISCONNECT_DET 0x85
+#define ROLE_SWAP_COMPELETE 0x87
+
+/* ccg firmware */
+#define CYACD_LINE_SIZE 527
+#define CCG4_ROW_SIZE 256
+#define FW1_METADATA_ROW 0x1FF
+#define FW2_METADATA_ROW 0x1FE
+#define FW_CFG_TABLE_SIG_SIZE 256
+
+static int secondary_fw_min_ver = 41;
+
+enum enum_flash_mode {
+ SECONDARY_BL, /* update secondary using bootloader */
+ PRIMARY, /* update primary using secondary */
+ SECONDARY, /* update secondary using primary */
+ FLASH_NOT_NEEDED, /* update not required */
+ FLASH_INVALID,
+};
+
+static const char * const ccg_fw_names[] = {
+ "ccg_boot.cyacd",
+ "ccg_primary.cyacd",
+ "ccg_secondary.cyacd"
+};
+
+struct ccg_dev_info {
+#define CCG_DEVINFO_FWMODE_SHIFT (0)
+#define CCG_DEVINFO_FWMODE_MASK (0x3 << CCG_DEVINFO_FWMODE_SHIFT)
+#define CCG_DEVINFO_PDPORTS_SHIFT (2)
+#define CCG_DEVINFO_PDPORTS_MASK (0x3 << CCG_DEVINFO_PDPORTS_SHIFT)
+ u8 mode;
+ u8 bl_mode;
+ __le16 silicon_id;
+ __le16 bl_last_row;
+} __packed;
+
+struct version_format {
+ __le16 build;
+ u8 patch;
+ u8 ver;
+#define CCG_VERSION_PATCH(x) ((x) << 16)
+#define CCG_VERSION(x) ((x) << 24)
+#define CCG_VERSION_MIN_SHIFT (0)
+#define CCG_VERSION_MIN_MASK (0xf << CCG_VERSION_MIN_SHIFT)
+#define CCG_VERSION_MAJ_SHIFT (4)
+#define CCG_VERSION_MAJ_MASK (0xf << CCG_VERSION_MAJ_SHIFT)
+} __packed;
+
+/*
+ * Firmware version 3.1.10 or earlier, built for NVIDIA has known issue
+ * of missing interrupt when a device is connected for runtime resume
+ */
+#define CCG_FW_BUILD_NVIDIA (('n' << 8) | 'v')
+#define CCG_OLD_FW_VERSION (CCG_VERSION(0x31) | CCG_VERSION_PATCH(10))
+
+/* Firmware for Tegra doesn't support UCSI ALT command, built
+ * for NVIDIA has known issue of reporting wrong capability info
+ */
+#define CCG_FW_BUILD_NVIDIA_TEGRA (('g' << 8) | 'n')
+
+/* Altmode offset for NVIDIA Function Test Board (FTB) */
+#define NVIDIA_FTB_DP_OFFSET (2)
+#define NVIDIA_FTB_DBG_OFFSET (3)
+
+struct version_info {
+ struct version_format base;
+ struct version_format app;
+};
+
+struct fw_config_table {
+ u32 identity;
+ u16 table_size;
+ u8 fwct_version;
+ u8 is_key_change;
+ u8 guid[16];
+ struct version_format base;
+ struct version_format app;
+ u8 primary_fw_digest[32];
+ u32 key_exp_length;
+ u8 key_modulus[256];
+ u8 key_exp[4];
+};
+
+/* CCGx response codes */
+enum ccg_resp_code {
+ CMD_NO_RESP = 0x00,
+ CMD_SUCCESS = 0x02,
+ FLASH_DATA_AVAILABLE = 0x03,
+ CMD_INVALID = 0x05,
+ FLASH_UPDATE_FAIL = 0x07,
+ INVALID_FW = 0x08,
+ INVALID_ARG = 0x09,
+ CMD_NOT_SUPPORT = 0x0A,
+ TRANSACTION_FAIL = 0x0C,
+ PD_CMD_FAIL = 0x0D,
+ UNDEF_ERROR = 0x0F,
+ INVALID_RESP = 0x10,
+};
+
+#define CCG_EVENT_MAX (EVENT_INDEX + 43)
+
+struct ccg_cmd {
+ u16 reg;
+ u32 data;
+ int len;
+ u32 delay; /* ms delay for cmd timeout */
+};
+
+struct ccg_resp {
+ u8 code;
+ u8 length;
+};
+
+struct ucsi_ccg_altmode {
+ u16 svid;
+ u32 mid;
+ u8 linked_idx;
+ u8 active_idx;
+#define UCSI_MULTI_DP_INDEX (0xff)
+ bool checked;
+} __packed;
+
+struct ucsi_ccg {
+ struct device *dev;
+ struct ucsi *ucsi;
+ struct i2c_client *client;
+
+ struct ccg_dev_info info;
+ /* version info for boot, primary and secondary */
+ struct version_info version[FW2 + 1];
+ u32 fw_version;
+ /* CCG HPI communication flags */
+ unsigned long flags;
+#define RESET_PENDING 0
+#define DEV_CMD_PENDING 1
+ struct ccg_resp dev_resp;
+ u8 cmd_resp;
+ int port_num;
+ int irq;
+ struct work_struct work;
+ struct mutex lock; /* to sync between user and driver thread */
+
+ /* fw build with vendor information */
+ u16 fw_build;
+ struct work_struct pm_work;
+
+ struct completion complete;
+
+ u64 last_cmd_sent;
+ bool has_multiple_dp;
+ struct ucsi_ccg_altmode orig[UCSI_MAX_ALTMODES];
+ struct ucsi_ccg_altmode updated[UCSI_MAX_ALTMODES];
+};
+
+static int ccg_read(struct ucsi_ccg *uc, u16 rab, u8 *data, u32 len)
+{
+ struct i2c_client *client = uc->client;
+ const struct i2c_adapter_quirks *quirks = client->adapter->quirks;
+ unsigned char buf[2];
+ struct i2c_msg msgs[] = {
+ {
+ .addr = client->addr,
+ .flags = 0x0,
+ .len = sizeof(buf),
+ .buf = buf,
+ },
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .buf = data,
+ },
+ };
+ u32 rlen, rem_len = len, max_read_len = len;
+ int status;
+
+ /* check any max_read_len limitation on i2c adapter */
+ if (quirks && quirks->max_read_len)
+ max_read_len = quirks->max_read_len;
+
+ pm_runtime_get_sync(uc->dev);
+ while (rem_len > 0) {
+ msgs[1].buf = &data[len - rem_len];
+ rlen = min_t(u16, rem_len, max_read_len);
+ msgs[1].len = rlen;
+ put_unaligned_le16(rab, buf);
+ status = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
+ if (status < 0) {
+ dev_err(uc->dev, "i2c_transfer failed %d\n", status);
+ pm_runtime_put_sync(uc->dev);
+ return status;
+ }
+ rab += rlen;
+ rem_len -= rlen;
+ }
+
+ pm_runtime_put_sync(uc->dev);
+ return 0;
+}
+
+static int ccg_write(struct ucsi_ccg *uc, u16 rab, const u8 *data, u32 len)
+{
+ struct i2c_client *client = uc->client;
+ unsigned char *buf;
+ struct i2c_msg msgs[] = {
+ {
+ .addr = client->addr,
+ .flags = 0x0,
+ }
+ };
+ int status;
+
+ buf = kzalloc(len + sizeof(rab), GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ put_unaligned_le16(rab, buf);
+ memcpy(buf + sizeof(rab), data, len);
+
+ msgs[0].len = len + sizeof(rab);
+ msgs[0].buf = buf;
+
+ pm_runtime_get_sync(uc->dev);
+ status = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
+ if (status < 0) {
+ dev_err(uc->dev, "i2c_transfer failed %d\n", status);
+ pm_runtime_put_sync(uc->dev);
+ kfree(buf);
+ return status;
+ }
+
+ pm_runtime_put_sync(uc->dev);
+ kfree(buf);
+ return 0;
+}
+
+static int ucsi_ccg_init(struct ucsi_ccg *uc)
+{
+ unsigned int count = 10;
+ u8 data;
+ int status;
+
+ data = CCGX_RAB_UCSI_CONTROL_STOP;
+ status = ccg_write(uc, CCGX_RAB_UCSI_CONTROL, &data, sizeof(data));
+ if (status < 0)
+ return status;
+
+ data = CCGX_RAB_UCSI_CONTROL_START;
+ status = ccg_write(uc, CCGX_RAB_UCSI_CONTROL, &data, sizeof(data));
+ if (status < 0)
+ return status;
+
+ /*
+ * Flush CCGx RESPONSE queue by acking interrupts. Above ucsi control
+ * register write will push response which must be cleared.
+ */
+ do {
+ status = ccg_read(uc, CCGX_RAB_INTR_REG, &data, sizeof(data));
+ if (status < 0)
+ return status;
+
+ if (!(data & DEV_INT))
+ return 0;
+
+ status = ccg_write(uc, CCGX_RAB_INTR_REG, &data, sizeof(data));
+ if (status < 0)
+ return status;
+
+ usleep_range(10000, 11000);
+ } while (--count);
+
+ return -ETIMEDOUT;
+}
+
+static void ucsi_ccg_update_get_current_cam_cmd(struct ucsi_ccg *uc, u8 *data)
+{
+ u8 cam, new_cam;
+
+ cam = data[0];
+ new_cam = uc->orig[cam].linked_idx;
+ uc->updated[new_cam].active_idx = cam;
+ data[0] = new_cam;
+}
+
+static bool ucsi_ccg_update_altmodes(struct ucsi *ucsi,
+ struct ucsi_altmode *orig,
+ struct ucsi_altmode *updated)
+{
+ struct ucsi_ccg *uc = ucsi_get_drvdata(ucsi);
+ struct ucsi_ccg_altmode *alt, *new_alt;
+ int i, j, k = 0;
+ bool found = false;
+
+ alt = uc->orig;
+ new_alt = uc->updated;
+ memset(uc->updated, 0, sizeof(uc->updated));
+
+ /*
+ * Copy original connector altmodes to new structure.
+ * We need this before second loop since second loop
+ * checks for duplicate altmodes.
+ */
+ for (i = 0; i < UCSI_MAX_ALTMODES; i++) {
+ alt[i].svid = orig[i].svid;
+ alt[i].mid = orig[i].mid;
+ if (!alt[i].svid)
+ break;
+ }
+
+ for (i = 0; i < UCSI_MAX_ALTMODES; i++) {
+ if (!alt[i].svid)
+ break;
+
+ /* already checked and considered */
+ if (alt[i].checked)
+ continue;
+
+ if (!DP_CONF_GET_PIN_ASSIGN(alt[i].mid)) {
+ /* Found Non DP altmode */
+ new_alt[k].svid = alt[i].svid;
+ new_alt[k].mid |= alt[i].mid;
+ new_alt[k].linked_idx = i;
+ alt[i].linked_idx = k;
+ updated[k].svid = new_alt[k].svid;
+ updated[k].mid = new_alt[k].mid;
+ k++;
+ continue;
+ }
+
+ for (j = i + 1; j < UCSI_MAX_ALTMODES; j++) {
+ if (alt[i].svid != alt[j].svid ||
+ !DP_CONF_GET_PIN_ASSIGN(alt[j].mid)) {
+ continue;
+ } else {
+ /* Found duplicate DP mode */
+ new_alt[k].svid = alt[i].svid;
+ new_alt[k].mid |= alt[i].mid | alt[j].mid;
+ new_alt[k].linked_idx = UCSI_MULTI_DP_INDEX;
+ alt[i].linked_idx = k;
+ alt[j].linked_idx = k;
+ alt[j].checked = true;
+ found = true;
+ }
+ }
+ if (found) {
+ uc->has_multiple_dp = true;
+ } else {
+ /* Didn't find any duplicate DP altmode */
+ new_alt[k].svid = alt[i].svid;
+ new_alt[k].mid |= alt[i].mid;
+ new_alt[k].linked_idx = i;
+ alt[i].linked_idx = k;
+ }
+ updated[k].svid = new_alt[k].svid;
+ updated[k].mid = new_alt[k].mid;
+ k++;
+ }
+ return found;
+}
+
+static void ucsi_ccg_update_set_new_cam_cmd(struct ucsi_ccg *uc,
+ struct ucsi_connector *con,
+ u64 *cmd)
+{
+ struct ucsi_ccg_altmode *new_port, *port;
+ struct typec_altmode *alt = NULL;
+ u8 new_cam, cam, pin;
+ bool enter_new_mode;
+ int i, j, k = 0xff;
+
+ port = uc->orig;
+ new_cam = UCSI_SET_NEW_CAM_GET_AM(*cmd);
+ new_port = &uc->updated[new_cam];
+ cam = new_port->linked_idx;
+ enter_new_mode = UCSI_SET_NEW_CAM_ENTER(*cmd);
+
+ /*
+ * If CAM is UCSI_MULTI_DP_INDEX then this is DP altmode
+ * with multiple DP mode. Find out CAM for best pin assignment
+ * among all DP mode. Priorite pin E->D->C after making sure
+ * the partner supports that pin.
+ */
+ if (cam == UCSI_MULTI_DP_INDEX) {
+ if (enter_new_mode) {
+ for (i = 0; con->partner_altmode[i]; i++) {
+ alt = con->partner_altmode[i];
+ if (alt->svid == new_port->svid)
+ break;
+ }
+ /*
+ * alt will always be non NULL since this is
+ * UCSI_SET_NEW_CAM command and so there will be
+ * at least one con->partner_altmode[i] with svid
+ * matching with new_port->svid.
+ */
+ for (j = 0; port[j].svid; j++) {
+ pin = DP_CONF_GET_PIN_ASSIGN(port[j].mid);
+ if (alt && port[j].svid == alt->svid &&
+ (pin & DP_CONF_GET_PIN_ASSIGN(alt->vdo))) {
+ /* prioritize pin E->D->C */
+ if (k == 0xff || (k != 0xff && pin >
+ DP_CONF_GET_PIN_ASSIGN(port[k].mid))
+ ) {
+ k = j;
+ }
+ }
+ }
+ cam = k;
+ new_port->active_idx = cam;
+ } else {
+ cam = new_port->active_idx;
+ }
+ }
+ *cmd &= ~UCSI_SET_NEW_CAM_AM_MASK;
+ *cmd |= UCSI_SET_NEW_CAM_SET_AM(cam);
+}
+
+/*
+ * Change the order of vdo values of NVIDIA test device FTB
+ * (Function Test Board) which reports altmode list with vdo=0x3
+ * first and then vdo=0x. Current logic to assign mode value is
+ * based on order in altmode list and it causes a mismatch of CON
+ * and SOP altmodes since NVIDIA GPU connector has order of vdo=0x1
+ * first and then vdo=0x3
+ */
+static void ucsi_ccg_nvidia_altmode(struct ucsi_ccg *uc,
+ struct ucsi_altmode *alt)
+{
+ switch (UCSI_ALTMODE_OFFSET(uc->last_cmd_sent)) {
+ case NVIDIA_FTB_DP_OFFSET:
+ if (alt[0].mid == USB_TYPEC_NVIDIA_VLINK_DBG_VDO)
+ alt[0].mid = USB_TYPEC_NVIDIA_VLINK_DP_VDO |
+ DP_CAP_DP_SIGNALING | DP_CAP_USB |
+ DP_CONF_SET_PIN_ASSIGN(BIT(DP_PIN_ASSIGN_E));
+ break;
+ case NVIDIA_FTB_DBG_OFFSET:
+ if (alt[0].mid == USB_TYPEC_NVIDIA_VLINK_DP_VDO)
+ alt[0].mid = USB_TYPEC_NVIDIA_VLINK_DBG_VDO;
+ break;
+ default:
+ break;
+ }
+}
+
+static int ucsi_ccg_read(struct ucsi *ucsi, unsigned int offset,
+ void *val, size_t val_len)
+{
+ struct ucsi_ccg *uc = ucsi_get_drvdata(ucsi);
+ u16 reg = CCGX_RAB_UCSI_DATA_BLOCK(offset);
+ struct ucsi_capability *cap;
+ struct ucsi_altmode *alt;
+ int ret;
+
+ ret = ccg_read(uc, reg, val, val_len);
+ if (ret)
+ return ret;
+
+ if (offset != UCSI_MESSAGE_IN)
+ return ret;
+
+ switch (UCSI_COMMAND(uc->last_cmd_sent)) {
+ case UCSI_GET_CURRENT_CAM:
+ if (uc->has_multiple_dp)
+ ucsi_ccg_update_get_current_cam_cmd(uc, (u8 *)val);
+ break;
+ case UCSI_GET_ALTERNATE_MODES:
+ if (UCSI_ALTMODE_RECIPIENT(uc->last_cmd_sent) ==
+ UCSI_RECIPIENT_SOP) {
+ alt = val;
+ if (alt[0].svid == USB_TYPEC_NVIDIA_VLINK_SID)
+ ucsi_ccg_nvidia_altmode(uc, alt);
+ }
+ break;
+ case UCSI_GET_CAPABILITY:
+ if (uc->fw_build == CCG_FW_BUILD_NVIDIA_TEGRA) {
+ cap = val;
+ cap->features &= ~UCSI_CAP_ALT_MODE_DETAILS;
+ }
+ break;
+ default:
+ break;
+ }
+ uc->last_cmd_sent = 0;
+
+ return ret;
+}
+
+static int ucsi_ccg_async_write(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len)
+{
+ u16 reg = CCGX_RAB_UCSI_DATA_BLOCK(offset);
+
+ return ccg_write(ucsi_get_drvdata(ucsi), reg, val, val_len);
+}
+
+static int ucsi_ccg_sync_write(struct ucsi *ucsi, unsigned int offset,
+ const void *val, size_t val_len)
+{
+ struct ucsi_ccg *uc = ucsi_get_drvdata(ucsi);
+ struct ucsi_connector *con;
+ int con_index;
+ int ret;
+
+ mutex_lock(&uc->lock);
+ pm_runtime_get_sync(uc->dev);
+ set_bit(DEV_CMD_PENDING, &uc->flags);
+
+ if (offset == UCSI_CONTROL && val_len == sizeof(uc->last_cmd_sent)) {
+ uc->last_cmd_sent = *(u64 *)val;
+
+ if (UCSI_COMMAND(uc->last_cmd_sent) == UCSI_SET_NEW_CAM &&
+ uc->has_multiple_dp) {
+ con_index = (uc->last_cmd_sent >> 16) &
+ UCSI_CMD_CONNECTOR_MASK;
+ con = &uc->ucsi->connector[con_index - 1];
+ ucsi_ccg_update_set_new_cam_cmd(uc, con, (u64 *)val);
+ }
+ }
+
+ ret = ucsi_ccg_async_write(ucsi, offset, val, val_len);
+ if (ret)
+ goto err_clear_bit;
+
+ if (!wait_for_completion_timeout(&uc->complete, msecs_to_jiffies(5000)))
+ ret = -ETIMEDOUT;
+
+err_clear_bit:
+ clear_bit(DEV_CMD_PENDING, &uc->flags);
+ pm_runtime_put_sync(uc->dev);
+ mutex_unlock(&uc->lock);
+
+ return ret;
+}
+
+static const struct ucsi_operations ucsi_ccg_ops = {
+ .read = ucsi_ccg_read,
+ .sync_write = ucsi_ccg_sync_write,
+ .async_write = ucsi_ccg_async_write,
+ .update_altmodes = ucsi_ccg_update_altmodes
+};
+
+static irqreturn_t ccg_irq_handler(int irq, void *data)
+{
+ u16 reg = CCGX_RAB_UCSI_DATA_BLOCK(UCSI_CCI);
+ struct ucsi_ccg *uc = data;
+ u8 intr_reg;
+ u32 cci;
+ int ret;
+
+ ret = ccg_read(uc, CCGX_RAB_INTR_REG, &intr_reg, sizeof(intr_reg));
+ if (ret)
+ return ret;
+
+ ret = ccg_read(uc, reg, (void *)&cci, sizeof(cci));
+ if (ret)
+ goto err_clear_irq;
+
+ if (UCSI_CCI_CONNECTOR(cci))
+ ucsi_connector_change(uc->ucsi, UCSI_CCI_CONNECTOR(cci));
+
+ if (test_bit(DEV_CMD_PENDING, &uc->flags) &&
+ cci & (UCSI_CCI_ACK_COMPLETE | UCSI_CCI_COMMAND_COMPLETE))
+ complete(&uc->complete);
+
+err_clear_irq:
+ ccg_write(uc, CCGX_RAB_INTR_REG, &intr_reg, sizeof(intr_reg));
+
+ return IRQ_HANDLED;
+}
+
+static int ccg_request_irq(struct ucsi_ccg *uc)
+{
+ unsigned long flags = IRQF_ONESHOT;
+
+ if (!has_acpi_companion(uc->dev))
+ flags |= IRQF_TRIGGER_HIGH;
+
+ return request_threaded_irq(uc->irq, NULL, ccg_irq_handler, flags, dev_name(uc->dev), uc);
+}
+
+static void ccg_pm_workaround_work(struct work_struct *pm_work)
+{
+ ccg_irq_handler(0, container_of(pm_work, struct ucsi_ccg, pm_work));
+}
+
+static int get_fw_info(struct ucsi_ccg *uc)
+{
+ int err;
+
+ err = ccg_read(uc, CCGX_RAB_READ_ALL_VER, (u8 *)(&uc->version),
+ sizeof(uc->version));
+ if (err < 0)
+ return err;
+
+ uc->fw_version = CCG_VERSION(uc->version[FW2].app.ver) |
+ CCG_VERSION_PATCH(uc->version[FW2].app.patch);
+
+ err = ccg_read(uc, CCGX_RAB_DEVICE_MODE, (u8 *)(&uc->info),
+ sizeof(uc->info));
+ if (err < 0)
+ return err;
+
+ return 0;
+}
+
+static inline bool invalid_async_evt(int code)
+{
+ return (code >= CCG_EVENT_MAX) || (code < EVENT_INDEX);
+}
+
+static void ccg_process_response(struct ucsi_ccg *uc)
+{
+ struct device *dev = uc->dev;
+
+ if (uc->dev_resp.code & ASYNC_EVENT) {
+ if (uc->dev_resp.code == RESET_COMPLETE) {
+ if (test_bit(RESET_PENDING, &uc->flags))
+ uc->cmd_resp = uc->dev_resp.code;
+ get_fw_info(uc);
+ }
+ if (invalid_async_evt(uc->dev_resp.code))
+ dev_err(dev, "invalid async evt %d\n",
+ uc->dev_resp.code);
+ } else {
+ if (test_bit(DEV_CMD_PENDING, &uc->flags)) {
+ uc->cmd_resp = uc->dev_resp.code;
+ clear_bit(DEV_CMD_PENDING, &uc->flags);
+ } else {
+ dev_err(dev, "dev resp 0x%04x but no cmd pending\n",
+ uc->dev_resp.code);
+ }
+ }
+}
+
+static int ccg_read_response(struct ucsi_ccg *uc)
+{
+ unsigned long target = jiffies + msecs_to_jiffies(1000);
+ struct device *dev = uc->dev;
+ u8 intval;
+ int status;
+
+ /* wait for interrupt status to get updated */
+ do {
+ status = ccg_read(uc, CCGX_RAB_INTR_REG, &intval,
+ sizeof(intval));
+ if (status < 0)
+ return status;
+
+ if (intval & DEV_INT)
+ break;
+ usleep_range(500, 600);
+ } while (time_is_after_jiffies(target));
+
+ if (time_is_before_jiffies(target)) {
+ dev_err(dev, "response timeout error\n");
+ return -ETIME;
+ }
+
+ status = ccg_read(uc, CCGX_RAB_RESPONSE, (u8 *)&uc->dev_resp,
+ sizeof(uc->dev_resp));
+ if (status < 0)
+ return status;
+
+ status = ccg_write(uc, CCGX_RAB_INTR_REG, &intval, sizeof(intval));
+ if (status < 0)
+ return status;
+
+ return 0;
+}
+
+/* Caller must hold uc->lock */
+static int ccg_send_command(struct ucsi_ccg *uc, struct ccg_cmd *cmd)
+{
+ struct device *dev = uc->dev;
+ int ret;
+
+ switch (cmd->reg & 0xF000) {
+ case DEV_REG_IDX:
+ set_bit(DEV_CMD_PENDING, &uc->flags);
+ break;
+ default:
+ dev_err(dev, "invalid cmd register\n");
+ break;
+ }
+
+ ret = ccg_write(uc, cmd->reg, (u8 *)&cmd->data, cmd->len);
+ if (ret < 0)
+ return ret;
+
+ msleep(cmd->delay);
+
+ ret = ccg_read_response(uc);
+ if (ret < 0) {
+ dev_err(dev, "response read error\n");
+ switch (cmd->reg & 0xF000) {
+ case DEV_REG_IDX:
+ clear_bit(DEV_CMD_PENDING, &uc->flags);
+ break;
+ default:
+ dev_err(dev, "invalid cmd register\n");
+ break;
+ }
+ return -EIO;
+ }
+ ccg_process_response(uc);
+
+ return uc->cmd_resp;
+}
+
+static int ccg_cmd_enter_flashing(struct ucsi_ccg *uc)
+{
+ struct ccg_cmd cmd;
+ int ret;
+
+ cmd.reg = CCGX_RAB_ENTER_FLASHING;
+ cmd.data = FLASH_ENTER_SIG;
+ cmd.len = 1;
+ cmd.delay = 50;
+
+ mutex_lock(&uc->lock);
+
+ ret = ccg_send_command(uc, &cmd);
+
+ mutex_unlock(&uc->lock);
+
+ if (ret != CMD_SUCCESS) {
+ dev_err(uc->dev, "enter flashing failed ret=%d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ccg_cmd_reset(struct ucsi_ccg *uc)
+{
+ struct ccg_cmd cmd;
+ u8 *p;
+ int ret;
+
+ p = (u8 *)&cmd.data;
+ cmd.reg = CCGX_RAB_RESET_REQ;
+ p[0] = RESET_SIG;
+ p[1] = CMD_RESET_DEV;
+ cmd.len = 2;
+ cmd.delay = 5000;
+
+ mutex_lock(&uc->lock);
+
+ set_bit(RESET_PENDING, &uc->flags);
+
+ ret = ccg_send_command(uc, &cmd);
+ if (ret != RESET_COMPLETE)
+ goto err_clear_flag;
+
+ ret = 0;
+
+err_clear_flag:
+ clear_bit(RESET_PENDING, &uc->flags);
+
+ mutex_unlock(&uc->lock);
+
+ return ret;
+}
+
+static int ccg_cmd_port_control(struct ucsi_ccg *uc, bool enable)
+{
+ struct ccg_cmd cmd;
+ int ret;
+
+ cmd.reg = CCGX_RAB_PDPORT_ENABLE;
+ if (enable)
+ cmd.data = (uc->port_num == 1) ?
+ PDPORT_1 : (PDPORT_1 | PDPORT_2);
+ else
+ cmd.data = 0x0;
+ cmd.len = 1;
+ cmd.delay = 10;
+
+ mutex_lock(&uc->lock);
+
+ ret = ccg_send_command(uc, &cmd);
+
+ mutex_unlock(&uc->lock);
+
+ if (ret != CMD_SUCCESS) {
+ dev_err(uc->dev, "port control failed ret=%d\n", ret);
+ return ret;
+ }
+ return 0;
+}
+
+static int ccg_cmd_jump_boot_mode(struct ucsi_ccg *uc, int bl_mode)
+{
+ struct ccg_cmd cmd;
+ int ret;
+
+ cmd.reg = CCGX_RAB_JUMP_TO_BOOT;
+
+ if (bl_mode)
+ cmd.data = TO_BOOT;
+ else
+ cmd.data = TO_ALT_FW;
+
+ cmd.len = 1;
+ cmd.delay = 100;
+
+ mutex_lock(&uc->lock);
+
+ set_bit(RESET_PENDING, &uc->flags);
+
+ ret = ccg_send_command(uc, &cmd);
+ if (ret != RESET_COMPLETE)
+ goto err_clear_flag;
+
+ ret = 0;
+
+err_clear_flag:
+ clear_bit(RESET_PENDING, &uc->flags);
+
+ mutex_unlock(&uc->lock);
+
+ return ret;
+}
+
+static int
+ccg_cmd_write_flash_row(struct ucsi_ccg *uc, u16 row,
+ const void *data, u8 fcmd)
+{
+ struct i2c_client *client = uc->client;
+ struct ccg_cmd cmd;
+ u8 buf[CCG4_ROW_SIZE + 2];
+ u8 *p;
+ int ret;
+
+ /* Copy the data into the flash read/write memory. */
+ put_unaligned_le16(REG_FLASH_RW_MEM, buf);
+
+ memcpy(buf + 2, data, CCG4_ROW_SIZE);
+
+ mutex_lock(&uc->lock);
+
+ ret = i2c_master_send(client, buf, CCG4_ROW_SIZE + 2);
+ if (ret != CCG4_ROW_SIZE + 2) {
+ dev_err(uc->dev, "REG_FLASH_RW_MEM write fail %d\n", ret);
+ mutex_unlock(&uc->lock);
+ return ret < 0 ? ret : -EIO;
+ }
+
+ /* Use the FLASH_ROW_READ_WRITE register to trigger */
+ /* writing of data to the desired flash row */
+ p = (u8 *)&cmd.data;
+ cmd.reg = CCGX_RAB_FLASH_ROW_RW;
+ p[0] = FLASH_SIG;
+ p[1] = fcmd;
+ put_unaligned_le16(row, &p[2]);
+ cmd.len = 4;
+ cmd.delay = 50;
+ if (fcmd == FLASH_FWCT_SIG_WR_CMD)
+ cmd.delay += 400;
+ if (row == 510)
+ cmd.delay += 220;
+ ret = ccg_send_command(uc, &cmd);
+
+ mutex_unlock(&uc->lock);
+
+ if (ret != CMD_SUCCESS) {
+ dev_err(uc->dev, "write flash row failed ret=%d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ccg_cmd_validate_fw(struct ucsi_ccg *uc, unsigned int fwid)
+{
+ struct ccg_cmd cmd;
+ int ret;
+
+ cmd.reg = CCGX_RAB_VALIDATE_FW;
+ cmd.data = fwid;
+ cmd.len = 1;
+ cmd.delay = 500;
+
+ mutex_lock(&uc->lock);
+
+ ret = ccg_send_command(uc, &cmd);
+
+ mutex_unlock(&uc->lock);
+
+ if (ret != CMD_SUCCESS)
+ return ret;
+
+ return 0;
+}
+
+static bool ccg_check_vendor_version(struct ucsi_ccg *uc,
+ struct version_format *app,
+ struct fw_config_table *fw_cfg)
+{
+ struct device *dev = uc->dev;
+
+ /* Check if the fw build is for supported vendors */
+ if (le16_to_cpu(app->build) != uc->fw_build) {
+ dev_info(dev, "current fw is not from supported vendor\n");
+ return false;
+ }
+
+ /* Check if the new fw build is for supported vendors */
+ if (le16_to_cpu(fw_cfg->app.build) != uc->fw_build) {
+ dev_info(dev, "new fw is not from supported vendor\n");
+ return false;
+ }
+ return true;
+}
+
+static bool ccg_check_fw_version(struct ucsi_ccg *uc, const char *fw_name,
+ struct version_format *app)
+{
+ const struct firmware *fw = NULL;
+ struct device *dev = uc->dev;
+ struct fw_config_table fw_cfg;
+ u32 cur_version, new_version;
+ bool is_later = false;
+
+ if (request_firmware(&fw, fw_name, dev) != 0) {
+ dev_err(dev, "error: Failed to open cyacd file %s\n", fw_name);
+ return false;
+ }
+
+ /*
+ * check if signed fw
+ * last part of fw image is fw cfg table and signature
+ */
+ if (fw->size < sizeof(fw_cfg) + FW_CFG_TABLE_SIG_SIZE)
+ goto out_release_firmware;
+
+ memcpy((uint8_t *)&fw_cfg, fw->data + fw->size -
+ sizeof(fw_cfg) - FW_CFG_TABLE_SIG_SIZE, sizeof(fw_cfg));
+
+ if (fw_cfg.identity != ('F' | 'W' << 8 | 'C' << 16 | 'T' << 24)) {
+ dev_info(dev, "not a signed image\n");
+ goto out_release_firmware;
+ }
+
+ /* compare input version with FWCT version */
+ cur_version = le16_to_cpu(app->build) | CCG_VERSION_PATCH(app->patch) |
+ CCG_VERSION(app->ver);
+
+ new_version = le16_to_cpu(fw_cfg.app.build) |
+ CCG_VERSION_PATCH(fw_cfg.app.patch) |
+ CCG_VERSION(fw_cfg.app.ver);
+
+ if (!ccg_check_vendor_version(uc, app, &fw_cfg))
+ goto out_release_firmware;
+
+ if (new_version > cur_version)
+ is_later = true;
+
+out_release_firmware:
+ release_firmware(fw);
+ return is_later;
+}
+
+static int ccg_fw_update_needed(struct ucsi_ccg *uc,
+ enum enum_flash_mode *mode)
+{
+ struct device *dev = uc->dev;
+ int err;
+ struct version_info version[3];
+
+ err = ccg_read(uc, CCGX_RAB_DEVICE_MODE, (u8 *)(&uc->info),
+ sizeof(uc->info));
+ if (err) {
+ dev_err(dev, "read device mode failed\n");
+ return err;
+ }
+
+ err = ccg_read(uc, CCGX_RAB_READ_ALL_VER, (u8 *)version,
+ sizeof(version));
+ if (err) {
+ dev_err(dev, "read device mode failed\n");
+ return err;
+ }
+
+ if (memcmp(&version[FW1], "\0\0\0\0\0\0\0\0",
+ sizeof(struct version_info)) == 0) {
+ dev_info(dev, "secondary fw is not flashed\n");
+ *mode = SECONDARY_BL;
+ } else if (le16_to_cpu(version[FW1].base.build) <
+ secondary_fw_min_ver) {
+ dev_info(dev, "secondary fw version is too low (< %d)\n",
+ secondary_fw_min_ver);
+ *mode = SECONDARY;
+ } else if (memcmp(&version[FW2], "\0\0\0\0\0\0\0\0",
+ sizeof(struct version_info)) == 0) {
+ dev_info(dev, "primary fw is not flashed\n");
+ *mode = PRIMARY;
+ } else if (ccg_check_fw_version(uc, ccg_fw_names[PRIMARY],
+ &version[FW2].app)) {
+ dev_info(dev, "found primary fw with later version\n");
+ *mode = PRIMARY;
+ } else {
+ dev_info(dev, "secondary and primary fw are the latest\n");
+ *mode = FLASH_NOT_NEEDED;
+ }
+ return 0;
+}
+
+static int do_flash(struct ucsi_ccg *uc, enum enum_flash_mode mode)
+{
+ struct device *dev = uc->dev;
+ const struct firmware *fw = NULL;
+ const char *p, *s;
+ const char *eof;
+ int err, row, len, line_sz, line_cnt = 0;
+ unsigned long start_time = jiffies;
+ struct fw_config_table fw_cfg;
+ u8 fw_cfg_sig[FW_CFG_TABLE_SIG_SIZE];
+ u8 *wr_buf;
+
+ err = request_firmware(&fw, ccg_fw_names[mode], dev);
+ if (err) {
+ dev_err(dev, "request %s failed err=%d\n",
+ ccg_fw_names[mode], err);
+ return err;
+ }
+
+ if (((uc->info.mode & CCG_DEVINFO_FWMODE_MASK) >>
+ CCG_DEVINFO_FWMODE_SHIFT) == FW2) {
+ err = ccg_cmd_port_control(uc, false);
+ if (err < 0)
+ goto release_fw;
+ err = ccg_cmd_jump_boot_mode(uc, 0);
+ if (err < 0)
+ goto release_fw;
+ }
+
+ eof = fw->data + fw->size;
+
+ /*
+ * check if signed fw
+ * last part of fw image is fw cfg table and signature
+ */
+ if (fw->size < sizeof(fw_cfg) + sizeof(fw_cfg_sig))
+ goto not_signed_fw;
+
+ memcpy((uint8_t *)&fw_cfg, fw->data + fw->size -
+ sizeof(fw_cfg) - sizeof(fw_cfg_sig), sizeof(fw_cfg));
+
+ if (fw_cfg.identity != ('F' | ('W' << 8) | ('C' << 16) | ('T' << 24))) {
+ dev_info(dev, "not a signed image\n");
+ goto not_signed_fw;
+ }
+ eof = fw->data + fw->size - sizeof(fw_cfg) - sizeof(fw_cfg_sig);
+
+ memcpy((uint8_t *)&fw_cfg_sig,
+ fw->data + fw->size - sizeof(fw_cfg_sig), sizeof(fw_cfg_sig));
+
+ /* flash fw config table and signature first */
+ err = ccg_cmd_write_flash_row(uc, 0, (u8 *)&fw_cfg,
+ FLASH_FWCT1_WR_CMD);
+ if (err)
+ goto release_fw;
+
+ err = ccg_cmd_write_flash_row(uc, 0, (u8 *)&fw_cfg + CCG4_ROW_SIZE,
+ FLASH_FWCT2_WR_CMD);
+ if (err)
+ goto release_fw;
+
+ err = ccg_cmd_write_flash_row(uc, 0, &fw_cfg_sig,
+ FLASH_FWCT_SIG_WR_CMD);
+ if (err)
+ goto release_fw;
+
+not_signed_fw:
+ wr_buf = kzalloc(CCG4_ROW_SIZE + 4, GFP_KERNEL);
+ if (!wr_buf) {
+ err = -ENOMEM;
+ goto release_fw;
+ }
+
+ err = ccg_cmd_enter_flashing(uc);
+ if (err)
+ goto release_mem;
+
+ /*****************************************************************
+ * CCG firmware image (.cyacd) file line format
+ *
+ * :00rrrrllll[dd....]cc/r/n
+ *
+ * :00 header
+ * rrrr is row number to flash (4 char)
+ * llll is data len to flash (4 char)
+ * dd is a data field represents one byte of data (512 char)
+ * cc is checksum (2 char)
+ * \r\n newline
+ *
+ * Total length: 3 + 4 + 4 + 512 + 2 + 2 = 527
+ *
+ *****************************************************************/
+
+ p = strnchr(fw->data, fw->size, ':');
+ while (p < eof) {
+ s = strnchr(p + 1, eof - p - 1, ':');
+
+ if (!s)
+ s = eof;
+
+ line_sz = s - p;
+
+ if (line_sz != CYACD_LINE_SIZE) {
+ dev_err(dev, "Bad FW format line_sz=%d\n", line_sz);
+ err = -EINVAL;
+ goto release_mem;
+ }
+
+ if (hex2bin(wr_buf, p + 3, CCG4_ROW_SIZE + 4)) {
+ err = -EINVAL;
+ goto release_mem;
+ }
+
+ row = get_unaligned_be16(wr_buf);
+ len = get_unaligned_be16(&wr_buf[2]);
+
+ if (len != CCG4_ROW_SIZE) {
+ err = -EINVAL;
+ goto release_mem;
+ }
+
+ err = ccg_cmd_write_flash_row(uc, row, wr_buf + 4,
+ FLASH_WR_CMD);
+ if (err)
+ goto release_mem;
+
+ line_cnt++;
+ p = s;
+ }
+
+ dev_info(dev, "total %d row flashed. time: %dms\n",
+ line_cnt, jiffies_to_msecs(jiffies - start_time));
+
+ err = ccg_cmd_validate_fw(uc, (mode == PRIMARY) ? FW2 : FW1);
+ if (err)
+ dev_err(dev, "%s validation failed err=%d\n",
+ (mode == PRIMARY) ? "FW2" : "FW1", err);
+ else
+ dev_info(dev, "%s validated\n",
+ (mode == PRIMARY) ? "FW2" : "FW1");
+
+ err = ccg_cmd_port_control(uc, false);
+ if (err < 0)
+ goto release_mem;
+
+ err = ccg_cmd_reset(uc);
+ if (err < 0)
+ goto release_mem;
+
+ err = ccg_cmd_port_control(uc, true);
+ if (err < 0)
+ goto release_mem;
+
+release_mem:
+ kfree(wr_buf);
+
+release_fw:
+ release_firmware(fw);
+ return err;
+}
+
+/*******************************************************************************
+ * CCG4 has two copies of the firmware in addition to the bootloader.
+ * If the device is running FW1, FW2 can be updated with the new version.
+ * Dual firmware mode allows the CCG device to stay in a PD contract and support
+ * USB PD and Type-C functionality while a firmware update is in progress.
+ ******************************************************************************/
+static int ccg_fw_update(struct ucsi_ccg *uc, enum enum_flash_mode flash_mode)
+{
+ int err = 0;
+
+ while (flash_mode != FLASH_NOT_NEEDED) {
+ err = do_flash(uc, flash_mode);
+ if (err < 0)
+ return err;
+ err = ccg_fw_update_needed(uc, &flash_mode);
+ if (err < 0)
+ return err;
+ }
+ dev_info(uc->dev, "CCG FW update successful\n");
+
+ return err;
+}
+
+static int ccg_restart(struct ucsi_ccg *uc)
+{
+ struct device *dev = uc->dev;
+ int status;
+
+ status = ucsi_ccg_init(uc);
+ if (status < 0) {
+ dev_err(dev, "ucsi_ccg_start fail, err=%d\n", status);
+ return status;
+ }
+
+ status = ccg_request_irq(uc);
+ if (status < 0) {
+ dev_err(dev, "request_threaded_irq failed - %d\n", status);
+ return status;
+ }
+
+ status = ucsi_register(uc->ucsi);
+ if (status) {
+ dev_err(uc->dev, "failed to register the interface\n");
+ return status;
+ }
+
+ pm_runtime_enable(uc->dev);
+ return 0;
+}
+
+static void ccg_update_firmware(struct work_struct *work)
+{
+ struct ucsi_ccg *uc = container_of(work, struct ucsi_ccg, work);
+ enum enum_flash_mode flash_mode;
+ int status;
+
+ status = ccg_fw_update_needed(uc, &flash_mode);
+ if (status < 0)
+ return;
+
+ if (flash_mode != FLASH_NOT_NEEDED) {
+ ucsi_unregister(uc->ucsi);
+ pm_runtime_disable(uc->dev);
+ free_irq(uc->irq, uc);
+
+ ccg_fw_update(uc, flash_mode);
+ ccg_restart(uc);
+ }
+}
+
+static ssize_t do_flash_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t n)
+{
+ struct ucsi_ccg *uc = i2c_get_clientdata(to_i2c_client(dev));
+ bool flash;
+
+ if (kstrtobool(buf, &flash))
+ return -EINVAL;
+
+ if (!flash)
+ return n;
+
+ if (uc->fw_build == 0x0) {
+ dev_err(dev, "fail to flash FW due to missing FW build info\n");
+ return -EINVAL;
+ }
+
+ schedule_work(&uc->work);
+ return n;
+}
+
+static DEVICE_ATTR_WO(do_flash);
+
+static struct attribute *ucsi_ccg_attrs[] = {
+ &dev_attr_do_flash.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(ucsi_ccg);
+
+static int ucsi_ccg_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct device *dev = &client->dev;
+ struct ucsi_ccg *uc;
+ int status;
+
+ uc = devm_kzalloc(dev, sizeof(*uc), GFP_KERNEL);
+ if (!uc)
+ return -ENOMEM;
+
+ uc->dev = dev;
+ uc->client = client;
+ uc->irq = client->irq;
+ mutex_init(&uc->lock);
+ init_completion(&uc->complete);
+ INIT_WORK(&uc->work, ccg_update_firmware);
+ INIT_WORK(&uc->pm_work, ccg_pm_workaround_work);
+
+ /* Only fail FW flashing when FW build information is not provided */
+ status = device_property_read_u16(dev, "ccgx,firmware-build",
+ &uc->fw_build);
+ if (status)
+ dev_err(uc->dev, "failed to get FW build information\n");
+
+ /* reset ccg device and initialize ucsi */
+ status = ucsi_ccg_init(uc);
+ if (status < 0) {
+ dev_err(uc->dev, "ucsi_ccg_init failed - %d\n", status);
+ return status;
+ }
+
+ status = get_fw_info(uc);
+ if (status < 0) {
+ dev_err(uc->dev, "get_fw_info failed - %d\n", status);
+ return status;
+ }
+
+ uc->port_num = 1;
+
+ if (uc->info.mode & CCG_DEVINFO_PDPORTS_MASK)
+ uc->port_num++;
+
+ uc->ucsi = ucsi_create(dev, &ucsi_ccg_ops);
+ if (IS_ERR(uc->ucsi))
+ return PTR_ERR(uc->ucsi);
+
+ ucsi_set_drvdata(uc->ucsi, uc);
+
+ status = ccg_request_irq(uc);
+ if (status < 0) {
+ dev_err(uc->dev, "request_threaded_irq failed - %d\n", status);
+ goto out_ucsi_destroy;
+ }
+
+ status = ucsi_register(uc->ucsi);
+ if (status)
+ goto out_free_irq;
+
+ i2c_set_clientdata(client, uc);
+
+ pm_runtime_set_active(uc->dev);
+ pm_runtime_enable(uc->dev);
+ pm_runtime_use_autosuspend(uc->dev);
+ pm_runtime_set_autosuspend_delay(uc->dev, 5000);
+ pm_runtime_idle(uc->dev);
+
+ return 0;
+
+out_free_irq:
+ free_irq(uc->irq, uc);
+out_ucsi_destroy:
+ ucsi_destroy(uc->ucsi);
+
+ return status;
+}
+
+static void ucsi_ccg_remove(struct i2c_client *client)
+{
+ struct ucsi_ccg *uc = i2c_get_clientdata(client);
+
+ cancel_work_sync(&uc->pm_work);
+ cancel_work_sync(&uc->work);
+ pm_runtime_disable(uc->dev);
+ ucsi_unregister(uc->ucsi);
+ ucsi_destroy(uc->ucsi);
+ free_irq(uc->irq, uc);
+}
+
+static const struct i2c_device_id ucsi_ccg_device_id[] = {
+ {"ccgx-ucsi", 0},
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, ucsi_ccg_device_id);
+
+static const struct acpi_device_id amd_i2c_ucsi_match[] = {
+ {"AMDI0042"},
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, amd_i2c_ucsi_match);
+
+static int ucsi_ccg_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct ucsi_ccg *uc = i2c_get_clientdata(client);
+
+ return ucsi_resume(uc->ucsi);
+}
+
+static int ucsi_ccg_runtime_suspend(struct device *dev)
+{
+ return 0;
+}
+
+static int ucsi_ccg_runtime_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct ucsi_ccg *uc = i2c_get_clientdata(client);
+
+ /*
+ * Firmware version 3.1.10 or earlier, built for NVIDIA has known issue
+ * of missing interrupt when a device is connected for runtime resume.
+ * Schedule a work to call ISR as a workaround.
+ */
+ if (uc->fw_build == CCG_FW_BUILD_NVIDIA &&
+ uc->fw_version <= CCG_OLD_FW_VERSION)
+ schedule_work(&uc->pm_work);
+
+ return 0;
+}
+
+static const struct dev_pm_ops ucsi_ccg_pm = {
+ .resume = ucsi_ccg_resume,
+ .runtime_suspend = ucsi_ccg_runtime_suspend,
+ .runtime_resume = ucsi_ccg_runtime_resume,
+};
+
+static struct i2c_driver ucsi_ccg_driver = {
+ .driver = {
+ .name = "ucsi_ccg",
+ .pm = &ucsi_ccg_pm,
+ .dev_groups = ucsi_ccg_groups,
+ .acpi_match_table = amd_i2c_ucsi_match,
+ },
+ .probe = ucsi_ccg_probe,
+ .remove = ucsi_ccg_remove,
+ .id_table = ucsi_ccg_device_id,
+};
+
+module_i2c_driver(ucsi_ccg_driver);
+
+MODULE_AUTHOR("Ajay Gupta <ajayg@nvidia.com>");
+MODULE_DESCRIPTION("UCSI driver for Cypress CCGx Type-C controller");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/usb/typec/ucsi/ucsi_stm32g0.c b/drivers/usb/typec/ucsi/ucsi_stm32g0.c
new file mode 100644
index 000000000..7b92f0c8d
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi_stm32g0.c
@@ -0,0 +1,775 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * UCSI driver for STMicroelectronics STM32G0 Type-C PD controller
+ *
+ * Copyright (C) 2022, STMicroelectronics - All Rights Reserved
+ * Author: Fabrice Gasnier <fabrice.gasnier@foss.st.com>.
+ */
+
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <asm/unaligned.h>
+
+#include "ucsi.h"
+
+/* STM32G0 I2C bootloader addr: 0b1010001x (See AN2606) */
+#define STM32G0_I2C_BL_ADDR (0xa2 >> 1)
+
+/* STM32G0 I2C bootloader max data size */
+#define STM32G0_I2C_BL_SZ 256
+
+/* STM32 I2C bootloader commands (See AN4221) */
+#define STM32_CMD_GVR 0x01 /* Gets the bootloader version */
+#define STM32_CMD_GVR_LEN 1
+#define STM32_CMD_RM 0x11 /* Reag memory */
+#define STM32_CMD_WM 0x31 /* Write memory */
+#define STM32_CMD_ADDR_LEN 5 /* Address len for go, mem write... */
+#define STM32_CMD_ERASE 0x44 /* Erase page, bank or all */
+#define STM32_CMD_ERASE_SPECIAL_LEN 3
+#define STM32_CMD_GLOBAL_MASS_ERASE 0xffff /* All-bank erase */
+
+/* STM32 I2C bootloader answer status */
+#define STM32G0_I2C_BL_ACK 0x79
+#define STM32G0_I2C_BL_NACK 0x1f
+#define STM32G0_I2C_BL_BUSY 0x76
+
+/* STM32G0 flash definitions */
+#define STM32G0_USER_OPTION_BYTES 0x1fff7800
+#define STM32G0_USER_OB_NBOOT0 BIT(26)
+#define STM32G0_USER_OB_NBOOT_SEL BIT(24)
+#define STM32G0_USER_OB_BOOT_MAIN (STM32G0_USER_OB_NBOOT0 | STM32G0_USER_OB_NBOOT_SEL)
+#define STM32G0_MAIN_MEM_ADDR 0x08000000
+
+/* STM32 Firmware definitions: additional commands */
+#define STM32G0_FW_GETVER 0x00 /* Gets the firmware version */
+#define STM32G0_FW_GETVER_LEN 4
+#define STM32G0_FW_RSTGOBL 0x21 /* Reset and go to bootloader */
+#define STM32G0_FW_KEYWORD 0xa56959a6
+
+/* ucsi_stm32g0_fw_info located at the end of the firmware */
+struct ucsi_stm32g0_fw_info {
+ u32 version;
+ u32 keyword;
+};
+
+struct ucsi_stm32g0 {
+ struct i2c_client *client;
+ struct i2c_client *i2c_bl;
+ bool in_bootloader;
+ u8 bl_version;
+ struct completion complete;
+ struct device *dev;
+ unsigned long flags;
+ const char *fw_name;
+ struct ucsi *ucsi;
+ bool suspended;
+ bool wakeup_event;
+};
+
+/*
+ * Bootloader commands helpers:
+ * - send command (2 bytes)
+ * - check ack
+ * Then either:
+ * - receive data
+ * - receive data + check ack
+ * - send data + check ack
+ * These operations depends on the command and have various length.
+ */
+static int ucsi_stm32g0_bl_check_ack(struct ucsi *ucsi)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->i2c_bl;
+ unsigned char ack;
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .len = 1,
+ .buf = &ack,
+ },
+ };
+ int ret;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_err(g0->dev, "i2c bl ack (%02x), error: %d\n", client->addr, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ /* The 'ack' byte should contain bootloader answer: ack/nack/busy */
+ switch (ack) {
+ case STM32G0_I2C_BL_ACK:
+ return 0;
+ case STM32G0_I2C_BL_NACK:
+ return -ENOENT;
+ case STM32G0_I2C_BL_BUSY:
+ return -EBUSY;
+ default:
+ dev_err(g0->dev, "i2c bl ack (%02x), invalid byte: %02x\n",
+ client->addr, ack);
+ return -EINVAL;
+ }
+}
+
+static int ucsi_stm32g0_bl_cmd_check_ack(struct ucsi *ucsi, unsigned int cmd, bool check_ack)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->i2c_bl;
+ unsigned char buf[2];
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = 0,
+ .len = sizeof(buf),
+ .buf = buf,
+ },
+ };
+ int ret;
+
+ /*
+ * Send STM32 bootloader command format is two bytes:
+ * - command code
+ * - XOR'ed command code
+ */
+ buf[0] = cmd;
+ buf[1] = cmd ^ 0xff;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_dbg(g0->dev, "i2c bl cmd %d (%02x), error: %d\n", cmd, client->addr, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ if (check_ack)
+ return ucsi_stm32g0_bl_check_ack(ucsi);
+
+ return 0;
+}
+
+static int ucsi_stm32g0_bl_cmd(struct ucsi *ucsi, unsigned int cmd)
+{
+ return ucsi_stm32g0_bl_cmd_check_ack(ucsi, cmd, true);
+}
+
+static int ucsi_stm32g0_bl_rcv_check_ack(struct ucsi *ucsi, void *data, size_t len, bool check_ack)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->i2c_bl;
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .len = len,
+ .buf = data,
+ },
+ };
+ int ret;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_err(g0->dev, "i2c bl rcv %02x, error: %d\n", client->addr, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ if (check_ack)
+ return ucsi_stm32g0_bl_check_ack(ucsi);
+
+ return 0;
+}
+
+static int ucsi_stm32g0_bl_rcv(struct ucsi *ucsi, void *data, size_t len)
+{
+ return ucsi_stm32g0_bl_rcv_check_ack(ucsi, data, len, true);
+}
+
+static int ucsi_stm32g0_bl_rcv_woack(struct ucsi *ucsi, void *data, size_t len)
+{
+ return ucsi_stm32g0_bl_rcv_check_ack(ucsi, data, len, false);
+}
+
+static int ucsi_stm32g0_bl_send(struct ucsi *ucsi, void *data, size_t len)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->i2c_bl;
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = 0,
+ .len = len,
+ .buf = data,
+ },
+ };
+ int ret;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_err(g0->dev, "i2c bl send %02x, error: %d\n", client->addr, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ return ucsi_stm32g0_bl_check_ack(ucsi);
+}
+
+/* Bootloader commands */
+static int ucsi_stm32g0_bl_get_version(struct ucsi *ucsi, u8 *bl_version)
+{
+ int ret;
+
+ ret = ucsi_stm32g0_bl_cmd(ucsi, STM32_CMD_GVR);
+ if (ret)
+ return ret;
+
+ return ucsi_stm32g0_bl_rcv(ucsi, bl_version, STM32_CMD_GVR_LEN);
+}
+
+static int ucsi_stm32g0_bl_send_addr(struct ucsi *ucsi, u32 addr)
+{
+ u8 data8[STM32_CMD_ADDR_LEN];
+
+ /* Address format: 4 bytes addr (MSB first) + XOR'ed addr bytes */
+ put_unaligned_be32(addr, data8);
+ data8[4] = data8[0] ^ data8[1] ^ data8[2] ^ data8[3];
+
+ return ucsi_stm32g0_bl_send(ucsi, data8, STM32_CMD_ADDR_LEN);
+}
+
+static int ucsi_stm32g0_bl_global_mass_erase(struct ucsi *ucsi)
+{
+ u8 data8[4];
+ u16 *data16 = (u16 *)&data8[0];
+ int ret;
+
+ data16[0] = STM32_CMD_GLOBAL_MASS_ERASE;
+ data8[2] = data8[0] ^ data8[1];
+
+ ret = ucsi_stm32g0_bl_cmd(ucsi, STM32_CMD_ERASE);
+ if (ret)
+ return ret;
+
+ return ucsi_stm32g0_bl_send(ucsi, data8, STM32_CMD_ERASE_SPECIAL_LEN);
+}
+
+static int ucsi_stm32g0_bl_write(struct ucsi *ucsi, u32 addr, const void *data, size_t len)
+{
+ u8 *data8;
+ int i, ret;
+
+ if (!len || len > STM32G0_I2C_BL_SZ)
+ return -EINVAL;
+
+ /* Write memory: len bytes -1, data up to 256 bytes + XOR'ed bytes */
+ data8 = kmalloc(STM32G0_I2C_BL_SZ + 2, GFP_KERNEL);
+ if (!data8)
+ return -ENOMEM;
+
+ ret = ucsi_stm32g0_bl_cmd(ucsi, STM32_CMD_WM);
+ if (ret)
+ goto free;
+
+ ret = ucsi_stm32g0_bl_send_addr(ucsi, addr);
+ if (ret)
+ goto free;
+
+ data8[0] = len - 1;
+ memcpy(data8 + 1, data, len);
+ data8[len + 1] = data8[0];
+ for (i = 1; i <= len; i++)
+ data8[len + 1] ^= data8[i];
+
+ ret = ucsi_stm32g0_bl_send(ucsi, data8, len + 2);
+free:
+ kfree(data8);
+
+ return ret;
+}
+
+static int ucsi_stm32g0_bl_read(struct ucsi *ucsi, u32 addr, void *data, size_t len)
+{
+ int ret;
+
+ if (!len || len > STM32G0_I2C_BL_SZ)
+ return -EINVAL;
+
+ ret = ucsi_stm32g0_bl_cmd(ucsi, STM32_CMD_RM);
+ if (ret)
+ return ret;
+
+ ret = ucsi_stm32g0_bl_send_addr(ucsi, addr);
+ if (ret)
+ return ret;
+
+ ret = ucsi_stm32g0_bl_cmd(ucsi, len - 1);
+ if (ret)
+ return ret;
+
+ return ucsi_stm32g0_bl_rcv_woack(ucsi, data, len);
+}
+
+/* Firmware commands (the same address as the bootloader) */
+static int ucsi_stm32g0_fw_cmd(struct ucsi *ucsi, unsigned int cmd)
+{
+ return ucsi_stm32g0_bl_cmd_check_ack(ucsi, cmd, false);
+}
+
+static int ucsi_stm32g0_fw_rcv(struct ucsi *ucsi, void *data, size_t len)
+{
+ return ucsi_stm32g0_bl_rcv_woack(ucsi, data, len);
+}
+
+/* UCSI ops */
+static int ucsi_stm32g0_read(struct ucsi *ucsi, unsigned int offset, void *val, size_t len)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->client;
+ u8 reg = offset;
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = 0,
+ .len = 1,
+ .buf = &reg,
+ },
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .len = len,
+ .buf = val,
+ },
+ };
+ int ret;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_err(g0->dev, "i2c read %02x, %02x error: %d\n", client->addr, reg, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ return 0;
+}
+
+static int ucsi_stm32g0_async_write(struct ucsi *ucsi, unsigned int offset, const void *val,
+ size_t len)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->client;
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .flags = 0,
+ }
+ };
+ unsigned char *buf;
+ int ret;
+
+ buf = kmalloc(len + 1, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = offset;
+ memcpy(&buf[1], val, len);
+ msg[0].len = len + 1;
+ msg[0].buf = buf;
+
+ ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+ kfree(buf);
+ if (ret != ARRAY_SIZE(msg)) {
+ dev_err(g0->dev, "i2c write %02x, %02x error: %d\n", client->addr, offset, ret);
+
+ return ret < 0 ? ret : -EIO;
+ }
+
+ return 0;
+}
+
+static int ucsi_stm32g0_sync_write(struct ucsi *ucsi, unsigned int offset, const void *val,
+ size_t len)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ int ret;
+
+ set_bit(COMMAND_PENDING, &g0->flags);
+
+ ret = ucsi_stm32g0_async_write(ucsi, offset, val, len);
+ if (ret)
+ goto out_clear_bit;
+
+ if (!wait_for_completion_timeout(&g0->complete, msecs_to_jiffies(5000)))
+ ret = -ETIMEDOUT;
+
+out_clear_bit:
+ clear_bit(COMMAND_PENDING, &g0->flags);
+
+ return ret;
+}
+
+static irqreturn_t ucsi_stm32g0_irq_handler(int irq, void *data)
+{
+ struct ucsi_stm32g0 *g0 = data;
+ u32 cci;
+ int ret;
+
+ if (g0->suspended)
+ g0->wakeup_event = true;
+
+ ret = ucsi_stm32g0_read(g0->ucsi, UCSI_CCI, &cci, sizeof(cci));
+ if (ret)
+ return IRQ_NONE;
+
+ if (UCSI_CCI_CONNECTOR(cci))
+ ucsi_connector_change(g0->ucsi, UCSI_CCI_CONNECTOR(cci));
+
+ if (test_bit(COMMAND_PENDING, &g0->flags) &&
+ cci & (UCSI_CCI_ACK_COMPLETE | UCSI_CCI_COMMAND_COMPLETE))
+ complete(&g0->complete);
+
+ return IRQ_HANDLED;
+}
+
+static const struct ucsi_operations ucsi_stm32g0_ops = {
+ .read = ucsi_stm32g0_read,
+ .sync_write = ucsi_stm32g0_sync_write,
+ .async_write = ucsi_stm32g0_async_write,
+};
+
+static int ucsi_stm32g0_register(struct ucsi *ucsi)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->client;
+ int ret;
+
+ /* Request alert interrupt */
+ ret = request_threaded_irq(client->irq, NULL, ucsi_stm32g0_irq_handler, IRQF_ONESHOT,
+ dev_name(g0->dev), g0);
+ if (ret) {
+ dev_err(g0->dev, "request IRQ failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = ucsi_register(ucsi);
+ if (ret) {
+ dev_err_probe(g0->dev, ret, "ucsi_register failed\n");
+ free_irq(client->irq, g0);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ucsi_stm32g0_unregister(struct ucsi *ucsi)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ struct i2c_client *client = g0->client;
+
+ ucsi_unregister(ucsi);
+ free_irq(client->irq, g0);
+}
+
+static void ucsi_stm32g0_fw_cb(const struct firmware *fw, void *context)
+{
+ struct ucsi_stm32g0 *g0;
+ const u8 *data, *end;
+ const struct ucsi_stm32g0_fw_info *fw_info;
+ u32 addr = STM32G0_MAIN_MEM_ADDR, ob, fw_version;
+ int ret, size;
+
+ if (!context)
+ return;
+
+ g0 = ucsi_get_drvdata(context);
+
+ if (!fw)
+ goto fw_release;
+
+ fw_info = (struct ucsi_stm32g0_fw_info *)(fw->data + fw->size - sizeof(*fw_info));
+
+ if (!g0->in_bootloader) {
+ /* Read running firmware version */
+ ret = ucsi_stm32g0_fw_cmd(g0->ucsi, STM32G0_FW_GETVER);
+ if (ret) {
+ dev_err(g0->dev, "Get version cmd failed %d\n", ret);
+ goto fw_release;
+ }
+ ret = ucsi_stm32g0_fw_rcv(g0->ucsi, &fw_version,
+ STM32G0_FW_GETVER_LEN);
+ if (ret) {
+ dev_err(g0->dev, "Get version failed %d\n", ret);
+ goto fw_release;
+ }
+
+ /* Sanity check on keyword and firmware version */
+ if (fw_info->keyword != STM32G0_FW_KEYWORD || fw_info->version == fw_version)
+ goto fw_release;
+
+ dev_info(g0->dev, "Flashing FW: %08x (%08x cur)\n", fw_info->version, fw_version);
+
+ /* Switch to bootloader mode */
+ ucsi_stm32g0_unregister(g0->ucsi);
+ ret = ucsi_stm32g0_fw_cmd(g0->ucsi, STM32G0_FW_RSTGOBL);
+ if (ret) {
+ dev_err(g0->dev, "bootloader cmd failed %d\n", ret);
+ goto fw_release;
+ }
+ g0->in_bootloader = true;
+
+ /* STM32G0 reboot delay */
+ msleep(100);
+ }
+
+ ret = ucsi_stm32g0_bl_global_mass_erase(g0->ucsi);
+ if (ret) {
+ dev_err(g0->dev, "Erase failed %d\n", ret);
+ goto fw_release;
+ }
+
+ data = fw->data;
+ end = fw->data + fw->size;
+ while (data < end) {
+ if ((end - data) < STM32G0_I2C_BL_SZ)
+ size = end - data;
+ else
+ size = STM32G0_I2C_BL_SZ;
+
+ ret = ucsi_stm32g0_bl_write(g0->ucsi, addr, data, size);
+ if (ret) {
+ dev_err(g0->dev, "Write failed %d\n", ret);
+ goto fw_release;
+ }
+ addr += size;
+ data += size;
+ }
+
+ dev_dbg(g0->dev, "Configure to boot from main flash\n");
+
+ ret = ucsi_stm32g0_bl_read(g0->ucsi, STM32G0_USER_OPTION_BYTES, &ob, sizeof(ob));
+ if (ret) {
+ dev_err(g0->dev, "read user option bytes failed %d\n", ret);
+ goto fw_release;
+ }
+
+ dev_dbg(g0->dev, "STM32G0_USER_OPTION_BYTES 0x%08x\n", ob);
+
+ /* Configure user option bytes to boot from main flash next time */
+ ob |= STM32G0_USER_OB_BOOT_MAIN;
+
+ /* Writing option bytes will also reset G0 for updates to be loaded */
+ ret = ucsi_stm32g0_bl_write(g0->ucsi, STM32G0_USER_OPTION_BYTES, &ob, sizeof(ob));
+ if (ret) {
+ dev_err(g0->dev, "write user option bytes failed %d\n", ret);
+ goto fw_release;
+ }
+
+ dev_info(g0->dev, "Starting, option bytes:0x%08x\n", ob);
+
+ /* STM32G0 FW boot delay */
+ msleep(500);
+
+ /* Register UCSI interface */
+ if (!ucsi_stm32g0_register(g0->ucsi))
+ g0->in_bootloader = false;
+
+fw_release:
+ release_firmware(fw);
+}
+
+static int ucsi_stm32g0_probe_bootloader(struct ucsi *ucsi)
+{
+ struct ucsi_stm32g0 *g0 = ucsi_get_drvdata(ucsi);
+ int ret;
+ u16 ucsi_version;
+
+ /* firmware-name is optional */
+ if (device_property_present(g0->dev, "firmware-name")) {
+ ret = device_property_read_string(g0->dev, "firmware-name", &g0->fw_name);
+ if (ret < 0)
+ return dev_err_probe(g0->dev, ret, "Error reading firmware-name\n");
+ }
+
+ if (g0->fw_name) {
+ /* STM32G0 in bootloader mode communicates at reserved address 0x51 */
+ g0->i2c_bl = i2c_new_dummy_device(g0->client->adapter, STM32G0_I2C_BL_ADDR);
+ if (IS_ERR(g0->i2c_bl)) {
+ ret = dev_err_probe(g0->dev, PTR_ERR(g0->i2c_bl),
+ "Failed to register bootloader I2C address\n");
+ return ret;
+ }
+ }
+
+ /*
+ * Try to guess if the STM32G0 is running a UCSI firmware. First probe the UCSI FW at its
+ * i2c address. Fallback to bootloader i2c address only if firmware-name is specified.
+ */
+ ret = ucsi_stm32g0_read(ucsi, UCSI_VERSION, &ucsi_version, sizeof(ucsi_version));
+ if (!ret || !g0->fw_name)
+ return ret;
+
+ /* Speculatively read the bootloader version that has a known length. */
+ ret = ucsi_stm32g0_bl_get_version(ucsi, &g0->bl_version);
+ if (ret < 0) {
+ i2c_unregister_device(g0->i2c_bl);
+ return ret;
+ }
+
+ /* Device in bootloader mode */
+ g0->in_bootloader = true;
+ dev_info(g0->dev, "Bootloader Version 0x%02x\n", g0->bl_version);
+
+ return 0;
+}
+
+static int ucsi_stm32g0_probe(struct i2c_client *client, const struct i2c_device_id *id)
+{
+ struct device *dev = &client->dev;
+ struct ucsi_stm32g0 *g0;
+ int ret;
+
+ g0 = devm_kzalloc(dev, sizeof(*g0), GFP_KERNEL);
+ if (!g0)
+ return -ENOMEM;
+
+ g0->dev = dev;
+ g0->client = client;
+ init_completion(&g0->complete);
+ i2c_set_clientdata(client, g0);
+
+ g0->ucsi = ucsi_create(dev, &ucsi_stm32g0_ops);
+ if (IS_ERR(g0->ucsi))
+ return PTR_ERR(g0->ucsi);
+
+ ucsi_set_drvdata(g0->ucsi, g0);
+
+ ret = ucsi_stm32g0_probe_bootloader(g0->ucsi);
+ if (ret < 0)
+ goto destroy;
+
+ /*
+ * Don't register in bootloader mode: wait for the firmware to be loaded and started before
+ * registering UCSI device.
+ */
+ if (!g0->in_bootloader) {
+ ret = ucsi_stm32g0_register(g0->ucsi);
+ if (ret < 0)
+ goto freei2c;
+ }
+
+ if (g0->fw_name) {
+ /*
+ * Asynchronously flash (e.g. bootloader mode) or update the running firmware,
+ * not to hang the boot process
+ */
+ ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, g0->fw_name, g0->dev,
+ GFP_KERNEL, g0->ucsi, ucsi_stm32g0_fw_cb);
+ if (ret < 0) {
+ dev_err_probe(dev, ret, "firmware request failed\n");
+ goto unregister;
+ }
+ }
+
+ return 0;
+
+unregister:
+ if (!g0->in_bootloader)
+ ucsi_stm32g0_unregister(g0->ucsi);
+freei2c:
+ if (g0->fw_name)
+ i2c_unregister_device(g0->i2c_bl);
+destroy:
+ ucsi_destroy(g0->ucsi);
+
+ return ret;
+}
+
+static void ucsi_stm32g0_remove(struct i2c_client *client)
+{
+ struct ucsi_stm32g0 *g0 = i2c_get_clientdata(client);
+
+ if (!g0->in_bootloader)
+ ucsi_stm32g0_unregister(g0->ucsi);
+ if (g0->fw_name)
+ i2c_unregister_device(g0->i2c_bl);
+ ucsi_destroy(g0->ucsi);
+}
+
+static int ucsi_stm32g0_suspend(struct device *dev)
+{
+ struct ucsi_stm32g0 *g0 = dev_get_drvdata(dev);
+ struct i2c_client *client = g0->client;
+
+ if (g0->in_bootloader)
+ return 0;
+
+ /* Keep the interrupt disabled until the i2c bus has been resumed */
+ disable_irq(client->irq);
+
+ g0->suspended = true;
+ g0->wakeup_event = false;
+
+ if (device_may_wakeup(dev) || device_wakeup_path(dev))
+ enable_irq_wake(client->irq);
+
+ return 0;
+}
+
+static int ucsi_stm32g0_resume(struct device *dev)
+{
+ struct ucsi_stm32g0 *g0 = dev_get_drvdata(dev);
+ struct i2c_client *client = g0->client;
+
+ if (g0->in_bootloader)
+ return 0;
+
+ if (device_may_wakeup(dev) || device_wakeup_path(dev))
+ disable_irq_wake(client->irq);
+
+ enable_irq(client->irq);
+
+ /* Enforce any pending handler gets called to signal a wakeup_event */
+ synchronize_irq(client->irq);
+
+ if (g0->wakeup_event)
+ pm_wakeup_event(g0->dev, 0);
+
+ g0->suspended = false;
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(ucsi_stm32g0_pm_ops, ucsi_stm32g0_suspend, ucsi_stm32g0_resume);
+
+static const struct of_device_id __maybe_unused ucsi_stm32g0_typec_of_match[] = {
+ { .compatible = "st,stm32g0-typec" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, ucsi_stm32g0_typec_of_match);
+
+static const struct i2c_device_id ucsi_stm32g0_typec_i2c_devid[] = {
+ {"stm32g0-typec", 0},
+ {},
+};
+MODULE_DEVICE_TABLE(i2c, ucsi_stm32g0_typec_i2c_devid);
+
+static struct i2c_driver ucsi_stm32g0_i2c_driver = {
+ .driver = {
+ .name = "ucsi-stm32g0-i2c",
+ .of_match_table = of_match_ptr(ucsi_stm32g0_typec_of_match),
+ .pm = pm_sleep_ptr(&ucsi_stm32g0_pm_ops),
+ },
+ .probe = ucsi_stm32g0_probe,
+ .remove = ucsi_stm32g0_remove,
+ .id_table = ucsi_stm32g0_typec_i2c_devid
+};
+module_i2c_driver(ucsi_stm32g0_i2c_driver);
+
+MODULE_AUTHOR("Fabrice Gasnier <fabrice.gasnier@foss.st.com>");
+MODULE_DESCRIPTION("STMicroelectronics STM32G0 Type-C controller");
+MODULE_LICENSE("Dual BSD/GPL");
+MODULE_ALIAS("platform:ucsi-stm32g0");
diff --git a/drivers/usb/typec/wusb3801.c b/drivers/usb/typec/wusb3801.c
new file mode 100644
index 000000000..a43a18d4b
--- /dev/null
+++ b/drivers/usb/typec/wusb3801.c
@@ -0,0 +1,435 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Willsemi WUSB3801 Type-C port controller driver
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/typec.h>
+
+#define WUSB3801_REG_DEVICE_ID 0x01
+#define WUSB3801_REG_CTRL0 0x02
+#define WUSB3801_REG_INT 0x03
+#define WUSB3801_REG_STAT 0x04
+#define WUSB3801_REG_CTRL1 0x05
+#define WUSB3801_REG_TEST00 0x06
+#define WUSB3801_REG_TEST01 0x07
+#define WUSB3801_REG_TEST02 0x08
+#define WUSB3801_REG_TEST03 0x09
+#define WUSB3801_REG_TEST04 0x0a
+#define WUSB3801_REG_TEST05 0x0b
+#define WUSB3801_REG_TEST06 0x0c
+#define WUSB3801_REG_TEST07 0x0d
+#define WUSB3801_REG_TEST08 0x0e
+#define WUSB3801_REG_TEST09 0x0f
+#define WUSB3801_REG_TEST0A 0x10
+#define WUSB3801_REG_TEST0B 0x11
+#define WUSB3801_REG_TEST0C 0x12
+#define WUSB3801_REG_TEST0D 0x13
+#define WUSB3801_REG_TEST0E 0x14
+#define WUSB3801_REG_TEST0F 0x15
+#define WUSB3801_REG_TEST10 0x16
+#define WUSB3801_REG_TEST11 0x17
+#define WUSB3801_REG_TEST12 0x18
+
+#define WUSB3801_DEVICE_ID_VERSION_ID GENMASK(7, 3)
+#define WUSB3801_DEVICE_ID_VENDOR_ID GENMASK(2, 0)
+
+#define WUSB3801_CTRL0_DIS_ACC_SUPPORT BIT(7)
+#define WUSB3801_CTRL0_TRY GENMASK(6, 5)
+#define WUSB3801_CTRL0_TRY_NONE (0x0 << 5)
+#define WUSB3801_CTRL0_TRY_SNK (0x1 << 5)
+#define WUSB3801_CTRL0_TRY_SRC (0x2 << 5)
+#define WUSB3801_CTRL0_CURRENT GENMASK(4, 3) /* SRC */
+#define WUSB3801_CTRL0_CURRENT_DEFAULT (0x0 << 3)
+#define WUSB3801_CTRL0_CURRENT_1_5A (0x1 << 3)
+#define WUSB3801_CTRL0_CURRENT_3_0A (0x2 << 3)
+#define WUSB3801_CTRL0_ROLE GENMASK(2, 1)
+#define WUSB3801_CTRL0_ROLE_SNK (0x0 << 1)
+#define WUSB3801_CTRL0_ROLE_SRC (0x1 << 1)
+#define WUSB3801_CTRL0_ROLE_DRP (0x2 << 1)
+#define WUSB3801_CTRL0_INT_MASK BIT(0)
+
+#define WUSB3801_INT_ATTACHED BIT(0)
+#define WUSB3801_INT_DETACHED BIT(1)
+
+#define WUSB3801_STAT_VBUS_DETECTED BIT(7)
+#define WUSB3801_STAT_CURRENT GENMASK(6, 5) /* SNK */
+#define WUSB3801_STAT_CURRENT_STANDBY (0x0 << 5)
+#define WUSB3801_STAT_CURRENT_DEFAULT (0x1 << 5)
+#define WUSB3801_STAT_CURRENT_1_5A (0x2 << 5)
+#define WUSB3801_STAT_CURRENT_3_0A (0x3 << 5)
+#define WUSB3801_STAT_PARTNER GENMASK(4, 2)
+#define WUSB3801_STAT_PARTNER_STANDBY (0x0 << 2)
+#define WUSB3801_STAT_PARTNER_SNK (0x1 << 2)
+#define WUSB3801_STAT_PARTNER_SRC (0x2 << 2)
+#define WUSB3801_STAT_PARTNER_AUDIO (0x3 << 2)
+#define WUSB3801_STAT_PARTNER_DEBUG (0x4 << 2)
+#define WUSB3801_STAT_ORIENTATION GENMASK(1, 0)
+#define WUSB3801_STAT_ORIENTATION_NONE (0x0 << 0)
+#define WUSB3801_STAT_ORIENTATION_CC1 (0x1 << 0)
+#define WUSB3801_STAT_ORIENTATION_CC2 (0x2 << 0)
+#define WUSB3801_STAT_ORIENTATION_BOTH (0x3 << 0)
+
+#define WUSB3801_CTRL1_SM_RESET BIT(0)
+
+#define WUSB3801_TEST01_VENDOR_SUB_ID (BIT(8) | BIT(6))
+
+#define WUSB3801_TEST02_FORCE_ERR_RCY BIT(8)
+
+#define WUSB3801_TEST0A_WAIT_VBUS BIT(5)
+
+struct wusb3801 {
+ struct typec_capability cap;
+ struct device *dev;
+ struct typec_partner *partner;
+ struct typec_port *port;
+ struct regmap *regmap;
+ struct regulator *vbus_supply;
+ unsigned int partner_type;
+ enum typec_port_type port_type;
+ enum typec_pwr_opmode pwr_opmode;
+ bool vbus_on;
+};
+
+static enum typec_role wusb3801_get_default_role(struct wusb3801 *wusb3801)
+{
+ switch (wusb3801->port_type) {
+ case TYPEC_PORT_SRC:
+ return TYPEC_SOURCE;
+ case TYPEC_PORT_SNK:
+ return TYPEC_SINK;
+ case TYPEC_PORT_DRP:
+ default:
+ if (wusb3801->cap.prefer_role == TYPEC_SOURCE)
+ return TYPEC_SOURCE;
+ return TYPEC_SINK;
+ }
+}
+
+static int wusb3801_map_port_type(enum typec_port_type type)
+{
+ switch (type) {
+ case TYPEC_PORT_SRC:
+ return WUSB3801_CTRL0_ROLE_SRC;
+ case TYPEC_PORT_SNK:
+ return WUSB3801_CTRL0_ROLE_SNK;
+ case TYPEC_PORT_DRP:
+ default:
+ return WUSB3801_CTRL0_ROLE_DRP;
+ }
+}
+
+static int wusb3801_map_pwr_opmode(enum typec_pwr_opmode mode)
+{
+ switch (mode) {
+ case TYPEC_PWR_MODE_USB:
+ default:
+ return WUSB3801_CTRL0_CURRENT_DEFAULT;
+ case TYPEC_PWR_MODE_1_5A:
+ return WUSB3801_CTRL0_CURRENT_1_5A;
+ case TYPEC_PWR_MODE_3_0A:
+ return WUSB3801_CTRL0_CURRENT_3_0A;
+ }
+}
+
+static unsigned int wusb3801_map_try_role(int role)
+{
+ switch (role) {
+ case TYPEC_NO_PREFERRED_ROLE:
+ default:
+ return WUSB3801_CTRL0_TRY_NONE;
+ case TYPEC_SINK:
+ return WUSB3801_CTRL0_TRY_SNK;
+ case TYPEC_SOURCE:
+ return WUSB3801_CTRL0_TRY_SRC;
+ }
+}
+
+static enum typec_orientation wusb3801_unmap_orientation(unsigned int status)
+{
+ switch (status & WUSB3801_STAT_ORIENTATION) {
+ case WUSB3801_STAT_ORIENTATION_NONE:
+ case WUSB3801_STAT_ORIENTATION_BOTH:
+ default:
+ return TYPEC_ORIENTATION_NONE;
+ case WUSB3801_STAT_ORIENTATION_CC1:
+ return TYPEC_ORIENTATION_NORMAL;
+ case WUSB3801_STAT_ORIENTATION_CC2:
+ return TYPEC_ORIENTATION_REVERSE;
+ }
+}
+
+static enum typec_pwr_opmode wusb3801_unmap_pwr_opmode(unsigned int status)
+{
+ switch (status & WUSB3801_STAT_CURRENT) {
+ case WUSB3801_STAT_CURRENT_STANDBY:
+ case WUSB3801_STAT_CURRENT_DEFAULT:
+ default:
+ return TYPEC_PWR_MODE_USB;
+ case WUSB3801_STAT_CURRENT_1_5A:
+ return TYPEC_PWR_MODE_1_5A;
+ case WUSB3801_STAT_CURRENT_3_0A:
+ return TYPEC_PWR_MODE_3_0A;
+ }
+}
+
+static int wusb3801_try_role(struct typec_port *port, int role)
+{
+ struct wusb3801 *wusb3801 = typec_get_drvdata(port);
+
+ return regmap_update_bits(wusb3801->regmap, WUSB3801_REG_CTRL0,
+ WUSB3801_CTRL0_TRY,
+ wusb3801_map_try_role(role));
+}
+
+static int wusb3801_port_type_set(struct typec_port *port,
+ enum typec_port_type type)
+{
+ struct wusb3801 *wusb3801 = typec_get_drvdata(port);
+ int ret;
+
+ ret = regmap_update_bits(wusb3801->regmap, WUSB3801_REG_CTRL0,
+ WUSB3801_CTRL0_ROLE,
+ wusb3801_map_port_type(type));
+ if (ret)
+ return ret;
+
+ wusb3801->port_type = type;
+
+ return 0;
+}
+
+static const struct typec_operations wusb3801_typec_ops = {
+ .try_role = wusb3801_try_role,
+ .port_type_set = wusb3801_port_type_set,
+};
+
+static int wusb3801_hw_init(struct wusb3801 *wusb3801)
+{
+ return regmap_write(wusb3801->regmap, WUSB3801_REG_CTRL0,
+ wusb3801_map_try_role(wusb3801->cap.prefer_role) |
+ wusb3801_map_pwr_opmode(wusb3801->pwr_opmode) |
+ wusb3801_map_port_type(wusb3801->port_type));
+}
+
+static void wusb3801_hw_update(struct wusb3801 *wusb3801)
+{
+ struct typec_port *port = wusb3801->port;
+ struct device *dev = wusb3801->dev;
+ unsigned int partner_type, status;
+ int ret;
+
+ ret = regmap_read(wusb3801->regmap, WUSB3801_REG_STAT, &status);
+ if (ret) {
+ dev_warn(dev, "Failed to read port status: %d\n", ret);
+ status = 0;
+ }
+ dev_dbg(dev, "status = 0x%02x\n", status);
+
+ partner_type = status & WUSB3801_STAT_PARTNER;
+
+ if (partner_type == WUSB3801_STAT_PARTNER_SNK) {
+ if (!wusb3801->vbus_on) {
+ ret = regulator_enable(wusb3801->vbus_supply);
+ if (ret)
+ dev_warn(dev, "Failed to enable VBUS: %d\n", ret);
+ wusb3801->vbus_on = true;
+ }
+ } else {
+ if (wusb3801->vbus_on) {
+ regulator_disable(wusb3801->vbus_supply);
+ wusb3801->vbus_on = false;
+ }
+ }
+
+ if (partner_type != wusb3801->partner_type) {
+ struct typec_partner_desc desc = {};
+ enum typec_data_role data_role;
+ enum typec_role pwr_role = wusb3801_get_default_role(wusb3801);
+
+ switch (partner_type) {
+ case WUSB3801_STAT_PARTNER_STANDBY:
+ break;
+ case WUSB3801_STAT_PARTNER_SNK:
+ pwr_role = TYPEC_SOURCE;
+ break;
+ case WUSB3801_STAT_PARTNER_SRC:
+ pwr_role = TYPEC_SINK;
+ break;
+ case WUSB3801_STAT_PARTNER_AUDIO:
+ desc.accessory = TYPEC_ACCESSORY_AUDIO;
+ break;
+ case WUSB3801_STAT_PARTNER_DEBUG:
+ desc.accessory = TYPEC_ACCESSORY_DEBUG;
+ break;
+ }
+
+ if (wusb3801->partner) {
+ typec_unregister_partner(wusb3801->partner);
+ wusb3801->partner = NULL;
+ }
+
+ if (partner_type != WUSB3801_STAT_PARTNER_STANDBY) {
+ wusb3801->partner = typec_register_partner(port, &desc);
+ if (IS_ERR(wusb3801->partner))
+ dev_err(dev, "Failed to register partner: %ld\n",
+ PTR_ERR(wusb3801->partner));
+ }
+
+ data_role = pwr_role == TYPEC_SOURCE ? TYPEC_HOST : TYPEC_DEVICE;
+ typec_set_data_role(port, data_role);
+ typec_set_pwr_role(port, pwr_role);
+ typec_set_vconn_role(port, pwr_role);
+ }
+
+ typec_set_pwr_opmode(wusb3801->port,
+ partner_type == WUSB3801_STAT_PARTNER_SRC
+ ? wusb3801_unmap_pwr_opmode(status)
+ : wusb3801->pwr_opmode);
+ typec_set_orientation(wusb3801->port,
+ wusb3801_unmap_orientation(status));
+
+ wusb3801->partner_type = partner_type;
+}
+
+static irqreturn_t wusb3801_irq(int irq, void *data)
+{
+ struct wusb3801 *wusb3801 = data;
+ unsigned int dummy;
+
+ /*
+ * The interrupt register must be read in order to clear the IRQ,
+ * but all of the useful information is in the status register.
+ */
+ regmap_read(wusb3801->regmap, WUSB3801_REG_INT, &dummy);
+
+ wusb3801_hw_update(wusb3801);
+
+ return IRQ_HANDLED;
+}
+
+static const struct regmap_config config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = WUSB3801_REG_TEST12,
+};
+
+static int wusb3801_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct fwnode_handle *connector;
+ struct wusb3801 *wusb3801;
+ const char *cap_str;
+ int ret;
+
+ wusb3801 = devm_kzalloc(dev, sizeof(*wusb3801), GFP_KERNEL);
+ if (!wusb3801)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, wusb3801);
+
+ wusb3801->dev = dev;
+
+ wusb3801->regmap = devm_regmap_init_i2c(client, &config);
+ if (IS_ERR(wusb3801->regmap))
+ return PTR_ERR(wusb3801->regmap);
+
+ wusb3801->vbus_supply = devm_regulator_get(dev, "vbus");
+ if (IS_ERR(wusb3801->vbus_supply))
+ return PTR_ERR(wusb3801->vbus_supply);
+
+ connector = device_get_named_child_node(dev, "connector");
+ if (!connector)
+ return -ENODEV;
+
+ ret = typec_get_fw_cap(&wusb3801->cap, connector);
+ if (ret)
+ goto err_put_connector;
+ wusb3801->port_type = wusb3801->cap.type;
+
+ ret = fwnode_property_read_string(connector, "typec-power-opmode", &cap_str);
+ if (ret)
+ goto err_put_connector;
+
+ ret = typec_find_pwr_opmode(cap_str);
+ if (ret < 0 || ret == TYPEC_PWR_MODE_PD)
+ goto err_put_connector;
+ wusb3801->pwr_opmode = ret;
+
+ /* Initialize the hardware with the devicetree settings. */
+ ret = wusb3801_hw_init(wusb3801);
+ if (ret)
+ goto err_put_connector;
+
+ wusb3801->cap.revision = USB_TYPEC_REV_1_2;
+ wusb3801->cap.accessory[0] = TYPEC_ACCESSORY_AUDIO;
+ wusb3801->cap.accessory[1] = TYPEC_ACCESSORY_DEBUG;
+ wusb3801->cap.orientation_aware = true;
+ wusb3801->cap.driver_data = wusb3801;
+ wusb3801->cap.ops = &wusb3801_typec_ops;
+
+ wusb3801->port = typec_register_port(dev, &wusb3801->cap);
+ if (IS_ERR(wusb3801->port)) {
+ ret = PTR_ERR(wusb3801->port);
+ goto err_put_connector;
+ }
+
+ /* Initialize the port attributes from the hardware state. */
+ wusb3801_hw_update(wusb3801);
+
+ ret = request_threaded_irq(client->irq, NULL, wusb3801_irq,
+ IRQF_ONESHOT, dev_name(dev), wusb3801);
+ if (ret)
+ goto err_unregister_port;
+
+ fwnode_handle_put(connector);
+
+ return 0;
+
+err_unregister_port:
+ typec_unregister_port(wusb3801->port);
+err_put_connector:
+ fwnode_handle_put(connector);
+
+ return ret;
+}
+
+static void wusb3801_remove(struct i2c_client *client)
+{
+ struct wusb3801 *wusb3801 = i2c_get_clientdata(client);
+
+ free_irq(client->irq, wusb3801);
+
+ if (wusb3801->partner)
+ typec_unregister_partner(wusb3801->partner);
+ typec_unregister_port(wusb3801->port);
+
+ if (wusb3801->vbus_on)
+ regulator_disable(wusb3801->vbus_supply);
+}
+
+static const struct of_device_id wusb3801_of_match[] = {
+ { .compatible = "willsemi,wusb3801" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, wusb3801_of_match);
+
+static struct i2c_driver wusb3801_driver = {
+ .probe_new = wusb3801_probe,
+ .remove = wusb3801_remove,
+ .driver = {
+ .name = "wusb3801",
+ .of_match_table = wusb3801_of_match,
+ },
+};
+
+module_i2c_driver(wusb3801_driver);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("Willsemi WUSB3801 Type-C port controller driver");
+MODULE_LICENSE("GPL");