//
 
//
// NOTE NOTE NOTE NOTE - Showing this on a web page means that
// occasional weird stuff happens even though I've coded it for a < PRE > 
// html type. Be sure to check the code carefully before trying to run it
// You might also just want to download the zip version which is 
// at http://academic1.bellevue.edu/robots/ssc5apure.zip
//
// ssc5a.c rev 1.2 (changed timing just a little more)
// includes DTR/CTS - 
// Meant to be used with CCS C, but can
// probably be used with others with modification.
//
// This is the inverted version. To change to non-inverted
// modify the ext_int_edge(L_TO_H) to ext_int_edge(H_TO_L)
// in main and the three lines in the serial routine that 
// are commented.
//
// A shot at making an ssc controller using a 16f84 - 4 mhz clock only
//
// 1. interrupt driven
// 2. handles 6 servos 
// 3. initial PWM is 1000 to 2000 us with 1500 as center (90 deg travel)
// 4. to change to 180 degree travel, increase PWM to 500 to 2500 range
//    to do that, pull pin a2 high - else pull it low.
// 5. resolution is 90/250 or 180/250 depending on which range 
//
// Format for interrupt (three bytes expected)
//
// 0.  use timeout, if timeout occurs (bad input), no changes
// 1.  first byte is a sync byte (255) and thrown away
// 2.  next byte is servo controller, number between 0 and 5, else return
// 3.  next byte is servo position - must be between 0 and 250
// 4.  clear DTR
// 5.  update servo controller number in array with servo position
//
//  note1: allowed bytes are 0 through 250, 251 - 255 reserved
//	   but not necessarily used.
//  note2: the serial routine is straight from Peter Anderson's example
//	   and that code really belongs to him. I modified it so it would
//	   use non-inverted data. Visit Peter Anderson's web page at:
//	   www.phanderson.com. The reason I used his code instead of the
//	   built-in CCS serial routines was because CCS getch() will lock
//  	   up the PIC if it doesn't get a character. This forces a reset and
//	   would be pretty ugly when used in a noisey environment.
//	   Peter Anderson's routine has a timeout built in.
//  note3: all servos react differently. If you use 180 degree mode, be 
//         sure the servo is not banging against stops! You may have to trim 
//	   back on the upper and lower limits to prevent this. If the 
//	   servo bangs against stops, it will overheat, break, etc.
//	   I have some Hi-Tec 300s that use 20-230 as the range to get
//	   180 degrees. Either more or less will bang the stops.
//  note4: I could have made this a helluva lot more cryptic (and efficient)
//	   but I decided to spell it out in case I wanted to understand it
//	   six months from now. Therefore, you will see lots of code that
//	   could be tweaked and made cleaner and faster.  In most cases
//	   I did it that way on purpose.  You should play with the code
//	   and make it way kewl.
//  note5: DTR is asserted with B7. If you don't wait until you've timed 
// 	   the servos, there will be jitter when you go get the interrupt
//	   Tie B7 to the CTS line of the data terminal (pin 8 on a 9 pin).
//
// Format for main loop 
//
// 0.  loop through array of values for the servo positions
// 1.  calculate values and send out appropriate pulses (one at a time)
// 2.  assert DTR to allow terminal to update if it feels like it
// 3.  delay 6 ms. total delay between updates will be about
//     15 ms (allows for time taken to update other five servos and
//     lets you have several interrupts during each service cycle)
// 4.  drop DTR low to turn off terminal updates while setting positions
// 5.  goto 0
//

#include < 16f84.h >
#include < defs_f84.h > // get these from www.phanderson.com for free
		      // (they are part of his example, called flash.zip)
#fuses xt,nowdt,noput,noprotect
#define RxData 0 // port B, pin 0 (external interrupt pin)
#define maxServos 5    // 0-5 are possible servo numbers
#use delay (clock=4000000)

int get_ch(long t_wait);
unsigned int position_array[6]={125,125,125,125,125,125};

// interrupt routine follows. For CCS compilers, the function that follows
// the compiler directive is the interrupt function. It doesn't matter 
// what its name is.

#int_ext
int_routine()
{
	// this has been done step by step. It could be a lot more
	// efficient - but this way you can read it and see what's what.
	// So far, it has worked fine this way.
	//
	// The general idea is to get 3 bytes on an interrupt. That will
	// equate to about 2.7 ms for each interrupt that occurs. 
	//
	// The three bytes are: 
	// 	1.  is thrown away
	//	2.  is the servo number that will be updated
	//	   and must be between 0 and 3 inclusive.
	//	3.  is a number between 0 and 250 which
	//	   will be multiplied by 4 or 8 by the main program.
	//	   If in 180 degree mode, then it is multipled by 8.
	//
	//	Peter Anderson's code for getting a character from the
	//	serial port includes a timeout value if a char is not
	//	sent. CCS doesn't seem to have this feature so the
	//	CCS serial input routines were not used.

   unsigned int servonum; // servo number passed in
   unsigned int serposition;  // servo position passed in
   unsigned int syncByte;   // syncByte - throw away

    syncByte=get_ch(1);
    servonum=get_ch(1);
    serposition=get_ch(1);

//
//  stop any more servicing on this cycle
//

    bit_clear(PORTB,7); 

//
//  sanity checks
//
    if(servonum==255) return;
    if(servonum>maxServos) return; 
    if(serposition>250) return;
//
//  update position array
//
    position_array[servonum]=serposition;
 }

void main(void)
{
   unsigned int x;
   unsigned long y;
   int skip=0;
   TRISA=0xFF; // define A as all inputs
   TRISB=0x01; // define B as all outputs except int port
   if(input(pin_a2)) skip=1; // set up for 90 or 180 degrees

   enable_interrupts(INT_EXT); // mandatory if you want externals
   ext_int_edge(L_TO_H);  // This is for inverted data, use H_TO_L for non-inverted
   enable_interrupts(GLOBAL); // mandatory to start it all
   bit_clear(portb,7);
   // now start main loop
   if(!skip){
    //
    // if here then 90 degree mode is selected. translates: 1000-2000 us
    // to get 90 degree mode, ground pin a2 else pull it high through 10k
    //
    while(1)
    {
     for(x=0;x<6;x++)      // step through the array and send out the data to b1-b6
     {
	 y=position_array[x];
         bit_set(PORTB,x+1);
	// 
	// ok, before you grimace on these next lines, I tried some
	// simple math routines, but it became more efficient  to do it
        // this way. If you find a way of cleaning this up (not to only
	// look good, but to work!) then fix this puppy up.
	//
	 delay_ms(1);
	 delay_us(y);
         delay_us(y);
         delay_us(y);
	 delay_us(y);   // this would be about 1000 us delay + 4 * position_array[x] in us   
	 bit_clear(PORTB,x+1);
     }
     enable_interrupts(GLOBAL);
     bit_set(PORTB,7);   // enable DTR (CTS-pin 8 with a 9 pin RS232 to PC)
     delay_ms(4);       // servo delay before next pulse - allows for 
                         // for a normal hobby servo - with all the calls
			 // and overhead, this ends up as about 15 ms 
    bit_clear(PORTB,7);
    delay_ms(2);	// short delay before turning off interrupts (allows for slow terminals)
    disable_interrupts(GLOBAL);  // it is bad to even get a noise interrupt
    }
   }
   //
   // if to here, then skip is selected. Which gets you 180 degrees
   // instead of 90 - translates: 500 to 2500 us pulse.
   // Note: be careful in this mode - many servos will bang
   // against stops if in 180 degree mode and 0 degrees or
   // 180 degrees is selected. Try 20 - 230 as min/max inputs
   // when you are first starting
   //
   while(1)
   {
     for(x=0;x<6;x++)      // step through the array and send out the data to b1-b6
     {
	 y=position_array[x];
         bit_set(PORTB,x+1);
	//
	// see comments in the above section about how the following
	// looks awful but works.
	//
         delay_us(230);
         delay_us(230);
         delay_us(y);
         delay_us(y);
         delay_us(y);
         delay_us(y);
         delay_us(y);
         delay_us(y);
         delay_us(y);
	 delay_us(y);   // this would be about 500 us delay + 8 * position_array[x] in us   
	 bit_clear(PORTB,x+1);
     }
     enable_interrupts(GLOBAL);
     bit_set(PORTB,7);   // enable DTR (CTS-pin 8 with a 9 pin RS232 on a PC)
     delay_ms(4);       // servo delay before next pulse - allows for 
                         // for a normal hobby servo - with all the calls
			 // and overhead, this ends up as about 15 ms
     bit_clear(PORTB,7);
     delay_ms(2);	// short delay before turning off interrupts (allows for slow terminals)
     disable_interrupts(GLOBAL); // shut off interrupts while starting servos
   }
}
//
// RS232 input with timeouts. 9600 8N1
// This code is a section of code written by Peter Anderson
// I modified it - any bugs are mine.
//
// Inverted version.
// Non-inverted pulses start high and drop low
// Inverted pulses start low and go high
//
// Meant to be a direct hookup to the PC (hook the
// transmit line (pin 3 on a 9 pin DIN) through a
// 47K resistor to the interrupt line. You can also
// hook up to an MCU by monitoring the DTR line (a2)
// until it goes high and then transmitting to B0
//

int get_ch(long t_wait)
// returns 0xff if no char recived within t_wait ms
{
   int one_ms_loop,  ser_loop, ser_data, ser_time;

   do
   {
        one_ms_loop = 100;	// 100 times 10 usecs
#asm

SCAN_1:
	CLRWDT
   NOP
	NOP
	NOP
	NOP
	BTFSC	PORTB, RxData	// check serial in - for inverted data
	GOTO 	SERIN_1		// if start bit (change BTFSC to BTFSS for non-inverted)
	DECFSZ 	one_ms_loop, F
	GOTO SCAN_1
#endasm
   }while(--t_wait);
   return(0xff);

#asm
SERIN_1:

SERIN_INV:
	MOVLW 8
	MOVWF ser_loop
	CLRF ser_data

	MOVLW 51         	// delay for 1.5 bits
	MOVWF ser_time		// 3 + 51 * 3 = 156 usecs for 9600

SERIN_INV_1:
	DECFSZ ser_time, F
	GOTO SERIN_INV_1

SERIN_INV_2:
	BTFSS PORTB, RxData
	BSF STATUS, C		// reverse for non-inverted (BSF, etc)
	BTFSC PORTB, RxData
	BCF STATUS, C           // reverse for non-inverted (BCF, etc)

	RRF ser_data, F

	MOVLW 23      		// one bit delay
	MOVWF ser_time		// 10 + 23 * 4 =  102 usecs

SERIN_INV_3:
	CLRWDT
	DECFSZ ser_time, F
	GOTO SERIN_INV_3

	DECFSZ ser_loop, F	// done?
	GOTO SERIN_INV_2	// get next bit

	MOVLW 10
	MOVWF ser_time		// wait for at least 1/2 bit

SERIN_INV_4:
	CLRWDT
	DECFSZ ser_time, F
	GOTO SERIN_INV_4
#endasm
   return(ser_data);
}