/* $OpenBSD: tcpci.c,v 1.3 2022/04/06 18:59:28 naddy Exp $ */ /* * Copyright (c) 2018 Patrick Wildt * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* #define TCPCI_DEBUG */ #ifdef TCPCI_DEBUG #define DPRINTF(x) printf x #else #define DPRINTF(x) #endif #define TCPC_VENDOR_ID 0x00 #define TCPC_PRODUCT_ID 0x02 #define TCPC_BCD_DEV 0x04 #define TCPC_TC_REV 0x06 #define TCPC_PD_REV 0x08 #define TCPC_PD_INT_REV 0x0a #define TCPC_ALERT 0x10 #define TCPC_ALERT_CC_STATUS (1 << 0) #define TCPC_ALERT_POWER_STATUS (1 << 1) #define TCPC_ALERT_RX_STATUS (1 << 2) #define TCPC_ALERT_RX_HARD_RST (1 << 3) #define TCPC_ALERT_TX_FAILED (1 << 4) #define TCPC_ALERT_TX_DISCARDED (1 << 5) #define TCPC_ALERT_TX_SUCCESS (1 << 6) #define TCPC_ALERT_V_ALARM_HI (1 << 7) #define TCPC_ALERT_V_ALARM_LO (1 << 8) #define TCPC_ALERT_FAULT (1 << 9) #define TCPC_ALERT_RX_BUF_OVF (1 << 10) #define TCPC_ALERT_VBUS_DISCNCT (1 << 11) #define TCPC_ALERT_MASK 0x12 #define TCPC_POWER_STATUS_MASK 0x14 #define TCPC_POWER_STATUS_VBUS_PRES (1 << 2) #define TCPC_FAULT_STATUS_MASK 0x15 #define TCPC_CONFIG_STD_OUTPUT 0x18 #define TCPC_TCPC_CTRL 0x19 #define TCPC_TCPC_CTRL_ORIENTATION (1 << 0) #define TCPC_TCPC_CTRL_BIST_MODE (1 << 1) #define TCPC_ROLE_CTRL 0x1a #define TCPC_ROLE_CTRL_CC1_SHIFT 0 #define TCPC_ROLE_CTRL_CC2_SHIFT 2 #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_ROLE_CTRL_CC_MASK 0x3 #define TCPC_ROLE_CTRL_RP_VAL_MASK (0x3 << 4) #define TCPC_ROLE_CTRL_RP_VAL_DEF (0x0 << 4) #define TCPC_ROLE_CTRL_RP_VAL_1_5 (0x1 << 4) #define TCPC_ROLE_CTRL_RP_VAL_3_0 (0x2 << 4) #define TCPC_ROLE_CTRL_DRP (1 << 6) #define TCPC_FAULT_CTRL 0x1b #define TCPC_POWER_CTRL 0x1c #define TCPC_POWER_CTRL_VCONN_ENABLE (1 << 0) #define TCPC_POWER_CTRL_FORCEDISCH (1 << 2) #define TCPC_POWER_CTRL_DIS_VOL_ALARM (1 << 5) #define TCPC_CC_STATUS 0x1d #define TCPC_CC_STATUS_CC1_SHIFT 0 #define TCPC_CC_STATUS_CC2_SHIFT 2 #define TCPC_CC_STATUS_CC_MASK 0x3 #define TCPC_CC_STATUS_TERM (1 << 4) #define TCPC_CC_STATUS_TOGGLING (1 << 5) #define TCPC_POWER_STATUS 0x1e #define TCPC_FAULT_STATUS 0x1f #define TCPC_FAULT_STATUS_CLEAR (1 << 7) #define TCPC_COMMAND 0x23 #define TCPC_COMMAND_WAKE_I2C 0x11 #define TCPC_COMMAND_DISABLE_VBUS_DETECT 0x22 #define TCPC_COMMAND_ENABLE_VBUS_DETECT 0x33 #define TCPC_COMMAND_DISABLE_SINK_VBUS 0x44 #define TCPC_COMMAND_SINK_VBUS 0x55 #define TCPC_COMMAND_DISABLE_SRC_VBUS 0x66 #define TCPC_COMMAND_SRC_VBUS_DEFAULT 0x77 #define TCPC_COMMAND_SRC_VBUS_HIGH 0x88 #define TCPC_COMMAND_LOOK4CONNECTION 0x99 #define TCPC_COMMAND_RXONEMORE 0xAA #define TCPC_COMMAND_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_MSG_HDR_INFO 0x2e #define TCPC_MSG_HDR_INFO_PWR_ROLE (1 << 0) #define TCPC_MSG_HDR_INFO_PD_REV10 (0 << 1) #define TCPC_MSG_HDR_INFO_PD_REV20 (1 << 1) #define TCPC_MSG_HDR_INFO_DATA_ROLE (1 << 3) #define TCPC_RX_DETECT 0x2f #define TCPC_RX_DETECT_SOP (1 << 0) #define TCPC_RX_DETECT_HARD_RESET (1 << 5) #define TCPC_RX_BYTE_CNT 0x30 #define TCPC_RX_BUF_FRAME_TYPE 0x31 #define TCPC_RX_HDR 0x32 #define TCPC_RX_DATA 0x34 /* through 0x4f */ #define TCPC_TRANSMIT 0x50 #define TCPC_TX_BYTE_CNT 0x51 #define TCPC_TX_HDR 0x52 #define TCPC_TX_DATA 0x54 /* through 0x6f */ #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 enum typec_cc_status { TYPEC_CC_OPEN, TYPEC_CC_RA, TYPEC_CC_RD, TYPEC_CC_RP_DEF, TYPEC_CC_RP_1_5, TYPEC_CC_RP_3_0, }; enum typec_data_role { TYPEC_DEVICE, TYPEC_HOST, }; enum typec_power_role { TYPEC_SINK, TYPEC_SOURCE, }; enum typec_polarity { TYPEC_POLARITY_CC1, TYPEC_POLARITY_CC2, }; struct tcpci_softc { struct device sc_dev; i2c_tag_t sc_tag; i2c_addr_t sc_addr; int sc_node; void *sc_ih; struct task sc_task; int sc_attached; enum typec_data_role sc_try_data; enum typec_power_role sc_try_power; uint32_t *sc_ss_sel; uint8_t sc_cc; uint8_t sc_vbus_det; }; int tcpci_match(struct device *, void *, void *); void tcpci_attach(struct device *, struct device *, void *); int tcpci_detach(struct device *, int); int tcpci_intr(void *); void tcpci_task(void *); void tcpci_cc_change(struct tcpci_softc *); void tcpci_power_change(struct tcpci_softc *); void tcpci_set_polarity(struct tcpci_softc *, int); void tcpci_set_vbus(struct tcpci_softc *, int, int); void tcpci_set_roles(struct tcpci_softc *, enum typec_data_role, enum typec_power_role); void tcpci_write_reg16(struct tcpci_softc *, uint8_t, uint16_t); uint16_t tcpci_read_reg16(struct tcpci_softc *, uint8_t); void tcpci_write_reg8(struct tcpci_softc *, uint8_t, uint8_t); uint8_t tcpci_read_reg8(struct tcpci_softc *, uint8_t); const struct cfattach tcpci_ca = { sizeof(struct tcpci_softc), tcpci_match, tcpci_attach, tcpci_detach, }; struct cfdriver tcpci_cd = { NULL, "tcpci", DV_DULL }; int tcpci_match(struct device *parent, void *match, void *aux) { struct i2c_attach_args *ia = aux; if (strcmp(ia->ia_name, "nxp,ptn5110") == 0) return 1; return 0; } void tcpci_attach(struct device *parent, struct device *self, void *aux) { struct tcpci_softc *sc = (struct tcpci_softc *)self; struct i2c_attach_args *ia = aux; int len; sc->sc_tag = ia->ia_tag; sc->sc_addr = ia->ia_addr; sc->sc_node = *(int *)ia->ia_cookie; /* Automatic DRP toggling should try first as ... */ sc->sc_try_data = TYPEC_HOST; sc->sc_try_power = TYPEC_SOURCE; pinctrl_byname(sc->sc_node, "default"); task_set(&sc->sc_task, tcpci_task, sc); sc->sc_ih = fdt_intr_establish(sc->sc_node, IPL_BIO, tcpci_intr, sc, sc->sc_dev.dv_xname); if (sc->sc_ih == NULL) { printf(": unable to establish interrupt\n"); return; } len = OF_getproplen(sc->sc_node, "ss-sel-gpios"); if (len > 0) { sc->sc_ss_sel = malloc(len, M_TEMP, M_WAITOK); OF_getpropintarray(sc->sc_node, "ss-sel-gpios", sc->sc_ss_sel, len); gpio_controller_config_pin(sc->sc_ss_sel, GPIO_CONFIG_OUTPUT); gpio_controller_set_pin(sc->sc_ss_sel, 1); } tcpci_write_reg16(sc, TCPC_ALERT, 0xffff); tcpci_write_reg8(sc, TCPC_FAULT_STATUS, 0x80); tcpci_write_reg8(sc, TCPC_POWER_STATUS_MASK, TCPC_POWER_STATUS_VBUS_PRES); tcpci_write_reg8(sc, TCPC_POWER_CTRL, tcpci_read_reg8(sc, TCPC_POWER_CTRL) & ~TCPC_POWER_CTRL_DIS_VOL_ALARM); tcpci_write_reg16(sc, TCPC_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 | TCPC_ALERT_RX_BUF_OVF | TCPC_ALERT_FAULT | TCPC_ALERT_V_ALARM_LO | TCPC_ALERT_POWER_STATUS); if (sc->sc_try_data == TYPEC_HOST) tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP | 0xa); else tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP | 0x5); tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_LOOK4CONNECTION); printf("\n"); } int tcpci_detach(struct device *self, int flags) { return 0; } int tcpci_intr(void *args) { struct tcpci_softc *sc = args; fdt_intr_disable(sc->sc_ih); task_add(systq, &sc->sc_task); return 1; } void tcpci_task(void *args) { struct tcpci_softc *sc = args; uint16_t status; status = tcpci_read_reg16(sc, TCPC_ALERT); tcpci_write_reg16(sc, TCPC_ALERT, status); DPRINTF(("%s: alert %x\n", __func__, status)); if (status & TCPC_ALERT_CC_STATUS) tcpci_cc_change(sc); if (status & TCPC_ALERT_POWER_STATUS) tcpci_power_change(sc); if (status & TCPC_ALERT_V_ALARM_LO) { tcpci_write_reg8(sc, TCPC_VBUS_VOLTAGE_ALARM_LO_CFG, 0); tcpci_write_reg8(sc, TCPC_POWER_CTRL, tcpci_read_reg8(sc, TCPC_POWER_CTRL) & ~TCPC_POWER_CTRL_FORCEDISCH); } if (status & TCPC_ALERT_FAULT) tcpci_write_reg8(sc, TCPC_FAULT_STATUS, tcpci_read_reg8(sc, TCPC_FAULT_STATUS) | TCPC_FAULT_STATUS_CLEAR); fdt_intr_enable(sc->sc_ih); } uint8_t tcpci_typec_to_rp(int typec) { switch (typec) { case TYPEC_CC_RP_DEF: return TCPC_ROLE_CTRL_RP_VAL_DEF; case TYPEC_CC_RP_1_5: return TCPC_ROLE_CTRL_RP_VAL_1_5; case TYPEC_CC_RP_3_0: return TCPC_ROLE_CTRL_RP_VAL_3_0; default: panic("%s:%d", __func__, __LINE__); } } int tcpci_cc_to_typec(int cc, int sink) { if (sink) { if (cc == 0x1) return TYPEC_CC_RP_DEF; if (cc == 0x2) return TYPEC_CC_RP_1_5; if (cc == 0x3) return TYPEC_CC_RP_3_0; } else { if (cc == 0x1) return TYPEC_CC_RA; if (cc == 0x2) return TYPEC_CC_RD; } return TYPEC_CC_OPEN; } int tcpci_cc_is_sink(int cc1, int cc2) { if ((cc1 == TYPEC_CC_RP_DEF || cc1 == TYPEC_CC_RP_1_5 || cc1 == TYPEC_CC_RP_3_0) && cc2 == TYPEC_CC_OPEN) return 1; if ((cc2 == TYPEC_CC_RP_DEF || cc2 == TYPEC_CC_RP_1_5 || cc2 == TYPEC_CC_RP_3_0) && cc1 == TYPEC_CC_OPEN) return 1; return 0; } int tcpci_cc_is_source(int cc1, int cc2) { if (cc1 == TYPEC_CC_RD && cc2 != TYPEC_CC_RD) return 1; if (cc2 == TYPEC_CC_RD && cc1 != TYPEC_CC_RD) return 1; return 0; } int tcpci_cc_is_audio(int cc1, int cc2) { if (cc1 == TYPEC_CC_RA && cc2 == TYPEC_CC_RA) return 1; return 0; } int tcpci_cc_is_audio_detached(int cc1, int cc2) { if (cc1 == TYPEC_CC_RA && cc2 == TYPEC_CC_OPEN) return 1; if (cc2 == TYPEC_CC_RA && cc1 == TYPEC_CC_OPEN) return 1; return 0; } void tcpci_cc_change(struct tcpci_softc *sc) { uint8_t cc, cc1, cc2; cc = tcpci_read_reg8(sc, TCPC_CC_STATUS); if (sc->sc_cc == cc) return; cc1 = (cc >> TCPC_ROLE_CTRL_CC1_SHIFT) & TCPC_ROLE_CTRL_CC_MASK; cc1 = tcpci_cc_to_typec(cc1, cc & TCPC_CC_STATUS_TERM); cc2 = (cc >> TCPC_ROLE_CTRL_CC2_SHIFT) & TCPC_ROLE_CTRL_CC_MASK; cc2 = tcpci_cc_to_typec(cc2, cc & TCPC_CC_STATUS_TERM); if (cc1 == TYPEC_CC_OPEN && cc2 == TYPEC_CC_OPEN) { /* No CC, wait for new connection. */ DPRINTF(("%s: disconnected\n", __func__)); tcpci_write_reg8(sc, TCPC_RX_DETECT, 0); tcpci_set_vbus(sc, 0, 0); tcpci_write_reg8(sc, TCPC_POWER_CTRL, tcpci_read_reg8(sc, TCPC_POWER_CTRL) & ~TCPC_POWER_CTRL_VCONN_ENABLE); tcpci_set_polarity(sc, TYPEC_POLARITY_CC1); if (sc->sc_try_data == TYPEC_HOST) { tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP | 0xa); } else { tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP | 0x5); } tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_LOOK4CONNECTION); sc->sc_attached = 0; } else if (tcpci_cc_is_source(cc1, cc2)) { /* Host */ DPRINTF(("%s: attached as source\n", __func__)); if (cc1 == TYPEC_CC_RD) tcpci_set_polarity(sc, TYPEC_POLARITY_CC1); else tcpci_set_polarity(sc, TYPEC_POLARITY_CC2); tcpci_set_roles(sc, TYPEC_HOST, TYPEC_SOURCE); tcpci_write_reg8(sc, TCPC_RX_DETECT, TCPC_RX_DETECT_SOP | TCPC_RX_DETECT_HARD_RESET); if ((cc1 == TYPEC_CC_RD && cc2 == TYPEC_CC_RA) || (cc2 == TYPEC_CC_RD && cc1 == TYPEC_CC_RA)) tcpci_write_reg8(sc, TCPC_POWER_CTRL, tcpci_read_reg8(sc, TCPC_POWER_CTRL) | TCPC_POWER_CTRL_VCONN_ENABLE); tcpci_set_vbus(sc, 1, 0); sc->sc_attached = 1; } else if (tcpci_cc_is_sink(cc1, cc2)) { /* Device */ DPRINTF(("%s: attached as sink\n", __func__)); if (cc1 != TYPEC_CC_OPEN) { tcpci_set_polarity(sc, TYPEC_POLARITY_CC1); tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC1_SHIFT | TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC2_SHIFT); } else { tcpci_set_polarity(sc, TYPEC_POLARITY_CC2); tcpci_write_reg8(sc, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_CC_OPEN << TCPC_ROLE_CTRL_CC1_SHIFT | TCPC_ROLE_CTRL_CC_RD << TCPC_ROLE_CTRL_CC2_SHIFT); } tcpci_set_roles(sc, TYPEC_DEVICE, TYPEC_SINK); tcpci_set_vbus(sc, 0, 0); sc->sc_attached = 1; } else if (tcpci_cc_is_audio_detached(cc1, cc2)) { /* Audio Detached */ DPRINTF(("%s: audio detached\n", __func__)); } else { panic("%s: unknown combination cc %x", __func__, cc); } sc->sc_cc = cc; } void tcpci_power_change(struct tcpci_softc *sc) { uint8_t power; if (tcpci_read_reg8(sc, TCPC_POWER_STATUS_MASK) == 0xff) DPRINTF(("%s: power reset\n", __func__)); power = tcpci_read_reg8(sc, TCPC_POWER_STATUS); power &= TCPC_POWER_STATUS_VBUS_PRES; if (sc->sc_vbus_det == power) return; DPRINTF(("%s: power %d\n", __func__, power)); sc->sc_vbus_det = power; } void tcpci_set_roles(struct tcpci_softc *sc, enum typec_data_role data, enum typec_power_role power) { uint8_t reg; reg = TCPC_MSG_HDR_INFO_PD_REV20; if (power == TYPEC_SOURCE) reg |= TCPC_MSG_HDR_INFO_PWR_ROLE; if (data == TYPEC_HOST) reg |= TCPC_MSG_HDR_INFO_DATA_ROLE; tcpci_write_reg8(sc, TCPC_MSG_HDR_INFO, reg); if (data == TYPEC_HOST) printf("%s: connected in host mode\n", sc->sc_dev.dv_xname); else printf("%s: connected in device mode\n", sc->sc_dev.dv_xname); } void tcpci_set_polarity(struct tcpci_softc *sc, int cc) { if (cc == TYPEC_POLARITY_CC1) { tcpci_write_reg8(sc, TCPC_TCPC_CTRL, 0); if (sc->sc_ss_sel) gpio_controller_set_pin(sc->sc_ss_sel, 1); } if (cc == TYPEC_POLARITY_CC2) { tcpci_write_reg8(sc, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_ORIENTATION); if (sc->sc_ss_sel) gpio_controller_set_pin(sc->sc_ss_sel, 0); } } void tcpci_set_vbus(struct tcpci_softc *sc, int source, int sink) { if (!source) tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_DISABLE_SRC_VBUS); if (!sink) tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_DISABLE_SINK_VBUS); if (!source && !sink) { tcpci_write_reg8(sc, TCPC_VBUS_VOLTAGE_ALARM_LO_CFG, 0x1c); tcpci_write_reg8(sc, TCPC_POWER_CTRL, tcpci_read_reg8(sc, TCPC_POWER_CTRL) | TCPC_POWER_CTRL_FORCEDISCH); } if (source) tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_SRC_VBUS_DEFAULT); if (sink) tcpci_write_reg8(sc, TCPC_COMMAND, TCPC_COMMAND_SINK_VBUS); } uint8_t tcpci_read_reg8(struct tcpci_softc *sc, uint8_t reg) { uint8_t val = 0; iic_acquire_bus(sc->sc_tag, 0); if (iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr, ®, sizeof(reg), &val, sizeof(val), 0)) { printf("%s: cannot read register 0x%x\n", sc->sc_dev.dv_xname, reg); } iic_release_bus(sc->sc_tag, 0); return val; } void tcpci_write_reg8(struct tcpci_softc *sc, uint8_t reg, uint8_t val) { iic_acquire_bus(sc->sc_tag, 0); if (iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr, ®, sizeof(reg), &val, sizeof(val), 0)) { printf("%s: cannot write register 0x%x\n", sc->sc_dev.dv_xname, reg); } iic_release_bus(sc->sc_tag, 0); } uint16_t tcpci_read_reg16(struct tcpci_softc *sc, uint8_t reg) { uint16_t val = 0; iic_acquire_bus(sc->sc_tag, 0); if (iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr, ®, sizeof(reg), &val, sizeof(val), 0)) { printf("%s: cannot read register 0x%x\n", sc->sc_dev.dv_xname, reg); } iic_release_bus(sc->sc_tag, 0); return val; } void tcpci_write_reg16(struct tcpci_softc *sc, uint8_t reg, uint16_t val) { iic_acquire_bus(sc->sc_tag, 0); if (iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr, ®, sizeof(reg), &val, sizeof(val), 0)) { printf("%s: cannot write register 0x%x\n", sc->sc_dev.dv_xname, reg); } iic_release_bus(sc->sc_tag, 0); }