| /* Copyright 2021 The ChromiumOS Authors |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| /* Kinetic KTU1125 USB-C Power Path Controller */ |
| |
| #include "common.h" |
| #include "console.h" |
| #include "hooks.h" |
| #include "i2c.h" |
| #include "ktu1125.h" |
| #include "system.h" |
| #include "timer.h" |
| #include "usb_charge.h" |
| #include "usb_pd.h" |
| #include "usb_pd_tcpm.h" |
| #include "usbc_ppc.h" |
| #include "util.h" |
| |
| #define CPRINTF(format, args...) cprintf(CC_USBPD, format, ##args) |
| #define CPRINTS(format, args...) cprints(CC_USBPD, format, ##args) |
| |
| static atomic_t irq_pending; /* Bitmask of ports signaling an interrupt. */ |
| |
| static void ktu1125_handle_interrupt(int port); |
| |
| static int read_reg(uint8_t port, int reg, int *regval) |
| { |
| return i2c_read8(ppc_chips[port].i2c_port, |
| ppc_chips[port].i2c_addr_flags, reg, regval); |
| } |
| |
| static int write_reg(uint8_t port, int reg, int regval) |
| { |
| return i2c_write8(ppc_chips[port].i2c_port, |
| ppc_chips[port].i2c_addr_flags, reg, regval); |
| } |
| |
| static int set_flags(const int port, const int addr, const int flags_to_set) |
| { |
| int val, rv; |
| |
| rv = read_reg(port, addr, &val); |
| if (rv) |
| return rv; |
| |
| val |= flags_to_set; |
| |
| return write_reg(port, addr, val); |
| } |
| |
| static int clr_flags(const int port, const int addr, const int flags_to_clear) |
| { |
| int val, rv; |
| |
| rv = read_reg(port, addr, &val); |
| if (rv) |
| return rv; |
| |
| val &= ~flags_to_clear; |
| |
| return write_reg(port, addr, val); |
| } |
| |
| static int set_field(const int port, const int addr, const int shift, |
| const int field_length, const int field_to_set) |
| { |
| int reg_val, new_reg_val; |
| int field_val, field_mask; |
| int rv; |
| |
| rv = read_reg(port, addr, ®_val); |
| if (rv != EC_SUCCESS) |
| return rv; |
| |
| field_mask = (BIT(field_length) - 1) << shift; |
| field_val = (field_to_set << shift) & field_mask; |
| |
| new_reg_val = reg_val; |
| new_reg_val &= ~field_mask; |
| new_reg_val |= field_val; |
| |
| if (new_reg_val == reg_val) |
| return EC_SUCCESS; |
| |
| return write_reg(port, addr, new_reg_val); |
| } |
| |
| #ifdef CONFIG_CMD_PPC_DUMP |
| static int ktu1125_dump(int port) |
| { |
| int i; |
| int data; |
| CPRINTF("PPC%d: KTU1125. Registers:\n", port); |
| |
| for (i = KTU1125_ID; i <= KTU1125_INT_DATA; i++) { |
| read_reg(port, i, &data); |
| CPRINTF("REG %02Xh = 0x%02x\n", i, data); |
| } |
| |
| cflush(); |
| return EC_SUCCESS; |
| } |
| #endif /* defined(CONFIG_CMD_PPC_DUMP) */ |
| |
| /* helper */ |
| static int ktu1125_power_path_control(int port, int enable) |
| { |
| int status = |
| enable ? |
| set_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_SW_AB_EN) : |
| clr_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_SW_AB_EN); |
| |
| if (status) { |
| CPRINTS("ppc p%d: Failed to %s power path", port, |
| enable ? "enable" : "disable"); |
| } |
| |
| return status; |
| } |
| |
| static int ktu1125_init(int port) |
| { |
| int regval; |
| int ctrl_sw_cfg = 0; |
| int set_sw_cfg = 0; |
| int set_sw2_cfg = 0; |
| int sysb_clp; |
| int status; |
| |
| /* Read and verify KTU1125 Vendor and Chip ID */ |
| status = read_reg(port, KTU1125_ID, ®val); |
| |
| if (status) { |
| ppc_prints("Failed to read device ID!", port); |
| return status; |
| } |
| |
| if (regval != KTU1125_VENDOR_DIE_IDS) { |
| ppc_err_prints("KTU1125 ID mismatch!", port, regval); |
| return regval; |
| } |
| |
| /* |
| * Setting control register CTRL_SW_CFG |
| */ |
| |
| /* Check if VBUS is present and set SW_AB_EN accordingly */ |
| status = read_reg(port, KTU1125_MONITOR_SNK, ®val); |
| if (status) { |
| ppc_err_prints("VBUS present error", port, status); |
| return 0; |
| } |
| |
| if (regval & KTU1125_SYSA_OK) |
| ctrl_sw_cfg = KTU1125_SW_AB_EN; |
| |
| status = write_reg(port, KTU1125_CTRL_SW_CFG, ctrl_sw_cfg); |
| if (status) { |
| ppc_err_prints("Failed to write CTRL_SW_CFG!", port, status); |
| return status; |
| } |
| |
| /* |
| * Setting control register SET_SW_CFG |
| */ |
| |
| #ifdef CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT |
| /* Set the sourcing current limit value */ |
| switch (CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT) { |
| case TYPEC_RP_3A0: |
| /* Set current limit to ~3A */ |
| sysb_clp = KTU1125_SYSB_ILIM_3_30; |
| break; |
| |
| case TYPEC_RP_1A5: |
| default: |
| /* Set current limit to ~1.5A */ |
| sysb_clp = KTU1125_SYSB_ILIM_1_70; |
| break; |
| } |
| #else /* !defined(CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT) */ |
| /* Default SRC current limit to ~1.5A */ |
| sysb_clp = KTU1125_SYSB_ILIM_1_70; |
| #endif /* defined(CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT) */ |
| |
| /* Set SYSB Current Limit Protection */ |
| set_sw_cfg |= sysb_clp << KTU1125_SYSB_CLP_SHIFT; |
| /* Set VCONN Current Limit Protection. |
| * Note: might be changed to 600mA in future |
| */ |
| set_sw_cfg |= KTU1125_VCONN_ILIM_0_40 << KTU1125_VCONN_CLP_SHIFT; |
| /* Disable Dead Battery resistance, because CC FETs are ON */ |
| set_sw_cfg |= KTU1125_RDB_DIS; |
| |
| status = write_reg(port, KTU1125_SET_SW_CFG, set_sw_cfg); |
| if (status) { |
| ppc_err_prints("Failed to write SET_SW_CFG!", port, status); |
| return status; |
| } |
| |
| /* |
| * Setting control register SET_SW2_CFG |
| */ |
| |
| /* Set T_HIC */ |
| set_sw2_cfg |= (KTU_T_HIC_MS_17 << KTU1125_T_HIC_SHIFT); |
| /* Set vbus discharge resistance */ |
| set_sw2_cfg |= (KTU1125_DIS_RES_1400 << KTU1125_DIS_RES_SHIFT); |
| /* |
| * Set the over voltage protection to the maximum (25V) to support |
| * sinking from a 20V PD charger. The common PPC code doesn't provide |
| * any hooks for indicating what the currently negotiated voltage is |
| */ |
| set_sw2_cfg |= (KTU1125_SYSB_VLIM_25_00 << KTU1125_OVP_BUS_SHIFT); |
| |
| status = write_reg(port, KTU1125_SET_SW2_CFG, set_sw2_cfg); |
| if (status) { |
| ppc_err_prints("Failed to write SET_SW2_CFG!", port, status); |
| return status; |
| } |
| |
| /* |
| * Don't proceed with the rest of initialization if we're sysjumping. |
| * We would have already done this before |
| */ |
| if (system_jumped_late()) |
| return EC_SUCCESS; |
| |
| /* |
| * Enable interrupts |
| */ |
| |
| /* Leave SYSA_OK and FRS masked for SNK group of interrupts */ |
| regval = KTU1125_SNK_MASK_ALL & (KTU1125_SYSA_OK | KTU1125_FR_SWAP); |
| status = write_reg(port, KTU1125_INTMASK_SNK, regval); |
| if (status) { |
| ppc_err_prints("Failed to write INTMASK_SNK!", port, status); |
| return status; |
| } |
| |
| /* Only leave VBUS_OK masked for SRC group of interrupts */ |
| regval = KTU1125_SRC_MASK_ALL & KTU1125_VBUS_OK; |
| status = write_reg(port, KTU1125_INTMASK_SRC, regval); |
| if (status) { |
| ppc_err_prints("Failed to write INTMASK_SRC!", port, status); |
| return status; |
| } |
| |
| /* Unmask the entire DATA group of interrupts */ |
| status = write_reg(port, KTU1125_INTMASK_DATA, ~KTU1125_DATA_MASK_ALL); |
| if (status) { |
| ppc_err_prints("Failed to write INTMASK_DATA!", port, status); |
| return status; |
| } |
| |
| return EC_SUCCESS; |
| } |
| |
| #ifdef CONFIG_USB_PD_VBUS_DETECT_PPC |
| static int ktu1125_is_vbus_present(int port) |
| { |
| int regval; |
| int rv; |
| |
| rv = read_reg(port, KTU1125_MONITOR_SNK, ®val); |
| if (rv) { |
| ppc_err_prints("VBUS present error", port, rv); |
| return 0; |
| } |
| |
| return !!(regval & KTU1125_SYSA_OK); |
| } |
| #endif /* defined(CONFIG_USB_PD_VBUS_DETECT_PPC) */ |
| |
| static int ktu1125_is_sourcing_vbus(int port) |
| { |
| int regval; |
| int rv; |
| |
| rv = read_reg(port, KTU1125_MONITOR_SRC, ®val); |
| if (rv) { |
| ppc_err_prints("Sourcing VBUS error", port, rv); |
| return 0; |
| } |
| |
| return !!(regval & KTU1125_VBUS_OK); |
| } |
| |
| #ifdef CONFIG_USBC_PPC_POLARITY |
| static int ktu1125_set_polarity(int port, int polarity) |
| { |
| /* |
| * KTU1125 doesn't need to be informed about polarity. |
| * Polarity is queried via pd_get_polarity when applying VCONN. |
| */ |
| ppc_prints("KTU1125 sets polarity only when applying VCONN", port); |
| return EC_SUCCESS; |
| } |
| #endif |
| |
| static int ktu1125_set_vbus_src_current_limit(int port, enum tcpc_rp_value rp) |
| { |
| int regval; |
| int status; |
| |
| /* |
| * Note that we chose the lowest current limit setting that is just |
| * above indicated Rp value. This is because these are minimum values |
| * and we must be able to provide the current that we advertise |
| */ |
| switch (rp) { |
| case TYPEC_RP_3A0: |
| regval = KTU1125_SYSB_ILIM_3_30; |
| break; |
| |
| case TYPEC_RP_1A5: |
| regval = KTU1125_SYSB_ILIM_1_70; |
| break; |
| |
| case TYPEC_RP_USB: |
| default: |
| regval = KTU1125_SYSB_ILIM_0_6; |
| break; |
| }; |
| |
| status = set_field(port, KTU1125_SET_SW_CFG, KTU1125_SYSB_CLP_SHIFT, |
| KTU1125_SYSB_CLP_LEN, regval); |
| if (status) |
| ppc_prints("Failed to set KTU1125_SET_SW_CFG!", port); |
| |
| return status; |
| } |
| |
| static int ktu1125_discharge_vbus(int port, int enable) |
| { |
| int status = enable ? set_flags(port, KTU1125_SET_SW2_CFG, |
| KTU1125_VBUS_DIS_EN) : |
| clr_flags(port, KTU1125_SET_SW2_CFG, |
| KTU1125_VBUS_DIS_EN); |
| |
| if (status) { |
| CPRINTS("ppc p%d: Failed to %s vbus discharge", port, |
| enable ? "enable" : "disable"); |
| return status; |
| } |
| |
| return EC_SUCCESS; |
| } |
| |
| #ifdef CONFIG_USBC_PPC_VCONN |
| static int ktu1125_set_vconn(int port, int enable) |
| { |
| int polarity; |
| int status; |
| int flags = KTU1125_VCONN_EN; |
| |
| polarity = polarity_rm_dts(pd_get_polarity(port)); |
| |
| if (enable) { |
| /* |
| * If polarity is CC1, then apply VCONN on CC2. |
| * else if polarity is CC2, then apply VCONN on CC1 |
| */ |
| flags |= polarity ? KTU1125_CC1S_VCONN : KTU1125_CC2S_VCONN; |
| status = set_flags(port, KTU1125_CTRL_SW_CFG, flags); |
| } else { |
| flags |= KTU1125_CC1S_VCONN | KTU1125_CC2S_VCONN; |
| status = clr_flags(port, KTU1125_CTRL_SW_CFG, flags); |
| } |
| |
| return status; |
| } |
| #endif |
| |
| #ifdef CONFIG_USB_PD_FRS_PPC |
| static int ktu1125_set_frs_enable(int port, int enable) |
| { |
| /* Enable/Disable FR_SWAP Interrupt */ |
| int status = |
| enable ? clr_flags(port, KTU1125_INTMASK_SNK, KTU1125_FR_SWAP) : |
| set_flags(port, KTU1125_INTMASK_SNK, KTU1125_FR_SWAP); |
| |
| if (status) { |
| ppc_prints("Failed to write KTU1125_INTMASK_SNK!", port); |
| return status; |
| } |
| |
| /* Set the FRS_EN bit */ |
| status = enable ? set_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_FRS_EN) : |
| clr_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_FRS_EN); |
| |
| return status; |
| } |
| #endif |
| |
| static int ktu1125_vbus_sink_enable(int port, int enable) |
| { |
| int sc_reg; |
| int rv; |
| |
| rv = read_reg(port, KTU1125_CTRL_SW_CFG, &sc_reg); |
| if (rv != EC_SUCCESS) { |
| ppc_err_prints("read CTRL_SW_CFG", port, rv); |
| return rv; |
| } |
| |
| if (enable) { |
| /* enable sink path */ |
| if ((sc_reg & (KTU1125_SW_AB_EN | KTU1125_POW_MODE)) == |
| KTU1125_SW_AB_EN) { |
| /* already in sink mode */ |
| return EC_SUCCESS; |
| } |
| sc_reg &= ~KTU1125_POW_MODE; |
| sc_reg |= KTU1125_SW_AB_EN; |
| } else { |
| /* disable sink path */ |
| if ((sc_reg & KTU1125_SW_AB_EN) == 0) { |
| /* SYSA and SYB power paths already disabled */ |
| return EC_SUCCESS; |
| } |
| if (sc_reg & KTU1125_POW_MODE) { |
| /* |
| * SYSA sink path off, SYSB source path on |
| * i.e. port in source mode |
| */ |
| return EC_SUCCESS; |
| } |
| /* port in sink mode, shut off power paths */ |
| sc_reg &= ~KTU1125_SW_AB_EN; |
| } |
| |
| rv = write_reg(port, KTU1125_CTRL_SW_CFG, sc_reg); |
| if (rv != EC_SUCCESS) { |
| ppc_err_prints("write CTRL_SW_CFG", port, rv); |
| return rv; |
| } |
| |
| return EC_SUCCESS; |
| } |
| |
| static int ktu1125_vbus_source_enable(int port, int enable) |
| { |
| /* Skip if VBUS SRC is already enabled/disabled */ |
| if (ktu1125_is_sourcing_vbus(port) == enable) |
| return EC_SUCCESS; |
| |
| /* Select active source */ |
| int rv = set_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_POW_MODE); |
| |
| if (rv) { |
| ppc_err_prints("Could not select SRC path", port, rv); |
| return rv; |
| } |
| |
| return ktu1125_power_path_control(port, enable); |
| } |
| |
| #ifdef CONFIG_USBC_PPC_SBU |
| static int ktu1125_set_sbu(int port, int enable) |
| { |
| int status = |
| enable ? |
| clr_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_SBU_SHUT) : |
| set_flags(port, KTU1125_CTRL_SW_CFG, KTU1125_SBU_SHUT); |
| |
| if (status) { |
| CPRINTS("ppc p%d: Failed to %s sbu", port, |
| enable ? "enable" : "disable"); |
| } |
| |
| return status; |
| } |
| #endif /* CONFIG_USBC_PPC_SBU */ |
| |
| static void ktu1125_irq_deferred(void) |
| { |
| int i; |
| uint32_t pending = atomic_clear(&irq_pending); |
| |
| for (i = 0; i < board_get_usb_pd_port_count(); i++) |
| if (BIT(i) & pending) |
| ktu1125_handle_interrupt(i); |
| } |
| DECLARE_DEFERRED(ktu1125_irq_deferred); |
| |
| void ktu1125_interrupt(int port) |
| { |
| atomic_or(&irq_pending, BIT(port)); |
| hook_call_deferred(&ktu1125_irq_deferred_data, 0); |
| } |
| |
| static void ktu1125_handle_interrupt(int port) |
| { |
| int attempt = 0; |
| |
| /* |
| * KTU1125's /INT pin is level, so process interrupts until it |
| * deasserts if the chip has a dedicated interrupt pin. |
| */ |
| #ifdef CONFIG_USBC_PPC_DEDICATED_INT |
| while (ppc_get_alert_status(port)) |
| #endif |
| { |
| int snk = 0; |
| int src = 0; |
| int data = 0; |
| |
| attempt++; |
| if (attempt > 1) |
| ppc_prints("Could not clear interrupts on first " |
| "try, retrying", |
| port); |
| |
| if (attempt > 10) { |
| ppc_prints("Rescheduling interrupt handler", port); |
| atomic_or(&irq_pending, BIT(port)); |
| hook_call_deferred(&ktu1125_irq_deferred_data, MSEC); |
| return; |
| } |
| |
| /* Clear the interrupt by reading all 3 registers */ |
| read_reg(port, KTU1125_INT_SNK, &snk); |
| read_reg(port, KTU1125_INT_SRC, &src); |
| read_reg(port, KTU1125_INT_DATA, &data); |
| |
| CPRINTS("ppc p%d: INTERRUPT snk=%02X src=%02X data=%02X", port, |
| snk, src, data); |
| |
| if (snk & KTU1125_FR_SWAP) |
| pd_got_frs_signal(port); |
| |
| if (snk & |
| (KTU1125_SYSA_SCP | KTU1125_SYSA_OCP | KTU1125_VBUS_OVP)) { |
| /* Log and PD reset */ |
| pd_handle_overcurrent(port); |
| } |
| |
| if (src & |
| (KTU1125_SYSB_CLP | KTU1125_SYSB_OCP | KTU1125_SYSB_SCP | |
| KTU1125_VCONN_CLP | KTU1125_VCONN_SCP)) { |
| /* Log and PD reset */ |
| pd_handle_overcurrent(port); |
| } |
| |
| if (data & (KTU1125_SBU2_OVP | KTU1125_SBU1_OVP)) { |
| /* Log and PD reset */ |
| pd_handle_overcurrent(port); |
| } |
| |
| if (data & (KTU1125_CC1_OVP | KTU1125_CC2_OVP)) { |
| ppc_prints("CC Over Voltage!", port); |
| pd_handle_cc_overvoltage(port); |
| } |
| } |
| } |
| |
| const struct ppc_drv ktu1125_drv = { |
| .init = &ktu1125_init, |
| .is_sourcing_vbus = &ktu1125_is_sourcing_vbus, |
| .vbus_sink_enable = &ktu1125_vbus_sink_enable, |
| .vbus_source_enable = &ktu1125_vbus_source_enable, |
| #ifdef CONFIG_USBC_PPC_POLARITY |
| .set_polarity = &ktu1125_set_polarity, |
| #endif |
| .set_vbus_source_current_limit = &ktu1125_set_vbus_src_current_limit, |
| .discharge_vbus = &ktu1125_discharge_vbus, |
| #ifdef CONFIG_USBC_PPC_SBU |
| .set_sbu = &ktu1125_set_sbu, |
| #endif |
| #ifdef CONFIG_USBC_PPC_VCONN |
| .set_vconn = &ktu1125_set_vconn, |
| #endif |
| #ifdef CONFIG_USB_PD_FRS_PPC |
| .set_frs_enable = &ktu1125_set_frs_enable, |
| #endif |
| #ifdef CONFIG_CMD_PPC_DUMP |
| .reg_dump = &ktu1125_dump, |
| #endif |
| #ifdef CONFIG_USB_PD_VBUS_DETECT_PPC |
| .is_vbus_present = &ktu1125_is_vbus_present, |
| #endif |
| .interrupt = &ktu1125_interrupt, |
| }; |