/*
 * NiCd, NiMh, LiIon akkulaturi
 * (c) Henry Palonen & Kristian Rosendahl
 *
 * Released under GPL (Gnu Public Licence)
 * 
 * Spring 2002
 *
*/

/*
 * LATAUS:
 * 
 * 1. akku kiinni
 * 2. valitaan muistipaikka (Sn<CR>, esim. S1<CR>)
 * 3. tarkistetaan parametrit
 * 4. komennolla "G<CR>" aloitetaan lataus
 * 5. mitataan akun jännite ja laitetaan se muistipaikkaan BATT_MEAS_VOLT
 * 6. laitetaan muistipaikan BATT_CURRENT arvo PWM1:een
 * 7. mitataan lämpötila ja verrataan sitä BATT_MAX_TEMP
 *    muistipaikkaan. Jos lämpötila on suurempi, lopetetaan lataus ja laitetaan
 *    PWM1:n arvoksi 0.
 * 8. mitataan akun jännite ja verrataan sitä BATT_MEAS_VOLT muuttujaan
 *    - jos jännite nyt yhtäsuuri tai suurempi kuin BATT_MEAS_VOLT, 
 *      talletetaan nykyinen jännite muistipaikkaan BATT_MEAS_VOLT
 *    - jos jännite on pienempi kuin BATT_MEAS_VOLT ja erotus on yhtäsuuri tai suurempi
 *      kuin BATT_MAX_DROP, lopetetaan lataus ja laitetaan PWM1:n
 *      arvoksi 0.
 * 9. tulostetaan mitatut arvot sarjaportista ja odotetaan viive MEAS_DELAY 
 *
*/

/*
 * Asetusten alustus:
 * 
 * Laitteessa 10 muistipaikkaa eri akuille (muistipaikat 0-9). Asetukset
 * talletetaan EEPROM-muistiin aina kun niitä muutetaan (ts. asetetut arvot
 * säilyvät myös ilman käyttöjännitettä.)
 * 
 * S1<CR>   = valitaan muistipaikka 1 käsittelyä varten
 * C1200<CR> = asetetaan valitun muistipaikan virta (C) arvoon 1200
 *
 * Ensimmäinen merkki ilmoittaa toiminnon:
 * 	 - S (Select) valitse käsiteltävä muistipaikka 0-9 
 *	 - Y (tYpe) 0=NiCd, 1=NiMh, 0=LiIon
 *       - C (Current) asetetaan latausvirta
 *       - T (Temperature) asetetaan katkaisu lämpötila
 *       - D (Drop) asetetaan latauksen päättävä jännitteenpudotus (=NiCd & NiMh jännitteenpudotus, 
 *           LiIon = jännite jolla virtaa pudotetaan)
 *       - L (Limit current) Latausvirta jolla LiIon lataus lopetetaan
 *       - I (tIme) asetetaan max lataus aika viiden sekunnin pykälin, jonka jälkeen katkaistaan lataus
 *       - O (stOp) katkaistaan lataus.
 *	 - G (Go) aloitetaan lataus.
 *	 - P (Print) tulostetaan tietyn muistipaikan kaikki asetukset
 *
 * Tämän jälkeen syötetään arvo:
 *	- S: 0-9 (muistipaikka)
 * 	- 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:stä tuleva "pic16f87x.h" joka sisältää prosessorikohtaiset
// muuttujat jne.
#include incfile

// 20Mhz kide
#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	// pinni josta mitataan akun jännitettä
#define TEMP_MEAS_PIN PIN_A1	// pinni josta mitataan akun lämpötilaa
#define SET_BUTTON_PIN PIN_C3
#define START_BUTTON_PIN PIN_C4
#define STOP_BUTTON_PIN PIN_C0

       

// "räätälöidyt" lcd-funktiot
#include "lcdakku.c"

#include <string.h>

#define MEAS_DELAY 50000	// mittausten välinen viive mikrosekunneissa (tämän viiven verran rs232 odottaa komentoja)
#define MEMORIES 9		// muistipaikkojen määrä
// 6 * 4 * MEMORIES + 6 = EEPROM:n tarve

// long vie tilaa 4 tavua, joten
// 6 * 4 * 9 + 6 = 222 tavua 

// laitekohtaiset parametrit (max määrä 9 kpl)
#define MEM_ADDR_VOLT_CAL	1			// mV / DA kalibrointitieto
#define MEM_ADDR_LAST_SEL_MEM	5			// viimeksi valittuna ollut muistipaikka
#define MEM_ADDR_AMPER_CAL	9			// mA / AD kalibrointi

// muistipaikkakohtaiset tiedot
#define MEM_ADDR_BATT_CURRENT 	1 * 4 * MEMORIES + 1	// muistipaikat latausvirralle
#define MEM_ADDR_BATT_TYPE 	2 * 4 * MEMORIES + 1	// muistipaikat akun tyypille
#define MEM_ADDR_MAX_TEMP 	3 * 4 * MEMORIES + 1	// muistipaikat maksimilämmöille
#define MEM_ADDR_MAX_TIME 	4 * 4 * MEMORIES + 1	// muistipaikat maksimiajalle
#define MEM_ADDR_BATT_VOLT_DROP 5 * 4 * MEMORIES + 1	// muistipaikat NiCd ja NiMh akkujen jännitepudotukselle ja
							// LiIon akun rajajännitteelle
#define MEM_ADDR_BATT_C_LIMIT 	6 * 4 * MEMORIES + 1	// muistipaikat virralle jossa lataus lopetetaan

#define READ  0
#define WRITE 1

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


#define INTS_PER_SECOND 76 // montako keskeytystä tulee sekunnissa (20 000 000 / (4*256*256))
#define BATT_C_DROP 1		// paljonko latausvirtaa pudotetaan kun täysi jännite on saavutettu


long BATT_CURRENT=0;		// akun latausvirta
int BATT_TYPE=TYPE_NICD;	// akun tyyppi
int BATT_MAX_TEMP=100;		// maksimilämpö
long BATT_MAX_TIME=720;		// maksimiaika
int BATT_VOLT_DROP=100;		// jännitepudotus jolla lataus katkaistaa (NiCd/NiMh) / jännite jolla virtaa
				// pienennetään (LiIon)
int BATT_C_LIMIT=25;		// latausvirta jonka alittuessa LiIon lataus päätetään

int1 CHARGE_IN_PROGRESS=FALSE;	// onko lataus käynnissä
int8 SELECTED_MEM;		// valittu muistipaikka
float MEAS_VOLT=0;		// mitattu jännite
long retval;			// palautusarvo (FIXME: onko pakko olla globaalina, ei kai ?)

short timeout_error;		// onko tullut aikakatkaisu sarjaliikenteeseen
byte int_count;			// montako keskeytystä on tullut
long seconds=0;			// latauksen kuluneet sekunnit
int16 cumulated_mAh=0;		// latausmäärä yhteensä
int16 mA_Per_10Sec=0;		// milliamppeerimäärä kymmenessä sekunnissa

long  MV_PER_STEP; 		// montako millivolttia on yksi A/D pykälä
int   MA_PER_STEP;		// montako milliamppeeria on yksi D/A pykälä
int8  lcd_info=0;		// mitä lcd:llä näytetään
int8  ten_seconds=0;		// kymmenet sekunnit

float VOLT_TMP;			// voltit
long  TEMP_TMP;			// lämpötila

				// komennot joilla latausta ohjataan
#define COMMAND_NOTHING 0
#define COMMAND_START 1

// pidä aina COMMAND_STOP_USER ensimmäisenä latauksen lopetussyynä ja
// COMMAND_STOP_TIME viimeisenä syynä koska vain niiden välissä olevat arvot
// katsotaan latauksen pysäyttämiseksi
#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;		// komento jonka käyttäjä antaa joka napeilla tai sarjaliikenteellä

// varataan bootloaderille tilaa
#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);

// funktio muuntaa stringin long-arvoksi
#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);
}

// funktio kirjoitta long-arvon eeprommiin
void l_write_eeprom(long int n, float data) {
	int i;

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

// funktio lukee long-arvon eepromista
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);
}


// funktio palauttaa ajan muodossa h:mm:ss
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);
}

// funktio hoitaa käyttäjän komentojen välityksen eteenpäin sekä
// laskee montako sekuntia lataus on ollut käynnissä
#int_rtcc			// RTCC (timer0) päälle
clock_isr() {			// joka kerta kun RTCC pyörähtää ympäri (255 -> 0), kutsutaan tätä funktiota,
				// tässä ohjelmassa n. 76 kertaa sekunnissa

	char aika[7];		// lcd:lle vietävä aika muodossa h:mm:ss

	// tutkitaan onko käyttäjä painanut näppäimiä
	
	// set-nappi vaihtaa muistipaikkoja 0 ... 9 -> 0 ... 9 
	if ( !input(SET_BUTTON_PIN) ) {
		SELECTED_MEM=SELECTED_MEM+4;	// float on 4 tavua
		if (SELECTED_MEM > 9*4)		// viimeinen muistipaikka
			SELECTED_MEM=0;
		printf(lcd_putc,"\fMEM: %u", SELECTED_MEM/4);
		delay_ms(50);		// nopea ja huono tapa...
	}
	 // start-nappi
	if (!input(START_BUTTON_PIN)) {
		COMMAND = COMMAND_START;
	}
	// stop-nappi
	if (!input(STOP_BUTTON_PIN)) {
		COMMAND = COMMAND_STOP_USER;
	}
	// jos latauksen aloittava komento on annettu joko sarjaliikenteellä
	// tai näppäimillä, aloitetaan tai lopetetaan lataus
	if (COMMAND == COMMAND_START) {
		start_charge();
		COMMAND=COMMAND_NOTHING;
	}

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

	if (CHARGE_IN_PROGRESS == TRUE) {	// lasketaan vain aika jonka lataus on päällä
		int_count--;

		if(int_count <= 0) {
			
			charge();			// kutsutaan varsinaisen latauksen hoitavaa funktiota
			
			seconds++;
			int_count=INTS_PER_SECOND;
			
			// onko kymmenes sekunti
			ten_seconds++;
			if (ten_seconds>9) {
				// FIXME: EI TOIMI VIELÄ, PITÄISI JAKAA JOKA
				// SEKUNTI 3600:LLA MUTTA JOSTAKIN SYYSTÄ SE EI
				// PELAA, KUMULOIDUKSI ARVOKSI TULEE NOLLA
				cumulated_mAh = cumulated_mAh + (BATT_CURRENT*100/36); // 1000 virhe
				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);
			// Voltit
			if (lcd_info>=2 && lcd_info<4)
				printf(lcd_putc,"\fC%S\n %2.2f V", aika, VOLT_TMP);
			
			// lämpötila
			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;
		}
	}
	
}


// funktio odottaa rs232-portista kirjainta viiveen ajan
#separate
char timed_getc() { 
	long timeout;

	timeout_error=FALSE;
	timeout=0;
	while(!kbhit() && (++timeout<MEAS_DELAY)) // odotetaan viiveen verran komentoa
		delay_us(10);
	if(kbhit())
		return(getc());
	else {
		timeout_error=TRUE;
		return(0);
	}
}

// funktio tulostaa valitun muistipaikan arvot
//#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);
}

// funktio hoitaa akun latauksen
#separate
void charge() {
	int16 PWM_TMP;
	
	// jännitemittaus
	set_adc_channel(VOLT_MEAS_PIN);
	delay_us(10);
	
	VOLT_TMP = read_adc();
	//printf("AD: %1.0f ", VOLT_TMP);
	
	// jos jännitettä 0 tai 1023, akku ei ilmeisesti ole kiinni
	if (VOLT_TMP == 0 || VOLT_TMP == 1023) {
		COMMAND=COMMAND_STOP_ZEROVOLT;
		return;
	}

	VOLT_TMP = VOLT_TMP * MV_PER_STEP / 10000;	// konvertoidaan oikeiksi volteiksi
	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;
				
	}
				
	// lämpötilakatkaisu
	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;
	}

	// aikakatkaisu
	if (seconds > BATT_MAX_TIME && BATT_MAX_TIME>0) {
		COMMAND=COMMAND_STOP_TIME;
		return;
	}
	
}

// funktio lopettaa akun latauksen
#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;
}

// funktio lukee arvot muistista käyttöön ja aloittaa akun latauksen
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;
}

// funktio kysyy käyttäjältä arvoa annetulle komennolle
#separate
void ask_rs232(char kirjain) {
	long int addr;
	int1 RW;	// luku vai kirjoitusoperaatio ?
	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) {	// muistipaikka käsitellään erikseen koska
						// long-tyyliset muuttujat
						// vievät muistitilaa 4x int...
			  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();
	}
}

// funktio alustaa prosessorin käyttöä varten
#separate
void alusta(void) {
	int last_selected_mem;
	// pwm-alustus
	setup_ccp1(CCP_PWM);
	setup_timer_2(T2_DIV_BY_1, 64, 1);
	// ad-alustus	
	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); // latausvirta nollaksi alussa
	
	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);

}

// Pääohjelma ei tee muuta kuin kysyy komentoa sarjaportista.
// Varsinaisen ohjauksen hoitaa keskeytysrutiini "clock_isr" kerran sekunnissa.
//
void main(void) {
	char kirjain;
	
	alusta();
	
	while (1) {
		
		kirjain=timed_getc();
		if (timeout_error == FALSE) {
			ask_rs232(kirjain);
		}
	}
}
