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

/* NPCX-specific ADC module for Chrome EC */

#include "adc.h"
#include "adc_chip.h"
#include "atomic.h"
#include "clock.h"
#include "clock_chip.h"
#include "console.h"
#include "common.h"
#include "gpio.h"
#include "hooks.h"
#include "registers.h"
#include "system.h"
#include "task.h"
#include "timer.h"
#include "util.h"

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

/* Maximum time we allow for an ADC conversion */
#define ADC_TIMEOUT_US            SECOND
#define ADC_CLK                   2000000
#define ADC_REGULAR_DLY           0x11
#define ADC_REGULAR_ADCCNF2       0x8B07
#define ADC_REGULAR_GENDLY        0x0100
#define ADC_REGULAR_MEAST         0x0001

/* ADC conversion mode */
enum npcx_adc_conversion_mode {
	ADC_CHN_CONVERSION_MODE   = 0,
	ADC_SCAN_CONVERSION_MODE  = 1
};

/* Global variables */
static volatile task_id_t task_waiting;

struct mutex adc_lock;

/**
 * Preset ADC operation clock.
 *
 * @param   none
 * @return  none
 * @notes   changed when initial or HOOK_FREQ_CHANGE command
 */
void adc_freq_changed(void)
{
	uint8_t prescaler_divider    = 0;

	/* Set clock prescaler divider to ADC module*/
	prescaler_divider = (uint8_t)(clock_get_apb1_freq() / ADC_CLK);
	if (prescaler_divider >= 1)
		prescaler_divider = prescaler_divider - 1;
	if (prescaler_divider > 0x3F)
		prescaler_divider = 0x3F;

	/* Set Core Clock Division Factor in order to obtain the ADC clock */
	SET_FIELD(NPCX_ATCTL, NPCX_ATCTL_SCLKDIV_FIELD, prescaler_divider);
}
DECLARE_HOOK(HOOK_FREQ_CHANGE, adc_freq_changed, HOOK_PRIO_DEFAULT);

/**
 * Flush an ADC sequencer and initiate a read.
 *
 * @param   input_ch    operation channel
 * @param   timeout		preset timeout
 * @return  TRUE/FALSE  success/fail
 * @notes   set SW-triggered interrupt conversion and one-shot mode in npcx chip
 */
static int start_single_and_wait(enum npcx_adc_input_channel input_ch
		, int timeout)
{
	int event;

	task_waiting = task_get_current();

	/* Stop ADC conversion first */
	SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_STOP);

	/* Set ADC conversion code to SW conversion mode */
	SET_FIELD(NPCX_ADCCNF, NPCX_ADCCNF_ADCMD_FIELD,
			ADC_CHN_CONVERSION_MODE);

	/* Set conversion type to one-shot type */
	CLEAR_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCRPTC);

	/* Update number of channel to be converted */
	SET_FIELD(NPCX_ASCADD, NPCX_ASCADD_SADDR_FIELD, input_ch);

	/* Clear End-of-Conversion Event status */
	SET_BIT(NPCX_ADCSTS, NPCX_ADCSTS_EOCEV);

	/* Enable ADC End-of-Conversion Interrupt if applicable */
	SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_INTECEN);

	/* Start conversion */
	SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_START);

	/* Wait for interrupt */
	event = task_wait_event_mask(TASK_EVENT_ADC_DONE, timeout);

	task_waiting = TASK_ID_INVALID;

	return (event == TASK_EVENT_ADC_DONE);

}

static uint16_t repetitive_enabled;
void npcx_set_adc_repetitive(enum npcx_adc_input_channel input_ch, int enable)
{
	mutex_lock(&adc_lock);

	/* Stop ADC conversion */
	SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_STOP);

	if (enable) {
		/* Forbid EC enter deep sleep during conversion. */
		disable_sleep(SLEEP_MASK_ADC);
		/* Turn on ADC */
		SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCEN);
		/* Set ADC conversion code to SW conversion mode */
		SET_FIELD(NPCX_ADCCNF, NPCX_ADCCNF_ADCMD_FIELD,
			  ADC_SCAN_CONVERSION_MODE);
		/* Update number of channel to be converted */
		SET_BIT(NPCX_ADCCS, input_ch);
		/* Set conversion type to repetitive (runs continuously) */
		SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCRPTC);
		repetitive_enabled |= BIT(input_ch);

		/* Start conversion */
		SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_START);
	} else {
		CLEAR_BIT(NPCX_ADCCS, input_ch);
		repetitive_enabled &= ~BIT(input_ch);

		if (!repetitive_enabled) {
			/* Turn off ADC */
			CLEAR_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCEN);
			/* Set ADC to one-shot mode */
			CLEAR_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCRPTC);
			/* Allow ec enter deep sleep */
			enable_sleep(SLEEP_MASK_ADC);
		} else {
			/* Start conversion again */
			SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_START);
		}
	}

	mutex_unlock(&adc_lock);
}

/**
 * Return the ADC value from CHNDAT register directly.
 *
 * @param   input_ch    channel number
 * @return  ADC data
 */
int adc_read_data(enum npcx_adc_input_channel input_ch)
{
	const struct adc_t *adc = adc_channels + input_ch;
	int value;
	uint16_t chn_data;

	chn_data = NPCX_CHNDAT(adc->input_ch);
	value = GET_FIELD(chn_data, NPCX_CHNDAT_CHDAT_FIELD) *
		 adc->factor_mul / adc->factor_div + adc->shift;
	return value;
}

/**
 * Start a single conversion and return the result
 *
 * @param   ch    operation channel
 * @return  ADC converted voltage or error message
 */
int adc_read_channel(enum adc_channel ch)
{
	const struct adc_t *adc = adc_channels + ch;
	int value;
	uint16_t chn_data;

	mutex_lock(&adc_lock);

	/* Forbid ec enter deep sleep during ADC conversion is proceeding. */
	disable_sleep(SLEEP_MASK_ADC);

	/* Turn on ADC */
	SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCEN);

	if (start_single_and_wait(adc->input_ch, ADC_TIMEOUT_US)) {
		chn_data = NPCX_CHNDAT(adc->input_ch);
		if ((adc->input_ch ==
			GET_FIELD(NPCX_ASCADD, NPCX_ASCADD_SADDR_FIELD))
			&& (IS_BIT_SET(chn_data,
					NPCX_CHNDAT_NEW))) {
			value = GET_FIELD(chn_data, NPCX_CHNDAT_CHDAT_FIELD) *
				adc->factor_mul / adc->factor_div + adc->shift;
		} else {
			value = ADC_READ_ERROR;
		}
	} else {
		value = ADC_READ_ERROR;
	}

	if (!repetitive_enabled) {
		/* Turn off ADC */
		CLEAR_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCEN);
		/* Allow ec enter deep sleep */
		enable_sleep(SLEEP_MASK_ADC);
	} else {
		/* Set ADC conversion code to SW conversion mode */
		SET_FIELD(NPCX_ADCCNF, NPCX_ADCCNF_ADCMD_FIELD,
		  ADC_SCAN_CONVERSION_MODE);
		/* Set conversion type to repetitive (runs continuously) */
		SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_ADCRPTC);
		/* Start conversion */
		SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_START);
	}

	mutex_unlock(&adc_lock);

	return value;
}

/* Board should register these callbacks with npcx_adc_cfg_thresh_int(). */
static void (*adc_thresh_irqs[NPCX_ADC_THRESH_CNT])(void);

void npcx_adc_thresh_int_enable(int threshold_idx, int enable)
{
	uint16_t thrcts;

	enable = !!enable;

	if ((threshold_idx < 1) || (threshold_idx > NPCX_ADC_THRESH_CNT)) {
		CPRINTS("Invalid ADC thresh index! (%d)",
			threshold_idx);
		return;
	}
	threshold_idx--; /* convert to 0-based */

	/* avoid clearing other threshold status */
	thrcts = NPCX_THRCTS & ~GENMASK(NPCX_ADC_THRESH_CNT - 1, 0);

	if (enable) {
		/* clear threshold status */
		SET_BIT(thrcts, threshold_idx);
		/* set enable threshold status */
		SET_BIT(thrcts, NPCX_THRCTS_THR1_IEN + threshold_idx);
	} else {
		CLEAR_BIT(thrcts, NPCX_THRCTS_THR1_IEN + threshold_idx);
	}
	NPCX_THRCTS = thrcts;
}

void npcx_adc_register_thresh_irq(int threshold_idx,
				  const struct npcx_adc_thresh_t *thresh_cfg)
{
	int npcx_adc_ch;
	int raw_val;
	int mul;
	int div;
	int shift;

	if ((threshold_idx < 1) || (threshold_idx > NPCX_ADC_THRESH_CNT)) {
		CPRINTS("Invalid ADC thresh index! (%d)",
			threshold_idx);
		return;
	}
	npcx_adc_ch = adc_channels[thresh_cfg->adc_ch].input_ch;

	if (!thresh_cfg->adc_thresh_cb) {
		CPRINTS("No callback for ADC Threshold %d!",
			threshold_idx);
		return;
	}

	/* Fill in the table */
	adc_thresh_irqs[threshold_idx-1] = thresh_cfg->adc_thresh_cb;

	/* Select the channel */
	SET_FIELD(NPCX_THRCTL(threshold_idx), NPCX_THRCTL_CHNSEL,
		  npcx_adc_ch);

	if (thresh_cfg->lower_or_higher)
		SET_BIT(NPCX_THRCTL(threshold_idx), NPCX_THRCTL_L_H);
	else
		CLEAR_BIT(NPCX_THRCTL(threshold_idx), NPCX_THRCTL_L_H);

	/* Set the threshold value. */
	mul = adc_channels[thresh_cfg->adc_ch].factor_mul;
	div = adc_channels[thresh_cfg->adc_ch].factor_div;
	shift = adc_channels[thresh_cfg->adc_ch].shift;

	raw_val = (thresh_cfg->thresh_assert - shift) * div / mul;
	CPRINTS("ADC THR%d: Setting THRVAL = %d, L_H: %d", threshold_idx,
		raw_val, thresh_cfg->lower_or_higher);
	SET_FIELD(NPCX_THRCTL(threshold_idx), NPCX_THRCTL_THRVAL,
		  raw_val);

#if NPCX_FAMILY_VERSION <= NPCX_FAMILY_NPCX7
	/* Disable deassertion threshold function */
	CLEAR_BIT(NPCX_THR_DCTL(threshold_idx), NPCX_THR_DCTL_THRD_EN);
#endif

	/* Enable threshold detection */
	SET_BIT(NPCX_THRCTL(threshold_idx), NPCX_THRCTL_THEN);
}

/**
 * ADC interrupt handler
 *
 * @param   none
 * @return  none
 * @notes   Only handle SW-triggered conversion in npcx chip
 */
void adc_interrupt(void)
{
	int i;
	uint16_t thrcts;

	if (IS_BIT_SET(NPCX_ADCCNF, NPCX_ADCCNF_INTECEN) &&
	    IS_BIT_SET(NPCX_ADCSTS, NPCX_ADCSTS_EOCEV)) {
		/* Disable End-of-Conversion Interrupt */
		CLEAR_BIT(NPCX_ADCCNF, NPCX_ADCCNF_INTECEN);

		/* Stop conversion for single-shot mode */
		if (!repetitive_enabled)
			SET_BIT(NPCX_ADCCNF, NPCX_ADCCNF_STOP);

		/* Clear End-of-Conversion Event status */
		SET_BIT(NPCX_ADCSTS, NPCX_ADCSTS_EOCEV);

		/* Wake up the task which was waiting for the interrupt */
		if (task_waiting != TASK_ID_INVALID)
			task_set_event(task_waiting, TASK_EVENT_ADC_DONE);
	}

	for (i = NPCX_THRCTS_THR1_STS; i < NPCX_ADC_THRESH_CNT; i++) {
		if (IS_BIT_SET(NPCX_THRCTS, NPCX_THRCTS_THR1_IEN + i) &&
		    IS_BIT_SET(NPCX_THRCTS, i)) {
			/* avoid clearing other threshold status */
			thrcts = NPCX_THRCTS &
					~GENMASK(NPCX_ADC_THRESH_CNT - 1, 0);
			/* Clear threshold status */
			SET_BIT(thrcts, i);
			NPCX_THRCTS = thrcts;
			if (adc_thresh_irqs[i])
				adc_thresh_irqs[i]();
		}
	}
}
DECLARE_IRQ(NPCX_IRQ_ADC, adc_interrupt, 4);

/**
 * ADC initial.
 *
 * @param none
 * @return none
 */
static void adc_init(void)
{
	/* Configure pins from GPIOs to ADCs */
	gpio_config_module(MODULE_ADC, 1);

	/* Enable ADC clock (bit4 mask = 0x10) */
	clock_enable_peripheral(CGC_OFFSET_ADC, CGC_ADC_MASK,
			CGC_MODE_RUN | CGC_MODE_SLEEP);

	/* Set Core Clock Division Factor in order to obtain the ADC clock */
	adc_freq_changed();

	/* Set regular speed */
	SET_FIELD(NPCX_ATCTL, NPCX_ATCTL_DLY_FIELD, (ADC_REGULAR_DLY - 1));

	/* Set the other ADC settings */
	NPCX_ADCCNF2 = ADC_REGULAR_ADCCNF2;
	NPCX_GENDLY = ADC_REGULAR_GENDLY;
	NPCX_MEAST = ADC_REGULAR_MEAST;

	task_waiting = TASK_ID_INVALID;

	/* Enable IRQs */
	task_enable_irq(NPCX_IRQ_ADC);
}
DECLARE_HOOK(HOOK_INIT, adc_init, HOOK_PRIO_INIT_ADC);
