/*
 * VCO, copyright 2009 Frank Buss (fb@frank-buss.de)
 * Creates a square wave from 50 Hz to 4 KHz on GP5,
 * when the DC input voltage at GP4 changes from 0.5 V to 2.5 V.
 */

#include <htc.h>

// chip configuration:
//   no program code protection
//   brown out detection disabled
//   GPIO on MCLR pin
//   power up timer enabled
//   watchdog diabled
//   internal 4 MHz oscillator, GPIO on GP4 and GP5
__CONFIG(UNPROTECT & BORDIS & MCLRDIS & PWRTEN & WDTDIS & INTIO);

// Fosc/4: the timer clock
float clock4 = 1e6;

// next TMR1 value
int nextTimer;

// splitted for TMR1H and TMR1L
unsigned char nextTimerHigh;
unsigned char nextTimerLow;

// read the AD value and calculate the next timer value
void updateNextTimer(void)
{
	static float filteredVoltage = 0.0f;
	float voltage;
	float freq;
	
	// get the voltage from the AD input. Vref is Vdd = 5 V, with 10 bit resolution = 1024 steps
	int adValue = (ADRESH << 8) | ADRESL;
	float rawVoltage = 5.0f / 1024.0f * ((float) adValue);

	// limits
	float minFreq = 50.0f;
	float maxFreq = 4000.0f;
	float minVoltage = 0.5f;
	float maxVoltage = 2.5f;

	// lowpass filter the voltage
	filteredVoltage = filteredVoltage + 0.1 * (rawVoltage - filteredVoltage);
	voltage = filteredVoltage;

	// limit voltage
	if (voltage < minVoltage) voltage = minVoltage;
	if (voltage > maxVoltage) voltage = maxVoltage;

	// calculate frequency: maxVoltage results in maxFreq and minVoltage in minFreq
	// interpolate linear inbetween
	freq = voltage - minVoltage;
	freq = freq / (maxVoltage - minVoltage);
	freq = freq * (maxFreq - minFreq) + minFreq;

	// the interrupt toggles the pin, so double the frequency
	freq *= 2;

	// calculate the next TMR1 value
	// interrupt frequency freq = clock4 / (0x10000 - TMR1), solved for TMR1:
	// TMR1 = (freq * 0x10000 - clock4) / freq
	nextTimer = (int)((freq * 65536.0f - clock4) / freq);

	// add some cycles for interrupt latency and loading the next value
	nextTimer += 30;
}

// main function
void main(void)
{
	// init variables
	nextTimer = 0;
	nextTimerHigh = 0;
	nextTimerLow = 0;
	
	// set GP4/AN3 to input, others to output
	TRISIO = 0b00010000;

	// enable analog input for AN3 and set conversion clock to Fosc/16
	ANSEL = 0b01011000;

	// disable pull-ups
	OPTION	= 0x80;

	// interrupt on TMR1 overflow
	PIE1 = 1;  

	// peripheral interrupt must be enabled for overflow interrupts
	PEIE = 1;  

	// start timer 1
	T1CON = 1;

	// clear interrupt flag
	TMR1IF = 0;

	// global interrupt enable
	GIE = 1;		
	
	// select channel AN3, use Vdd for reference, right justified result
	// and enable AD converter
	ADCON0 = 0b10001101;

	// read analog input and set new timer value
	while (1) {
		// wait for AD conversion
		if (GODONE == 0) {
			// AD conversion is ready, prepare next TMR value
			nextTimerHigh = (nextTimer >> 8) & 0xff;
			nextTimerLow = nextTimer & 0xff;

			// calculate next TMR value
			updateNextTimer();
		}
	}
}

// interrupt handler
static void interrupt
isr(void)
{
	static int output = 1;
	GPIO5 = output;

	// stop timer 1
	T1CON = 0;

	// write new timer value
	TMR1H = nextTimerHigh;
	TMR1L = nextTimerLow;

	// start timer 1
	T1CON = 1;

	// toggle next output value
	output ^= 1;

	// start next AD conversion
	GODONE = 1;

	// clear interrupt flag
	TMR1IF = 0;
}
