Hi, On Sun, Jun 21, 2026 at 10:06:40AM +0800, Peng Fan (OSS) wrote: > From: Peng Fan <[email protected]> > > 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 <[email protected]> > --- > 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
I think it makes sense to move TYPEC_TCPM dependency as follows to
avoid having it in all drivers:
if TYPEC_TCPM
menu "TCPM drivers"
config TYPEC_FUSB302
...
config TYPEC_TCPCI
...
endif
> + 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>
why?
> +#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"
why? This should not be used outside of the tcpm core itself, hence
the internal suffix.
> +#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;
> +}
I suppose each of the dev_dbg should be
if (ret)
dev_err(...)
else
dev_dbg(...)
> +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;
> +}
I think it would be sensible to have a get_connector_node helper
provided in tcpm.c considering it is exactly the same for fusb302
and expected to be the same for other chips.
> +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.
> + */
This depends on the board, right? If you have a system with a
battery, turning VBUS sourcing off should be safe.
> + 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();
This should be moved up to generic TCPM code.
> + 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();
same - this belongs into TCPM.
> + 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;
> + }
> +}
I would add these two helpers in tcpci.c.
> +
> +#endif /* __USB_TCPCI_H */
Greetings,
-- Sebastian
signature.asc
Description: PGP signature
