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

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

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

/* Maximum time we allow for an ADC conversion */
#define ADC_TIMEOUT_US SECOND

static volatile task_id_t task_waiting_on_ss[LM4_ADC_SEQ_COUNT];

static void configure_gpio(void)
{
	int i, port, mask;

	/* Use analog function for AIN */
	for (i = 0; i < ADC_CH_COUNT; ++i) {
		if (adc_channels[i].gpio_mask) {
			mask = adc_channels[i].gpio_mask;
			port = adc_channels[i].gpio_port;
			LM4_GPIO_DEN(port) &= ~mask;
			LM4_GPIO_AMSEL(port) |= mask;
		}
	}
}

/**
 * Flush an ADC sequencer and initiate a read.
 *
 * @param seq		Sequencer to read
 * @return Raw ADC value.
 */
static int flush_and_read(enum lm4_adc_sequencer seq)
{
	/*
	 * This is currently simple because we can dedicate a sequencer to each
	 * ADC channel.  If we have enough channels that's no longer possible,
	 * this code will need to become more complex.  For example, we could:
	 *
	 * 1) Read them all using a timer interrupt, and then return the most
	 * recent value?  This is lowest-latency for the caller, but won't
	 * return accurate data if read frequently.
	 *
	 * 2) Reserve SS3 for reading a single value, and configure it on each
	 * read?  Needs mutex if we could have multiple callers; doesn't matter
	 * if just used for debugging.
	 *
	 * 3) Both?
	 */
	volatile uint32_t scratch  __attribute__((unused));
	int event;

	/* Empty the FIFO of any previous results */
	while (!(LM4_ADC_SSFSTAT(seq) & 0x100))
		scratch = LM4_ADC_SSFIFO(seq);

	/*
	 * This assumes we don't have multiple tasks accessing the same
	 * sequencer. Add mutex lock if needed.
	 */
	task_waiting_on_ss[seq] = task_get_current();

	/* Clear the interrupt status */
	LM4_ADC_ADCISC |= 0x01 << seq;

	/* Enable interrupt */
	LM4_ADC_ADCIM |= 0x01 << seq;

	/* Initiate sample sequence */
	LM4_ADC_ADCPSSI |= 0x01 << seq;

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

	/* Disable interrupt */
	LM4_ADC_ADCIM &= ~(0x01 << seq);

	task_waiting_on_ss[seq] = TASK_ID_INVALID;

	if (!(event & TASK_EVENT_ADC_DONE))
		return ADC_READ_ERROR;

	/* Read the FIFO and convert to temperature */
	return LM4_ADC_SSFIFO(seq);
}

/**
 * Configure an ADC sequencer to be dedicated for an ADC input.
 *
 * @param seq		Sequencer to configure
 * @param ain_id	ADC input to use
 * @param ssctl		Value for sampler sequencer control register
 *
 */
static void adc_configure(const struct adc_t *adc)
{
	const enum lm4_adc_sequencer seq = adc->sequencer;

	/* Configure sample sequencer */
	LM4_ADC_ADCACTSS &= ~(0x01 << seq);

	/* Trigger sequencer by processor request */
	LM4_ADC_ADCEMUX = (LM4_ADC_ADCEMUX & ~(0xf << (seq * 4))) | 0x00;

	/* Sample internal temp sensor */
	if (adc->channel == LM4_AIN_NONE) {
		LM4_ADC_SSMUX(seq) = 0x00;
		LM4_ADC_SSEMUX(seq) = 0x00;
	} else {
		LM4_ADC_SSMUX(seq) = adc->channel & 0xf;
		LM4_ADC_SSEMUX(seq) = adc->channel >> 4;
	}
	LM4_ADC_SSCTL(seq) = adc->flag;

	/* Enable sample sequencer */
	LM4_ADC_ADCACTSS |= 0x01 << seq;
}

int adc_read_channel(enum adc_channel ch)
{
	const struct adc_t *adc = adc_channels + ch;
	static uint32_t ch_busy_mask;
	static struct mutex adc_clock;
	int rv;

	/*
	 * TODO(crbug.com/314121): Generalize ADC reads such that any task can
	 * trigger a read of any channel.
	 */

	/*
	 * Enable ADC clock and set a bit in ch_busy_mask to signify that this
	 * channel is busy. Note, this function may be called from multiple
	 * tasks, but each channel may be read by only one task. If assert
	 * fails, then it means multiple tasks are trying to read same channel.
	 */
	mutex_lock(&adc_clock);
	ASSERT(!(ch_busy_mask & (1UL << ch)));
	clock_enable_peripheral(CGC_OFFSET_ADC, 0x1,
			CGC_MODE_RUN | CGC_MODE_SLEEP);
	ch_busy_mask |= (1UL << ch);
	mutex_unlock(&adc_clock);

	rv = flush_and_read(adc->sequencer);

	/*
	 * If no ADC channels are busy, then disable ADC clock to conserve
	 * power.
	 */
	mutex_lock(&adc_clock);
	ch_busy_mask &= ~(1UL << ch);
	if (!ch_busy_mask)
		clock_disable_peripheral(CGC_OFFSET_ADC, 0x1,
			CGC_MODE_RUN | CGC_MODE_SLEEP);
	mutex_unlock(&adc_clock);

	if (rv == ADC_READ_ERROR)
		return ADC_READ_ERROR;

	return rv * adc->factor_mul / adc->factor_div + adc->shift;
}

/*****************************************************************************/
/* Interrupt handlers */

/**
 * Handle an interrupt on the specified sample sequencer.
 */
static void handle_interrupt(int ss)
{
	int id = task_waiting_on_ss[ss];

	/* Clear the interrupt status */
	LM4_ADC_ADCISC = (0x1 << ss);

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

static void ss0_interrupt(void) { handle_interrupt(0); }
static void ss1_interrupt(void) { handle_interrupt(1); }
static void ss2_interrupt(void) { handle_interrupt(2); }
static void ss3_interrupt(void) { handle_interrupt(3); }

DECLARE_IRQ(LM4_IRQ_ADC0_SS0, ss0_interrupt, 2);
DECLARE_IRQ(LM4_IRQ_ADC0_SS1, ss1_interrupt, 2);
DECLARE_IRQ(LM4_IRQ_ADC0_SS2, ss2_interrupt, 2);
DECLARE_IRQ(LM4_IRQ_ADC0_SS3, ss3_interrupt, 2);

/*****************************************************************************/
/* Console commands */

#ifdef CONFIG_CMD_ECTEMP
static int command_ectemp(int argc, char **argv)
{
	int t = adc_read_channel(ADC_CH_EC_TEMP);
	ccprintf("EC temperature is %d K = %d C\n", t, K_TO_C(t));
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(ectemp, command_ectemp,
			NULL,
			"Print EC temperature");
#endif

/*****************************************************************************/
/* Initialization */

static void adc_init(void)
{
	int i;

	/* Configure GPIOs */
	configure_gpio();

	/*
	 * Temporarily enable the PLL when turning on the clock to the ADC
	 * module, to work around chip errata (10.4).  No need to notify
	 * other modules; the PLL isn't enabled long enough to matter.
	 */
	clock_enable_pll(1, 0);

	/* Enable ADC0 module in run and sleep modes. */
	clock_enable_peripheral(CGC_OFFSET_ADC, 0x1,
			CGC_MODE_RUN | CGC_MODE_SLEEP);

	/*
	 * Use external voltage references (VREFA+, VREFA-) instead of
	 * VDDA and GNDA.
	 */
	LM4_ADC_ADCCTL = 0x01;

	/* Use internal oscillator */
	LM4_ADC_ADCCC = 0x1;

	/* Disable the PLL now that the ADC is using the internal oscillator */
	clock_enable_pll(0, 0);

	/* No tasks waiting yet */
	for (i = 0; i < LM4_ADC_SEQ_COUNT; i++)
		task_waiting_on_ss[i] = TASK_ID_INVALID;

	/* Enable IRQs */
	task_enable_irq(LM4_IRQ_ADC0_SS0);
	task_enable_irq(LM4_IRQ_ADC0_SS1);
	task_enable_irq(LM4_IRQ_ADC0_SS2);
	task_enable_irq(LM4_IRQ_ADC0_SS3);

	/* 2**6 = 64x oversampling */
	LM4_ADC_ADCSAC = 6;

	/* Initialize ADC sequencer */
	for (i = 0; i < ADC_CH_COUNT; ++i)
		adc_configure(adc_channels + i);

	/* Disable ADC0 module until it is needed to conserve power. */
	clock_disable_peripheral(CGC_OFFSET_ADC, 0x1,
			CGC_MODE_RUN | CGC_MODE_SLEEP);
}
DECLARE_HOOK(HOOK_INIT, adc_init, HOOK_PRIO_INIT_ADC);
