/*
 * solar_led.c : Solar LED with ATtiny45
 *
 * Fuse: CKDIV8 cleared. All other fuses remain at default. 
 *
 * Pin assignment: 
 *    PB0 - pin 5: output controls the power to the solar lamp
 *    ADC3 (PB3) - pin 2: input from battery pack
 *    ADC2 (PB4) - pin 3: input from solar panel 
 *    ADC1 (PB2) - pin 7: input from output power
 *
 * First version: 31-Oct-2016
 * Last update: 29-Jul-2017
 * Author : Stephan Laage-Witt
 */ 

#define F_CPU 8000000

#include <avr/io.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <util/delay.h>

// ADC channels
#define ADC_BATTERY 3
#define ADC_SOLAR 2
#define ADC_OUT 1

// pin definitions
#define BIT_POWER_SWITCH 1

// ADC threshold values. Calulation: ADC value = target_voltage * (33 / 133) * (1024 / 1100)
#define BATT_LOW 692					// threshold to switch LED off: 3.0V  
#define BATT_HIGH 808					// threshold to re-enable LED: 3.5V
#define SOL_LOW 230						// solar panel threshold to switch LED on: 1.0V
#define SOL_HIGH 808					// solar panel threshold to switch LED off: 3,5V

#define VOUT_TARGET 650					// approx. 4.5 V

#define ON 255
#define OFF 0

// state  definitions 
#define STATE_DAY 1
#define STATE_NIGHT 2
#define STATE_BAT_LOW 3
#define STATE_TIME_OUT 4

// timing parameters 
#define TIME_OUT 14400					// 4 hours * 60 min * 60 sec
#define SWITCH_DELAY 300				// 5 min * 60 sec

// global variables ----------------------------------------------------------------------------------
volatile uint8_t interrupt_cnt = 0;		// interrupt counter
																												
/* interrupt service handler for watchdog timer ------------------------------------------------------*/
ISR(WDT_vect) {
	WDTCR |= (1 << WDIE);		// wake up CPU and re-enable watchdog interrupt - that's all
}

/* switch_lamp -----------------------------------------------------------------------*
* Switches LED output on or off. Sets OCR0A and PortB / BIT_POWER_SWITCH
* Changes sleep mode: "idle" for lamp on, "power down" for lamp off
* Input: mode (ON or OFF) 
*/
void switch_lamp(const uint8_t mode) {
	if (mode == ON) {
		TCCR0A = (1 << COM0A0) | (1 << COM0A1) | (0 << COM0B0) | (0 << COM0B1);		// connect OC0A to compare match, OC0B disconnected (p. 78)
		TCCR0A |= (1 << WGM01) | (1 << WGM00);										// set OC0A on compare match (data sheet page 101)
		TCCR0B = (0 << CS02) | (0 << CS01) | (1 << CS00);							// no pre-scaling
		OCR0A = 255;																// begin without voltage step-up, and increase it from here 
		PORTB &= ~(1 << BIT_POWER_SWITCH);											// switch LED power on
		set_sleep_mode(SLEEP_MODE_IDLE);											// set sleep mode to "idle", keep timer for PWM running
		return;
	};
	if (mode == OFF) {
		PORTB |= (1 << BIT_POWER_SWITCH);											// switch power off
		TCCR0A = 0;																	// stop PWM on OCR0A
		TCCR0B = 0;
		set_sleep_mode(SLEEP_MODE_PWR_DOWN);										// set sleep mode to "power down"
	};
}

/* adjust_vout -----------------------------------------------------------------------*
* Sets OCR0A depending on adc_out
* Iterative calls ensure approximation to targeted output voltage
* Input: adc_out - measurement of current output voltage
*/
void adjust_vout(const uint16_t adc_out) {
	if (adc_out < VOUT_TARGET) {
		if (OCR0A > 120) 
			OCR0A = OCR0A - 1;
	}; 
	if (adc_out > VOUT_TARGET) {
		if (OCR0A < 255)
			OCR0A = OCR0A + 1;
	};
}

/* get_adc --------------------------------------------------------------------------------*
* Reads ADC
* Input: ADC channel
* Returns: ADC value (16 bit, right adjusted)
*/
uint16_t get_adc(const uint8_t adc_channel) {
	ADMUX  = (1<<REFS1) | adc_channel;				// set channel 
	ADCSRA |= (1<<ADSC);							// start ADC conversion
	while ((ADCSRA & (1 << ADSC)) > 0);				// wait for the end of the conversion	
	return(ADCW);									// return result
}

/* main -----------------------------------------------------------------------------------*/
int main(void)
{
	uint8_t current_state = STATE_DAY;
	uint16_t time_count = 0, switch_count = 0;
	
	DDRB = 0b11100011;								// set OC0A and PB1 to output, and ADC channels to input
	PORTB = 0b11100010;								// switch off power and pull up resisters 
	
	// show that we are alive 
	_delay_ms(1000);
	PORTB &= ~(1 << BIT_POWER_SWITCH);				// switch power on
	_delay_ms(2000);								// keep it on for 2 seconds
	switch_lamp(OFF);								// initial status: lamp off
	
	// setup watchdog timer
	cli();											// disable all interrupts
	MCUSR = 0;										// clear MCU status register
	WDTCR = 0;
	WDTCR = (1 << WDIE) | (0 << WDP3) | (1 << WDP2) | (1 << WDP1) | (0 << WDP0);	// setup watchdog for interrupt (not reset), 1 sec cycle time
	sei();											// enable interrupts - system is up and running 

    while (1) { 
			
		// initialize ADC
		ADMUX  = (1<<REFS1) | (1<<REFS0) | (0<<ADLAR);					// 10 bit, right adjusted, internal reference 1.1V
		ADCSRA = (1<<ADEN) | (1<<ADPS2) | (0<<ADPS1) | (0<<ADPS0);		// scaling factor: 16 -> 62500 kHz
		get_adc(ADC_SOLAR);												// run the ADC, but ignore the result of the first conversion
		
		switch (current_state) {
			
			case STATE_DAY:
				if (get_adc(ADC_SOLAR) < SOL_LOW) {
					++switch_count;
					if (switch_count >= SWITCH_DELAY) {					
						current_state = STATE_NIGHT;
						time_count = 0;
						switch_count = 0;
						switch_lamp(ON);
					};
				} else {
					switch_count = 0;
				};
				break;

			case STATE_NIGHT:
				if (time_count >= TIME_OUT) {
					current_state = STATE_TIME_OUT;
					switch_count = 0;
					switch_lamp(OFF);
				} else if (get_adc(ADC_BATTERY) < BATT_LOW) {
					current_state = STATE_BAT_LOW;
					switch_count = 0;
					switch_lamp(OFF);
				} else if (get_adc(ADC_SOLAR) > SOL_HIGH) {
					++switch_count;
					if (switch_count >= SWITCH_DELAY) {
						current_state = STATE_DAY;
						switch_count = 0;
						switch_lamp(OFF);
					};
				} else {
					switch_count = 0;
				};
				adjust_vout(get_adc(ADC_OUT));
				++time_count;
				break;

			case STATE_TIME_OUT:
			case STATE_BAT_LOW:
				if (get_adc(ADC_SOLAR) > SOL_HIGH) {
					++switch_count;
					if (switch_count >= SWITCH_DELAY) {
						current_state = STATE_DAY;
					};
				} else {
					switch_count = 0;
				};
				break;
		};

		// shut down ADC to save energy
		ADCSRA = (0<<ADEN); 											// switch off ADC
		ADMUX  = (0<<REFS1) | (0<<REFS0) | (0<<ADLAR);					// switch off internal reference 1.1V

		// good night (for one second)
		sleep_mode();
	}
}
