// SPDX-License-Identifier: GPL-2.0+ /* * Copyright 2022 Google, Inc * * USB-C module to reduce wakeups due to contaminants. */ #include #include #include #include #include #include #include #include "tcpci_maxim.h" enum fladc_select { CC1_SCALE1 = 1, CC1_SCALE2, CC2_SCALE1, CC2_SCALE2, SBU1, SBU2, }; #define FLADC_1uA_LSB_MV 25 /* High range CC */ #define FLADC_CC_HIGH_RANGE_LSB_MV 208 /* Low range CC */ #define FLADC_CC_LOW_RANGE_LSB_MV 126 /* 1uA current source */ #define FLADC_CC_SCALE1 1 /* 5 uA current source */ #define FLADC_CC_SCALE2 5 #define FLADC_1uA_CC_OFFSET_MV 300 #define FLADC_CC_HIGH_RANGE_OFFSET_MV 624 #define FLADC_CC_LOW_RANGE_OFFSET_MV 378 #define CONTAMINANT_THRESHOLD_SBU_K 1000 #define CONTAMINANT_THRESHOLD_CC_K 1000 #define READ1_SLEEP_MS 10 #define READ2_SLEEP_MS 5 #define STATUS_CHECK(reg, mask, val) (((reg) & (mask)) == (val)) #define IS_CC_OPEN(cc_status) \ (STATUS_CHECK((cc_status), TCPC_CC_STATUS_CC1_MASK << TCPC_CC_STATUS_CC1_SHIFT, \ TCPC_CC_STATE_SRC_OPEN) && STATUS_CHECK((cc_status), \ TCPC_CC_STATUS_CC2_MASK << \ TCPC_CC_STATUS_CC2_SHIFT, \ TCPC_CC_STATE_SRC_OPEN)) static int max_contaminant_adc_to_mv(struct max_tcpci_chip *chip, enum fladc_select channel, bool ua_src, u8 fladc) { /* SBU channels only have 1 scale with 1uA. */ if ((ua_src && (channel == CC1_SCALE2 || channel == CC2_SCALE2 || channel == SBU1 || channel == SBU2))) /* Mean of range */ return FLADC_1uA_CC_OFFSET_MV + (fladc * FLADC_1uA_LSB_MV); else if (!ua_src && (channel == CC1_SCALE1 || channel == CC2_SCALE1)) return FLADC_CC_HIGH_RANGE_OFFSET_MV + (fladc * FLADC_CC_HIGH_RANGE_LSB_MV); else if (!ua_src && (channel == CC1_SCALE2 || channel == CC2_SCALE2)) return FLADC_CC_LOW_RANGE_OFFSET_MV + (fladc * FLADC_CC_LOW_RANGE_LSB_MV); dev_err_once(chip->dev, "ADC ERROR: SCALE UNKNOWN"); return -EINVAL; } static int max_contaminant_read_adc_mv(struct max_tcpci_chip *chip, enum fladc_select channel, int sleep_msec, bool raw, bool ua_src) { struct regmap *regmap = chip->data.regmap; u8 fladc; int ret; /* Channel & scale select */ ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCINSEL_MASK, channel << ADC_CHANNEL_OFFSET); if (ret < 0) return ret; /* Enable ADC */ ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCEN, ADCEN); if (ret < 0) return ret; usleep_range(sleep_msec * 1000, (sleep_msec + 1) * 1000); ret = max_tcpci_read8(chip, TCPC_VENDOR_FLADC_STATUS, &fladc); if (ret < 0) return ret; /* Disable ADC */ ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCEN, 0); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_ADC_CTRL1, ADCINSEL_MASK, 0); if (ret < 0) return ret; if (!raw) return max_contaminant_adc_to_mv(chip, channel, ua_src, fladc); else return fladc; } static int max_contaminant_read_resistance_kohm(struct max_tcpci_chip *chip, enum fladc_select channel, int sleep_msec, bool raw) { struct regmap *regmap = chip->data.regmap; int mv; int ret; if (channel == CC1_SCALE1 || channel == CC2_SCALE1 || channel == CC1_SCALE2 || channel == CC2_SCALE2) { /* Enable 1uA current source */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCLPMODESEL_MASK, ULTRA_LOW_POWER_MODE); if (ret < 0) return ret; /* Enable 1uA current source */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, UA_1_SRC); if (ret < 0) return ret; /* OVP disable */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCOVPDIS, CCOVPDIS); if (ret < 0) return ret; mv = max_contaminant_read_adc_mv(chip, channel, sleep_msec, raw, true); if (mv < 0) return ret; /* OVP enable */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCOVPDIS, 0); if (ret < 0) return ret; /* returns KOhm as 1uA source is used. */ return mv; } ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBUOVPDIS, SBUOVPDIS); if (ret < 0) return ret; /* SBU switches auto configure when channel is selected. */ /* Enable 1ua current source */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBURPCTRL, SBURPCTRL); if (ret < 0) return ret; mv = max_contaminant_read_adc_mv(chip, channel, sleep_msec, raw, true); if (mv < 0) return ret; /* Disable current source */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBURPCTRL, 0); if (ret < 0) return ret; /* OVP disable */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, SBUOVPDIS, 0); if (ret < 0) return ret; return mv; } static int max_contaminant_read_comparators(struct max_tcpci_chip *chip, u8 *vendor_cc_status2_cc1, u8 *vendor_cc_status2_cc2) { struct regmap *regmap = chip->data.regmap; int ret; /* Enable 80uA source */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, UA_80_SRC); if (ret < 0) return ret; /* Enable comparators */ ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCOMPEN, CCCOMPEN); if (ret < 0) return ret; /* Sleep to allow comparators settle */ usleep_range(5000, 6000); ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_ORIENTATION, PLUG_ORNT_CC1); if (ret < 0) return ret; usleep_range(5000, 6000); ret = max_tcpci_read8(chip, VENDOR_CC_STATUS2, vendor_cc_status2_cc1); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_ORIENTATION, PLUG_ORNT_CC2); if (ret < 0) return ret; usleep_range(5000, 6000); ret = max_tcpci_read8(chip, VENDOR_CC_STATUS2, vendor_cc_status2_cc2); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCOMPEN, 0); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL_MASK, 0); if (ret < 0) return ret; return 0; } static int max_contaminant_detect_contaminant(struct max_tcpci_chip *chip) { int cc1_k, cc2_k, sbu1_k, sbu2_k, ret; u8 vendor_cc_status2_cc1 = 0xff, vendor_cc_status2_cc2 = 0xff; u8 role_ctrl = 0, role_ctrl_backup = 0; int inferred_state = NOT_DETECTED; ret = max_tcpci_read8(chip, TCPC_ROLE_CTRL, &role_ctrl); if (ret < 0) return NOT_DETECTED; role_ctrl_backup = role_ctrl; role_ctrl = 0x0F; ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl); if (ret < 0) return NOT_DETECTED; cc1_k = max_contaminant_read_resistance_kohm(chip, CC1_SCALE2, READ1_SLEEP_MS, false); if (cc1_k < 0) goto exit; cc2_k = max_contaminant_read_resistance_kohm(chip, CC2_SCALE2, READ2_SLEEP_MS, false); if (cc2_k < 0) goto exit; sbu1_k = max_contaminant_read_resistance_kohm(chip, SBU1, READ1_SLEEP_MS, false); if (sbu1_k < 0) goto exit; sbu2_k = max_contaminant_read_resistance_kohm(chip, SBU2, READ2_SLEEP_MS, false); if (sbu2_k < 0) goto exit; ret = max_contaminant_read_comparators(chip, &vendor_cc_status2_cc1, &vendor_cc_status2_cc2); if (ret < 0) goto exit; if ((!(CC1_VUFP_RD0P5 & vendor_cc_status2_cc1) || !(CC2_VUFP_RD0P5 & vendor_cc_status2_cc2)) && !(CC1_VUFP_RD0P5 & vendor_cc_status2_cc1 && CC2_VUFP_RD0P5 & vendor_cc_status2_cc2)) inferred_state = SINK; else if ((cc1_k < CONTAMINANT_THRESHOLD_CC_K || cc2_k < CONTAMINANT_THRESHOLD_CC_K) && (sbu1_k < CONTAMINANT_THRESHOLD_SBU_K || sbu2_k < CONTAMINANT_THRESHOLD_SBU_K)) inferred_state = DETECTED; if (inferred_state == NOT_DETECTED) max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); else max_tcpci_write8(chip, TCPC_ROLE_CTRL, (TCPC_ROLE_CTRL_DRP | 0xA)); return inferred_state; exit: max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); return NOT_DETECTED; } static int max_contaminant_enable_dry_detection(struct max_tcpci_chip *chip) { struct regmap *regmap = chip->data.regmap; u8 temp; int ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL3, CCWTRDEB_MASK | CCWTRSEL_MASK | WTRCYCLE_MASK, CCWTRDEB_1MS << CCWTRDEB_SHIFT | CCWTRSEL_1V << CCWTRSEL_SHIFT | WTRCYCLE_4_8_S << WTRCYCLE_SHIFT); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_ROLE_CTRL, TCPC_ROLE_CTRL_DRP, TCPC_ROLE_CTRL_DRP); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL1, CCCONNDRY, CCCONNDRY); if (ret < 0) return ret; ret = max_tcpci_read8(chip, TCPC_VENDOR_CC_CTRL1, &temp); if (ret < 0) return ret; ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCLPMODESEL_MASK, ULTRA_LOW_POWER_MODE); if (ret < 0) return ret; ret = max_tcpci_read8(chip, TCPC_VENDOR_CC_CTRL2, &temp); if (ret < 0) return ret; /* Enable Look4Connection before sending the command */ ret = regmap_update_bits(regmap, TCPC_TCPC_CTRL, TCPC_TCPC_CTRL_EN_LK4CONN_ALRT, TCPC_TCPC_CTRL_EN_LK4CONN_ALRT); if (ret < 0) return ret; ret = max_tcpci_write8(chip, TCPC_COMMAND, TCPC_CMD_LOOK4CONNECTION); if (ret < 0) return ret; return 0; } bool max_contaminant_is_contaminant(struct max_tcpci_chip *chip, bool disconnect_while_debounce) { u8 cc_status, pwr_cntl; int ret; ret = max_tcpci_read8(chip, TCPC_CC_STATUS, &cc_status); if (ret < 0) return false; ret = max_tcpci_read8(chip, TCPC_POWER_CTRL, &pwr_cntl); if (ret < 0) return false; if (chip->contaminant_state == NOT_DETECTED || chip->contaminant_state == SINK) { if (!disconnect_while_debounce) msleep(100); ret = max_tcpci_read8(chip, TCPC_CC_STATUS, &cc_status); if (ret < 0) return false; if (IS_CC_OPEN(cc_status)) { u8 role_ctrl, role_ctrl_backup; ret = max_tcpci_read8(chip, TCPC_ROLE_CTRL, &role_ctrl); if (ret < 0) return false; role_ctrl_backup = role_ctrl; role_ctrl |= 0x0F; role_ctrl &= ~(TCPC_ROLE_CTRL_DRP); ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl); if (ret < 0) return false; chip->contaminant_state = max_contaminant_detect_contaminant(chip); ret = max_tcpci_write8(chip, TCPC_ROLE_CTRL, role_ctrl_backup); if (ret < 0) return false; if (chip->contaminant_state == DETECTED) { max_contaminant_enable_dry_detection(chip); return true; } } return false; } else if (chip->contaminant_state == DETECTED) { if (STATUS_CHECK(cc_status, TCPC_CC_STATUS_TOGGLING, 0)) { chip->contaminant_state = max_contaminant_detect_contaminant(chip); if (chip->contaminant_state == DETECTED) { max_contaminant_enable_dry_detection(chip); return true; } } } return false; } MODULE_DESCRIPTION("MAXIM TCPC CONTAMINANT Module"); MODULE_AUTHOR("Badhri Jagan Sridharan "); MODULE_LICENSE("GPL");