/* Copyright 2015 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.
 *
 * TI bq25890/bq25892/bq25895 battery charger driver.
 */

#include "config.h"
#include "bq2589x.h"
#include "charger.h"
#include "common.h"
#include "console.h"
#include "hooks.h"
#include "i2c.h"
#include "printf.h"
#include "util.h"

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

/* 5V Boost settings */
#ifndef CONFIG_CHARGER_BQ2589X_BOOST
#define CONFIG_CHARGER_BQ2589X_BOOST BQ2589X_BOOST_DEFAULT
#endif

/* IR compensation settings */
#ifndef CONFIG_CHARGER_BQ2589X_IR_COMP
#define CONFIG_CHARGER_BQ2589X_IR_COMP BQ2589X_IR_COMP_DEFAULT
#endif

/* Termination current limit setting */
#ifndef CONFIG_CHARGER_TERM_CURRENT_LIMIT
#define CONFIG_CHARGER_TERM_CURRENT_LIMIT BQ2589X_TERM_CURRENT_LIMIT_DEFAULT
#endif

/* Charger information */
static const struct charger_info bq2589x_charger_info = {
	.name         = "bq2589x",
	.voltage_max  = 4608,
	.voltage_min  = 3840,
	.voltage_step = 16,
	.current_max  = 5056,
	.current_min  = 0,
	.current_step = 64,
	.input_current_max  = 3250,
	.input_current_min  = 100,
	.input_current_step = 50,
};

static int bq2589x_read(int reg, int *value)
{
	return i2c_read8(I2C_PORT_CHARGER, BQ2589X_ADDR, reg, value);
}

static int bq2589x_write(int reg, int value)
{
	return i2c_write8(I2C_PORT_CHARGER, BQ2589X_ADDR, reg, value);
}

static int bq2589x_watchdog_reset(void)
{
	int rv, val;

	rv = bq2589x_read(BQ2589X_REG_CFG2, &val);
	if (rv)
		return rv;
	val |= BQ2589X_CFG2_WD_RST;
	return bq2589x_write(BQ2589X_REG_CFG2, val);
}

static int bq2589x_set_terminate_current(int current)
{
	int reg_val, rv;
	int val = (current - 64) / 64;

	rv = bq2589x_read(BQ2589X_REG_PRE_CHG_CURR, &reg_val);
	if (rv)
		return rv;
	reg_val = (reg_val & ~0xf) | (val & 0xf);
	return bq2589x_write(BQ2589X_REG_PRE_CHG_CURR, reg_val);
}

#ifdef CONFIG_CHARGER_OTG
int charger_enable_otg_power(int enabled)
{
	int val, rv;

	rv = bq2589x_read(BQ2589X_REG_CFG2, &val);
	if (rv)
		return rv;
	val = (val & ~(BQ2589X_CFG2_CHG_CONFIG | BQ2589X_CFG2_OTG_CONFIG))
	    | (enabled ? BQ2589X_CFG2_OTG_CONFIG : BQ2589X_CFG2_CHG_CONFIG);
	return bq2589x_write(BQ2589X_REG_CFG2, val);
}
#endif

int charger_set_input_current(int input_current)
{
	int value, rv;
	const struct charger_info * const info = charger_get_info();

	input_current -= info->input_current_min;
	if (input_current < 0)
		input_current = 0;

	rv = bq2589x_read(BQ2589X_REG_INPUT_CURR, &value);
	if (rv)
		return rv;
	value = value & ~(0x3f);
	value |= (input_current / info->input_current_step) & 0x3f;
	return bq2589x_write(BQ2589X_REG_INPUT_CURR, value);
}

int charger_get_input_current(int *input_current)
{
	int rv, value;
	const struct charger_info * const info = charger_get_info();

	rv = bq2589x_read(BQ2589X_REG_INPUT_CURR, &value);
	if (rv)
		return rv;

	*input_current = (value & 0x3f) * info->input_current_step
			+ info->input_current_min;

	return EC_SUCCESS;
}

int charger_manufacturer_id(int *id)
{
	return EC_ERROR_UNIMPLEMENTED;
}

int charger_device_id(int *id)
{
	int res = bq2589x_read(BQ2589X_REG_ID, id);

	if (res == EC_SUCCESS)
		*id &= BQ2589X_DEVICE_ID_MASK;

	return res;
}

int charger_get_option(int *option)
{
	/* Ignored: does not exist */
	*option = 0;
	return EC_SUCCESS;
}

int charger_set_option(int option)
{
	/* Ignored: does not exist */
	return EC_SUCCESS;
}

const struct charger_info *charger_get_info(void)
{
	return &bq2589x_charger_info;
}

int charger_get_status(int *status)
{
	/* TODO(crosbug.com/p/38603) implement using REG0C value */
	*status = 0;
	return EC_SUCCESS;
}

int charger_set_mode(int mode)
{
	return EC_SUCCESS;
}

int charger_get_current(int *current)
{
	int rv, val;
	const struct charger_info * const info = charger_get_info();

	rv = bq2589x_read(BQ2589X_REG_CHG_CURR, &val);
	if (rv)
		return rv;
	*current = val * info->current_step + info->current_min;
	return EC_SUCCESS;
}

int charger_set_current(int current)
{
	const struct charger_info * const info = charger_get_info();

	current = charger_closest_current(current);

	return bq2589x_write(BQ2589X_REG_CHG_CURR,
			     current / info->current_step);
}

int charger_get_voltage(int *voltage)
{
	int rv, val;
	const struct charger_info * const info = charger_get_info();

	rv = bq2589x_read(BQ2589X_REG_CHG_VOLT, &val);
	if (rv)
		return rv;
	val = (val >> 2) & 0x3f;
	*voltage = val * info->voltage_step + info->voltage_min;
	return EC_SUCCESS;
}

int charger_set_voltage(int voltage)
{
	int rv, val;
	const struct charger_info * const info = charger_get_info();

	voltage = charger_closest_voltage(voltage);

	rv = bq2589x_read(BQ2589X_REG_CHG_VOLT, &val);
	if (rv)
		return rv;
	val = val & 0x3;
	val |= ((voltage - info->voltage_min) / info->voltage_step) << 2;
	return bq2589x_write(BQ2589X_REG_CHG_VOLT, val);
}

int charger_discharge_on_ac(int enable)
{
	int rv, val;

	rv = bq2589x_read(BQ2589X_REG_INPUT_CURR, &val);
	if (rv)
		return rv;

	if (enable)
		val |= BQ2589X_INPUT_CURR_EN_HIZ;
	else
		val &= ~BQ2589X_INPUT_CURR_EN_HIZ;

	return bq2589x_write(BQ2589X_REG_INPUT_CURR, val);
}

/* Charging power state initialization */
int charger_post_init(void)
{
#ifdef CONFIG_CHARGER_ILIM_PIN_DISABLED
	int val, rv;
	/* Ignore ILIM pin value */
	rv = bq2589x_read(BQ2589X_REG_INPUT_CURR, &val);
	if (rv)
		return rv;
	val &= ~BQ2589X_INPUT_CURR_EN_ILIM;
	rv = bq2589x_write(BQ2589X_REG_INPUT_CURR, val);
	if (rv)
		return rv;
#endif /* CONFIG_CHARGER_ILIM_PIN_DISABLED */

	/* Input current controlled by extpower module. Do nothing here. */
	return EC_SUCCESS;
}

/*****************************************************************************/
/* Hardware current ramping (aka ICO: Input Current Optimizer) */

#ifdef CONFIG_CHARGE_RAMP_HW
int charger_set_hw_ramp(int enable)
{
	int val, rv;

	rv = i2c_read8(I2C_PORT_CHARGER, BQ2589X_ADDR, BQ2589X_REG_CFG1, &val);
	if (rv)
		return rv;

	if (enable)
		val |= BQ2589X_CFG1_ICO_EN;
	else
		val &= ~BQ2589X_CFG1_ICO_EN;

	return i2c_write8(I2C_PORT_CHARGER, BQ2589X_ADDR, BQ2589X_REG_CFG1,
			  val);
}

int chg_ramp_is_stable(void)
{
	int val, rv;

	rv = bq2589x_read(BQ2589X_REG_ID, &val);
	if (!rv && (val & BQ2589X_ID_ICO_OPTIMIZED))
		return 1;
	else
		return 0;
}

int chg_ramp_is_detected(void)
{
	return 1;
}

int chg_ramp_get_current_limit(void)
{
	int input_ma, rv;

	rv = bq2589x_read(BQ2589X_REG_ADC_INPUT_CURR, &input_ma);

	return rv ? -1 : 100 + (input_ma & 0x3f) * 50;
}
#endif /* CONFIG_CHARGE_RAMP_HW */

/*****************************************************************************/
/* Hooks */

static void bq2589x_init(void)
{
	int val;

	if (charger_device_id(&val) || val != BQ2589X_DEVICE_ID) {
		CPRINTF("BQ2589X incorrent ID: 0x%02x\n", val);
		return;
	}

	/*
	 * Disable I2C watchdog timer.
	 *
	 * TODO(crosbug.com/p/38603): Re-enable watchdog timer and kick it
	 * periodically in charger task.
	 */
	if (bq2589x_read(BQ2589X_REG_TIMER, &val))
		return;
	val &= ~0x30;
	if (bq2589x_write(BQ2589X_REG_TIMER, val))
		return;

	if (bq2589x_set_terminate_current(CONFIG_CHARGER_TERM_CURRENT_LIMIT))
		return;

	if (bq2589x_watchdog_reset())
		return;

	if (bq2589x_write(BQ2589X_REG_IR_COMP, CONFIG_CHARGER_BQ2589X_IR_COMP))
		return;

	if (bq2589x_write(BQ2589X_REG_BOOST_MODE, CONFIG_CHARGER_BQ2589X_BOOST))
		return;

	CPRINTF("BQ2589%c initialized\n",
		BQ2589X_DEVICE_ID == BQ25890_DEVICE_ID ? '0' :
		(BQ2589X_DEVICE_ID == BQ25895_DEVICE_ID ? '5' : '2'));
}
DECLARE_HOOK(HOOK_INIT, bq2589x_init, HOOK_PRIO_LAST);

/*****************************************************************************/
/* Console commands */
#ifdef CONFIG_CMD_CHARGER
static int command_bq2589x(int argc, char **argv)
{
	int i;
	int value;
	int rv;
	int batt_mv, sys_mv, vbus_mv, chg_ma, input_ma;

	/* Trigger one ADC conversion */
	bq2589x_read(BQ2589X_REG_CFG1, &value);
	bq2589x_write(BQ2589X_REG_CFG1, value | BQ2589X_CFG1_CONV_START);
	do {
		bq2589x_read(BQ2589X_REG_CFG1, &value);
	} while (value & BQ2589X_CFG1_CONV_START); /* Wait for End of Conv. */

	bq2589x_read(BQ2589X_REG_ADC_BATT_VOLT, &batt_mv);
	bq2589x_read(BQ2589X_REG_ADC_SYS_VOLT, &sys_mv);
	bq2589x_read(BQ2589X_REG_ADC_VBUS_VOLT, &vbus_mv);
	bq2589x_read(BQ2589X_REG_ADC_CHG_CURR, &chg_ma);
	bq2589x_read(BQ2589X_REG_ADC_INPUT_CURR, &input_ma);
	ccprintf("ADC Batt %dmV Sys %dmV VBUS %dmV Chg %dmA Input %dmA\n",
		2304 + (batt_mv & 0x7f) * 20, 2304 + sys_mv * 20,
		2600 + (vbus_mv & 0x7f) * 100,
		chg_ma * 50, 100 + (input_ma & 0x3f) * 50);

	ccprintf("REG:");
	for (i = 0; i <= 0x14; ++i)
		ccprintf(" %02x", i);
	ccprintf("\n");

	ccprintf("VAL:");
	for (i = 0; i <= 0x14; ++i) {
		rv = bq2589x_read(i, &value);
		if (rv)
			return rv;
		ccprintf(" %02x", value);
	}
	ccprintf("\n");

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(bq2589x, command_bq2589x,
			NULL, NULL);
#endif
