/*
 * NiCd, NiMh, LiIon charger
 * (c) Henry Palonen & Kristian Rosendahl
 *
 * Released under GPL (Gnu Public Licence)
 * 
 * Spring 2002
 * English Translation - Spring 2006
*/

/*
 * NiCd and NiMh CHARGE:
 * 
 * 1. attach battery 
 * 2. choose memory (Sn<CR>, eg. S1<CR>)
 * 3. check parameters
 * 4. command "G<CR>" starts charging
 * 5. measure battery voltage, and save to BATT_MEAS_VOLT
 * 6. put memoryplaces value BATT_CURRENT to PWM1
 * 7. measure temperature and check BATT_MAX_TEMP variable
 *    . If higher, stop charging and put PWM1 0.
 * 8. measure battery voltage and check BATT_MEAS_VOLT variable
 *    - if voltage equals or is greater than BATT_MEAS_VOLT, 
 *      save voltage to variable BATT_MEAS_VOLT
 *    - if voltage is lower than BATT_MEAS_VOLT and difference is greater or equal  
 *      than BATT_MAX_DROP, stop charging and put PWM1 = 0.
 * 9. print measured values from serial port and wait for delay MEAS_DELAY 
 * 10. rotate to place 5. and start over
*/

/*
 * Settings :
 * 
 * 10 memories for different batteries (0-9). Settings are saved when they are
 * changed - saving to EEPROM, so no need for stand-by voltage.
 * 
 * S1<CR>   = select memory 1
 * C1200<CR> = set Current at selected memory to value 1200
 *
 * First character tells what command does:
 * 	 - S (Select) memory 0-9 
 *	 - Y (tYpe) 0=NiCd, 1=NiMh, 0=LiIon
 *       - C (Current) set charge current
 *       - T (Temperature) maximum temperature
 *       - D (Drop) 
 *       	 For NiCd & NiMh batteries:
 *       		voltage drop, "deltaV" that tells that battery is full
 *           For LiIon batteries:
 *           	Maximum allowed LiIon battery voltage - when
 *           	reached, software must start to drop charge current by amount of
 *           	BATT_C_DROP (defaults to 1 A/D tick, see definition below)
 *       - L (Limit current) LiIon charge current that stops the charge when
 *       reached (eg. when charge current eventually drops to this, decide that
 *       battery is full and stop charge.)
 *       - I (tIme) max charge time in 5 sec steps
 *       - O (stOp) stop charge immediately
 *	 - G (Go) start charge.
 *	 - P (Print) print everything to the console
 *
 * after first character there comes a value:
 *	- S: 0-9 (memory)
 * 	- Y: 1=NiCd, 2=NiMh, 3=LiIon
 * 	- C: 0-1024  (default 100)
 * 	- T: 0-9999  (default 100)
 * 	- D: 0-9999  (default 100)
 * 	- L: 0-9999  (default 25)
 * 	- I: 0-9999 (default 720) = 1h (3600s / 5s)
 * 	- O: -
 * 	- G: - 
 * 	- P: - 
 * 	
*/

// Makefile has this "pic16f87x.h" that includes processor type specific things
#include incfile

// 20Mhz crystal
#use DELAY (clock=20000000)
//#use DELAY (clock=12000000)
// #use RS232 (baud=19200, xmit=PIN_C4,rcv=PIN_C5,invert) // vanha
#use RS232 (baud=19200, xmit=PIN_C6,rcv=PIN_C7)
#fuses HS,NOWDT,NOPROTECT

#define VOLT_MEAS_PIN PIN_A0	// pin that has battery voltage
#define TEMP_MEAS_PIN PIN_A1	// pin that has battery temperature
#define SET_BUTTON_PIN PIN_C3
#define START_BUTTON_PIN PIN_C4
#define STOP_BUTTON_PIN PIN_C0


// lcd-functions
#include "lcdakku.c"

#include <string.h>

#define MEAS_DELAY 50000	// delay between measurements, also delay that RS232 waits for commands to arrive
#define MEMORIES 9		// how many memories
// 6 * 4 * MEMORIES + 6 = how much eeprom-memory is needed

// long is 4 bytes, so 
// 6 * 4 * 9 + 6 = 222 bytes

// charger calibration (max calibration parameters 9 kpl)
#define MEM_ADDR_VOLT_CAL	1			// mV / DA
#define MEM_ADDR_LAST_SEL_MEM	5			// last selected memoryplace
#define MEM_ADDR_AMPER_CAL	9			// mA / AD

// per memoryplace
#define MEM_ADDR_BATT_CURRENT 	1 * 4 * MEMORIES + 1	// current
#define MEM_ADDR_BATT_TYPE 	2 * 4 * MEMORIES + 1	// battery type (NiCd,NiMh,LiIon)
#define MEM_ADDR_MAX_TEMP 	3 * 4 * MEMORIES + 1	// max temp
#define MEM_ADDR_MAX_TIME 	4 * 4 * MEMORIES + 1	// max time
#define MEM_ADDR_BATT_VOLT_DROP 5 * 4 * MEMORIES + 1	// NiCd ja NiMh voltage drop and 
													// LiIon battery limit
													// voltage
#define MEM_ADDR_BATT_C_LIMIT 	6 * 4 * MEMORIES + 1	// stop current

#define READ  0
#define WRITE 1

#define TYPE_NICD  0
#define TYPE_NIMH  1
#define TYPE_LIION 2


#define INTS_PER_SECOND 76 // how many interrupts per second (20 000 000 / (4*256*256))
#define BATT_C_DROP 1		// how much current is dropped when full voltage is achieved

long BATT_CURRENT=0;		// battery current
int BATT_TYPE=TYPE_NICD;	// battery type
int BATT_MAX_TEMP=100;		// max temperature
long BATT_MAX_TIME=720;		// max time
int BATT_VOLT_DROP=100;		// voltage that stops sthe charge (NiCd/NiMh)
							// voltage that drops the current (LiIon)
int BATT_C_LIMIT=25;		// current that when reached the charge is ended

int1 CHARGE_IN_PROGRESS=FALSE;	// is there currently charging in place
int8 SELECTED_MEM;		// selected memoryplace
float MEAS_VOLT=0;		// measured voltage
long retval;			// 

short timeout_error;		// serial port timeout
byte int_count;			// how many interrupts
long seconds=0;			// charging seconds
int16 cumulated_mAh=0;		// cumulated mAh amount
int16 mA_Per_10Sec=0;		// milliampers in 10 seconds

long  MV_PER_STEP; 		// how many millivolts is on A/D tick
int   MA_PER_STEP;		// how many milliamps is on D/A tick
int8  lcd_info=0;		// what to show in lcd
int8  ten_seconds=0;		// tens of seconds

float VOLT_TMP;			// volts
long  TEMP_TMP;			// temperature

				// commands that control the charging
#define COMMAND_NOTHING 0
#define COMMAND_START 1

// keep always COMMAND_STOP_USER first and
// COMMAND_STOP_TIME last, because only values between them
// are seen as valid stop reason
#define COMMAND_STOP_USER 2
#define COMMAND_STOP_VOLT 3
#define COMMAND_STOP_TEMP 4
#define COMMAND_STOP_CURRENT 5
#define COMMAND_STOP_ZEROVOLT 6
#define COMMAND_STOP_TIME 7

int8 COMMAND=COMMAND_NOTHING;		// command that user enters via the buttons or via the serial port

// for future bootloader
#ORG 0x1F00,0x1FFF //for the 8k 16F876/7
void loader() { }

void start_charge(void);
#separate
void stop_charge(char);
#separate
char timed_getc(void);
#separate
void charge(void);

// string conversion to long value
#separate
signed long atol(char *s)
{
   signed long result;
   int sign, base, index;
   char c;

   index = 0;
   sign = 0;
   base = 10;
   result = 0;

   if(s)
      c = s[index++];

   if (c >= '0' && c <= '9')
   {
      // The number is a decimal number
      if (base == 10)
      {
         while (c >= '0' && c <= '9')
         {
            result = 10*result + (c - '0');
            c = s[index++];
         }
      }
   }

   return(result);
}

#separate
void get_string(char * s,int max) {
   int len;
   char c;

   max--;
   len=0;
   do {	 
	   
     c=getc();
     if(c==8) {  // Backspace
        if(len>0) {
          len--;
          putc(c);
          putc(' ');
          putc(c);
        }
     } else if ((c>=' ')&&(c<='~'))
       if(len<max) {
         s[len++]=c;
         putc(c);
       }
   } while(c!=13);
   s[len]=0;
}

#separate
unsigned long get_long() {
  char s[5];
  unsigned long l;

  get_string(s, 5); // 0001
  l=atol(s);
  return(l);
}

// long to eeprom
void l_write_eeprom(long int n, float data) {
	int i;

	for (i = 0; i < 4; i++)
		write_eeprom(i + n, *(&data + i) );
}

// long from eeprom
float l_read_eeprom(long int n) {
	int i;
	float data;

	for (i = 0; i < 4; i++)
		*(&data + i) = read_eeprom( i + n);

	return(data);
}


// time in h:mm:ss format
char laske_aika(int16 s) {
        char aika[7];
        int min=0;
	int hour=0;
        
        if (s>59) {
                while(s>59) {
                        s = s - 59;
                        if (s>1)
                                min++;
                        if (min>59) {
                                hour++;
                                min=0;
                        }
                }       
        }
        sprintf(aika, "%u:%02u:%02lu", hour,min,s);
        return (aika);
}

// forwards users commands and counts how many seconds the charge-process
// has been on
#int_rtcc			// RTCC (timer0) on
clock_isr() {			// every time RTCC rotates (255->0), this gets called
				// it's about 76 times per second in this program

	char aika[7];		// time to lcd in h:mm:ss-format

	// if user has pressed buttons
	
	// set-button changes from memories 0 ... 9 -> 0 ... 9 
	if ( !input(SET_BUTTON_PIN) ) {
		SELECTED_MEM=SELECTED_MEM+4;	// float is 4 bytes
		if (SELECTED_MEM > 9*4)		// last memory
			SELECTED_MEM=0;
		printf(lcd_putc,"\fMEM: %u", SELECTED_MEM/4);
		delay_ms(50);		// quick and dirty debounce
	}
	 // start-button
	if (!input(START_BUTTON_PIN)) {
		COMMAND = COMMAND_START;
	}
	// stop-button
	if (!input(STOP_BUTTON_PIN)) {
		COMMAND = COMMAND_STOP_USER;
	}
	// if charge start/stop is issued by rs232 or by buttons, start/stop charge
	if (COMMAND == COMMAND_START) {
		start_charge();
		COMMAND=COMMAND_NOTHING;
	}

	// stopping charge
	if (COMMAND >= COMMAND_STOP_USER && COMMAND <= COMMAND_STOP_TIME) {
		stop_charge(COMMAND);
		COMMAND=COMMAND_NOTHING;
	}

	if (CHARGE_IN_PROGRESS == TRUE) {	// count time that charge has been on
		int_count--;

		if(int_count <= 0) {
			
			charge();			// call function that handles charging
			
			seconds++;
			int_count=INTS_PER_SECOND;
			
			// if it's ten-second mark
			ten_seconds++;
			if (ten_seconds>9) {
				// FIXME: DOESN'T WORK YET. SHOULD DIVIDE EVERY SECOND BY 3600
				// BUT FOR SOME REASON IT DOESN'T WORK - CUMULATED VALUE IS
				// ZERO !?
				cumulated_mAh = cumulated_mAh + (BATT_CURRENT*100/36); // 1000 error
				ten_seconds=0;
			}
			
			sprintf(aika, laske_aika(seconds));
			
			// mAh
			if (lcd_info>=0 && lcd_info<2)
				printf(lcd_putc,"\fC%S\n %4lumAh",aika, cumulated_mAh/1000);
			// Volts
			if (lcd_info>=2 && lcd_info<4)
				printf(lcd_putc,"\fC%S\n %2.2f V", aika, VOLT_TMP);
			
			// temperature
			if (lcd_info>=4)
				printf(lcd_putc,"\fC%S\n %5lu C", aika, TEMP_TMP);
			
			lcd_info++;
			delay_ms(10);
			
			if (lcd_info>5)
				lcd_info=0;
		}
	}
	
}


// waits for character to arrive from rs232 for delay-time
#separate
char timed_getc() { 
	long timeout;

	timeout_error=FALSE;
	timeout=0;
	while(!kbhit() && (++timeout<MEAS_DELAY)) // wait for delay
		delay_us(10);
	if(kbhit())
		return(getc());
	else {
		timeout_error=TRUE;
		return(0);
	}
}

// prints whole memory bank to console
//#separate
void dump_memory(void) {
	printf("\r\nMe: %u\r\n",SELECTED_MEM / 4);
	retval = l_read_eeprom(MEM_ADDR_VOLT_CAL);
	printf("mV: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_AMPER_CAL);
	printf("mA: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_BATT_TYPE + SELECTED_MEM);
	printf("Ty: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_BATT_CURRENT + SELECTED_MEM);
	printf("Cu: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_MAX_TEMP + SELECTED_MEM);
	printf("Te: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_BATT_VOLT_DROP + SELECTED_MEM);
	printf("Dr: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_MAX_TIME + SELECTED_MEM);
	printf("tI: %lu\r\n", retval);
	retval = l_read_eeprom(MEM_ADDR_BATT_C_LIMIT + SELECTED_MEM);
	printf("Li: %lu\r\n", retval);
}

// actual function that handles charging process
#separate
void charge() {
	int16 PWM_TMP;
	
	// measure volts
	set_adc_channel(VOLT_MEAS_PIN);
	delay_us(10);
	
	VOLT_TMP = read_adc();
	//printf("AD: %1.0f ", VOLT_TMP);
	
	// if voltage is 0 tai 1023, battery is propably not connected
	if (VOLT_TMP == 0 || VOLT_TMP == 1023) {
		COMMAND=COMMAND_STOP_ZEROVOLT;
		return;
	}

	VOLT_TMP = VOLT_TMP * MV_PER_STEP / 10000;	// convert to real volts
	PWM_TMP = BATT_CURRENT / MA_PER_STEP;
	
	printf("V: %2.2f,P: %2.2f,C: %lu (P: %lu)",VOLT_TMP, MEAS_VOLT, BATT_CURRENT,PWM_TMP);
	
	
	switch (BATT_TYPE) {
		case TYPE_NIMH:	break;
				
		case TYPE_NICD: delay_us(10);
				set_pwm1_duty(PWM_TMP);
				
				if (VOLT_TMP > MEAS_VOLT)
					MEAS_VOLT = VOLT_TMP;

				if (VOLT_TMP*1000 <= ((MEAS_VOLT*1000) - BATT_VOLT_DROP)) {
					COMMAND=COMMAND_STOP_VOLT;
					return;
				}
				break;
				
		case TYPE_LIION: delay_us(10);
				 set_pwm1_duty(PWM_TMP);
				
				 if (VOLT_TMP*1000 >= BATT_VOLT_DROP) {
					BATT_CURRENT = BATT_CURRENT - BATT_C_DROP;
					PWM_TMP = BATT_CURRENT / MA_PER_STEP;
				 	set_pwm1_duty(PWM_TMP);
				 }
				 if (BATT_CURRENT <= BATT_C_LIMIT) {
					COMMAND=COMMAND_STOP_CURRENT;
					return;
				 }
				 break;
				
	}
				
	// see if temperature limit has been reached
	set_adc_channel(TEMP_MEAS_PIN);
	delay_us(10);
	TEMP_TMP = read_adc();
	TEMP_TMP = ((0.004883 * TEMP_TMP) - 0.33)*33; //(5V/1024*AD-0,33V)*100/3
	
	printf(",T: %lu,S: %lu,mA: %04lu\r\n", TEMP_TMP, seconds, cumulated_mAh/1000);
	
	if (TEMP_TMP > BATT_MAX_TEMP) {
		COMMAND=COMMAND_STOP_TEMP;
		return;
	}

	// see if time limit has been reached
	if (seconds > BATT_MAX_TIME && BATT_MAX_TIME>0) {
		COMMAND=COMMAND_STOP_TIME;
		return;
	}
	
}

// stops the charge and puts total cumulated time and stop reason to console.
#separate
void stop_charge(int reason) {
	char aika[7];
	printf("\r\nSTOP: %i\r\n",reason);

	sprintf(aika, laske_aika(seconds));
	printf(lcd_putc,"\fS%S\n%4lum(%i)",aika, cumulated_mAh/1000,reason);
	
	CHARGE_IN_PROGRESS = FALSE;
	seconds=0;
	set_pwm1_duty(0);
	MEAS_VOLT=0;
	cumulated_mAh=0;
}

// reads values from memory and starts charging process
void start_charge(void) {
	printf("\r\nSTART\r\n");
	MV_PER_STEP = l_read_eeprom(MEM_ADDR_VOLT_CAL);
	MA_PER_STEP = l_read_eeprom(MEM_ADDR_AMPER_CAL);
	BATT_TYPE = l_read_eeprom(MEM_ADDR_BATT_TYPE + SELECTED_MEM);
	BATT_CURRENT = l_read_eeprom(MEM_ADDR_BATT_CURRENT + SELECTED_MEM);
	BATT_VOLT_DROP = l_read_eeprom(MEM_ADDR_BATT_VOLT_DROP + SELECTED_MEM);
	BATT_MAX_TEMP = l_read_eeprom(MEM_ADDR_MAX_TEMP + SELECTED_MEM);
	BATT_MAX_TIME = l_read_eeprom(MEM_ADDR_MAX_TIME + SELECTED_MEM);
	BATT_C_LIMIT = l_read_eeprom(MEM_ADDR_BATT_C_LIMIT + SELECTED_MEM);
	dump_memory();
	
	CHARGE_IN_PROGRESS=TRUE;
	int_count=INTS_PER_SECOND;
}

// asks for user input for specific command
#separate
void ask_rs232(char kirjain) {
	long int addr;
	int1 RW;	// read or write
	long value;

	kirjain=toupper(kirjain);
	switch (kirjain) {
			case 'O': 
				  COMMAND = COMMAND_STOP_USER;
				  RW = READ;
				  break;
			case 'G': 
				  COMMAND = COMMAND_START;
				  RW = READ;
				  break;
			case 'P': RW = READ;
				  dump_memory();
				  break;
			case 'S': RW = WRITE;
				  break;
			case 'Y': RW = WRITE;
				  addr = MEM_ADDR_BATT_TYPE + SELECTED_MEM;
				  break;
			case 'C': RW = WRITE;
				  addr = MEM_ADDR_BATT_CURRENT + SELECTED_MEM;
				  break;
			case 'T': RW = WRITE;
				  addr = MEM_ADDR_MAX_TEMP + SELECTED_MEM;
				  break;
			case 'D': RW = WRITE;
				  addr = MEM_ADDR_BATT_VOLT_DROP + SELECTED_MEM;
				  break;
			case 'L': RW = WRITE;
				  addr = MEM_ADDR_BATT_C_LIMIT+ SELECTED_MEM;
				  break;
			case 'I': RW = WRITE;
				  addr = MEM_ADDR_MAX_TIME + SELECTED_MEM;
				  break;
			case 'V': RW = WRITE;
				  addr = MEM_ADDR_VOLT_CAL;
				  break;
			case 'A': RW = WRITE;
				  addr = MEM_ADDR_AMPER_CAL;
				  break;
	}
	if (RW == WRITE) {
		value = get_long();
		if (strcmp(kirjain,'S') == 0) {	// this is handles separately because long takes 4x
			  SELECTED_MEM = value * 4;
			  printf("M: %u\r\n",SELECTED_MEM/4);
			  printf(lcd_putc,"\fMEM: %u", SELECTED_MEM/4);
			  addr = MEM_ADDR_LAST_SEL_MEM;
			  value = SELECTED_MEM;
		}
		
		l_write_eeprom(addr, value);
		retval = l_read_eeprom(addr);
		//printf("W: %lu -> %lu / R: %lu <- %lu)\r\n", value, addr, retval,addr);
		dump_memory();
	}
}

// initializes the processor
#separate
void alusta(void) {
	int last_selected_mem;
	// pwm-initialization
	setup_ccp1(CCP_PWM);
	setup_timer_2(T2_DIV_BY_1, 64, 1);
	// A/D init
	setup_port_a(ALL_ANALOG);
	setup_adc(adc_clock_internal);
	
	set_rtcc(0);

	setup_counters(RTCC_INTERNAL, RTCC_DIV_256);
	enable_interrupts(INT_RTCC);
	enable_interrupts(GLOBAL);
	
	set_pwm1_duty(0); // no charge at start
	
	lcd_init();
	lcd_putc("\fPIC ACCU\n v0.10");
	
	printf("PIC ACCU v0.10\r\n(c) 2002 / Henry Palonen & Kristian Rosendahl\r\n");
	delay_ms(1000);
	
	last_selected_mem = l_read_eeprom(MEM_ADDR_LAST_SEL_MEM);
	SELECTED_MEM = last_selected_mem;
	printf(lcd_putc,"\fMEM: %u", SELECTED_MEM/4);

}

// Main function just sits and waits for command to arrive from serial port
// Actual control is handled by interrupt routine "clock_isk" once every second
//
void main(void) {
	char kirjain;
	
	alusta();
	
	while (1) {
		
		kirjain=timed_getc();
		if (timeout_error == FALSE) {
			ask_rs232(kirjain);
		}
	}
}
