// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2022 Google, Inc
*
* USB-C module to reduce wakeups due to contaminants.
*/
#include <linux/bitfield.h>
#include <linux/device.h>
#include <linux/irqreturn.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/usb/tcpci.h>
#include <linux/usb/tcpm.h>
#include <linux/usb/typec.h>
#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 IS_CC_OPEN(cc_status) \
(FIELD_GET(TCPC_CC_STATUS_CC1, cc_status) == TCPC_CC_STATE_SRC_OPEN \
&& FIELD_GET(TCPC_CC_STATUS_CC2, cc_status) == 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,
FIELD_PREP(ADCINSEL, channel));
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,
FIELD_PREP(ADCINSEL, 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,
FIELD_PREP(CCLPMODESEL, ULTRA_LOW_POWER_MODE));
if (ret < 0)
return ret;
/* Enable 1uA current source */
ret = regmap_update_bits(regmap, TCPC_VENDOR_CC_CTRL2, CCRPCTRL,
FIELD_PREP(CCRPCTRL, 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,
FIELD_PREP(CCRPCTRL, 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,
FIELD_PREP(CCRPCTRL, 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 | CCWTRSEL | WTRCYCLE,
FIELD_PREP(CCWTRDEB, CCWTRDEB_1MS)
| FIELD_PREP(CCWTRSEL, CCWTRSEL_1V)
| FIELD_PREP(WTRCYCLE, WTRCYCLE_4_8_S));
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,
FIELD_PREP(CCLPMODESEL,
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,
bool *cc_handled)
{
u8 cc_status, pwr_cntl;
int ret;
*cc_handled = true;
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;
}
}
} else if (chip->contaminant_state == DETECTED) {
if (!(cc_status & TCPC_CC_STATUS_TOGGLING)) {
chip->contaminant_state = max_contaminant_detect_contaminant(chip);
if (chip->contaminant_state == DETECTED) {
max_contaminant_enable_dry_detection(chip);
return true;
}
}
}
*cc_handled = false;
return false;
}
MODULE_DESCRIPTION("MAXIM TCPC CONTAMINANT Module");
MODULE_AUTHOR("Badhri Jagan Sridharan <[email protected]>");
MODULE_LICENSE("GPL");