    /* 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();
    		}
    	}

    }