EihiS

July 9, 2010

A pic18f452 based 8 servo controller

Filed under: PIC progs and schematics — Tags: , , , , , , , — admin @ 11:44 am

This electronic project’s goal is to drive 8 servos directly , using a pic18f452 microcontroller.
Each of the 8 servo’s channels are driven by a potentiometer, wich position is directly converted to a pulse length, thus allowing each servo to reach the desired position.
Additionaly, a 16×2 chars display can be connected (it’s fully operationnal, but optional) to allow for reading of the 8-Bit data for each of the 8 channels ( this can be usefull when searching for specific ‘key’ positions of a robot for example)

It’s not a big project, but i think it’s a good start point for people who wants to learn more about the microchip pic’s programming techniques.
In this project, we use 3 main elements :

  • A pic 18f452 at 40Mhz clocking
  • A cmos 4067 used as an analogic multiplexer
  • A hd44780 compatible 2*16 characters display

So we are going to deal with analogic/digital conversion, addressing from the pic to the 4067 , and manage the commands between the pic and the display.

The pic’s program is written in C , using CC8E (free edition).

Here is the schematic (click to see full sized picture)

RV1,RV2 are here for test purposes and show how the potentiometers have to be connected to POT-CONN.
SW1(reset switch) is also there for test purposes.
VCC is 5V, and we use a separate line for SERVO power (i.e a 6V power line)
The pic’s outputs are limited thru the 8 resistors of 10k.If you dont do this, the pic will suffer of overload and will no longer output the whole pulses on each of it’s ports.
RB5 is not used , to enable an In circuit Low voltage programming.That pin must be set to ground if you dont want to enter ‘program mode’ ( the pulldown resistor is not shown on this schematic).

The logic behavior is pretty simple :

Each 18ms (an interrupt routine has been setup into the pic using Timer0 ) , we scan the 8 input lines of the cd4067 and convert the select analogique signal present on RA0 to a digital one.

These oscillograms show the voltage on the analogue-out pin of the 4067 :
( 1V/Div-vert & 2ms/Div-hor)

A closer look : the 8 analogue values follow each others during the conversion process

The A/D conversion is done in 10-bits, but we keep only the 8 leftmost bits so we have a 8-bit value as a result of the conversion for our program.
At this time, we put the 8 pwm lines to a logical ‘1′ state.
Right after this conversion time, we start increasing internal counters starting from zero and compare it to match the converted 8bit value.
When the internal counter equals the converted value, we bring down the corresponding pwm line to a zero logic level. So, for each channels, the pulse width will be directly related to the potentiometer’s value.
After that process, we program the timer0 value to match a 18ms time between each new pulses :

Aspect of the ouput pulses on the SERVO-0 channel ouput  :
1V/Div-vert & 5ms/Div-hor

You’ll notice that i’ve used a 16 channels mux ,but a 8 channel mux already exists in cmos technology : that’s for test conveniences because the actual program can be modified and more analogic inputs could be usefull for specific projetcs.
The output connectors for servos a JR-like.

The display part is out of the 18ms interrupt routine and runs in the forever-main loop of the pic.
I’ve put some fonctions to simplify writing to the display , wich is a hd44780 (industry standard) compatible display.
For this project’s building i’ve used a noritake-itron display i had in my hands. it’s complete ref is CU16025ECPB-W3J.

It’s a VFD (pretty neat green fluo light) build to replace the classic LCD 16×2 displays.
The only difference is that it does not use the pin3 (VEE) of its connector :
For LCD displays, that pin is used to tune the brightness with a potentiometer connected to it.
On that VFD , the brightness is setup by a software command.

Once running, the display shows the 8 servo’s 8bit A/D converted value, as shown of on the above picture.
The full system can run without the display connected , so if you dont have one .. dont worry about it :
I arranged the display routines to wait for the display to be updated without reading the busy flag thus enabling the display to be disconnected ( not present )

Now here is the code part (cc8e ,  C language. the code is compact as a one only file (no linker needed) )

 

#pragma chip PIC18F452
#pragma sharedAllocation
#include “int18XXX.h”
/*
####################################################################################
PIC 18F452 @ 40MHz , 8-SERVO A/D DRIVER
####################################################################################
*/
//
//
#define OUTPUT 0b00000000;
#define INPUT  0b11111111;
#define INBIT  1;
#define OUTBIT  0;
//
//
void InitialiseADC (void);    // A/D conversion Init routine
void ReadADC(void);     // A/D conversion READ routine
void Delay(unsigned long howmuch); // delay routine
void DelayLong(unsigned long howmuch);
void Delay2(unsigned long howmuch); //
void _highPriorityInt(void);  // interrupt handling
void LCD_PUTCMD(unsigned int what); // LCD put command routine
void LCD_WRITESTR(unsigned int y,unsigned int pos,unsigned int indice);
void LCD_WRITECHAR(unsigned int y,unsigned int pos,unsigned int what);
//
//
unsigned char _SERVO_VAL[8];  // SERVO channel counter
unsigned char _READ_VAL[8];   // storing current servo’s values from ADC
unsigned char _ADC_MUX_CHANNEL;  // active mux channel for a/d conv
unsigned char _DISPLAY_TOGGLE;  // for display routines : 4 up channels or 4 down channels display toggle
//
const struct  {      // display strings structure
  const char *str;
} txtp[] = {
  “PIC18 SERVODRIVE\0″,
  “0123456789ABCDEF\0″,
  “READY\0″,
  “WAITING\0″,
  “RECEIVED:\0″,   
  “TRANSMIT:\0″,   
};
//
#pragma origin 0×8
interrupt highPriorityIntServer(void)
{
    // W, STATUS and BSR are saved to shadow registers
    // handle the interrupt
    // 8 code words available including call and RETFIE
    _highPriorityInt();
 // restore W, STATUS and BSR from shadow registers
    #pragma fastMode
}
#pragma origin 0×18
interrupt lowPriorityIntServer(void)
{
    // W, STATUS and BSR are saved by the next macro.
    int_save_registers
    int_restore_registers // W, STATUS and BSR
}
void _highPriorityInt(void)
{
    uns16 sv_FSR0 = FSR0; // saves used registers
  
 
 unsigned char c;
 unsigned int loop_timer;
 unsigned int limit;
 
 //
 
 limit=0×0;
 if (TMR0IF)
 {
  // set all pwm channels (0 to 7) to high logic level (pulse high)
  loop_timer=0;
  PORTB=0b11011111;
  PORTC.3=1;
  //
  TMR0IE=0;
  PORTC.4=0;         // enable MUX decode
  //
  
  for (c=0;c<8;c++)
  {
    PORTC.0=c.0;
    nop();
    PORTC.1=c.1;
    nop();
    PORTC.2=c.2;
    nop();
    // 
    Delay(50);      // this delay and the conversion time (..)
    _ADC_MUX_CHANNEL=c;    // will generate a 600us pulse (minimum length)
    ReadADC();      // — sequential read of each channels –
    _SERVO_VAL[c]=0;    // — init channel upcounters –
  }
  PORTC.4=1;       // disable MUX decode
  
  // updates pulse high or low
  while (loop_timer<(unsigned int)0xff)
  {
   //
   
   if (_SERVO_VAL[0]==_READ_VAL[0]) { PORTB.0=0;} else { _SERVO_VAL[0]++;}
   if (_SERVO_VAL[1]==_READ_VAL[1]) { PORTB.1=0;} else { _SERVO_VAL[1]++;}
   if (_SERVO_VAL[2]==_READ_VAL[2]) { PORTB.2=0;} else { _SERVO_VAL[2]++;}
   if (_SERVO_VAL[3]==_READ_VAL[3]) { PORTB.3=0;} else { _SERVO_VAL[3]++;} 
   if (_SERVO_VAL[4]==_READ_VAL[4]) { PORTB.4=0;} else { _SERVO_VAL[4]++;}
   if (_SERVO_VAL[5]==_READ_VAL[5]) { PORTC.3=0;} else { _SERVO_VAL[5]++;}
   if (_SERVO_VAL[6]==_READ_VAL[6]) { PORTB.6=0;} else { _SERVO_VAL[6]++;}
   if (_SERVO_VAL[7]==_READ_VAL[7]) { PORTB.7=0;} else { _SERVO_VAL[7]++;}
   loop_timer++;
   // ajusting timing to reach a 2.4 max pulse width, from 0.6ms pulse min width
   nop();nop();nop();nop();nop();
   nop();nop();nop();nop();nop();
   nop();nop();nop();nop();nop();
   nop();nop();nop();nop();nop();
   nop();nop();nop();nop();nop();
  }
  //
  TMR0H=0xed;       // setup timer0 value : will match 18ms , @40MHz clock between pulses time
  TMR0L=0×40; //e5
  
 
  TMR0IF=0;       // reset timer0 interrupt flag
 }
 
 TMR0IE=1;        // enables back TIMER0 overflow interrupts
 //
  FSR0 = sv_FSR0;       // restore used registers
 
}
//
void main(void)
{ // some vars
  unsigned char c;
 // lcd potentiometer display related vars
 static const char chaine[]=”0123456789abcdef”; // hexadecimal chars
 unsigned char disp_var;
 unsigned char ypos;
 unsigned char trs;
 //
 _DISPLAY_TOGGLE=0;  // toggles 0=(0…3) and 1=(4…7) potentiometer values display
 ypos=0;
 trs=0;
//
  //
 TXEN=0;
 SPEN=0;
 // for TIMER0 interrupt
 TMR0IP=1;    // timer 0 interupt is high priority
 TMR0IF=0;    // clears timer 0 interrupt bit flag
  RCON.7=1;    // enables priority levels on interrupts
 //
 SSPEN=0;   // disable synchronous serial port
 T1CON.0=0;    // no timer 1
 T2CON.2=0;    // disable timer 2
 T3CON.0=0;    // disable timer 3
 CCP1CON=0;    // no CCP1
 CCP2CON=0;    // no CCP2
 RBIE=0;     // disable port B change interrupt
 INT2IE=0;
 INT1IE=0;    // not external interrupts enabled
 PIE1=0;     // no various interrupts (check databook)
 //
 TRISA.5=OUTBIT;   // RS line of display
 TRISA.4=OUTBIT;   // R/w line of display
 TRISA.3=OUTBIT;   // E line of display
 TRISB = OUTPUT;   // PWM output servo 0.1.2.3.4.5.6.7
 TRISD = OUTPUT;   // output datas for display
 TRISC = OUTPUT;   // output channel select (4bits) - only 3 are needed ( 8 servos)
 //
 LATA=0;
 LATB=0;     // clears B port latches ( pgm pin as input is a problem..)
 LATC=0;
 //
 _ADC_MUX_CHANNEL=0;
 InitialiseADC();  // init ADC and PIC ports as needed by the hardware
 PORTA=PORTA&0b1100011;    // Rs,r/w and E are low
 PORTD=0;       // clears display data bus
 // —- init display first :
 Delay2(2000);      // waits for power-up display module RESET done.
 LCD_PUTCMD(0b00110000) ;    // 8 bit interface, 2 lines (old val=0×38)
 LCD_PUTCMD(0b00001100);    // display on, cursor off, blinking off
 Delay2(200);
 LCD_PUTCMD(0b00000001);    // LCD CLEAR
 Delay2(200);
 LCD_PUTCMD(0b00000010);    //cursor home, init address counter to 0 ..
 // —- displays welcome message
 LCD_WRITESTR(1,0,0);    // (row 1..2,xpos 0…F,string number).
 DelayLong(0×300);     // pause
 LCD_PUTCMD(0b00000001);    // LCD CLEAR
 Delay2(2000);
 // —- starting up timer interruption and counting
 TMR0H=0×00;
 TMR0L=0×01;
 T0CON=0b10000100;  // timer0 = ON, internal clock, prescaler 1:256 ,16bit count
       // enabling interrupts, both high and/or low priority
 //GIEL = 1;      // enable low priority interrupt
    GIEH = 1;     // enable high priority interrupt
 // fin interruptions program
 TMR0IE=1;    // starts timer0 interupts
 while(1)
 {
  if (_DISPLAY_TOGGLE==0)
   {
    ypos=0;trs=0;
   }
  else
   {
    ypos=1;trs=4;
   }
  
  for (c=0;c<4;c++)
   {
    // updating display with the 8 potentiometer values
    disp_var=_READ_VAL[c+trs]>>4;
    PORTA.5=0; // register mode
    nop();
    PORTA.4=0; // write mode
    if (ypos==0) { PORTD=0×80+c*4;} else { PORTD=0xc0+c*4;}
    PORTA.3=1;
    Delay(200);
    PORTA.3=0;
    nop();
    //
    PORTA.5=1; // data mode
    nop();
    PORTA.4=0; // write mode
    PORTD=chaine[disp_var];
    PORTA.3=1;
    Delay(200);
    PORTA.3=0;
    nop();
    //
    disp_var=_READ_VAL[c+trs]&0b00001111;
    PORTA.5=1; // data mode
    nop();
    PORTA.4=0; // write mode
    PORTD=chaine[disp_var];
    PORTA.3=1;
    Delay(200);
    PORTA.3=0;
    nop();
    
   }
   _DISPLAY_TOGGLE=!_DISPLAY_TOGGLE; 
  
  }
 
}
// end MAIN
// ###########################################################################
//
// SUB ROUTINES following :
//
//
// ### DELAY ROUTINE ( For interrupt use )
void Delay(unsigned long howmuch)
{
 unsigned long ma;
 char count=0;
 for( ma=0;ma<=howmuch;ma++) {
  // PORTC.0=count;
  count++;
 } 
}
void DelayLong(unsigned long howmuch)
{
 unsigned long ma;
 unsigned long ko;
 char count=0;
 for( ma=0;ma<=howmuch;ma++) {
  for (ko=0;ko<1000;ko++) { nop();}
  count++;
 } 
}
//
// ### DELAY ROUTINE (For general purpose use)
void Delay2(unsigned long howmuch)
{
 unsigned long ma;
 char count=0;
 for( ma=0;ma<=howmuch;ma++) {
  // PORTC.0=count;
  count++;
 } 
}
// ### init conversion A/D
void InitialiseADC (void) {
 TRISA.0 = INBIT;   // RA0 as analogue input for A/D conversion
 ADCON1 = 0b01000100; // left justified result, an0 analog input, vcc/gnd, b6=1 (pour fosc/64)
 RCIE=0;     // no interrupt on AD / godone
 ADIE   = 0;        /* Masking the interrupt */
   ADIF    = 0;         /* Resetting the ADC interupt bit */ 
 PORTC.3=0;
 ADCON0=0b10000001; // A/D on , B7,6=10 pour fosc/64 , b2=1 pour conversion START
 Delay(100); 
}
// ###  A/D conversion request
void ReadADC(void)
{
 
 ADRESL = 0;        /* Resetting the ADRES value register */
 ADRESH = 0;       
 
  ADCON0=ADCON0|0b100; // GO !
 while((ADCON0&0b00000100)) continue; // tant que bit 2 est a 1, attente de fin de conversion…      
   _READ_VAL[_ADC_MUX_CHANNEL]=ADRESH;
 if (_READ_VAL[_ADC_MUX_CHANNEL]==0xff) { _READ_VAL[_ADC_MUX_CHANNEL]–;}
}
// ______________________________________________________________
//
// ########## LCD RELATED ################################
//
// ______________________________________________________________
void LCD_PUTCMD(unsigned int what)
{
 PORTA.5=0; // register mode
 nop();
 PORTA.4=0; // write mode
 PORTD=what;
 PORTA.3=1;
 Delay2(300);
 PORTA.3=0;
}
//
// ________________________________________________________________
void LCD_WRITESTR(unsigned int y,unsigned int pos,unsigned int indice)
{
 unsigned char the_char;
 unsigned char char_pos;
 if (pos!=0xFF)
 {
 PORTA.5=0; // register mode
 nop();
 PORTA.4=0; // write mode
 if (y==1) { PORTD=0×80+pos;} else { PORTD=0xc0+pos;}
 PORTA.3=1;
 Delay2(300);
 PORTA.3=0;
 nop();
 }
 //
 PORTA.5=1; // data mode
 nop();
 PORTA.4=0; // write mode
 char_pos=0;
 while (txtp[indice].str[char_pos]!=0)  // loop to display full string, ended by a zero ascii char
 {
  the_char=txtp[indice].str[char_pos];
  PORTD=the_char;
  char_pos++;
  PORTA.3=1;
  Delay2(300);
  PORTA.3=0;
 }
}
//
// ______________________________________________________________
//
// where : pos=0xff -> writes char after the previous one OR pos =0 … 0xF = line position
// y=1 : first row, y=2 second row of the display ( if  pos=0xff is not used )
void LCD_WRITECHAR(unsigned int y,unsigned int pos,unsigned int what)
{
 if (pos!=0xFF)
 {
 PORTA.5=0; // register mode
 nop();
 PORTA.4=0; // write mode
 if (y==1) { PORTD=0×80+pos;} else { PORTD=0xc0+pos;}
 PORTA.3=1;
 Delay2(300);
 PORTA.3=0;
 nop();
 }
 //
 PORTA.5=1; // data mode
 nop();
 PORTA.4=0; // write mode
 PORTD=what;
 PORTA.3=1;
 Delay2(300);
 PORTA.3=0;
 nop();
}

 

 

The full source is here for your convenience.  ( the PWM pulse length timing may need to be adapted depending on the servo’s youre using but looks to work fine with my basic servos )

If you dont have the compiler, you can directly use scd_displayed2.HEX for you favorite pic programmer.

ENJOY ! (be smart, put some comments about this post if it helped you ! thanks)

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

May 23, 2010

A remote 4 SERVOs Driving module diagram

Filed under: PIC progs and schematics — Tags: , , , , , — admin @ 6:48 pm

I’ve moved and been away for a little time…

Now i’m back with a project about the remote control of servomotors with a PIC as controller , and discrete ICs to generate the correct timings pulses necessary to drive the servos.

In the way i look at the things, i’m having trouble with the fact that the PIC microcontroller could be used only as a PWM generator, that’s the reason why i decided to develop a remote board , that could take in charge the generation of correct sized pulses in the 20ms period , providing that the PIC has pre-loaded the duration time into each timers.

I came out with a concrete solution designed to be stack-able and easily customizable for all of you , robots addicts ! It uses 4516 CMOS counters and 4011 classic CMOS gates

Take a look at this ( click to enlarge )

So what the hell is that ?

The clock for each counters is the same one , a 8KHz tunable clock, wich input is ‘clock in’ on my diagram.

A ‘load pulse’ is generated on the counter’s ‘Sx_LOAD’ input : it enables the loading of the data PD0,PD1,PD3,PD4 (binary) into the selected counter ( each of them have a separate ‘load data’ line ).
Each counter is counting down so it will be set at the binary value PD when the ‘load pulse’ is applied, then imediatly after the pulse has come to the low level, they will start counting down.In this mean time, the ‘carry out’ pin of the counter gets High, hence the ‘SERVO_x’ pin also.
When then counter reachs the ‘zero’ value, it’s ‘Carry Out’ gets low. so the generated ‘SERVO_x’ pulse will also get down, and the NAND gate then disables the clock to reach the counter’s clock input : the counter stops counting.

Then a new cycle can start again, by loading a new value into PD by a ‘load_pulse’ on the related pin, after , in the case of servomotors driving, around 20ms (minus the servo pulse time) of waiting time.

I could have added a 2->4 decoder onto the 4 servo board, but i have first decided to keep the ‘Sx_LOAD’ as-is to keep that diagram simple and easily customizable.

One of the important thing is that the counters are 4 bit counters, wich means that the servo will have a 4 bit resolution for positioning, hence a 11.25° resolution. ( 16 * 11.25° = 180° ) .

The timing are also important to understand :

A standard servo is operating from around 0.5ms to 2.5ms pulse length ( thats what i’ve experienced for my side ).
If you get deeper into my diagram logic, you’ll catch that the generated ’servo pulse’s’ duration is the sum of the ‘load_pulse’ time with the counter’s preset datas down count.
If we use a 0.5ms ‘load pulse’ duration, the preset datas combined with the 8KHz clock pulses will provide a covering of the needed time range , from 0.5ms to 2.375ms.

With that kind of remote timers, the robot’s microcontroller just has to manage the ‘load pulses’ and PD datas each 20ms.

Some of you could ask : so and..whats the gain there ?

If the microcontroller takes 0.5ms for each ‘load pulse’, and each load pulse must come every 20ms, then we can achieve a 40 servos driving with one microcontroller (theorical calculation)
Practically lets take a look at a 32 servos system :

  1. 4 Bits port for PD ( duration data )
  2. 6 Bits port for Servo Selection ( a 6 to 32 decoder , wich routes the ‘load_pulse’ to the needed servo) + 1 extra bit to enable decoding.

We have around 11 output Port-bits to achieve a 32 servo driving system - lets say it’s 12bits.
The only routine we have to code is a timer routing into the microcontroller.This routine has to happend every 20ms.
Each time the routine is executed, it programs PD0,PD1,PD2,PD3 of servo number ‘N’ then it programs the ’servo select’ port (6bits) , with the number ‘N’ , then sends a pulse of 0.5ms duration enabling the servo selection decoding (a ’decode_and_load_enable’ bit).
then the routine skips to the next servo in the list and repeat the cycle from the start : programming PD0,PD1,PD2,PD3, for the next servo ,  and so on for the 32 servos.

As we’ve already seen before,  if we use 32 servos, the complete routine will take about 0.5ms x 32 = 16ms.
So we are free with an extra processor time of about 4ms before the 20ms deadline.
If for example, we use a PIC18F @ 32MHz , those 4ms mean a lot of instructions and time to do other stuff than just driving the servos : our microcontroller has now time to do interesting stuff like converting probe datas , doing neural computations and so on , as a 4ms time @32 MHz means 32000 instructions.

Stay tuned !

 

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

cat{ } { post_69 } { } 2009-2015 EIhIS Powered by WordPress