Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

18 servo controller

Not open for further replies.


New Member
Here is my problem:

I need to control 18 servos for a hexapod robot. I need to be able to control all 18 servos simultaneously. I have been looking at the PWM setup, but I am not sure if this would be best, since there are not many channels on each PIC.

Option 1:
*use 6 PICs with at least 3 PWM outputs (one for each leg)
*each leg PIC controlled by master PIC (brain, tells legs what coordinates to move to, also handles sensors (maybe another 10 pins))

Option 2
*use 18F master PIC as brain(sensor, decision handling, gives leg positions to slave PIC)
*use one slave PIC with 18 outputs and bit-bang the pins. (receives move direction, stop, body pitch/roll/yaw, etc. modes from master)

Option 3
*find a PIC with 18 PWM channels

I am hoping some of you have some experience with this, cause I cannot make up my mind as to what would be the better choice. I will be using an 18F series for the brain, since making a crude AI will be easier with C.
I don't know how useful this will be for you, but might get some ideas from what they did. I pick it up on AVRFreaks yesterday. It controls 20 servos, the source is in 'C', and uses 2 x 4017. I think they used a ATmega16, a 40 pin chip, but only 3 pins go to the 4017s. Just took a brief look, don't know 'C'.


    44.6 KB · Views: 1,031
It looks like a much simpler design then I need to use. It looks like they are cascading the servos by using the decade counter? I need to be able to control each servo in real time.

Will a mux for the PWM signal work? Or does that signal need to be present on the servo input at all times?
You can do something similar on the PIC using the CCP module "compare" mode. The following example uses a single 74HC4017 IC and 2 PIC pins for 9 channels (on the 74HC4017 Q1..Q9 output pins). This could be expanded to 18 channels by adding another 74HC4017 driven by the CCP2 pin (and some additional interrupt code).

static unsigned char Qn = 0;
static unsigned int  Qarray [] = { 1500, 1500, 1500, 1500,
                                   1500, 1500, 1500, 1500,
                                   1500, 20000 };  // 800..2200 range

void isr_hi ()
  *  K8LH Crazy-8 HiRez Hard 8-channel 74HC4017 Servo Algorithm  *
  if (PIR1bits.CCP1IF == 1)       // if CCP1 "compare" interrupt
    CCPR1H++;                     // avoid update "match"
    CCPR1 += Qarray[Qn];          // update "compare" int value
    CCPR1H--;                     // fix CCPR1H
    LATCbits.LATC1 = 0;           // clear CCP1 "CLK" line
    PIR1bits.CCP1IF = 0;          // clear CCP1 interrupt flag

    Qarray[9] -= Qarray[Qn++];    // adjust end-of-period off time

    if (Qn = 10)                  // if end-of-period
    { Qn = 0;                     // reset Qn array index
      Qarray[9] = 21000;          // reset 20.0-msec period and
      LATAbits.LATA0 = 1;         // toggle 74HC4017 "CLR" line
      LATAbits.LATA0 = 0;         // to force Q0 output sync'
Last edited:
wschroeder said:
Code for 30 servos @ 256 resolution with a single PIC:

Also, I have a board and superb PC software using a USB PIC to drive the servos:

**broken link removed**
Ouch!!! That's almost a "standard" (isochronous) soft PWM routine but you destroy the Servo array values and rely on refreshing the array from the serial port for each 20 msec cycle. And it seems the same pulse width or PWM setting applied to all of the servos results in different timing (jitter) for the different servo port groups. I would not recommend this algorithm or method for anything more than a "learning exercise"!

Instead consider using a "standard" (isochronous) soft PWM routine which doesn't destroy the PWM array values. It'll continue to ouput pulses cycle after cycle and allows you to update one or more PWM array values at any time or interval (during your 17.5 msec 'window'). The soft PWM routine also correctly handles PWM array values of 0 (handy for LEDs) since it updates the output port from a shadow register.

        clrf    PWM             ;
        setf    Shadow          ; shadow bits all on
PWM256  movf    PWM,W           ; PWM counter 0..255
        cpfsgt  Servo+0         ; servo 0 (0..255) match?
        bcf     Shadow,0        ; yes, turn off output bit
        cpfsgt  Servo+1         ;
        bcf     Shadow,1        ;
        cpfsgt  Servo+2         ;
        bcf     Shadow,2        ;
        cpfsgt  Servo+3         ;
        bcf     Shadow,3        ;
        cpfsgt  Servo+4         ;
        bcf     Shadow,4        ;
        cpfsgt  Servo+5         ;
        bcf     Shadow,5        ;
        cpfsgt  Servo+6         ;
        bcf     Shadow,6        ;
        cpfsgt  Servo+7         ;
        bcf     Shadow,7        ;
        movff   Shadow,LATB     ; update output port
        incfsz  PWM,F           ; bump PWM counter
        bra     PWM256          ; do it again
Last edited:

It doesn't need to make a copy as each servo value is decremented 256 times.

I also don't understand why you think the original code would jitter? There would be a slight discrepancy between the first and last servo but it could be easily fixed (have a similar line of code to set the bits initially) and wouldn't cause jitter anyway.

Oops, I missed that (each array element decremented 256 times). Sorry.

But as far as jitter, he's turning on all of the port LAT bits at the same time at the beginning of the code but turning off the bits one group at a time with many cycles difference between groups. I don't see the "easy fix" you mention without using shadow registers. Please enlighten me some more? Thanks.
Last edited:
The easy fix is to turn the bits on with similar code to in the PWM loop but with just a bsf followed by a nop for each output. This would be placed before the 500uS delay.

I don't understand. Can you show me?

Also, that code doesn't seem capable of handling Servo PWM values of 0 for the minimum 500 msec timing.
Worst case is if the last servo pin in the decfsz array is 0 movement and nearly 1/256 resolution time has passed in the On state. Since 500us of delay at the beginning is an arbitrary value for this example, there is room for adjustment at this point to fine tune out any real physical servo movement for a 0 servo at the beginning of the routine. Maybe I should more accurately say that the resolution is 255 with a spare do nothing loop at the beginning.
The code to turn on the outputs would be,
	bsf	 PORTA.0
	bsf	 PORTA.1
	bsf	 PORTA.2
	bsf	 PORTA.3
	bsf	 PORTA.5
	bsf	 PORTB.0

This would be repeated for all outputs in the same order as the pwm loop.

Then would come the 500uS delay.

Then would come the loop that consist of,
             decfsz    _servo+0, 1,0 
             iorlw     1 
             decfsz    _servo+1, 1,0 
             iorlw     2 
             decfsz    _servo+2, 1,0 
             iorlw     4 
             decfsz    _servo+3, 1,0

The value of zero is the maximum value. The valid values being 1 to 256. You can of course shorten the 500uS delay.

I do have other code where I incremented all the servo values to make 0 = 0:

i = 30
FSR0ptr = @servo
Loop Until i = 0

I apologize.

There really is no jitter other than the normal plus or minus 1 cycle jitter associated with interrupt entry. All of the servo pulses maintain the same timing from 20 msec cycle to 20 msec cycle.

It's just pulse width timing that's off a bit;

Servo 00 (LATA) = 1 ---> 501.2 usecs
Servo 05 (LATB) = 1 ---> 503.0 usecs
Servo 13 (LATC) = 1 ---> 504.4 usecs
Servo 19 (LATD) = 1 ---> 506.2 usecs

No jitter...


Yes, of course. Now that's a simple and elegant and (embarrassingly) intuitive solution. Of course you'd need some extra 'nops' to account for those instructions writing the the LATx registers too.

Then modify that delay_us(500) statement to take into account the 12 cycles that occur before writing LATA and you've eliminated the pulse width timing problems...
Last edited:

You don't have to apologize. Your evaluations are always worthy of note. I didn't get involved with PIC's until a couple years ago, so some of my code can certainly use some refinement. I would now write the reload of the Timer by adding the 20ms number... a trick I learned a few months ago and before I wrote the PIC RTC examples for SF.

By the way, some of your ideas have turned out to be an inspiration for some new stuff I'm working on.
Ok guys, I'm impressed.

Between the two of you you've shown me that a jitter-free lo-rez 'soft' solution with more than 8 channels (which has eluded me) is feasable and probably practical (just not in the current form of Warren's demo')

Now, could you build interface code that would allow single channel serial updates instead of that 30 byte serial update? Are there any pins left over to allow using one pin for hardware handshaking to the host to signal when we're sitting in that 17.5 msec 'window' (where we can use the serial port) instead of sending that flag character?


I learn a lot by disecting code and your code is no exception. In this case, decrementing the pwm array values 256 times is better because it would work with 16F' devices too.
Ambient said:
Will a mux for the PWM signal work? Or does that signal need to be present on the servo input at all times?
You need to provide each Servo with a pulse of 1000 to 2000 usecs duration every 20 msecs.

You can use the CCP module PWM mode but you need to break up the 20 msec servo period into smaller faster PWM frames in order to maintain usable hi-rez pulse width resolution.

You can multiplex the PWM output signal by using it to drive one of the enable lines on a 74HC238 IC. Change the address lines on the 74HC4017 to direct a 1000 to 2000 usec PWM pulse to one of the eight 74HC238 outputs every 2500 usec interval.

While this provides a wonderful hi-rez no-jitter solution, it will only provide you with 16 servo outputs using two 74HC238 ICs and 5 pins (CCP1, CCP2, and 3 pins for decoder address lines). The following example is for 4 MHz (prescaler 1:1) or 16 MHz (prescaler 4:1) clocks.

/*                                                               *
*  array1 and array2 elements hold Servo pulse width values     *
*  ranging from 3000..9000 in 250-nsec 'ticks' representing an  *
*  extended pulse width range of 750-usecs to 2250-usecs        *
*                                                               */
static unsigned int array1 [] = { 6000, 6000, 6000, 6000,
                                  6000, 6000, 6000, 6000 };
static unsigned int array2 [] = { 6000, 6000, 6000, 6000,
                                  6000, 6000, 6000, 6000 };

*  K8LH 16-Channel 74HC238 Hi-Rez 'PWM' Servo Algorithm Driver  *
#pragma interrupt isr_hi

void isr_hi ()
{ if(PIR1bits.TMR2IF)
  { PIR1bits.TMR2IF = 0;            // clear TMR2 interrupt flag
    *                                                            *
    *  setup 74HC238 address lines and our work variables for    *
    *  the next servo channel interval during the final (10th)   *
    *  frame of the current servo channel interval (the final    *
    *  frame in each interval will always have a 0% duty cycle   *
    *  and all 74HC238 outputs are 'off' so it will be safe to   *
    *  change the address lines)                                 *
    *                                                            */
    if(frame == 10)             // 10 PWM frames/Servo interval
    { frame = 0;                // reset PWM frame counter
      select %= 8;              // 8 Servo intervals/20-msec cycle
      pulse1 = array1[select];
      pulse2 = array2[select];
      PORTB = (PORTB & 0b11111000) | select;
      select++;                 // bump for next interval
    frame++;                    // bump for next interrupt
    *  setup CCP1 PWM duty cycle for next 250-usec PWM 'frame'   *
    *                                                            */
    if(pulse1 > 1000)           // if pulse1 > 250-usecs
    { CCPR1L = 250;             // do a 100% duty cycle frame
      pulse1 -= 1000;           // subtract 250-usecs
    else                        // do a variable or 0% frame
    { CCP1CONbits.CCP1Y = (pulse1 & 1);
      CCP1CONbits.CCP1X = (pulse1 & 2);
      CCPR1L = (pulse1 >>= 2);
      pulse1 = 0;               // remaining frames are %0

    *  setup CCP2 PWM duty cycle for next 250-usec PWM 'frame'   *
    *                                                            */
    if(pulse2 > 1000)           // if pulse2 > 250-usecs
    { CCPR2L = 250;             // do a 100% duty cycle frame
      pulse2 -= 1000;           // subtract 250-usecs
    else                        // do a variable or 0% frame
    { CCP2CONbits.CCP2Y = (pulse1 & 1);
      CCP2CONbits.CCP2X = (pulse1 & 2);
      CCPR2L = (pulse2 >>= 2);
      pulse2 = 0;               // remaining frames are %0
If 16 channels isn't enough and a software or hardware 18 channel solution seems like too much baggage, you might consider using a couple 9 channel "slaves" connected via RS232. The drawing below shows 8 channels but it could easily be changed to include the 74HC4017 Q9 output for 9 channels with an 800..2200 usec range for each servo.

Last edited:
An interesting proposition for the communication. The TX pin could be used as a hardware CTS I suppose and then turn on USART/UART for RX only on the RX pin. I don't know if this is what you're inferring. Or, are you thinking of a more compact method of transmitting the data?

I'm referring to your previous posting above.... not the last post.
Last edited:
You've got it. Could hardware handshaking eliminate the requirement for the Host to watch for that flag character. Just to simplify host requirements...
Not open for further replies.

Latest threads