Let's Make Robots!

8servo controller using single 74hc595

I gave it a try tonight, since I always wandered whether it can be done. Control 8 servos over two wires, that's pretty attractive.

So the answer is yes, it can... and no it can not... Why?

Pros:

- It worked. I managed to control 8 servos independently (they are indpendently postionable but can not be individually switched off - which would be nice in some circumstances (power saving))

- On Atmega 168 @ 16 Hz I achieved resolution of 125 steps per 1 ms and since I managed control the servo in the range of almost 0.5 - 2.5 ms I get  ~ 250 steps per sth around 180 degrees 

- It indeed uses only 2 wires to control 8 servos

- the interface is very simple - you just call a procedure to set a servo position

- the circuit is dead simple also - it uses a single 74HC595

Cons:

- the handing of the add-on circuit on the microcontroller side is quite time-consuming. The interrupt routine gets called every 128 clocks, although mostly it only increases the counter, it sorta looks like the processor is busy some 50% of its time

- that asks for an implementation using a stand-alone microcontroller (sth like Attiny 2313) to leave the main unit free to do what's it supposed to do, but then you don't need the 595 at all - just one attiny would do, and you'd need a quarz resonator (20 or maybe 24 MHz overcocked) bunch of caps and at least 2 lines for data transmission and a protocol to controll the stuff.

So... I don't know... What do you think? Which way to go next?

Photo session:

top - just a single chip and pin headers

sx1.jpg

 

bottom:

red +5v, blue 0v, yellow - clock, brown = data, black = reset hooked up to 5vsx3.jpg

testbed: homebrew arduino used as dev.board programmed & debugged over ISP+debugwire using avrdragon

sx2.jpg

I also hooked it up to LEDs on my Attiny2313 proto board which helped a lot when debugging (while debuggin I slowed down the counter 64 times to see whether all shifts ok)

sx4.jpg

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

So here it goes

Da diagram

8servos.gif

 Da code


 #include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define CLOCKPIN 7            // define your pins here
#define DATAPIN 6            
#define CDPORT    PORTD        // and port
#define CDDDR    DDRD

#define CLOCK_HIGH    CDPORT |= (1<<CLOCKPIN)        // some macros to make things clean and tidy...
#define CLOCK_LOW    CDPORT &= ~(1<<CLOCKPIN)    // ...further on
#define DATA_HIGH    CDPORT |= (1<<DATAPIN)
#define DATA_LOW    CDPORT &= ~(1<<DATAPIN)

#define MS            2000    // 1 ms lenght in Timer1 cycles
#define PULSETRAIN    22*MS    // total lenght of the overall pulsetrain - may be tweaked depending
                            // on the range servos will move - should be a couple of ms grater
                            // than 8 times maximum indiv. servo pulse lenght
                            // i.e. for 8x 2,5 ms pulses you get 22

unsigned int servopos[8], sum, tail;    // that's what's being used
volatile unsigned char servo;    // this one changes inside the ISR


void init_ports( void )        
{
    CDDDR |= (1<<CLOCKPIN) | (1<<DATAPIN);
    CDPORT &= ~((1<<CLOCKPIN) | (1<<DATAPIN));
}

void init_timers( void )
{
    TCCR1B = (0<<CS12) | (1<<CS11) | (0<<CS10);     // F_CPU/8
    OCR1A = MS;                                        // delay 1 ms before first OCR

    TIMSK1 |= (1<<OCIE1A);                            // Enable output compere interrupt
    sei();                                            // Enable interrupts
}

SIGNAL( SIG_OUTPUT_COMPARE1A )        
{
        if( !servo )    // if this is the first servo
        {
            DATA_HIGH;    // feed pulse into the shift register
            CLOCK_HIGH;    // tick to shift the pulse inside it
            CLOCK_LOW;    // the pulse will appear on physical output after next clock tick
            DATA_LOW;    
        }
        
        CLOCK_HIGH;        // for each servo not only the first one, tick to shift
        CLOCK_LOW;                

        if( servo == 8)    // if all done with all servos, wait for the rest of the 22 ms (or whatever)
                        // pulsetrain period
        {    
            OCR1A += tail; // get back after the tail of the pulse train ends
            servo = 0;       // start with the first servo on the next occassion
        }
        else
        {
            OCR1A += servopos[servo];    // keep the pulse on each ouptut for how long it's necessary
            servo++;                    // next servo next time
        }
}

void servo_initialize( void )        // pretty self-explanatory
{
    unsigned char i;

    sum = 0;
    for( i=0; i<8; i++ )
    {
        servopos[i] = MS;        // 1 ms
        sum+=servopos[i];
    }
    tail = PULSETRAIN - sum;        // 20ms - all pulses

    servo = 0;


    CLOCK_LOW;        // start the clock with low

    DATA_HIGH;        // output 1 to data input

    CLOCK_HIGH;        // shift it into the register
    CLOCK_LOW;        

    DATA_LOW;        // nothing more to feed
}

// use this procedure to set individual servos to positions
// as it keeps track of the tail variable
void set_servo( unsigned char servo, unsigned int pos )
{
                            // sum holds the lenght of all 8 servo pulses
    sum -= servopos[servo];    // so first subtract the old length
    sum += pos;                // then add the new one
    servopos[servo] = pos;    // store where it belongs
    tail = PULSETRAIN - sum; // update tail variable
}


void main( void )                // demo usage
{
    int i;
    unsigned int val;
    int dval = 1;

    init_ports();                // initialize
    servo_initialize();
    init_timers();                
        
    val = MS+MS/2;                // center position 1.5 ms
    while( 1 )
    {
                                // the DEMO
        for( i=0; i<8; i++ )    // move all servos
            set_servo( i, val );
        
        val += dval;            
        
        if( val > MS+MS+MS/2 )  // up until 2.5 ms then sweep down
            dval = -1;            // change direction

        if( val < MS*5/8 )        // down to 0.625 ms then sweep up
            dval = 1;            // change direction
        
        _delay_ms( 20 );        // delay for a while
    }
}

 


Da explanation

 

What i do is

- I reall do not need a shift register with a latch here - so I tied latch and data clocks together, which according to spec. sheet keeps the data clock one tick ahead of the latch

- I feed only one high bit to the shift register and keep it at corresponding servo's output for the duration of the servo pulse lengh, then I shift the pulse onto another servo and repeat 

- after all servos are done, I wait a bit so that the whole cycle is ca. 20 - 22 ms and each servo gets its data roughly with that period

I guess this is how it is done in RC receivers

 

perfect

great

 

I'll have to convert it into arduino library now

 

 

I tried to quickly change the code and now the ISR is called less frequently (once per 16 000 clock cycles in case of 1ms pulse) so 60 wasted cycles in prologue/epilogue is not an issue now. It was just a basic test - I still need to check out everything carefully - but this has to wait

EDIT

Now I think I've nailed it down. I guess it The resolution also improved now it is 2000 per ms.

 

jka - I did't do it the way you did ... to keep you thinking. I'll showw the code here after I clean it up a bit and after I try out one more approach which occured to me this morning. It looks promising.

Anyway precission timing should not be a problem since microcontrollers are meant for precisely timed stuff (consider an OSD circuit /there are some done with AVRs/ - the timing is pretty important there - yet it )

These chips are suited for really fast things. I mean relly fast. Their maximum clock speed is 100 MHz. The task i've been trying to solve is not very time-consuming in principle. The processor just hast to wait for the right time to output clock pulse. The main problem with CPU usage I have results from that I coded it in C and in C without any tweaks a lot of time is wasted in compiler-generated ISR prologue. If I have say 128 clocks between each ISR call and 64 of that is wasted by compiler for pushing / poping registers on stack. If i tweak it, I'll probably have something of any use. But when I woke up It occured to me I can simpify the code a bit and let the hardware handle the timing alltogether. So I guess I'll give it another try tonight.

 Funny, I was working on a 16 servo controller using two 595's. It usd two wires for serial comm. + one wire per 595 for the select line. I ran into some timing problems, so I put the project on hold. I'll continue the project, when I get some time, but I don't think that I will be using 595's. I like the fact that you only use 2 wires + 2 per 595, but I think that precision timing is a little difficult when using the serial protocol.

Since you only use two wires, serial clock + serial data, I suspect that you have tied the latch pin to +5v (or is it active low? can't remember...), so data goes directly to the output pins? Isn't it a problem, that first bits are shifted through the other outputs, so the bit7-servo will see a lot of "noise" from the data for the other 7 servos shifting by? My strategy was to use the extra pin for the latch pin, so I wouldn't disturb the servos when the data was shifted in. 

By the way, if you leave the bit for a servo 0 all the time and use the extra pin for the latching, then the servo will power down, becuase it will never see a high bit. 

I was suprised to see anyone attempt servo control with a serial in / parallel out IC because as you mentioned it requires a lot of proccessing time to maintain the constantly changing outputs.  I think the chip is better suited for digital outputs that don't vary so frequently such as status LEDs.

I am impressed that you got that much resolution. I have not used an Atmega 168 before, only Z80s and picaxe.