/* vim: set sw=8 ts=8 si : */
/*************************************************************************
Title:    linuxfocus frequency counter
Author:   guido socher <guido[at]linuxfocus.org>
Copyright: GPL
**************************************************************************/

#include <io.h>
#include <progmem.h>
#include <interrupt.h>
#include <string-avr.h>
#include <stdlib.h>
#include <sig-avr.h>
#include "lcd.h"	/* these are the prototypes and defines for lcd.c */
#include "avr-util.h"
#include "uart.h"

/* output line to scan for button up/down */
#define BUTTONDOWN PD5
#define BUTTONDOWNDDR DDRD
#define BUTTONDOWNPORT PORTD
#define BUTTONUP PD6
#define BUTTONUPDDR DDRD
#define BUTTONUPPORT PORTD

/* Input Push Buttons */
#define SBBUTTON PIND7
#define SBBUTTONDDR DDRD
#define SBBUTTONPIN PIND
#define SBBUTTONPORT PORTD
#define I_BUTTON PINC4
#define I_BUTTONDDR DDRC
#define I_BUTTONPIN PINC
#define I_BUTTONPORT PORTC
#define U_BUTTON PINB3
#define U_BUTTONDDR DDRB
#define U_BUTTONPIN PINB
#define U_BUTTONPORT PORTB
#define UUBUTTON PINB0
#define UUBUTTONDDR DDRB
#define UUBUTTONPIN PINB
#define UUBUTTONPORT PORTB

/* current limit input and led */
#define ILIMIT PIND3
#define ILIMITDDR DDRD
#define ILIMITPIN PIND
#define ILED PC5
#define ILEDDDR DDRC
#define ILEDPORT PORTC
/* PWMOUT0= current control, software pwm */
#define PWMOUT0 PB2
#define PWMOUT0DDR DDRB
#define PWMOUT0PORT PORTB
/* PWMOUT1= voltage control, internal hardware pwm  */
#define PWMOUT1 PB1
#define PWMOUT1DDR DDRB
#define PWMOUT1PORT PORTB
static char uart_outbuf[UART_RX_BUFFER_SIZE+1];

/* max 2.2A output, can be up to 3A (3000), 
* change, depends on the transformer you have */
#define MAX_I 2200 
/* minial stepping for I, set to 50 for MAX_I=3000
* otherwise set to 25, change this dependent on hardware */
#define IMINSTEP 25
static unsigned char ipwm_h; /* time that PWM for I is high */
static unsigned char ipwm_phase; /* flag indicating whether we are now producing a 1 or a zero */
static int ival; /* 16 bit, the current in mA */

/* max 16V output, change dependent on the transformer you have */
#define MAX_U 160
static int uval; /* 16 bit, the Voltage in 0.1 V units, eg. 50=5V */
static unsigned char mode; /* 0=normal operation, 1=I-limit, 2=standby */
static unsigned char oldmode; /* 0=normal operation, 1=I-limit, 2=standby */
static unsigned char tmpval[3]; /* 24 bit, needed for exact math in set_u */
static unsigned char tmpval2[3]; /* 24 bit, needed for exact math in set_u */

#define reply_ok() uart_sendstr_P("ok\n")
#define reply_err() uart_sendstr_P("err\n")

/* there is only support for 16bit math operations in libc.
* to get more accurate math we have to write our own functions */

/* *x is 24 bit, result is in x (x=in/out value), y *256+256 must be < 32768*/
unsigned char divXbyY(unsigned char *x,unsigned char y){
        unsigned char loop,rest;
        unsigned int tmp; // 16 bit
        loop=3;
        /* start with highest byte */
        rest=0;
        while(loop>0){
                tmp=rest * 256 + x[loop-1];
                x[loop-1]=tmp / y;
                rest=tmp %  y;
                loop--;
        }
        return(rest);
}

/* *x is 24 bit, result is in x (x=in/out value), the bigger
* number must go into x, 8bit of x times y must never go over 32768 */
void multiXbyY(unsigned char *x,int y){
        unsigned char loop;
        unsigned int tmp,over; // 16 bit
        loop=0;
        over=0;
        while(loop<3){
                tmp= x[loop] * y + over;
                over=tmp/256;
                x[loop]=tmp - (over*256);
                loop++;
        }
}


/* convert a 2 time 8 bit interger (16 bit=i[0] and i[1], i[2] is set to zero, 
* i is 24bit but only 16 bit are used) to decimal ascii and
* print it using the printfun. Padd all the digits to 4 (but we can handle full
* 16 bit if needed)
* If addcomma is set then the number will be printed by 1/10. That is
* 35 will be " 3.5" and 3 will be " 0.3" */
/* note: this function modifies *i !! */
void longtoascii(void (*printfun)(char),unsigned char *i,unsigned char addcomma)
{
        char str[7];
        unsigned char pos=7;
	unsigned char j=0;
	i[2]=0;
        do{
		str[pos-1]=divXbyY(i,10);
		str[pos-1]+= '0';
		pos--;
		j++;
		/* write a  "," after the last digit from the right */
		if (j==1 && addcomma==1){
			str[pos-1]=','; 
			pos--;
		}
        }while((i[0]|i[1]) && pos > 0);
	if (str[pos] == ',' && pos > 0){
		/* add the missing zero for 3 -> 0.3 */
		str[pos-1]='0';
		pos--;
	}
	/* add spaces to padd up to 4 digits */
	j=pos-3;
	while(j>0){
		(*printfun)(' ');
		j--;
	}
        /* now reverse the string and print */
        while(pos<7){
                (*printfun)(str[pos]);
                pos++;
        }
}



/* this function will be called in if the 8bit timer goes from ff to 00 */
SIGNAL(SIG_OVERFLOW0)
{
	/* stop 8bit timer */
	outp(0,TCCR0);
	if (ipwm_phase==1 && ipwm_h !=0){
		/* now we will produce a "1" at the output */
		/* time that we should remain "1" is ipwm_h */
		outp(0xFF-ipwm_h,TCNT0); /* set start value */
		sbi(PWMOUT0PORT,PWMOUT0); /* 1 */
		ipwm_phase=0;
	}else{
		/* now we will produce a "0" at the output */
		/* time that we should remain "1" is ipwm_h */
		outp(ipwm_h,TCNT0); /* set start value */
		cbi(PWMOUT0PORT,PWMOUT0); /* 0 */
		ipwm_phase=1;
	}
	/* start 8bit timer with 62500Hz, clock/64 */
	outp(3,TCCR0);
}

/* set current limit in a non linear way, 0=up, 1=down */
unsigned char step_i(unsigned char direction)
{
	if (ival<200){
		/* step in 50mA (or 25) units and round to the next 50 (or 25)*/
		ival=ival/IMINSTEP; /* round off values set via rs232 */
		ival=ival*IMINSTEP;
		if (direction){
			ival-=IMINSTEP;
		}else{
			ival+=IMINSTEP;
		}
		return(0);
	}
	if (ival<1000){
		/* step in 100mA units and round to the next 100 */
		ival=ival/100; /* round off values set via rs232 */
		ival=ival*100;
		if (direction){
			ival-=100;
		}else{
			ival+=100;
		}
		return(0);
	}
	/* more than 1A */
	/* step in 200mA units and round to the next 200 */
	ival=ival/200; /* round off values set via rs232 */
	ival=ival*200;
	if (direction){
		ival-=200;
	}else{
		ival+=200;
	}
	return(0);
}

/* set the current limit , reads ival and writes ipwm_h */
void set_i(void)
{
	int a; 
	if (ival>MAX_I){
		ival=MAX_I;
	}
	if (ival<50){ /* at least 50 mA, with 8bit this is the lower limit */
		ival=50;
	}
	if (ival<140){
		/* change this to calibrate */
		/* y=(a/1000) *x */
		//a=65;   /* use this if R38=33K */
		a=94; /* use this if R38=120K */
	}else if (ival<500){
		/* change this to calibrate */
		/* y=(a/1000) *x */
		//a=75;  /* use this if R38=33K */ 
		a=107; /* use this if R38=120K */
	}else{
		/* change this to calibrate */
		/* y=(a/1000) *x */
		//a=82; /* use this if R38=33K */
		a=111; /* use this if R38=120K */
	}
	tmpval[0]=ival & 0xff;
	tmpval[1]=(ival >> 8);
	tmpval[2]=0;
	multiXbyY(tmpval,a);
	divXbyY(tmpval,100);
	divXbyY(tmpval,10);
	ipwm_h=tmpval[0];
	/* debug, activate to calibarate */
	//longtoascii(uart_sendchar,tmpval,0);
	//uart_sendchar(' ');
}

/* set the voltage , reads uval and writes OCR1 */
void set_u(void)
{
	int a; 
	unsigned char c,b;
	if (uval>MAX_U){
		uval=MAX_U;
	}
	if (uval<0){
		uval=0;
	}
	/* below 1V the accuracy is limited because the values are
	* too much quantisized (the digital resolution gets smaller) 
	* If you draw a curve you find that it is 99% linear */

	/* remove the programming cable if you calibrate. It influences
	* the output values slightly */
	if (uval<5){ 
		/* change this to calibrate */
		a=250;   /* y=(a/(b*c)*x */
		b=10;
		c=10;
	}else if (uval<25){
		/* change this to calibrate */
		a=305;   /* y=(a/(b*c)*x */
		b=10;
		c=10;
		/*--- end calibrate ---*/
	}else if (uval<120){
		/* change this to calibrate */
		a=793;   /* y=(a/(b*c)*x */
		b=10;
		c=25;
		/*--- end calibrate ---*/
	}else{
		/* change this to calibrate */
		a=637;   /* y=(a/(b*c)*x */
		b=20;
		c=10;
		/*--- end calibrate ---*/
	}
	/* 24bit math for better accuraty */
	tmpval[0]=a & 0xff;
	tmpval[1]=(a >> 8) & 0xff;
	tmpval[2]=0;
	multiXbyY(tmpval,uval); /* uval is smaller than tmpval */
	divXbyY(tmpval,b);
	divXbyY(tmpval,c);
	if (mode!=2){ /* do not mess up standby state */
		outp( tmpval[1],OCR1H); /* set pwm high time*/
		outp( tmpval[0],OCR1L); /* set pwm high time*/
	}
	/* debug, activate to calibarate */
	//longtoascii(uart_sendchar,tmpval,0);
	//uart_sendchar(' ');
}

void toggle_standby(void){
	if (mode == 2){
		/* set U to zero in standby but do not modify 
		* the displayed value */
		outp( 0,OCR1H); /* set pwm high time*/
		outp( 0,OCR1L); /* set pwm high time*/
		ipwm_h=0; /* current = 0 */
	}
	if (mode ==0){
		/* activate voltage output again */
		sbi(ILEDPORT,ILED); /* red led off */
		set_i();
		set_u();
	}
}

/* update rs232*/
void updaters232(void)
{
	uart_sendstr_P("u:");
	tmpval[0]=uval & 0xff;
	tmpval[1]=(uval >> 8);
	longtoascii(uart_sendchar,tmpval,0);
	if (mode==2){
		uart_sendstr_P(" s:1 i:");
	}else{
		uart_sendstr_P(" s:0 i:");
	}
	tmpval[0]=ival & 0xff;
	tmpval[1]=(ival >> 8);
	longtoascii(uart_sendchar,tmpval,0);
	if (mode==1){
		uart_sendstr_P(" l:1\n");
	}else{
		uart_sendstr_P(" l:0\n");
	}
}

/* update display */
void updatedisplay(void)
{
	lcd_clrscr();
	lcd_puts_P("u: ");
	tmpval[0]=uval & 0xff;
	tmpval[1]=(uval >> 8);
	longtoascii(lcd_putc,tmpval,1);
	lcd_puts_P(" V ");
	if (mode==0){
		lcd_puts_P("<-");
	}
	if (mode==2){
		lcd_puts_P("standby");
	}
	lcd_gotoxy(0,1);
	lcd_puts_P("i: ");
	tmpval[0]=ival & 0xff;
	tmpval[1]=(ival >> 8);
	longtoascii(lcd_putc,tmpval,0);
	lcd_puts_P(" mA ");
	if (mode==1){
		lcd_puts_P("<-");
	}
}


int main(void)
{
	unsigned char i,rxresult,status,j,ilimitdebounce;
	unsigned int ignorebutton;
	unsigned int blink; /* blink in standby mode */
	char cmd;
	char *val;


	/* initialize display, cursor off */
	lcd_init(LCD_DISP_ON);
	/* current limit detection as input line */
	cbi(ILIMITDDR,ILIMIT);
	/* red current limit LED as output */
	sbi(ILEDDDR,ILED);
	sbi(ILEDPORT,ILED);
	/* initialize rs232 */
	uart_init();

	/* initialize PWM output 0 (current control) as output */
	sbi(PWMOUT0DDR,PWMOUT0);
	/* setup the 8bit timer for current control */
	sbi(TIMSK,TOIE0); /* enable T0 overflow0 interrupt */
	outp(0,TCNT0); /* start value */
	/* start 8bit timer with 62500Hz, clock/64 overflow0 every 64 */
	outp(3,TCCR0);
	ipwm_phase=0; /* initialize */
	ival=100; /* initialize default current */

	/* initialize PWM output 1 (voltage control) as output */
	sbi(PWMOUT1DDR,PWMOUT1);
	/* setup the 16bit timer for voltage control, in the
	* 4433 this timer supports hardware pwm therefore we
	* do not have to implement it in software */
	outp(0x83,TCCR1A); /* enable 10bit pwm and clear on compare match */ 
	outp(0x2,TCCR1B); /* run at 1/8 of crystal freq */
        /* write high byte first */
        outp(0x0,TCNT1H); /* set counter to zero*/
        outp(0x0,TCNT1L); /* set counter to zero*/
	uval=50; /* initialize default voltage, 50=5.0V */

	mode=0;
	oldmode=mode;

	ignorebutton=0;
	ilimitdebounce=0;
	blink=0;

        /* button as digital input */
        cbi(I_BUTTONDDR,I_BUTTON);
        cbi(U_BUTTONDDR,U_BUTTON);
        cbi(UUBUTTONDDR,UUBUTTON);
        cbi(SBBUTTONDDR,SBBUTTON);
        /* pull up resistor on */
        sbi(I_BUTTONPORT,I_BUTTON);
        sbi(U_BUTTONPORT,U_BUTTON);
        sbi(UUBUTTONPORT,UUBUTTON);
        sbi(SBBUTTONPORT,SBBUTTON);
        /* the buttons are multiplexed, enable output lines */
        sbi(BUTTONDOWNDDR,BUTTONDOWN);
        sbi(BUTTONUPDDR,BUTTONUP);


	sei(); /* enable interrupt */

	/* now we can use uart/lcd display */
	set_i();
	set_u();
	updatedisplay();

	while(1){
                /* first check up (++) buttons */
                sbi(BUTTONDOWNPORT,BUTTONDOWN);
                cbi(BUTTONUPPORT,BUTTONUP);
                j=0;
		/* ignorebutton is needed for debounce */
                while(j<2 && ignorebutton==0){
			if (bit_is_clear(I_BUTTONPIN,I_BUTTON)){
				step_i(j);
				set_i();
				updatedisplay();
				updaters232();
				ignorebutton=15000;
			}
			if (bit_is_clear(U_BUTTONPIN,U_BUTTON)){
				if (j==0){
					uval+=1;
				}else{
					uval-=1;
				}
				set_u();
				updatedisplay();
				updaters232();
				ignorebutton=15000;
			}
			if (bit_is_clear(UUBUTTONPIN,UUBUTTON)){
				if (j==0){
					uval+=10;
				}else{
					uval-=10;
				}
				set_u();
				updatedisplay();
				updaters232();
				ignorebutton=15000;
			}
			if (bit_is_clear(SBBUTTONPIN,SBBUTTON)){
				if (mode != 2){
					mode=2;
				}else{
					mode=0; /* if i-limit is active then this will change below */
				}
				toggle_standby();
				ignorebutton=20000;
			}
                        /* now check down (--) buttons */
                        cbi(BUTTONDOWNPORT,BUTTONDOWN);
                        sbi(BUTTONUPPORT,BUTTONUP);
                        j++;
                }

		if (ignorebutton) ignorebutton--;
		if (ilimitdebounce) ilimitdebounce--;
		if (mode!=2){ /* not standby */
			if (bit_is_clear(ILIMITPIN,ILIMIT)){
				mode=1;
				ilimitdebounce=200;
				/* red led on */
				cbi(ILEDPORT,ILED);
			}else{
				if(mode==1 && ilimitdebounce==0){
					mode=0;
					/* red led off */
					sbi(ILEDPORT,ILED);
				}
			}
		}

		if (mode!=oldmode){
			updatedisplay();
			updaters232();
		}
		oldmode=mode;

		/* flash red led in standby */
		if (mode==2){
			if (blink > 15000){
				blink=0;
			}
			if (blink <2000){
				cbi(ILEDPORT,ILED); /* red led on */
			}else{
				sbi(ILEDPORT,ILED); /* red led off */
			}
			blink++;
		}

		rxresult=uart_getrxbufnl(uart_outbuf);
		if (rxresult==0){
			continue;
		}
		/* valid command are: 
		* i=0-3000 u=0-300 s=0-1 s=? u=? i=? */
		/* now parse the comands and act on them */
		if (strlen(uart_outbuf) < 3) {
			reply_err();
			continue;
		}
		if (uart_outbuf[1] != '='){
			reply_err();
			continue;
		}
		val=&(uart_outbuf[2]);
		cmd=uart_outbuf[0];
		status=0;
		/* act on the commands */
		if (cmd=='i'){
			if (*val== '?'){
				updaters232();
				continue;
			}
			ival=(int)atoi(val);
			set_i();
			updatedisplay();
			status=1;
		}
		/* act on the commands */
		if (cmd=='u'){
			if (*val== '?'){
				updaters232();
				continue;
			}
			uval=(int)atoi(val);
			set_u();
			updatedisplay();
			status=1;
		}
		if (cmd=='s'){
			i=*val;
			if (i == '?'){
				updaters232();
				continue;
			}
			if (i == '1'){
				mode=2;
				status=1;
			}
			if (i == '0'){
				mode=0; /* if i-limit is active then this will change in the next loop */
				status=1;
			}
			toggle_standby();
		}
		/* command handling done. now return status */
		if (status==1){
			reply_ok();
		}else{
			reply_err();
		}
	}

}