/* Copyright 2019 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.
 *
 * Renesas (Intersil) ISL-9241 battery charger driver.
 */

/* TODO(b/175881324) */
#ifndef CONFIG_ZEPHYR
#include "adc.h"
#endif
#include "battery.h"
#include "battery_smart.h"
#include "charger.h"
#include "charge_state.h"
#include "console.h"
#include "common.h"
#include "hooks.h"
#include "i2c.h"
#include "isl9241.h"
#include "system.h"
#include "task.h"
#include "timer.h"
#include "util.h"

#ifndef CONFIG_CHARGER_NARROW_VDC
#error "ISL9241 is a NVDC charger, please enable CONFIG_CHARGER_NARROW_VDC."
#endif

/* Sense resistor default values in milli Ohm */
#define ISL9241_DEFAULT_RS1 20	/* Input current sense resistor */
#define ISL9241_DEFAULT_RS2 10	/* Battery charge current sense resistor */

#define BOARD_RS1 CONFIG_CHARGER_SENSE_RESISTOR_AC
#define BOARD_RS2 CONFIG_CHARGER_SENSE_RESISTOR

#define BC_REG_TO_CURRENT(REG) (((REG) * ISL9241_DEFAULT_RS2) / BOARD_RS2)
#define BC_CURRENT_TO_REG(CUR) (((CUR) * BOARD_RS2) / ISL9241_DEFAULT_RS2)

#define AC_REG_TO_CURRENT(REG) (((REG) * ISL9241_DEFAULT_RS1) / BOARD_RS1)
#define AC_CURRENT_TO_REG(CUR) (((CUR) * BOARD_RS1) / ISL9241_DEFAULT_RS1)

/* Console output macros */
#define CPRINTF(format, args...) cprintf(CC_CHARGER, format, ## args)

static int learn_mode;

/* Mutex for CONTROL1 register, that can be updated from multiple tasks. */
K_MUTEX_DEFINE(control1_mutex);

/* Charger parameters */
static const struct charger_info isl9241_charger_info = {
	.name         = CHARGER_NAME,
	.voltage_max  = CHARGE_V_MAX,
	.voltage_min  = CHARGE_V_MIN,
	.voltage_step = CHARGE_V_STEP,
	.current_max  = CHARGE_I_MAX,
	.current_min  = CHARGE_I_MIN,
	.current_step = CHARGE_I_STEP,
	.input_current_max  = INPUT_I_MAX,
	.input_current_min  = INPUT_I_MIN,
	.input_current_step = INPUT_I_STEP,
};

static enum ec_error_list isl9241_discharge_on_ac(int chgnum, int enable);

static inline enum ec_error_list isl9241_read(int chgnum, int offset,
					      int *value)
{
	return i2c_read16(chg_chips[chgnum].i2c_port,
			  chg_chips[chgnum].i2c_addr_flags,
			  offset, value);
}

static inline enum ec_error_list isl9241_write(int chgnum, int offset,
					       int value)
{
	return i2c_write16(chg_chips[chgnum].i2c_port,
			   chg_chips[chgnum].i2c_addr_flags,
			   offset, value);
}

static inline enum ec_error_list isl9241_update(int chgnum, int offset,
						uint16_t mask,
						enum mask_update_action action)
{
	return i2c_update16(chg_chips[chgnum].i2c_port,
			    chg_chips[chgnum].i2c_addr_flags,
			    offset, mask, action);
}

/* chip specific interfaces */

/*****************************************************************************/
/* Charger interfaces */
static enum ec_error_list isl9241_set_input_current_limit(int chgnum,
							  int input_current)
{
	int rv;
	uint16_t reg = AC_CURRENT_TO_REG(input_current);

	rv = isl9241_write(chgnum, ISL9241_REG_ADAPTER_CUR_LIMIT1, reg);
	if (rv)
		return rv;

	return isl9241_write(chgnum, ISL9241_REG_ADAPTER_CUR_LIMIT2, reg);
}

static enum ec_error_list isl9241_get_input_current_limit(int chgnum,
							  int *input_current)
{
	int rv;

	rv = isl9241_read(chgnum, ISL9241_REG_ADAPTER_CUR_LIMIT1,
			  input_current);
	if (rv)
		return rv;

	*input_current = AC_REG_TO_CURRENT(*input_current);
	return EC_SUCCESS;
}

static enum ec_error_list isl9241_manufacturer_id(int chgnum, int *id)
{
	return isl9241_read(chgnum, ISL9241_REG_MANUFACTURER_ID, id);
}

static enum ec_error_list isl9241_device_id(int chgnum, int *id)
{
	return isl9241_read(chgnum, ISL9241_REG_DEVICE_ID, id);
}

static enum ec_error_list isl9241_get_option(int chgnum, int *option)
{
	int rv;
	uint32_t controls;
	int reg;

	rv = isl9241_read(chgnum, ISL9241_REG_CONTROL0, &reg);
	if (rv)
		return rv;

	controls = reg;
	rv = isl9241_read(chgnum, ISL9241_REG_CONTROL1, &reg);
	if (rv)
		return rv;

	controls |= reg << 16;
	*option = controls;
	return EC_SUCCESS;
}

static enum ec_error_list isl9241_set_option(int chgnum, int option)
{
	int rv;

	rv = isl9241_write(chgnum, ISL9241_REG_CONTROL0, option & 0xFFFF);
	if (rv)
		return rv;

	return isl9241_write(chgnum, ISL9241_REG_CONTROL1,
			     (option >> 16) & 0xFFFF);
}

static const struct charger_info *isl9241_get_info(int chgnum)
{
	return &isl9241_charger_info;
}

static enum ec_error_list isl9241_get_status(int chgnum, int *status)
{
	int rv;
	int reg;

	/* Level 2 charger */
	*status = CHARGER_LEVEL_2;

	/* Charge inhibit status */
	rv = isl9241_read(chgnum, ISL9241_REG_MIN_SYSTEM_VOLTAGE, &reg);
	if (rv)
		return rv;
	if (!reg)
		*status |= CHARGER_CHARGE_INHIBITED;

	/* Battery present & AC present status */
	rv = isl9241_read(chgnum, ISL9241_REG_INFORMATION2, &reg);
	if (rv)
		return rv;
	if (!(reg & ISL9241_INFORMATION2_BATGONE_PIN))
		*status |= CHARGER_BATTERY_PRESENT;
	if (reg & ISL9241_INFORMATION2_ACOK_PIN)
		*status |= CHARGER_AC_PRESENT;

	return EC_SUCCESS;
}

static enum ec_error_list isl9241_set_mode(int chgnum, int mode)
{
	int rv;

	/*
	 * See crosbug.com/p/51196. Always disable learn mode unless it was set
	 * explicitly.
	 */
	if (!learn_mode) {
		rv = isl9241_discharge_on_ac(chgnum, 0);
		if (rv)
			return rv;
	}

	/*
	 * Charger inhibit
	 * MinSystemVoltage 0x00h = disables all battery charging
	 */
	rv = isl9241_write(chgnum, ISL9241_REG_MIN_SYSTEM_VOLTAGE,
		mode & CHARGE_FLAG_INHIBIT_CHARGE ?
		0 : battery_get_info()->voltage_min);
	if (rv)
		return rv;

	/* POR reset */
	if (mode & CHARGE_FLAG_POR_RESET) {
		rv = isl9241_write(chgnum, ISL9241_REG_CONTROL3,
			ISL9241_CONTROL3_DIGITAL_RESET);
	}

	return rv;
}

static enum ec_error_list isl9241_get_current(int chgnum, int *current)
{
	int rv;

	rv = isl9241_read(chgnum, ISL9241_REG_CHG_CURRENT_LIMIT, current);
	if (rv)
		return rv;

	*current = BC_REG_TO_CURRENT(*current);
	return EC_SUCCESS;
}

static enum ec_error_list isl9241_set_current(int chgnum, int current)
{
	return isl9241_write(chgnum, ISL9241_REG_CHG_CURRENT_LIMIT,
				BC_CURRENT_TO_REG(current));
}

static enum ec_error_list isl9241_get_voltage(int chgnum, int *voltage)
{
	return isl9241_read(chgnum, ISL9241_REG_MAX_SYSTEM_VOLTAGE, voltage);
}

static enum ec_error_list isl9241_set_voltage(int chgnum, int voltage)
{
	return isl9241_write(chgnum, ISL9241_REG_MAX_SYSTEM_VOLTAGE, voltage);
}

static enum ec_error_list isl9241_get_vbus_voltage(int chgnum, int port,
						   int *voltage)
{
	int adc_val = 0;
	int ctl3_val;
	int rv;

	/* Get current Control3 value */
	rv = isl9241_read(chgnum, ISL9241_REG_CONTROL3, &ctl3_val);
	if (rv)
		goto error;

	/* Enable ADC */
	if (!(ctl3_val & ISL9241_CONTROL3_ENABLE_ADC)) {
		rv = isl9241_write(chgnum, ISL9241_REG_CONTROL3,
				   ctl3_val | ISL9241_CONTROL3_ENABLE_ADC);
		if (rv)
			goto error;
	}

	/* Read voltage ADC value */
	rv = isl9241_read(chgnum, ISL9241_REG_VIN_ADC_RESULTS, &adc_val);
	if (rv)
		goto error_restore_ctl3;

	/*
	 * Adjust adc_val
	 *
	 * raw adc_val has VIN_ADC in bits [13:6], so shift this down
	 * this puts adc_val in the range of 0..255, which maps to 0..24.48V
	 * each step in adc_val is 96mv
	 */
	adc_val >>= ISL9241_VIN_ADC_BIT_OFFSET;
	adc_val *= ISL9241_VIN_ADC_STEP_MV;
	*voltage = adc_val;

error_restore_ctl3:
	/* Restore Control3 value */
	if (!(ctl3_val & ISL9241_CONTROL3_ENABLE_ADC))
		(void)isl9241_write(chgnum, ISL9241_REG_CONTROL3, ctl3_val);

error:
	if (rv)
		CPRINTF("Could not read VBUS ADC! Error: %d\n", rv);

	return rv;
}

static enum ec_error_list isl9241_post_init(int chgnum)
{
	return EC_SUCCESS;
}

static enum ec_error_list isl9241_discharge_on_ac(int chgnum, int enable)
{
	int rv;

	mutex_lock(&control1_mutex);

	rv = isl9241_update(chgnum, ISL9241_REG_CONTROL1,
			    ISL9241_CONTROL1_LEARN_MODE,
			    (enable) ? MASK_SET : MASK_CLR);
	if (!rv)
		learn_mode = enable;

	mutex_unlock(&control1_mutex);
	return rv;
}

int isl9241_set_ac_prochot(int chgnum, int ma)
{
	int rv;
	uint16_t reg;

	/*
	 * The register reserves bits [6:0] and bits [15:13].
	 * This routine should ensure these bits are not set
	 * before writing the register.
	 */
	if (ma > AC_REG_TO_CURRENT(ISL9241_AC_PROCHOT_CURRENT_MAX))
		reg = ISL9241_AC_PROCHOT_CURRENT_MAX;
	else if (ma < AC_REG_TO_CURRENT(ISL9241_AC_PROCHOT_CURRENT_MIN))
		reg = ISL9241_AC_PROCHOT_CURRENT_MIN;
	else
		reg = AC_CURRENT_TO_REG(ma);

	rv = isl9241_write(chgnum, ISL9241_REG_AC_PROCHOT, reg);
	if (rv)
		CPRINTF("set_ac_prochot failed (%d)\n", rv);

	return rv;
}

int isl9241_set_dc_prochot(int chgnum, int ma)
{
	int rv;

	/*
	 * The register reserves bits [7:0] and bits [15:14].
	 * This routine should ensure these bits are not set
	 * before writing the register.
	 */
	if (ma > ISL9241_DC_PROCHOT_CURRENT_MAX)
		ma = ISL9241_DC_PROCHOT_CURRENT_MAX;
	else if (ma < ISL9241_DC_PROCHOT_CURRENT_MIN)
		ma = ISL9241_DC_PROCHOT_CURRENT_MIN;

	rv = isl9241_write(chgnum, ISL9241_REG_DC_PROCHOT, ma);
	if (rv)
		CPRINTF("set_dc_prochot failed (%d)\n", rv);

	return rv;
}

/*****************************************************************************/
/* ISL-9241 initialization */
static void isl9241_init(int chgnum)
{
#ifdef CONFIG_ISL9241_SWITCHING_FREQ
	int ctl_val;
#endif

	const struct battery_info *bi = battery_get_info();

	/*
	 * Set the MaxSystemVoltage to battery maximum,
	 * 0x00=disables switching charger states
	 */
	if (isl9241_write(chgnum, ISL9241_REG_MAX_SYSTEM_VOLTAGE,
			  bi->voltage_max))
		goto init_fail;

	/*
	 * Set the MinSystemVoltage to battery minimum,
	 * 0x00=disables all battery charging
	 */
	if (isl9241_write(chgnum, ISL9241_REG_MIN_SYSTEM_VOLTAGE,
			  bi->voltage_min))
		goto init_fail;

	/*
	 * Set control2 register to
	 * [15:13]: Trickle Charging Current (battery pre-charge current)
	 * [10:9] : Prochot# Debounce time (1000us)
	 */
	if (isl9241_update(chgnum, ISL9241_REG_CONTROL2,
			   (ISL9241_CONTROL2_TRICKLE_CHG_CURR(
				bi->precharge_current) |
			    ISL9241_CONTROL2_PROCHOT_DEBOUNCE_1000),
			   MASK_SET))
		goto init_fail;

	/*
	 * Set control3 register to
	 * [14]: ACLIM Reload (Do not reload)
	 */
	if (isl9241_update(chgnum, ISL9241_REG_CONTROL3,
			   ISL9241_CONTROL3_ACLIM_RELOAD,
			   MASK_SET))
		goto init_fail;

	/*
	 * Set control4 register to
	 * [13]: Slew rate control enable (sets VSYS ramp to 8mV/us)
	 */
	if (isl9241_update(chgnum, ISL9241_REG_CONTROL4,
			   ISL9241_CONTROL4_SLEW_RATE_CTRL,
			   MASK_SET))
		goto init_fail;

#ifndef CONFIG_CHARGE_RAMP_HW
	if (isl9241_update(chgnum, ISL9241_REG_CONTROL0,
			   ISL9241_CONTROL0_INPUT_VTG_REGULATION,
			   MASK_SET))
		goto init_fail;
#endif

#ifdef CONFIG_ISL9241_SWITCHING_FREQ
	if (isl9241_read(chgnum, ISL9241_REG_CONTROL1, &ctl_val))
		goto init_fail;
	ctl_val &= ~ISL9241_CONTROL1_SWITCHING_FREQ_MASK;
	ctl_val |= ((CONFIG_ISL9241_SWITCHING_FREQ << 7) &
		     ISL9241_CONTROL1_SWITCHING_FREQ_MASK);
	if (isl9241_write(chgnum, ISL9241_REG_CONTROL1, ctl_val))
		goto init_fail;
#endif

	/*
	 * No need to proceed with the rest of init if we sysjump'd to this
	 * image as the input current limit has already been set.
	 */
	if (system_jumped_late())
		return;

	/* Initialize the input current limit to the board's default. */
	if (isl9241_set_input_current_limit(chgnum,
					    CONFIG_CHARGER_INPUT_CURRENT))
		goto init_fail;

	return;

init_fail:
	CPRINTF("ISL9241_init failed!\n");
}

/*****************************************************************************/
/* Hardware current ramping */

#ifdef CONFIG_CHARGE_RAMP_HW
static enum ec_error_list isl9241_set_hw_ramp(int chgnum, int enable)
{
	/* HW ramp is controlled by input voltage regulation reference bits */
	return isl9241_update(chgnum, ISL9241_REG_CONTROL0,
			      ISL9241_CONTROL0_INPUT_VTG_REGULATION,
			      (enable) ? MASK_CLR : MASK_SET);
}

static int isl9241_ramp_is_stable(int chgnum)
{
	/*
	 * Since ISL cannot read the current limit that the ramp has settled
	 * on, then we can never consider the ramp stable, because we never
	 * know what the stable limit is.
	 */
	return 0;
}

static int isl9241_ramp_is_detected(int chgnum)
{
	return 1;
}

static int isl9241_ramp_get_current_limit(int chgnum)
{
	int reg;

	if (isl9241_read(chgnum, ISL9241_REG_IADP_ADC_RESULTS, &reg))
		return 0;

	/* LSB value of register = 22.2mA */
	return (reg * 222) / 10;
}
#endif /* CONFIG_CHARGE_RAMP_HW */

/*
 * When fully charged in a low-power state, the ISL9241 may get stuck
 * in CCM. Toggle learning mode for 50 ms to enter DCM and save power.
 * This is a workaround provided by Renesas. See b/183771327.
 * Note: the charger_get_state() returns the last known charge value,
 * so need to check the battery is not disconnected when the system
 * comes from the battery cutoff.
 */
static void isl9241_restart_charge_voltage_when_full(void)
{
	if (!chipset_in_or_transitioning_to_state(CHIPSET_STATE_ON)
	    && charge_get_state() == PWR_STATE_CHARGE_NEAR_FULL
	    && battery_get_disconnect_state() == BATTERY_NOT_DISCONNECTED) {
		charger_discharge_on_ac(1);
		msleep(50);
		charger_discharge_on_ac(0);
	}
}
DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE,
	     isl9241_restart_charge_voltage_when_full,
	     HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND,
	     isl9241_restart_charge_voltage_when_full,
	     HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN,
	     isl9241_restart_charge_voltage_when_full,
	     HOOK_PRIO_DEFAULT);

/*****************************************************************************/
#ifdef CONFIG_CMD_CHARGER_DUMP
static void dump_reg_range(int chgnum, int low, int high)
{
	int reg;
	int regval;
	int rv;

	for (reg = low; reg <= high; reg++) {
		CPRINTF("[%Xh] = ", reg);
		rv = isl9241_read(chgnum, reg, &regval);
		if (!rv)
			CPRINTF("0x%04x\n", regval);
		else
			CPRINTF("ERR (%d)\n", rv);
		cflush();
	}
}

static int command_isl9241_dump(int argc, char **argv)
{
	char *e;
	int chgnum = 0;

	if (argc >= 2) {
		chgnum = strtoi(argv[1], &e, 10);
		if (*e)
			return EC_ERROR_PARAM1;
	}

	dump_reg_range(chgnum, 0x14, 0x15);
	dump_reg_range(chgnum, 0x38, 0x40);
	dump_reg_range(chgnum, 0x43, 0x43);
	dump_reg_range(chgnum, 0x47, 0x4F);
	dump_reg_range(chgnum, 0x80, 0x87);
	dump_reg_range(chgnum, 0x90, 0x91);
	dump_reg_range(chgnum, 0xFE, 0xFF);

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(charger_dump, command_isl9241_dump,
			"charger_dump <chgnum>",
			"Dumps ISL9241 registers");
#endif /* CONFIG_CMD_CHARGER_DUMP */

const struct charger_drv isl9241_drv = {
	.init = &isl9241_init,
	.post_init = &isl9241_post_init,
	.get_info = &isl9241_get_info,
	.get_status = &isl9241_get_status,
	.set_mode = &isl9241_set_mode,
	.get_current = &isl9241_get_current,
	.set_current = &isl9241_set_current,
	.get_voltage = &isl9241_get_voltage,
	.set_voltage = &isl9241_set_voltage,
	.discharge_on_ac = &isl9241_discharge_on_ac,
	.get_vbus_voltage = &isl9241_get_vbus_voltage,
	.set_input_current_limit = &isl9241_set_input_current_limit,
	.get_input_current_limit = &isl9241_get_input_current_limit,
	.manufacturer_id = &isl9241_manufacturer_id,
	.device_id = &isl9241_device_id,
	.get_option = &isl9241_get_option,
	.set_option = &isl9241_set_option,
#ifdef CONFIG_CHARGE_RAMP_HW
	.set_hw_ramp = &isl9241_set_hw_ramp,
	.ramp_is_stable = &isl9241_ramp_is_stable,
	.ramp_is_detected = &isl9241_ramp_is_detected,
	.ramp_get_current_limit = &isl9241_ramp_get_current_limit,
#endif
};
