/*
 * adc.c: probe, user config, and user IO (switch and button) reading
 *
 * performs a scanning read on all used ADC inputs and will DMA
 * the results to working variables in one-shot mode. this ADC
 * only uses main voltage as a reference, so scans also include
 * VREFINT for compensation. There is no way to configure a
 * different analog channel (including VREFINT) as the reference
 * voltage - only the analog supply can be the reference.
 * 
 * because most inputs are high impedance, which is not ideal,
 * sampling time is increased. this results in a final ADC sample rate
 * of about 33KHz, still well beyond what is needed in this application.
 * this value may change in the future.
 * 
 * note: attempted to use DMA for this and scanning all channels
 * at once, but that didn't work. would not reliably read channels.
 * tried many different variations and it just wasn't working.
 * because of this I moved to using interrupt and the built-in
 * wait mode just in case things went slow.
 *
 * file creation: 20231015 0121
 */


#include <stdlib.h>

#include "adc.h"
#include "testo.h"



#define SAMPLE_TIME     LL_ADC_SAMPLINGTIME_239CYCLES_5

#define ADC_SEQ_RDY		0xff
#define ADC_SEQ_STARTUP	0xfe

#define CONF_CALIBRATE	(1 << 0)
#define CONF_USEPROBE	(1 << 1)




static uint16_t adc_read[ADC_CHANNELS];
static uint16_t adc_hist[ADC_CHANNELS][ADC_HISTLEN];

uint16_t adc_avg[ADC_CHANNELS];

static uint8_t adc_seq = 0;
static uint8_t adc_idx = ADC_HISTLEN + 1;

static int16_t vref;
static uint8_t calibrate = 0;



void adc_init()
{
    // ensure ADC is DISABLED to write to registers
	// this is opposite of what the RM says... the RM is wrong
    ADC1->CR = 0;
    
    // enable VREFINT
    ADC->CCR = ADC_CCR_VREFEN;	// ADC_CCR_TSEN | 

	// enable end of conversion interrupt
	ADC1->IER = ADC_IER_EOCIE;
	
	// run in one-shot mode (all channel conversion per trigger)
	// with wait mode (anti-overrun) active
	ADC1->CFGR1 = ADC_CFGR1_OVRMOD | ADC_CFGR1_WAIT;

    // configure scan channels, sampling time (11 = temp, 12 = vrefint)
    ADC1->SMPR = SAMPLE_TIME;
	ADC1->CHSELR = CONF_SET1_AN | PROBE_AN | 		// note: SET1 and VREFEXT are
                    CONF_MODE_AN | CONF_SET0_AN |	// shared and reflect one chan
                    ADC_CHSELR_CHSEL12;

	// calibration configuration
    ADC1->CCSR = LL_ADC_CAL_SAMPLINGTIME_8CYCLES | ADC_CCSR_CALSEL;

	// and enable the interrupt source in the NVIC
	NVIC_EnableIRQ(ADC_COMP_IRQn);
    NVIC_SetPriority(ADC_COMP_IRQn, 3);
}

/*
 * internal function for stopping ADC conversion in progress.
 */
void adc_stop()
{
	if (ADC1->CR & ADC_CR_ADSTART) {
		ADC1->CR |= ADC_CR_ADSTP;
		while (ADC1->CR & ADC_CR_ADSTP);
	}
}

/*
 * internal function for setting up the ADC for a read.
 */
void adc_go()
{
	// stop if conversion in progress (it shouldn't ever be)
	adc_stop();

	// restart sequence
	ADC1->CR  = ADC_CR_ADEN;	// enable ADC
	adc_seq = 0;				// reset channel read sequence
	ADC1->ISR = 0x1e;			// clear all interrupt flags (per DS; mistranslated)	
    ADC1->CR  = ADC_CR_ADSTART | ADC_CR_ADEN;	// begin conversion
}

/*
 * when switching AN0 to/from SET1 and VREFEXT, we need to clear
 * old history stored for the first analog input as well as reset the
 * history index to start over. finally we need to trigger the ADC
 * to get fresh data.
 */
void adc_switch0()
{
	uint8_t i;

	adc_read[0] = 0;
	adc_avg[0] = 0;
	for (i = 0; i < ADC_HISTLEN; i++) {
		adc_hist[0][i] = 0;
	}

	// reset history index
	adc_idx = 0;
	
	// force ADC read to get fresh data
	adc_go();
}

uint8_t adc_next()
{
	uint8_t i, j;
	int16_t w;	
		
	// on first boot, do a calibration
	if (adc_idx > ADC_HISTLEN) {
		// nothing to copy when first starting
		adc_idx = 0;
		calibrate = 1;
	}
	
	// start ADC calibration
    if (calibrate) {
		adc_stop();

		// disable ADC and start calibration
        ADC1->CR &= ~(ADC_CR_ADEN);
        ADC1->CR |=   ADC_CR_ADCAL;

		// clear cal flag; reset sequence to allow conversions to start
		calibrate = 0;
		adc_seq = ADC_SEQ_RDY;
		
		// wait for calibration to complete, if started
		while (ADC1->CR & ADC_CR_ADCAL) {};

		// do our first round of conversions
		adc_go();

		return ADC_SEQ_STARTUP;
    }	

	if (adc_seq == ADC_SEQ_RDY) {
		// copy read data to history
		for (i = 0; i < ADC_CHANNELS; i++) {
			adc_hist[i][adc_idx] = adc_read[i];
		}

		// and start over again
		adc_idx++;
		
		// if we're looping, then average our last reads
		if (adc_idx >= ADC_HISTLEN) {
			adc_idx = 0;

			// average old analog data
			for (i = 0; i < ADC_CHANNELS; i++) {
				adc_avg[i] = 0;
				for (j = 0; j < ADC_HISTLEN; j++) {
					adc_avg[i] += adc_hist[i][j];
				}
				adc_avg[i] >>= 4;
			}

			// check vref to determine if we need to recalibrate
			w = adc_avg[ADC_VREF_INT];
			if ((abs(vref - w) > 3) || (abs(w - vref) > 3)) {
				calibrate = 1;
				vref = adc_avg[ADC_VREF_INT];
			}
		}
	}

	adc_go();

	return adc_idx;
}

void ADC_COMP_IRQHandler()
{
	if (adc_seq < ADC_CHANNELS) {
		adc_read[adc_seq++] = ADC1->DR;

		// is this the end of conversions?
		if (ADC1->ISR & ADC_ISR_EOSEQ) {
			adc_seq = ADC_SEQ_RDY;
		}
	} else {
		// we shouldn't be here. something with ADC fucked up.
		// reset ADC, clear sequence, and start again
		adc_go();
	}

	// clear all interrupt flags
	ADC1->ISR = 0x1e;
}