[PATCH v2 04/10] usb: tcpm: add TCPCI (Type-C Port Controller Interface) driver
Peng Fan (OSS)
peng.fan at oss.nxp.com
Sun Jun 21 04:06:40 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 *)®);
+ 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 *)®);
+ 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, ®);
+ 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, ®);
+ 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