/* Copyright 2021 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.
 */

/* USBC functions for RO */

#include "common.h"
#include "console.h"
#include "driver/tcpm/tcpci.h"
#include "driver/tcpm/ps8xxx.h"
#include "sn5s330.h"
#include "hooks.h"
#include "i2c.h"
#include "system.h"
#include "timer.h"
#include "usb_charge.h"
#include "usb_pd_tcpm.h"
#include "usb_pd.h"
#include "usbc_ppc.h"
#include "util.h"
#include "registers.h"
#include "ucpd-stm32gx.h"

#define CPRINTS(format, args...) cprints(CC_SYSTEM, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_SYSTEM, format, ## args)

enum usbc_states {
	UNATTACHED_SNK,
	ATTACH_WAIT_SNK,
	ATTACHED_SNK,
};

/* Variables used to manage the simple usbc state machine */
static int usbc_port;
static int usbc_state;
static int usbc_vbus;
static enum tcpc_cc_voltage_status cc1_v;
static enum tcpc_cc_voltage_status cc2_v;

__maybe_unused static __const_data const char * const usbc_state_names[] = {
	[UNATTACHED_SNK] = "Unattached.SNK",
	[ATTACH_WAIT_SNK] = "AttachWait.SNK",
	[ATTACHED_SNK] = "Attached.SNK",
};

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 baseboard_ppc_enable_sink_path(int port)
{
	int regval;
	int status;
	int retries;

	/*
	 * It seems that sometimes setting the FUNC_SET1 register fails
	 * initially.  Therefore, we'll retry a couple of times.
	 */
	retries = 0;
	do {
		status = write_reg(port, SN5S330_FUNC_SET1, SN5S330_ILIM_3_06);
		if (status) {
			retries++;
			msleep(1);
		} else {
			break;
		}
	} while (retries < 10);

	/* Turn off dead battery resistors, turn on CC FETs */
	status = read_reg(port, SN5S330_FUNC_SET4, &regval);
	if (!status) {
		regval |= SN5S330_CC_EN;
		status = write_reg(port, SN5S330_FUNC_SET4, regval);
	}
	if (status) {
		return status;
	}

	/* Enable sink path via PP2 */
	status = read_reg(port, SN5S330_FUNC_SET3, &regval);
	if (!status) {
		regval &= ~SN5S330_PP1_EN;
		regval |= SN5S330_PP2_EN;
		status = write_reg(port, SN5S330_FUNC_SET3, regval);
	}
	if (status) {
		return status;
	}

	return EC_SUCCESS;
}

static void baseboard_ucpd_apply_rd(int port)
{
	uint32_t cfgr1_reg;
	uint32_t moder_reg;
	uint32_t cr;

	/* Ensure that clock to UCPD is enabled */
	STM32_RCC_APB1ENR2 |= STM32_RCC_APB1ENR2_UPCD1EN;

	/* Make sure CC1/CC2 pins PB4/PB6 are set for analog mode */
	moder_reg = STM32_GPIO_MODER(GPIO_B);
	moder_reg |= 0x3300;
	STM32_GPIO_MODER(GPIO_B) = moder_reg;
	/*
	 * CFGR1 must be written when UCPD peripheral is disabled. Note that
	 * disabling ucpd causes the peripheral to quit any ongoing activity and
	 * sets all ucpd registers back their default values.
	 */

	cfgr1_reg = STM32_UCPD_CFGR1_PSC_CLK_VAL(UCPD_PSC_DIV - 1) |
		STM32_UCPD_CFGR1_TRANSWIN_VAL(UCPD_TRANSWIN_CNT - 1) |
		STM32_UCPD_CFGR1_IFRGAP_VAL(UCPD_IFRGAP_CNT - 1) |
		STM32_UCPD_CFGR1_HBITCLKD_VAL(UCPD_HBIT_DIV - 1);
	STM32_UCPD_CFGR1(port) = cfgr1_reg;

	/* Enable ucpd  */
	STM32_UCPD_CFGR1(port) |= STM32_UCPD_CFGR1_UCPDEN;

	/* Apply Rd to both CC lines */
	cr = STM32_UCPD_CR(port);
	cr |= STM32_UCPD_CR_ANAMODE | STM32_UCPD_CR_CCENABLE_MASK;
	STM32_UCPD_CR(port) = cr;

	/*
	 * After exiting reset, stm32gx will have dead battery mode enabled by
	 * default which connects Rd to CC1/CC2. This should be disabled when EC
	 * is powered up.
	 */
	STM32_PWR_CR3 |= STM32_PWR_CR3_UCPD1_DBDIS;
}


static void baseboard_ucpd_get_cc(int port, enum tcpc_cc_voltage_status *cc1,
	enum tcpc_cc_voltage_status *cc2)
{
	int vstate_cc1;
	int vstate_cc2;
	int anamode;
	uint32_t sr;

	/*
	 * cc_voltage_status is determined from vstate_cc bit field in the
	 * status register. The meaning of the value vstate_cc depends on
	 * current value of ANAMODE (src/snk).
	 *
	 * vstate_cc maps directly to cc_state from tcpci spec when ANAMODE = 1,
	 * but needs to be modified slightly for case ANAMODE = 0.
         *
	 * If presenting Rp (source), then need to to a circular shift of
	 * vstate_ccx value:
	 *     vstate_cc | cc_state
	 *     ------------------
	 *        0     ->    1
	 *        1     ->    2
	 *        2     ->    0
	 */

	/* Get vstate_ccx values and power role */
	sr = STM32_UCPD_SR(port);
	/* Get Rp or Rd active */
	anamode = !!(STM32_UCPD_CR(port) & STM32_UCPD_CR_ANAMODE);
	vstate_cc1 = (sr & STM32_UCPD_SR_VSTATE_CC1_MASK) >>
		STM32_UCPD_SR_VSTATE_CC1_SHIFT;
	vstate_cc2 = (sr & STM32_UCPD_SR_VSTATE_CC2_MASK) >>
		STM32_UCPD_SR_VSTATE_CC2_SHIFT;

	/* Do circular shift if port == source */
	if (anamode) {
		if (vstate_cc1 != STM32_UCPD_SR_VSTATE_RA)
			vstate_cc1 += 4;
		if (vstate_cc2 != STM32_UCPD_SR_VSTATE_RA)
			vstate_cc2 += 4;
	} else {
		if (vstate_cc1 != STM32_UCPD_SR_VSTATE_OPEN)
			vstate_cc1 = (vstate_cc1 + 1) % 3;
		if (vstate_cc2 != STM32_UCPD_SR_VSTATE_OPEN)
			vstate_cc2 = (vstate_cc2 + 1) % 3;
	}

	*cc1 = vstate_cc1;
	*cc2 = vstate_cc2;
}

static int baseboard_rp_is_present(enum tcpc_cc_voltage_status cc1,
				   enum tcpc_cc_voltage_status cc2)
{
	return (cc1 >= TYPEC_CC_VOLT_RP_DEF || cc2 >= TYPEC_CC_VOLT_RP_DEF);
}

static void baseboard_usbc_check_connect(void);
DECLARE_DEFERRED(baseboard_usbc_check_connect);

static void baseboard_usbc_check_connect(void)
{
	enum tcpc_cc_voltage_status cc1;
	enum tcpc_cc_voltage_status cc2;
	int ppc_reg;
	enum usbc_states enter_state = usbc_state;

	/*
	 * In RO, the only usbc related requirement is to enable the stm32g4
	 * USB-EP to be enumerated by the host attached to C0. To prevent D+
	 * being pulled high prior to VBUS presence, the EC uses GPIO_BPWR_DET
	 * to signal the USB hub that VBUS is present. Therefore, we need a
	 * simple usbc state machine to detect an attach (Rp and VBUS) event so
	 * this GPIO signal is properly controlled in RO.
	 *
	 * Note that RO only runs until the RWSIG timer expires and jumps to RW,
	 * and in RW, the full usb-pd stack is initialized and run.
	 */

	/* Get current CC voltage levels */
	baseboard_ucpd_get_cc(usbc_port, &cc1, &cc2);
	/* Update VBUS state */
	if (!read_reg(usbc_port, SN5S330_INT_STATUS_REG3, &ppc_reg))
		usbc_vbus = ppc_reg & SN5S330_VBUS_GOOD;

	switch (usbc_state) {
	case UNATTACHED_SNK:
		/*
		 * Require either CC1 or CC2 to have a valid Rp CC voltage level
		 * to advance to ATTACH_WAIT_SNK.
		 */
		if (baseboard_rp_is_present(cc1, cc2))
			usbc_state = ATTACH_WAIT_SNK;
		break;
	case ATTACH_WAIT_SNK:
		/*
		 * This state handles debounce by ensuring the CC voltages are
		 * the same between two state machine iterations. If this
		 * condition is met, and VBUS is present, then advance to
		 * ATTACHED_SNK and set GPIO_BPWR_DET.
		 *
		 * If Rp voltage is no longer detected, then return to
		 * UNATTACHED_SNK.
		 */
		if (usbc_vbus && cc1 == cc1_v && cc2 == cc2_v) {
			usbc_state = ATTACHED_SNK;
			gpio_set_level(GPIO_BPWR_DET, 1);
		} else if (!baseboard_rp_is_present(cc1, cc2)) {
			usbc_state = UNATTACHED_SNK;
		}
		break;
	case ATTACHED_SNK:
		/*
		 * In this state, only checking for VBUS going away to indicate
		 * a detach event and inform the USB hub via GPIO_BPWR_DET.
		 */
		if (!usbc_vbus) {
			usbc_state = UNATTACHED_SNK;
			gpio_set_level(GPIO_BPWR_DET, 0);
		}
		break;
	}

	/* Save CC voltage for debounce check */
	cc1_v = cc1;
	cc2_v = cc2;

	if (enter_state != usbc_state)
		CPRINTS("%s: cc1 = %d, cc2 = %d vbus = %d",
			usbc_state_names[usbc_state], cc1, cc2, usbc_vbus);

	hook_call_deferred(&baseboard_usbc_check_connect_data,
			   PD_T_TRY_CC_DEBOUNCE);
}

int baseboard_usbc_init(int port)
{
	int rv;

	/* Initialize ucpd and apply Rd to CC lines */
	baseboard_ucpd_apply_rd(port);
	/* Initialize ppc to enable sink path */
	rv = baseboard_ppc_enable_sink_path(port);
	if (rv)
		CPRINTS("ppc init failed!");
	/* Save host port value */
	usbc_port = port;
	/* Start RO usbc attach state machine */
	gpio_set_level(GPIO_BPWR_DET, 0);
	/* Start simple usbc state machine */
	baseboard_usbc_check_connect();

	return rv;
}

#ifdef SECTION_IS_RW
int c1_ps8805_is_vbus_present(int port)
{
	int vbus;

	vbus = tcpm_check_vbus_level(port, VBUS_PRESENT);

	return vbus;
}

int c1_ps8805_is_sourcing_vbus(int port)
{
	int rv;
	int level;

	rv = ps8805_gpio_get_level(port, PS8805_GPIO_1, &level);
	if (rv)
		return 0;

	return level;
}


int c1_ps8805_vbus_source_enable(int port, int enable)
{

	return ps8805_gpio_set_level(port, PS8805_GPIO_1, enable);
}

#ifdef GPIO_USBC_UF_ATTACHED_SRC
static int ppc_ocp_count;

static void baseboard_usb3_manage_vbus(void)
{
	int level = gpio_get_level(GPIO_USBC_UF_ATTACHED_SRC);

	/*
	 * GPIO_USBC_UF_MUX_VBUS_EN is an output from the PS8803 which tracks if
	 * C2 is attached. When it's attached, this signal will be high. Use
	 * this level to control PPC VBUS on/off.
	 */
	ppc_vbus_source_enable(USB_PD_PORT_USB3, level);
	CPRINTS("C2: State = %s", level ? "Attached.SRC " : "Unattached.SRC");

	/* Reset OCP event counter for detach */
	if (!level) {
		ppc_ocp_count = 0;

#ifdef GPIO_USB_HUB_OCP_NOTIFY
	/*
	 * In the case of an OCP event on this port, the usb hub should be
	 * notified via a GPIO signal. Following, an OCP, the attached.src state
	 * for the usb3 only port is checked again. If it's attached, then make
	 * sure the OCP notify signal is reset.
	 */
		gpio_set_level(GPIO_USB_HUB_OCP_NOTIFY, 1);
#endif
	}
}
DECLARE_DEFERRED(baseboard_usb3_manage_vbus);

void baseboard_usb3_check_state(void)
{
	hook_call_deferred(&baseboard_usb3_manage_vbus_data, 0);
}

void baseboard_usbc_usb3_enable_interrupts(int enable)
{
	if (enable) {
		/* Enable VBUS control interrupt for C2 */
		gpio_enable_interrupt(GPIO_USBC_UF_ATTACHED_SRC);
		/* Enable PPC interrupt */
		gpio_enable_interrupt(GPIO_USBC_UF_PPC_INT_ODL);
	} else {
		/* Disable VBUS control interrupt for C2 */
		gpio_disable_interrupt(GPIO_USBC_UF_ATTACHED_SRC);
		/* Disable PPC interrupt */
		gpio_disable_interrupt(GPIO_USBC_UF_PPC_INT_ODL);
	}
}

int baseboard_config_usbc_usb3_ppc(void)
{
	int rv;

	/*
	 * This port is not usb-pd capable, but there is a ppc which must be
	 * initialized, and keep the VBUS switch enabled.
	 */
	rv = ppc_init(USB_PD_PORT_USB3);
	if (rv)
		return rv;

	/* Need to set current limit to 3A to match advertised value */
	ppc_set_vbus_source_current_limit(USB_PD_PORT_USB3, TYPEC_RP_3A0);
	/* Reset OCP event counter */
	ppc_ocp_count = 0;

	/* Check state at init time */
	baseboard_usb3_manage_vbus();

	/* Enable attached.src and PPC interrupts */
	baseboard_usbc_usb3_enable_interrupts(1);

	return EC_SUCCESS;
}

static void baseboard_usbc_usb3_handle_interrupt(void)
{
	int port = USB_PD_PORT_USB3;

	/*
	 * SN5S330's /INT pin is level, so process interrupts until it
	 * deasserts if the chip has a dedicated interrupt pin.
	 */
	while (gpio_get_level(GPIO_USBC_UF_PPC_INT_ODL) == 0) {
		int rise = 0;
		int fall = 0;

		read_reg(port, SN5S330_INT_TRIP_RISE_REG1, &rise);
		read_reg(port, SN5S330_INT_TRIP_FALL_REG1, &fall);

		/* Notify the system about the overcurrent event. */
		if (rise & SN5S330_ILIM_PP1_MASK) {
			CPRINTS("usb3_ppc: VBUS OC!");
			gpio_set_level(GPIO_USB_HUB_OCP_NOTIFY, 0);
			if (++ppc_ocp_count < 5)
				hook_call_deferred(&baseboard_usb3_manage_vbus_data,
						   USB_HUB_OCP_RESET_MSEC);
			else
				CPRINTS("usb3_ppc: VBUS OC limit reached!");
		}

		/* Clear the interrupt sources. */
		write_reg(port, SN5S330_INT_TRIP_RISE_REG1, rise);
		write_reg(port, SN5S330_INT_TRIP_FALL_REG1, fall);

		read_reg(port, SN5S330_INT_TRIP_RISE_REG2, &rise);
		read_reg(port, SN5S330_INT_TRIP_FALL_REG2, &fall);

		/*
		 * VCONN may be latched off due to an overcurrent.  Indicate
		 * when the VCONN overcurrent happens.
		 */
		if (rise & SN5S330_VCONN_ILIM)
			CPRINTS("usb3_ppc: VCONN OC!");

		/*
		 * CC overvoltage event. There is not action to take here, but
		 * log the event.
		 */
		if (rise & SN5S330_CC1_CON || rise & SN5S330_CC2_CON)
			CPRINTS("usb3_ppc: CC OV!");

		/* Clear the interrupt sources. */
		write_reg(port, SN5S330_INT_TRIP_RISE_REG2, rise);
		write_reg(port, SN5S330_INT_TRIP_FALL_REG2, fall);

	}
}
DECLARE_DEFERRED(baseboard_usbc_usb3_handle_interrupt);

void baseboard_usbc_usb3_irq(void)
{
	hook_call_deferred(&baseboard_usbc_usb3_handle_interrupt_data, 0);
}

#endif /* defined(GPIO_USBC_UF_ATTACHED_SRC) */
#endif /* defined(SECTION_IS_RW) */

