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 *)&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.
> +      */

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, &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();

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

Attachment: signature.asc
Description: PGP signature

Reply via email to