[PATCH 05/11] usb: tcpm: add TCPCI (Type-C Port Controller Interface) driver

Peng Fan (OSS) peng.fan at oss.nxp.com
Wed Jun 17 10:23:04 CEST 2026


From: Peng Fan <peng.fan at nxp.com>

Add a generic TCPCI-compliant driver for USB Type-C Port Controller
Interface chips such as NXP PTN5110. The driver implements the
dm_tcpm_ops interface and communicates with the TCPC via I2C.

Key features:
- Full TCPCI register-level implementation for PD sink negotiation
- Proper TX buffer handling (burst write from TX_HDR)
- Alert-based polling for RX, TX completion, CC and VBUS changes
- Re-entrant poll protection to prevent state machine corruption
- Low power mode with RX_DETECT disable and alert masking to
  prevent post-negotiation Hard Reset from PD sources
- Stale RX buffer drain during initialization for dead-battery
  boot scenarios

Signed-off-by: Peng Fan <peng.fan at nxp.com>
---
 drivers/usb/tcpm/Kconfig  |   9 +
 drivers/usb/tcpm/Makefile |   1 +
 drivers/usb/tcpm/tcpci.c  | 778 ++++++++++++++++++++++++++++++++++++++++++++++
 include/usb/tcpci.h       | 185 +++++++++++
 4 files changed, 973 insertions(+)

diff --git a/drivers/usb/tcpm/Kconfig b/drivers/usb/tcpm/Kconfig
index 9be4b496e82..695c49b64ce 100644
--- a/drivers/usb/tcpm/Kconfig
+++ b/drivers/usb/tcpm/Kconfig
@@ -14,3 +14,12 @@ config TYPEC_FUSB302
 	  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_TCPCI
+	tristate "TCPCI Type-C chip driver"
+	depends on DM && DM_I2C && TYPEC_TCPM
+	help
+	  Generic TCPCI (USB Type-C Port Controller Interface)
+	  compliant chip driver that works with Type-C Port
+	  Controller Manager to provide USB PD and USB Type-C
+	  functionalities. Supports NXP PTN5110 and similar.
diff --git a/drivers/usb/tcpm/Makefile b/drivers/usb/tcpm/Makefile
index 668d33155bf..5cabcc06e0a 100644
--- a/drivers/usb/tcpm/Makefile
+++ b/drivers/usb/tcpm/Makefile
@@ -2,3 +2,4 @@
 
 obj-$(CONFIG_TYPEC_TCPM) += tcpm.o tcpm-uclass.o
 obj-$(CONFIG_TYPEC_FUSB302) += fusb302.o
+obj-$(CONFIG_TYPEC_TCPCI) += tcpci.o
diff --git a/drivers/usb/tcpm/tcpci.c b/drivers/usb/tcpm/tcpci.c
new file mode 100644
index 00000000000..9ade6d16009
--- /dev/null
+++ b/drivers/usb/tcpm/tcpci.c
@@ -0,0 +1,778 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 NXP
+ *
+ * USB Type-C Port Controller Interface I2C driver for U-Boot.
+ */
+
+#define LOG_CATEGORY UCLASS_TCPM
+
+#include <dm.h>
+#include <i2c.h>
+#include <watchdog.h>
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <dm/device_compat.h>
+#include <time.h>
+#include <usb/tcpm.h>
+#include <usb/tcpci.h>
+#include "tcpm-internal.h"
+
+#define PD_RETRY_COUNT_DEFAULT		3
+#define PD_RETRY_COUNT_3_0_OR_HIGHER	2
+
+struct tcpci_chip {
+	u16 alert_mask;
+	bool controls_vbus;
+	bool in_poll;
+};
+
+static int tcpci_read8(struct udevice *dev, u8 reg, u8 *val)
+{
+	int ret;
+
+	ret = dm_i2c_read(dev, reg, val, 1);
+	if (ret)
+		dev_err(dev, "cannot read 0x%02x, ret=%d\n", reg, ret);
+
+	dev_dbg(dev, "read 0x%02x, val=0x%02x\n", reg, *val);
+
+	return ret;
+}
+
+static int tcpci_write8(struct udevice *dev, u8 reg, u8 val)
+{
+	int ret;
+
+	ret = dm_i2c_write(dev, reg, &val, 1);
+	if (ret)
+		dev_err(dev, "cannot write 0x%02x to 0x%02x, ret=%d\n",
+			val, reg, ret);
+	dev_dbg(dev, "write 0x%02x, val=0x%02x\n", reg, val);
+
+	return ret;
+}
+
+static int tcpci_read16(struct udevice *dev, u8 reg, u16 *val)
+{
+	int ret;
+
+	ret = dm_i2c_read(dev, reg, (u8 *)val, 2);
+	if (ret)
+		dev_err(dev, "cannot read 16-bit 0x%02x, ret=%d\n", reg, ret);
+
+	dev_dbg(dev, "read16 0x%02x, val=0x%04x\n", reg, *val);
+
+	return ret;
+}
+
+static int tcpci_write16(struct udevice *dev, u8 reg, u16 val)
+{
+	int ret;
+
+	ret = dm_i2c_write(dev, reg, (u8 *)&val, 2);
+	if (ret)
+		dev_err(dev, "cannot write 16-bit 0x%04x to 0x%02x, ret=%d\n",
+			val, reg, ret);
+	dev_dbg(dev, "write16 0x%02x, val=0x%04x\n", reg, val);
+
+	return ret;
+}
+
+static int tcpci_get_connector_node(struct udevice *dev, ofnode *connector_node)
+{
+	dev_dbg(dev, "%s: entering\n", __func__);
+
+	*connector_node = dev_read_subnode(dev, "connector");
+	if (!ofnode_valid(*connector_node)) {
+		dev_err(dev, "'connector' node is not found\n");
+		return -ENODEV;
+	}
+
+	dev_dbg(dev, "%s: connector node found\n", __func__);
+	return 0;
+}
+
+static int tcpci_set_cc(struct udevice *dev, enum typec_cc_status cc)
+{
+	unsigned int reg;
+	int ret;
+	u8 power_status;
+	bool vconn_pres;
+	enum typec_cc_polarity polarity = TYPEC_POLARITY_CC1;
+
+	dev_dbg(dev, "%s: entering cc=%d\n", __func__, cc);
+
+	ret = tcpci_read8(dev, TCPC_POWER_STATUS, &power_status);
+	if (ret < 0)
+		return ret;
+
+	vconn_pres = !!(power_status & TCPC_POWER_STATUS_VCONN_PRES);
+	if (vconn_pres) {
+		u8 ctrl;
+
+		ret = tcpci_read8(dev, TCPC_TCPC_CTRL, &ctrl);
+		if (ret < 0)
+			return ret;
+
+		if (ctrl & TCPC_TCPC_CTRL_ORIENTATION)
+			polarity = TYPEC_POLARITY_CC2;
+	}
+
+	switch (cc) {
+	case TYPEC_CC_RA:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_RA) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_RA));
+		break;
+	case TYPEC_CC_RD:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_RD) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_RD));
+		break;
+	case TYPEC_CC_RP_DEF:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+				  TCPC_ROLE_CTRL_RP_VAL_DEF));
+		break;
+	case TYPEC_CC_RP_1_5:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+				  TCPC_ROLE_CTRL_RP_VAL_1_5));
+		break;
+	case TYPEC_CC_RP_3_0:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_RP) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+				  TCPC_ROLE_CTRL_RP_VAL_3_0));
+		break;
+	case TYPEC_CC_OPEN:
+	default:
+		reg = (FIELD_PREP(TCPC_ROLE_CTRL_CC1, TCPC_ROLE_CTRL_CC_OPEN) |
+		       FIELD_PREP(TCPC_ROLE_CTRL_CC2, TCPC_ROLE_CTRL_CC_OPEN));
+		break;
+	}
+
+	if (vconn_pres) {
+		if (polarity == TYPEC_POLARITY_CC2)
+			reg = (reg & ~TCPC_ROLE_CTRL_CC1) |
+			      FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+					 TCPC_ROLE_CTRL_CC_OPEN);
+		else
+			reg = (reg & ~TCPC_ROLE_CTRL_CC2) |
+			      FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+					 TCPC_ROLE_CTRL_CC_OPEN);
+	}
+
+	ret = tcpci_write8(dev, TCPC_ROLE_CTRL, (u8)reg);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_start_toggling(struct udevice *dev,
+				enum typec_port_type port_type,
+				enum typec_cc_status cc)
+{
+	unsigned int reg = 0;
+	int ret;
+
+	dev_dbg(dev, "%s: entering port_type=%d cc=%d\n", __func__,
+		port_type, cc);
+
+	switch (port_type) {
+	case TYPEC_PORT_DRP:
+		reg |= TCPC_ROLE_CTRL_DRP;
+		fallthrough;
+	case TYPEC_PORT_SRC:
+		switch (cc) {
+		default:
+		case TYPEC_CC_RP_DEF:
+			reg |= FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+					  TCPC_ROLE_CTRL_RP_VAL_DEF);
+			break;
+		case TYPEC_CC_RP_1_5:
+			reg |= FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+					  TCPC_ROLE_CTRL_RP_VAL_1_5);
+			break;
+		case TYPEC_CC_RP_3_0:
+			reg |= FIELD_PREP(TCPC_ROLE_CTRL_RP_VAL,
+					  TCPC_ROLE_CTRL_RP_VAL_3_0);
+			break;
+		}
+
+		reg |= (FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+				   TCPC_ROLE_CTRL_CC_RP) |
+			FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+				   TCPC_ROLE_CTRL_CC_RP));
+		ret = tcpci_write8(dev, TCPC_ROLE_CTRL, (u8)reg);
+		if (ret < 0)
+			return ret;
+		ret = tcpci_write8(dev, TCPC_COMMAND, TCPC_CMD_LOOK4CONNECTION);
+		if (ret < 0)
+			return ret;
+		dev_dbg(dev, "%s: done\n", __func__);
+		return 0;
+	case TYPEC_PORT_SNK:
+		reg |= (FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+				   TCPC_ROLE_CTRL_CC_RD) |
+			FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+				   TCPC_ROLE_CTRL_CC_RD));
+		ret = tcpci_write8(dev, TCPC_ROLE_CTRL, (u8)reg);
+		if (ret < 0)
+			return ret;
+		dev_dbg(dev, "%s: done sink CC=Rd\n", __func__);
+		return 0;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int tcpci_get_cc(struct udevice *dev, enum typec_cc_status *cc1,
+			enum typec_cc_status *cc2)
+{
+	unsigned int role_control, reg;
+	int ret;
+
+	dev_dbg(dev, "%s: entering\n", __func__);
+
+	ret = tcpci_read8(dev, TCPC_ROLE_CTRL, (u8 *)&role_control);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_read8(dev, TCPC_CC_STATUS, (u8 *)&reg);
+	if (ret < 0)
+		return ret;
+
+	*cc1 = tcpci_to_typec_cc(FIELD_GET(TCPC_CC_STATUS_CC1, reg),
+				 reg & TCPC_CC_STATUS_TERM ||
+				 tcpc_presenting_rd(role_control, CC1));
+	*cc2 = tcpci_to_typec_cc(FIELD_GET(TCPC_CC_STATUS_CC2, reg),
+				 reg & TCPC_CC_STATUS_TERM ||
+				 tcpc_presenting_rd(role_control, CC2));
+
+	dev_dbg(dev, "%s: cc1=%d cc2=%d\n", __func__, *cc1, *cc2);
+	return 0;
+}
+
+static int tcpci_set_polarity(struct udevice *dev,
+			      enum typec_cc_polarity polarity)
+{
+	unsigned int reg;
+	enum typec_cc_status cc1, cc2;
+	int ret;
+
+	dev_dbg(dev, "%s: entering polarity=%d\n", __func__, polarity);
+
+	ret = tcpci_read8(dev, TCPC_ROLE_CTRL, (u8 *)&reg);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_get_cc(dev, &cc1, &cc2);
+	if (ret < 0)
+		return ret;
+
+	if (reg & TCPC_ROLE_CTRL_DRP) {
+		reg &= ~TCPC_ROLE_CTRL_DRP;
+
+		if (polarity == TYPEC_POLARITY_CC2) {
+			reg &= ~TCPC_ROLE_CTRL_CC2;
+			if (cc2 == TYPEC_CC_RD)
+				reg |= FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+						  TCPC_ROLE_CTRL_CC_RP);
+			else if (cc2 >= TYPEC_CC_RP_DEF)
+				reg |= FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+						  TCPC_ROLE_CTRL_CC_RD);
+		} else {
+			reg &= ~TCPC_ROLE_CTRL_CC1;
+			if (cc1 == TYPEC_CC_RD)
+				reg |= FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+						  TCPC_ROLE_CTRL_CC_RP);
+			else if (cc1 >= TYPEC_CC_RP_DEF)
+				reg |= FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+						  TCPC_ROLE_CTRL_CC_RD);
+		}
+
+		/* Transitioning from DRP toggling: set unused CC to OPEN */
+		if (polarity == TYPEC_POLARITY_CC2)
+			reg = (reg & ~TCPC_ROLE_CTRL_CC1) |
+			      FIELD_PREP(TCPC_ROLE_CTRL_CC1,
+					 TCPC_ROLE_CTRL_CC_OPEN);
+		else
+			reg = (reg & ~TCPC_ROLE_CTRL_CC2) |
+			      FIELD_PREP(TCPC_ROLE_CTRL_CC2,
+					 TCPC_ROLE_CTRL_CC_OPEN);
+	}
+
+	ret = tcpci_write8(dev, TCPC_ROLE_CTRL, (u8)reg);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_write8(dev, TCPC_TCPC_CTRL,
+			   (polarity == TYPEC_POLARITY_CC2) ?
+			   TCPC_TCPC_CTRL_ORIENTATION : 0);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_set_vconn(struct udevice *dev, bool enable)
+{
+	int ret;
+	u8 reg;
+
+	dev_dbg(dev, "%s: entering enable=%d\n", __func__, enable);
+
+	ret = tcpci_read8(dev, TCPC_POWER_CTRL, &reg);
+	if (ret < 0)
+		return ret;
+
+	if (enable)
+		reg |= TCPC_POWER_CTRL_VCONN_ENABLE;
+	else
+		reg &= ~TCPC_POWER_CTRL_VCONN_ENABLE;
+
+	ret = tcpci_write8(dev, TCPC_POWER_CTRL, reg);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_set_vbus(struct udevice *dev, bool on, bool charge)
+{
+	int ret;
+	u8 power_status;
+
+	dev_dbg(dev, "%s: entering on=%d charge=%d\n", __func__, on, charge);
+
+	ret = tcpci_read8(dev, TCPC_POWER_STATUS, &power_status);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: POWER_STATUS=0x%02x VBUS_PRES=%d SOURCING=%d SINKING=%d\n",
+		__func__, power_status,
+		!!(power_status & TCPC_POWER_STATUS_VBUS_PRES),
+		!!(power_status & TCPC_POWER_STATUS_SOURCING_VBUS),
+		!!(power_status & TCPC_POWER_STATUS_SINKING_VBUS));
+
+	/*
+	 * If VBUS is present, don't send any VBUS COMMAND register writes
+	 * at all - the PTN5110 uses a shared power path and disabling
+	 * source or sink while VBUS is active cuts the board's power.
+	 */
+	if (power_status & TCPC_POWER_STATUS_VBUS_PRES) {
+		dev_dbg(dev, "%s: VBUS present, skipping all VBUS commands\n",
+			__func__);
+		return 0;
+	}
+
+	if (!on) {
+		ret = tcpci_write8(dev, TCPC_COMMAND,
+				   TCPC_CMD_DISABLE_SRC_VBUS);
+		if (ret < 0)
+			return ret;
+	}
+
+	if (!charge) {
+		ret = tcpci_write8(dev, TCPC_COMMAND,
+				   TCPC_CMD_DISABLE_SINK_VBUS);
+		if (ret < 0)
+			return ret;
+	}
+
+	if (on) {
+		ret = tcpci_write8(dev, TCPC_COMMAND,
+				   TCPC_CMD_SRC_VBUS_DEFAULT);
+		if (ret < 0)
+			return ret;
+	}
+
+	if (charge) {
+		ret = tcpci_write8(dev, TCPC_COMMAND,
+				   TCPC_CMD_SINK_VBUS);
+		if (ret < 0)
+			return ret;
+	}
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_set_pd_rx(struct udevice *dev, bool enable)
+{
+	u8 reg = 0;
+	int ret;
+
+	dev_dbg(dev, "%s: entering enable=%d\n", __func__, enable);
+
+	if (enable)
+		reg = TCPC_RX_DETECT_SOP | TCPC_RX_DETECT_HARD_RESET;
+
+	ret = tcpci_write8(dev, TCPC_RX_DETECT, reg);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_set_roles(struct udevice *dev, bool attached,
+			   enum typec_role role, enum typec_data_role data)
+{
+	unsigned int reg;
+	int ret;
+
+	dev_dbg(dev, "%s: entering attached=%d role=%d data=%d\n",
+		__func__, attached, role, data);
+
+	reg = FIELD_PREP(TCPC_MSG_HDR_INFO_REV, PD_REV20);
+	if (role == TYPEC_SOURCE)
+		reg |= TCPC_MSG_HDR_INFO_PWR_ROLE;
+	if (data == TYPEC_HOST)
+		reg |= TCPC_MSG_HDR_INFO_DATA_ROLE;
+
+	ret = tcpci_write8(dev, TCPC_MSG_HDR_INFO, (u8)reg);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done\n", __func__);
+	return 0;
+}
+
+static int tcpci_get_vbus(struct udevice *dev)
+{
+	u8 reg;
+	int ret;
+
+	ret = tcpci_read8(dev, TCPC_POWER_STATUS, &reg);
+	if (ret < 0)
+		return 0;
+
+	dev_dbg(dev, "%s: vbus=%d\n", __func__,
+		!!(reg & TCPC_POWER_STATUS_VBUS_PRES));
+	return !!(reg & TCPC_POWER_STATUS_VBUS_PRES);
+}
+
+static int tcpci_pd_transmit(struct udevice *dev,
+			     enum tcpm_transmit_type type,
+			     const struct pd_message *msg,
+			     unsigned int negotiated_rev)
+{
+	u16 header = msg ? le16_to_cpu(msg->header) : 0;
+	unsigned int reg, cnt;
+	int ret;
+	u8 byte_cnt;
+	u8 buf[TCPC_TRANSMIT_BUFFER_MAX_LEN];
+	u8 pos = 0;
+
+	cnt = msg ? pd_header_cnt(header) * 4 : 0;
+
+	schedule();
+
+	byte_cnt = cnt + 2;
+
+	ret = dm_i2c_write(dev, TCPC_TX_BYTE_CNT, &byte_cnt, 1);
+	if (ret < 0)
+		return ret;
+
+	if (msg) {
+		memcpy(&buf[pos], &msg->header, sizeof(msg->header));
+		pos += sizeof(msg->header);
+
+		if (cnt > 0) {
+			memcpy(&buf[pos], msg->payload, cnt);
+			pos += cnt;
+		}
+
+		ret = dm_i2c_write(dev, TCPC_TX_HDR, buf, byte_cnt);
+		if (ret < 0)
+			return ret;
+	}
+
+	reg = FIELD_PREP(TCPC_TRANSMIT_RETRY,
+			 (negotiated_rev > PD_REV20
+			  ? PD_RETRY_COUNT_3_0_OR_HIGHER
+			  : PD_RETRY_COUNT_DEFAULT));
+	reg |= FIELD_PREP(TCPC_TRANSMIT_TYPE, type);
+	ret = tcpci_write8(dev, TCPC_TRANSMIT, (u8)reg);
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+static int tcpci_init(struct udevice *dev)
+{
+	struct tcpci_chip *chip = dev_get_priv(dev);
+	ulong timeout_ms = 1000;
+	u8 power_status;
+	u16 alert_mask;
+	ulong start;
+	int ret;
+	u8 cnt;
+	u8 rx_buf[64];
+
+	dev_dbg(dev, "%s: entering\n", __func__);
+	dev_dbg(dev, "TCPCI: init start\n");
+
+	start = get_timer(0);
+	do {
+		ret = tcpci_read8(dev, TCPC_POWER_STATUS, &power_status);
+		if (ret < 0)
+			return ret;
+
+		if (!(power_status & TCPC_POWER_STATUS_UNINIT))
+			break;
+
+	} while (get_timer(0) < (start + timeout_ms));
+
+	if (power_status & TCPC_POWER_STATUS_UNINIT) {
+		dev_err(dev, "TCPC initialization timed out\n");
+		return -ETIMEDOUT;
+	}
+
+	dev_dbg(dev, "%s: TCPC ready after wait\n", __func__);
+	dev_dbg(dev, "%s: POWER_STATUS=0x%02x\n", __func__, power_status);
+
+	ret = tcpci_write8(dev, TCPC_FAULT_STATUS, 0xff);
+	if (ret < 0)
+		return ret;
+
+	/* Mask all fault alerts (ALL_REG_RST_TO_DEFAULT, VCONN_OC) */
+	ret = tcpci_write8(dev, TCPC_FAULT_STATUS_MASK, 0xff);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_write16(dev, TCPC_ALERT, 0xffff);
+	if (ret < 0)
+		return ret;
+
+	/* Drain any stale RX buffer (e.g. from dead battery phase) */
+	ret = tcpci_read8(dev, TCPC_RX_BYTE_CNT, &cnt);
+	if (ret == 0 && cnt > 0 && cnt <= sizeof(rx_buf)) {
+		dm_i2c_read(dev, TCPC_RX_BUF_FRAME_TYPE, rx_buf, cnt);
+		dev_dbg(dev, "%s: drained %d bytes from stale RX buffer\n",
+			__func__, cnt);
+		tcpci_write16(dev, TCPC_ALERT, TCPC_ALERT_RX_STATUS);
+	}
+
+	power_status = chip->controls_vbus ? TCPC_POWER_STATUS_VBUS_PRES : 0;
+	ret = tcpci_write8(dev, TCPC_POWER_STATUS_MASK, power_status);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_write8(dev, TCPC_COMMAND, TCPC_CMD_ENABLE_VBUS_DETECT);
+	if (ret < 0)
+		return ret;
+
+	alert_mask = 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 (chip->controls_vbus)
+		alert_mask |= TCPC_ALERT_POWER_STATUS;
+
+	chip->alert_mask = alert_mask;
+
+	/* Program hardware ALERT_MASK so unwanted bits never fire */
+	ret = tcpci_write16(dev, TCPC_ALERT_MASK, chip->alert_mask);
+	if (ret < 0)
+		return ret;
+
+	dev_dbg(dev, "%s: done alert_mask=0x%04x controls_vbus=%d\n",
+		__func__, alert_mask, chip->controls_vbus);
+	return 0;
+}
+
+static void tcpci_poll_event(struct udevice *dev)
+{
+	struct tcpci_chip *chip = dev_get_priv(dev);
+	u16 status;
+	int max_iter = 5;
+	int ret;
+	bool was_in_poll = chip->in_poll;
+
+	schedule();
+	chip->in_poll = true;
+
+	do {
+		u16 raw;
+
+		ret = tcpci_read16(dev, TCPC_ALERT, &raw);
+		if (ret < 0)
+			break;
+
+		if (raw & ~TCPC_ALERT_RX_STATUS)
+			tcpci_write16(dev, TCPC_ALERT,
+				      raw & ~TCPC_ALERT_RX_STATUS);
+
+		if (raw & TCPC_ALERT_FAULT) {
+			u8 fault;
+			tcpci_read8(dev, TCPC_FAULT_STATUS, &fault);
+			tcpci_write8(dev, TCPC_FAULT_STATUS, fault);
+		}
+
+		status = raw & chip->alert_mask;
+		if (!status)
+			break;
+
+		if (status & TCPC_ALERT_CC_STATUS)
+			tcpm_cc_change(dev);
+
+		if (status & TCPC_ALERT_POWER_STATUS)
+			tcpm_vbus_change(dev);
+
+		if (status & TCPC_ALERT_RX_STATUS) {
+			struct pd_message msg;
+			u16 header = 0;
+			u8 cnt;
+			u8 rx_buf[64];
+
+			/*
+			 * On re-entrant calls (during TX poll helper), the
+			 * state machine is locked.  Drain and clear the
+			 * FIFO without processing, letting the NEXT poll
+			 * (after state machine unlocks) handle the message.
+			 */
+			if (was_in_poll) {
+				ret = tcpci_read8(dev, TCPC_RX_BYTE_CNT, &cnt);
+				if (ret == 0 && cnt > 0 && cnt <= sizeof(rx_buf))
+					dm_i2c_read(dev, TCPC_RX_BUF_FRAME_TYPE,
+						    rx_buf, cnt);
+				tcpci_write16(dev, TCPC_ALERT, TCPC_ALERT_RX_STATUS);
+				continue;
+			}
+
+			memset(&msg, 0, sizeof(msg));
+
+			ret = tcpci_read8(dev, TCPC_RX_BYTE_CNT, &cnt);
+			if (ret < 0)
+				continue;
+
+			if (cnt > 0 && cnt <= sizeof(rx_buf)) {
+				ret = dm_i2c_read(dev, TCPC_RX_BUF_FRAME_TYPE,
+						  rx_buf, cnt);
+				if (ret < 0)
+					continue;
+
+				if (cnt >= 3) {
+					header = rx_buf[1] | (rx_buf[2] << 8);
+					msg.header = cpu_to_le16(header);
+
+					if (cnt > 3) {
+						unsigned int payload_cnt = cnt - 3;
+						if (payload_cnt > sizeof(msg.payload))
+							payload_cnt = sizeof(msg.payload);
+						memcpy(&msg.payload, &rx_buf[3],
+						       payload_cnt);
+					}
+				}
+			}
+
+			tcpci_write16(dev, TCPC_ALERT, TCPC_ALERT_RX_STATUS);
+
+			if (header != 0)
+				tcpm_pd_receive(dev, &msg);
+			}
+
+		if (status & TCPC_ALERT_RX_HARD_RST) {
+			pd_trace("ALERT_RX_HARD_RST", raw);
+			dev_dbg(dev, "%s: RX_HARD_RST\n", __func__);
+			tcpm_pd_hard_reset(dev);
+		}
+
+		if (status & TCPC_ALERT_TX_SUCCESS) {
+			dev_dbg(dev, "%s: TX_SUCCESS\n", __func__);
+			tcpm_pd_transmit_complete(dev, TCPC_TX_SUCCESS);
+		} else if (status & TCPC_ALERT_TX_DISCARDED) {
+			dev_dbg(dev, "%s: TX_DISCARDED\n", __func__);
+			tcpm_pd_transmit_complete(dev, TCPC_TX_DISCARDED);
+		} else if (status & TCPC_ALERT_TX_FAILED) {
+			dev_dbg(dev, "%s: TX_FAILED\n", __func__);
+			tcpm_pd_transmit_complete(dev, TCPC_TX_FAILED);
+		}
+	} while (--max_iter > 0);
+
+	chip->in_poll = was_in_poll;
+}
+
+static int tcpci_enter_low_power_mode(struct udevice *dev, bool attached,
+				      bool pd_capable)
+{
+	int ret;
+
+	dev_dbg(dev, "%s: entering attached=%d pd_capable=%d\n",
+		__func__, attached, pd_capable);
+
+	/*
+	 * Stop SOP reception so TCPCI hardware won't auto-send GoodCRC
+	 * for incoming messages.  Without this, the source may send
+	 * Get_Sink_Cap or Get_Status after negotiation completes;
+	 * TCPCI auto-ACKs with GoodCRC, source starts SenderResponseTimer,
+	 * nobody sends a response, timer expires -> Hard Reset -> VBUS
+	 * drops -> board loses power.
+	 *
+	 * Per PD spec 6.5.2, failure to receive GoodCRC does NOT trigger
+	 * Hard Reset, so source stays in Ready state with VBUS up.
+	 */
+	u8 pwr_ctrl, pwr_status;
+
+	tcpci_read8(dev, TCPC_POWER_CTRL, &pwr_ctrl);
+	tcpci_read8(dev, TCPC_POWER_STATUS, &pwr_status);
+	pd_trace("LOW_PWR_PWR_CTRL", pwr_ctrl);
+	pd_trace("LOW_PWR_PWR_STAT", pwr_status);
+
+	pd_trace("LOW_PWR_ENTER", 0);
+	ret = tcpci_write8(dev, TCPC_RX_DETECT, 0);
+	if (ret < 0)
+		return ret;
+
+	pd_trace("LOW_PWR_RX_OFF", 0);
+	ret = tcpci_write16(dev, TCPC_ALERT_MASK, 0);
+	if (ret < 0)
+		return ret;
+
+	ret = tcpci_write16(dev, TCPC_ALERT, 0xffff);
+	if (ret < 0)
+		return ret;
+
+	pd_trace("LOW_PWR_DONE", 0);
+	return 0;
+}
+
+static struct dm_tcpm_ops tcpci_ops = {
+	.get_connector_node = tcpci_get_connector_node,
+	.init = tcpci_init,
+	.get_vbus = tcpci_get_vbus,
+	.set_cc = tcpci_set_cc,
+	.get_cc = tcpci_get_cc,
+	.set_polarity = tcpci_set_polarity,
+	.set_vconn = tcpci_set_vconn,
+	.set_vbus = tcpci_set_vbus,
+	.set_pd_rx = tcpci_set_pd_rx,
+	.set_roles = tcpci_set_roles,
+	.start_toggling = tcpci_start_toggling,
+	.pd_transmit = tcpci_pd_transmit,
+	.poll_event = tcpci_poll_event,
+	.enter_low_power_mode = tcpci_enter_low_power_mode,
+};
+
+static const struct udevice_id tcpci_ids[] = {
+	{ .compatible = "nxp,ptn5110" },
+	{ .compatible = "tcpci" },
+	{ }
+};
+
+U_BOOT_DRIVER(tcpci) = {
+	.name = "tcpci",
+	.id = UCLASS_TCPM,
+	.of_match = tcpci_ids,
+	.ops = &tcpci_ops,
+	.priv_auto = sizeof(struct tcpci_chip),
+};
diff --git a/include/usb/tcpci.h b/include/usb/tcpci.h
new file mode 100644
index 00000000000..f87d10c6c53
--- /dev/null
+++ b/include/usb/tcpci.h
@@ -0,0 +1,185 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright 2015-2017 Google, Inc
+ *
+ * USB Type-C Port Controller Interface.
+ */
+
+#ifndef __USB_TCPCI_H
+#define __USB_TCPCI_H
+
+#include <linux/bitops.h>
+#include <usb/pd.h>
+
+#define TCPC_VENDOR_ID			0x0
+#define TCPC_PRODUCT_ID			0x2
+#define TCPC_BCD_DEV			0x4
+#define TCPC_TC_REV			0x6
+#define TCPC_PD_REV			0x8
+#define TCPC_PD_INT_REV			0xa
+
+#define TCPC_ALERT			0x10
+#define TCPC_ALERT_EXTND		BIT(14)
+#define TCPC_ALERT_EXTENDED_STATUS	BIT(13)
+#define TCPC_ALERT_VBUS_DISCNCT		BIT(11)
+#define TCPC_ALERT_RX_BUF_OVF		BIT(10)
+#define TCPC_ALERT_FAULT		BIT(9)
+#define TCPC_ALERT_V_ALARM_LO		BIT(8)
+#define TCPC_ALERT_V_ALARM_HI		BIT(7)
+#define TCPC_ALERT_TX_SUCCESS		BIT(6)
+#define TCPC_ALERT_TX_DISCARDED		BIT(5)
+#define TCPC_ALERT_TX_FAILED		BIT(4)
+#define TCPC_ALERT_RX_HARD_RST		BIT(3)
+#define TCPC_ALERT_RX_STATUS		BIT(2)
+#define TCPC_ALERT_POWER_STATUS		BIT(1)
+#define TCPC_ALERT_CC_STATUS		BIT(0)
+
+#define TCPC_ALERT_MASK			0x12
+#define TCPC_POWER_STATUS_MASK		0x14
+
+#define TCPC_FAULT_STATUS_MASK			0x15
+#define TCPC_FAULT_STATUS_MASK_VCONN_OC		BIT(1)
+
+#define TCPC_EXTENDED_STATUS_MASK		0x16
+#define TCPC_EXTENDED_STATUS_MASK_VSAFE0V	BIT(0)
+
+#define TCPC_ALERT_EXTENDED_MASK	0x17
+#define TCPC_SINK_FAST_ROLE_SWAP	BIT(0)
+
+#define TCPC_CONFIG_STD_OUTPUT		0x18
+#define TCPC_CONFIG_STD_OUTPUT_ORIENTATION_MASK		BIT(0)
+#define TCPC_CONFIG_STD_OUTPUT_ORIENTATION_NORMAL	0
+#define TCPC_CONFIG_STD_OUTPUT_ORIENTATION_FLIPPED	1
+
+#define TCPC_TCPC_CTRL			0x19
+#define TCPC_TCPC_CTRL_ORIENTATION	BIT(0)
+#define PLUG_ORNT_CC1			0
+#define PLUG_ORNT_CC2			1
+#define TCPC_TCPC_CTRL_BIST_TM		BIT(1)
+#define TCPC_TCPC_CTRL_EN_LK4CONN_ALRT	BIT(6)
+
+#define TCPC_EXTENDED_STATUS		0x20
+#define TCPC_EXTENDED_STATUS_VSAFE0V	BIT(0)
+
+#define TCPC_ROLE_CTRL			0x1a
+#define TCPC_ROLE_CTRL_DRP		BIT(6)
+#define TCPC_ROLE_CTRL_RP_VAL		GENMASK(5, 4)
+#define TCPC_ROLE_CTRL_RP_VAL_DEF	0x0
+#define TCPC_ROLE_CTRL_RP_VAL_1_5	0x1
+#define TCPC_ROLE_CTRL_RP_VAL_3_0	0x2
+#define TCPC_ROLE_CTRL_CC2		GENMASK(3, 2)
+#define TCPC_ROLE_CTRL_CC1		GENMASK(1, 0)
+#define TCPC_ROLE_CTRL_CC_RA		0x0
+#define TCPC_ROLE_CTRL_CC_RP		0x1
+#define TCPC_ROLE_CTRL_CC_RD		0x2
+#define TCPC_ROLE_CTRL_CC_OPEN		0x3
+
+#define TCPC_FAULT_CTRL			0x1b
+
+#define TCPC_POWER_CTRL			0x1c
+#define TCPC_POWER_CTRL_VCONN_ENABLE	BIT(0)
+#define TCPC_POWER_CTRL_BLEED_DISCHARGE	BIT(3)
+#define TCPC_POWER_CTRL_AUTO_DISCHARGE	BIT(4)
+#define TCPC_DIS_VOLT_ALRM		BIT(5)
+#define TCPC_POWER_CTRL_VBUS_VOLT_MON	BIT(6)
+#define TCPC_FAST_ROLE_SWAP_EN		BIT(7)
+
+#define TCPC_CC_STATUS			0x1d
+#define TCPC_CC_STATUS_TOGGLING		BIT(5)
+#define TCPC_CC_STATUS_TERM		BIT(4)
+#define TCPC_CC_STATUS_CC2		GENMASK(3, 2)
+#define TCPC_CC_STATUS_CC1		GENMASK(1, 0)
+#define TCPC_CC_STATE_SRC_OPEN		0
+
+#define TCPC_POWER_STATUS		0x1e
+#define TCPC_POWER_STATUS_DBG_ACC_CON	BIT(7)
+#define TCPC_POWER_STATUS_UNINIT	BIT(6)
+#define TCPC_POWER_STATUS_SOURCING_VBUS	BIT(4)
+#define TCPC_POWER_STATUS_VBUS_DET	BIT(3)
+#define TCPC_POWER_STATUS_VBUS_PRES	BIT(2)
+#define TCPC_POWER_STATUS_VCONN_PRES	BIT(1)
+#define TCPC_POWER_STATUS_SINKING_VBUS	BIT(0)
+
+#define TCPC_FAULT_STATUS		0x1f
+#define TCPC_FAULT_STATUS_ALL_REG_RST_TO_DEFAULT BIT(7)
+#define TCPC_FAULT_STATUS_VCONN_OC	BIT(1)
+
+#define TCPC_ALERT_EXTENDED		0x21
+
+#define TCPC_COMMAND			0x23
+#define TCPC_CMD_WAKE_I2C		0x11
+#define TCPC_CMD_DISABLE_VBUS_DETECT	0x22
+#define TCPC_CMD_ENABLE_VBUS_DETECT	0x33
+#define TCPC_CMD_DISABLE_SINK_VBUS	0x44
+#define TCPC_CMD_SINK_VBUS		0x55
+#define TCPC_CMD_DISABLE_SRC_VBUS	0x66
+#define TCPC_CMD_SRC_VBUS_DEFAULT	0x77
+#define TCPC_CMD_SRC_VBUS_HIGH		0x88
+#define TCPC_CMD_LOOK4CONNECTION	0x99
+#define TCPC_CMD_RXONEMORE		0xAA
+#define TCPC_CMD_I2C_IDLE		0xFF
+
+#define TCPC_DEV_CAP_1			0x24
+#define TCPC_DEV_CAP_2			0x26
+#define TCPC_STD_INPUT_CAP		0x28
+#define TCPC_STD_OUTPUT_CAP		0x29
+#define TCPC_STD_OUTPUT_CAP_ORIENTATION	BIT(0)
+
+#define TCPC_MSG_HDR_INFO		0x2e
+#define TCPC_MSG_HDR_INFO_DATA_ROLE	BIT(3)
+#define TCPC_MSG_HDR_INFO_REV		GENMASK(2, 1)
+#define TCPC_MSG_HDR_INFO_PWR_ROLE	BIT(0)
+
+#define TCPC_RX_DETECT			0x2f
+#define TCPC_RX_DETECT_HARD_RESET	BIT(5)
+#define TCPC_RX_DETECT_SOP		BIT(0)
+#define TCPC_RX_DETECT_SOP1		BIT(1)
+#define TCPC_RX_DETECT_SOP2		BIT(2)
+#define TCPC_RX_DETECT_DBG1		BIT(3)
+#define TCPC_RX_DETECT_DBG2		BIT(4)
+
+#define TCPC_RX_BYTE_CNT		0x30
+#define TCPC_RX_BUF_FRAME_TYPE		0x31
+#define TCPC_RX_BUF_FRAME_TYPE_SOP	0
+#define TCPC_RX_BUF_FRAME_TYPE_SOP1	1
+#define TCPC_RX_HDR			0x32
+#define TCPC_RX_DATA			0x34
+
+#define TCPC_TRANSMIT			0x50
+#define TCPC_TRANSMIT_RETRY		GENMASK(5, 4)
+#define TCPC_TRANSMIT_TYPE		GENMASK(2, 0)
+
+#define TCPC_TX_BYTE_CNT		0x51
+#define TCPC_TX_HDR			0x52
+#define TCPC_TX_DATA			0x54
+
+#define TCPC_VBUS_VOLTAGE			0x70
+#define TCPC_VBUS_SINK_DISCONNECT_THRESH	0x72
+#define TCPC_VBUS_STOP_DISCHARGE_THRESH		0x74
+#define TCPC_VBUS_VOLTAGE_ALARM_HI_CFG		0x76
+#define TCPC_VBUS_VOLTAGE_ALARM_LO_CFG		0x78
+
+#define TCPC_TRANSMIT_BUFFER_MAX_LEN		31
+
+#define tcpc_presenting_rd(reg, cc) \
+	(!(TCPC_ROLE_CTRL_DRP & (reg)) && \
+	 FIELD_GET(TCPC_ROLE_CTRL_## cc, reg) == TCPC_ROLE_CTRL_CC_RD)
+
+static inline enum typec_cc_status tcpci_to_typec_cc(unsigned int cc, bool sink)
+{
+	switch (cc) {
+	case 0x1:
+		return sink ? TYPEC_CC_RP_DEF : TYPEC_CC_RA;
+	case 0x2:
+		return sink ? TYPEC_CC_RP_1_5 : TYPEC_CC_RD;
+	case 0x3:
+		if (sink)
+			return TYPEC_CC_RP_3_0;
+		fallthrough;
+	case TCPC_CC_STATE_SRC_OPEN:
+	default:
+		return TYPEC_CC_OPEN;
+	}
+}
+
+#endif /* __USB_TCPCI_H */

-- 
2.51.0



More information about the U-Boot mailing list