474 lines
13 KiB
C
474 lines
13 KiB
C
/* Copyright 2018 The Chromium OS Authors. All rights reserved.
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
/* NX20P348x USB-C Power Path Controller */
|
|
|
|
#include "common.h"
|
|
#include "console.h"
|
|
#include "driver/ppc/nx20p348x.h"
|
|
#include "gpio.h"
|
|
#include "hooks.h"
|
|
#include "i2c.h"
|
|
#include "system.h"
|
|
#include "tcpm.h"
|
|
#include "usb_charge.h"
|
|
#include "usb_pd_tcpm.h"
|
|
#include "usb_pd.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 uint32_t irq_pending; /* Bitmask of ports signaling an interrupt. */
|
|
|
|
#define NX20P348X_DB_EXIT_FAIL_THRESHOLD 10
|
|
static int db_exit_fail_count[CONFIG_USB_PD_PORT_COUNT];
|
|
|
|
#define NX20P348X_FLAGS_SOURCE_ENABLED BIT(0)
|
|
static uint8_t flags[CONFIG_USB_PD_PORT_COUNT];
|
|
|
|
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 nx20p348x_set_ovp_limit(int port)
|
|
{
|
|
int rv;
|
|
int reg;
|
|
|
|
/* Set VBUS over voltage threshold (OVLO) */
|
|
rv = read_reg(port, NX20P348X_OVLO_THRESHOLD_REG, ®);
|
|
if (rv)
|
|
return rv;
|
|
/* OVLO threshold is 3 bit field */
|
|
reg &= ~NX20P348X_OVLO_THRESHOLD_MASK;
|
|
/* Set SNK OVP to 23.0 V */
|
|
reg |= NX20P348X_OVLO_23_0;
|
|
rv = write_reg(port, NX20P348X_OVLO_THRESHOLD_REG, reg);
|
|
if (rv)
|
|
return rv;
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int nx20p348x_is_sourcing_vbus(int port)
|
|
{
|
|
return flags[port] & NX20P348X_FLAGS_SOURCE_ENABLED;
|
|
}
|
|
|
|
static int nx20p348x_set_vbus_source_current_limit(int port,
|
|
enum tcpc_rp_value rp)
|
|
{
|
|
int regval;
|
|
int status;
|
|
|
|
status = read_reg(port, NX20P348X_5V_SRC_OCP_THRESHOLD_REG, ®val);
|
|
if (status)
|
|
return status;
|
|
|
|
regval &= ~NX20P348X_ILIM_MASK;
|
|
|
|
/* We need buffer room for all current values. */
|
|
switch (rp) {
|
|
case TYPEC_RP_3A0:
|
|
regval |= NX20P348X_ILIM_3_200;
|
|
break;
|
|
|
|
case TYPEC_RP_1A5:
|
|
regval |= NX20P348X_ILIM_1_600;
|
|
break;
|
|
|
|
case TYPEC_RP_USB:
|
|
default:
|
|
regval |= NX20P348X_ILIM_0_600;
|
|
break;
|
|
};
|
|
|
|
|
|
return write_reg(port, NX20P348X_5V_SRC_OCP_THRESHOLD_REG, regval);
|
|
}
|
|
|
|
static int nx20p348x_discharge_vbus(int port, int enable)
|
|
{
|
|
int regval;
|
|
int status;
|
|
|
|
status = read_reg(port, NX20P348X_DEVICE_CONTROL_REG, ®val);
|
|
if (status)
|
|
return status;
|
|
|
|
if (enable)
|
|
regval |= NX20P348X_CTRL_VBUSDIS_EN;
|
|
else
|
|
regval &= ~NX20P348X_CTRL_VBUSDIS_EN;
|
|
|
|
status = write_reg(port, NX20P348X_DEVICE_CONTROL_REG, regval);
|
|
if (status) {
|
|
CPRINTS("Failed to %s vbus discharge",
|
|
enable ? "enable" : "disable");
|
|
return status;
|
|
}
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int nx20p348x_vbus_sink_enable(int port, int enable)
|
|
{
|
|
int status;
|
|
int rv;
|
|
int control = enable ? NX20P348X_SWITCH_CONTROL_HVSNK : 0;
|
|
|
|
enable = !!enable;
|
|
|
|
#if defined(CONFIG_USBC_PPC_NX20P3481)
|
|
rv = write_reg(port, NX20P348X_SWITCH_CONTROL_REG, control);
|
|
#elif defined(CONFIG_USBC_PPC_NX20P3483)
|
|
/*
|
|
* We cannot use an EC GPIO for EN_SNK since an EC reset will float the
|
|
* GPIO thus browning out the board (without a battery).
|
|
*/
|
|
rv = tcpm_set_snk_ctrl(port, enable);
|
|
#else
|
|
#error "Either the NX20P3481 or NX20P3483 must be selected"
|
|
#endif
|
|
if (rv)
|
|
return rv;
|
|
|
|
/*
|
|
* Read switch status register. The bit definitions for switch control
|
|
* and switch status resister are identical, so the control value can be
|
|
* compared against the status value. The control switch has a debounce
|
|
* (15 msec) before the status will reflect the control command.
|
|
*/
|
|
msleep(NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC);
|
|
rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &status);
|
|
if (rv)
|
|
return rv;
|
|
|
|
return (status & NX20P348X_SWITCH_STATUS_MASK) == control ?
|
|
EC_SUCCESS : EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
static int nx20p348x_vbus_source_enable(int port, int enable)
|
|
{
|
|
int status;
|
|
int rv;
|
|
uint8_t previous_flags = flags[port];
|
|
int control = enable ? NX20P348X_SWITCH_CONTROL_5VSRC : 0;
|
|
|
|
enable = !!enable;
|
|
|
|
#if defined(CONFIG_USBC_PPC_NX20P3481)
|
|
rv = write_reg(port, NX20P348X_SWITCH_CONTROL_REG, control);
|
|
#elif defined(CONFIG_USBC_PPC_NX20P3483)
|
|
/*
|
|
* For parity's sake, we should not use an EC GPIO for EN_SRC since we
|
|
* cannot use it for EN_SNK (for brown out reason listed above).
|
|
*/
|
|
rv = tcpm_set_src_ctrl(port, enable);
|
|
#else
|
|
#error "Either the NX20P3481 or NX20P3483 must be selected"
|
|
#endif
|
|
if (rv)
|
|
return rv;
|
|
|
|
/* Cache the anticipated Vbus state */
|
|
if (enable)
|
|
flags[port] |= NX20P348X_FLAGS_SOURCE_ENABLED;
|
|
else
|
|
flags[port] &= ~NX20P348X_FLAGS_SOURCE_ENABLED;
|
|
|
|
/*
|
|
* Read switch status register. The bit definitions for switch control
|
|
* and switch status resister are identical, so the control value can be
|
|
* compared against the status value. The control switch has a debounce
|
|
* (15 msec) before the status will reflect the control command.
|
|
*/
|
|
msleep(NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC);
|
|
rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &status);
|
|
if (rv) {
|
|
flags[port] = previous_flags;
|
|
return rv;
|
|
}
|
|
|
|
if ((status & NX20P348X_SWITCH_STATUS_MASK) != control) {
|
|
flags[port] = previous_flags;
|
|
return EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int nx20p348x_init(int port)
|
|
{
|
|
int reg;
|
|
int mask;
|
|
int mode;
|
|
int rv;
|
|
enum tcpc_rp_value initial_current_limit;
|
|
|
|
/* Mask interrupts for interrupt 2 register */
|
|
mask = ~NX20P348X_INT2_EN_ERR;
|
|
rv = write_reg(port, NX20P348X_INTERRUPT2_MASK_REG, mask);
|
|
if (rv)
|
|
return rv;
|
|
|
|
/* Mask interrupts for interrupt 1 register */
|
|
mask = ~(NX20P348X_INT1_OC_5VSRC | NX20P348X_INT1_SC_5VSRC |
|
|
NX20P348X_INT1_RCP_5VSRC | NX20P348X_INT1_DBEXIT_ERR);
|
|
#ifdef CONFIG_USBC_PPC_NX20P3481
|
|
/* Unmask Fast Role Swap detect interrupt */
|
|
mask &= ~NX20P348X_INT1_FRS_DET;
|
|
#endif
|
|
rv = write_reg(port, NX20P348X_INTERRUPT1_MASK_REG, mask);
|
|
if (rv)
|
|
return rv;
|
|
|
|
/* Clear any pending interrupts by reading interrupt registers */
|
|
read_reg(port, NX20P348X_INTERRUPT1_REG, ®);
|
|
read_reg(port, NX20P348X_INTERRUPT2_REG, ®);
|
|
|
|
/* Get device mode */
|
|
rv = read_reg(port, NX20P348X_DEVICE_STATUS_REG, &mode);
|
|
if (rv)
|
|
return rv;
|
|
mode &= NX20P348X_DEVICE_MODE_MASK;
|
|
|
|
/* Check if dead battery mode is active. */
|
|
if (mode == NX20P348X_MODE_DEAD_BATTERY) {
|
|
/*
|
|
* If in dead battery mode, must enable HV SNK mode prior to
|
|
* exiting dead battery mode or VBUS path will get cut off and
|
|
* system will lose power. Before DB mode is exited, the device
|
|
* mode will not reflect the correct value and therefore the
|
|
* return value isn't useful here.
|
|
*/
|
|
nx20p348x_vbus_sink_enable(port, 1);
|
|
|
|
/* Exit dead battery mode. */
|
|
rv = read_reg(port, NX20P348X_DEVICE_CONTROL_REG, ®);
|
|
if (rv)
|
|
return rv;
|
|
reg |= NX20P348X_CTRL_DB_EXIT;
|
|
rv = write_reg(port, NX20P348X_DEVICE_CONTROL_REG, reg);
|
|
if (rv)
|
|
return rv;
|
|
}
|
|
|
|
/*
|
|
* Set VBUS over voltage threshold (OVLO). Note that while the PPC is in
|
|
* dead battery mode, OVLO is forced to 6.8V, so this setting must be
|
|
* done after dead battery mode is exited.
|
|
*/
|
|
nx20p348x_set_ovp_limit(port);
|
|
|
|
/* Set the Vbus current limit after dead battery mode exit */
|
|
#ifdef CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT
|
|
initial_current_limit = CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT;
|
|
#else
|
|
initial_current_limit = TYPEC_RP_1A5;
|
|
#endif
|
|
nx20p348x_set_vbus_source_current_limit(port, initial_current_limit);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static void nx20p348x_handle_interrupt(int port)
|
|
{
|
|
int reg;
|
|
int control_reg;
|
|
|
|
/*
|
|
* Read interrupt 1 status register. Note, interrupt register is
|
|
* automatically cleared by reading.
|
|
*/
|
|
read_reg(port, NX20P348X_INTERRUPT1_REG, ®);
|
|
|
|
/* Check for DBEXIT error */
|
|
if (reg & NX20P348X_INT1_DBEXIT_ERR) {
|
|
int mask_reg;
|
|
|
|
/*
|
|
* This failure is not expected. If for some reason, this
|
|
* keeps happening, then log an error and mask the interrupt to
|
|
* prevent interrupt floods.
|
|
*/
|
|
if (++db_exit_fail_count[port] >=
|
|
NX20P348X_DB_EXIT_FAIL_THRESHOLD) {
|
|
CPRINTS("Port %d PPC failed to exit DB mode", port);
|
|
if (read_reg(port, NX20P348X_INTERRUPT1_MASK_REG,
|
|
&mask_reg)) {
|
|
mask_reg |= NX20P348X_INT1_DBEXIT_ERR;
|
|
write_reg(port, NX20P348X_INTERRUPT1_MASK_REG,
|
|
mask_reg);
|
|
}
|
|
}
|
|
read_reg(port, NX20P348X_DEVICE_CONTROL_REG, &control_reg);
|
|
reg |= NX20P348X_CTRL_DB_EXIT;
|
|
write_reg(port, NX20P348X_DEVICE_CONTROL_REG, control_reg);
|
|
/*
|
|
* If DB exit mode failed, then the OVP limit setting done in
|
|
* the init routine will not be successful. Set the OVP limit
|
|
* again here.
|
|
*/
|
|
nx20p348x_set_ovp_limit(port);
|
|
}
|
|
|
|
/* Check for 5V OC interrupt */
|
|
if (reg & NX20P348X_INT1_OC_5VSRC) {
|
|
CPRINTS("C%d: PPC detected Vbus overcurrent!", port);
|
|
pd_handle_overcurrent(port);
|
|
}
|
|
|
|
/* Check for Vbus reverse current protection */
|
|
if (reg & NX20P348X_INT1_RCP_5VSRC)
|
|
CPRINTS("C%d: PPC detected Vbus reverse current!", port);
|
|
|
|
/* Check for Vbus short protection */
|
|
if (reg & NX20P348X_INT1_SC_5VSRC)
|
|
CPRINTS("C%d: PPC Vbus short detected!", port);
|
|
|
|
#ifdef CONFIG_USBC_PPC_NX20P3481
|
|
/* Check for FRS detection */
|
|
if (reg & NX20P348X_INT1_FRS_DET) {
|
|
/*
|
|
* TODO(b/113069469): Need to check for CC status and verifiy
|
|
* that a sink is attached to continue with FRS. If a sink is
|
|
* not attached, then this FRS detect is a false detect which is
|
|
* triggered when removing an external charger. If FRS was
|
|
* detected by the PPC, then it has automatically enabled the
|
|
* 5V SRC mode and this must be undone for a proper detach.
|
|
*/
|
|
/* Check CC status */
|
|
|
|
/*
|
|
* False detect, disable SRC mode which was enabled by
|
|
* NX20P3481.
|
|
*/
|
|
CPRINTS("C%d: PPC FRS false detect, disabling SRC mode!", port);
|
|
nx20p348x_vbus_source_enable(port, 0);
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Read interrupt 2 status register. Note, interrupt register is
|
|
* automatically cleared by reading.
|
|
*/
|
|
/*
|
|
* TODO (b/75272421): Not sure if any of these interrupts
|
|
* will be used. Might want to use EN_ERR which tracks when both
|
|
* SNK_EN and SRC_EN are set. However, since for the Analogix TCPC
|
|
* these values aren't controlled by the EC directly, not sure what
|
|
* action if any can be taken.
|
|
*/
|
|
read_reg(port, NX20P348X_INTERRUPT2_REG, ®);
|
|
}
|
|
|
|
static void nx20p348x_irq_deferred(void)
|
|
{
|
|
int i;
|
|
uint32_t pending = atomic_read_clear(&irq_pending);
|
|
|
|
for (i = 0; i < CONFIG_USB_PD_PORT_COUNT; i++)
|
|
if (BIT(i) & pending)
|
|
nx20p348x_handle_interrupt(i);
|
|
}
|
|
DECLARE_DEFERRED(nx20p348x_irq_deferred);
|
|
|
|
void nx20p348x_interrupt(int port)
|
|
{
|
|
atomic_or(&irq_pending, BIT(port));
|
|
hook_call_deferred(&nx20p348x_irq_deferred_data, 0);
|
|
}
|
|
|
|
#ifdef CONFIG_CMD_PPC_DUMP
|
|
static int nx20p348x_dump(int port)
|
|
{
|
|
int reg_addr;
|
|
int reg;
|
|
int rv;
|
|
|
|
ccprintf("Port %d NX20P348X registers\n", port);
|
|
for (reg_addr = NX20P348X_DEVICE_ID_REG; reg_addr <=
|
|
NX20P348X_DEVICE_CONTROL_REG; reg_addr++) {
|
|
rv = read_reg(port, reg_addr, ®);
|
|
if (rv) {
|
|
ccprintf("nx20p: Failed to read register 0x%x\n",
|
|
reg_addr);
|
|
return rv;
|
|
}
|
|
ccprintf("[0x%02x]: 0x%02x\n", reg_addr, reg);
|
|
|
|
/* Flush every call otherwise buffer may get full */
|
|
cflush();
|
|
}
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
#endif /* defined(CONFIG_CMD_PPC_DUMP) */
|
|
|
|
/*
|
|
* TODO (b/112697473): The NX20P348x PPCs do not support vbus detection or vconn
|
|
* generation. However, if a different PPC does support these features and needs
|
|
* these config options, then these functions do need to exist. The
|
|
* configuration for what each PPC supports should be converted to bits within
|
|
* a flags variable that is part of the ppc_config_t struct.
|
|
*/
|
|
#ifdef CONFIG_USB_PD_VBUS_DETECT_PPC
|
|
static int nx20p348x_is_vbus_present(int port)
|
|
{
|
|
return EC_ERROR_UNIMPLEMENTED;
|
|
}
|
|
#endif /* defined(CONFIG_USB_PD_VBUS_DETECT_PPC) */
|
|
|
|
#ifdef CONFIG_USBC_PPC_POLARITY
|
|
static int nx20p348x_set_polarity(int port, int polarity)
|
|
{
|
|
return EC_ERROR_UNIMPLEMENTED;
|
|
}
|
|
#endif
|
|
|
|
#ifdef CONFIG_USBC_PPC_VCONN
|
|
static int nx20p348x_set_vconn(int port, int enable)
|
|
{
|
|
return EC_ERROR_UNIMPLEMENTED;
|
|
}
|
|
#endif
|
|
|
|
const struct ppc_drv nx20p348x_drv = {
|
|
.init = &nx20p348x_init,
|
|
.is_sourcing_vbus = &nx20p348x_is_sourcing_vbus,
|
|
.vbus_sink_enable = &nx20p348x_vbus_sink_enable,
|
|
.vbus_source_enable = &nx20p348x_vbus_source_enable,
|
|
#ifdef CONFIG_CMD_PPC_DUMP
|
|
.reg_dump = &nx20p348x_dump,
|
|
#endif /* defined(CONFIG_CMD_PPC_DUMP) */
|
|
.set_vbus_source_current_limit =
|
|
&nx20p348x_set_vbus_source_current_limit,
|
|
.discharge_vbus = &nx20p348x_discharge_vbus,
|
|
#ifdef CONFIG_USB_PD_VBUS_DETECT_PPC
|
|
.is_vbus_present = &nx20p348x_is_vbus_present,
|
|
#endif /* defined(CONFIG_USB_PD_VBUS_DETECT_PPC) */
|
|
#ifdef CONFIG_USBC_PPC_POLARITY
|
|
.set_polarity = &nx20p348x_set_polarity,
|
|
#endif /* defined(CONFIG_USBC_PPC_POLARITY) */
|
|
#ifdef CONFIG_USBC_PPC_VCONN
|
|
.set_vconn = &nx20p348x_set_vconn,
|
|
#endif /* defined(CONFIG_USBC_PPC_VCONN) */
|
|
};
|