Friday, August 17, 2012

Arduino Serial Servos

Creative Commons License
RCArduinoSerialServos by DuaneB is licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License.
Based on a work at rcarduino.blogspot.com.


It is possible to drive a bank of upto 10 independently controlled servos using just two Arduino pins. The concept can easily be expanded to 20 servos using only four pins.

There is very little that is new in the world and this technique definitely isn't new but here is my explanation and a ready made library and test sketch for you to try.

If you have read any of the previous posts on the RCArduino blog you will know that a servo is controlled using a pulse which is between 1000 and 2000 microseconds long.

Its all here if you need a refresher -
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html

At its most basic a pulse is simply setting a pin high and then setting it low again. The Servo library created by Michael Margolis does this in software, its well know, well supported and a very flexible library.

Here is a quick overview of the Servo Library
http://rcarduino.blogspot.com/2012/01/can-i-control-more-than-x-servos-with.html

Flexibility comes at a cost.

If you were to review the source code of the servo library you would see that a lot of the code is there to give users the choice of which pins to use. By moving this flexibility to hardware we can reduce the code required to generate servo signals and also reduce the Arduino resources to just two pins for each bank of ten servos. The code is also much small and faster as a result.

How does it work ?
The RCArduinoSerialServo library relies on a 4017 decade counter - its an incredibly simple chip that is often used in introductory courses to create LED Chasers.

Sample LED Chaser Using 4017 - The first line represent the clock signal, lines O0 to O9 represent the 4017 outputs. The clock switches the outputs on in sequence giving an LED chaser effect.



The chip has 10 outputs which are switched on in sequence based on a clock signal. This is very similar to what the Arduino Servo Library does in software, it sets the first servo pin high, then sets a timer to come back and set the pin low to end the pulse, at the same time, its sets the next servo pin high to begin that servos pulse, then sets the timer to come back again to end this new pulse and begin the next one.

We can achieve the same result using the 4017 Decade counter and only two Arduino pins. The pin we are most interested in is the clock pin of the 4017 Counter. When we pulse the clock pin, the 4017 will switch the current pin low and set the next pin high - effectively ending the current servo pulse and beginning a new one - just like the hardware LED Chaser above and the Servo Library does in software.

In order to control servos all we need to do is generate a clock pulse based on the required servo pulse durations.

Serial Servos - By controlling the space between clock pulses, we can control the pulse duration for each of 10 channels using only 1 Arduino pin.




Taking a closer look at the clock pulse - 

Here we can clearly see that the clock pulse itself is very short, around 1 millionth of a second. However by varying the time between clock pulses we can control the time that each channel is high - the channel pulse width. 

The second and third clock pulse are clearly closer together and this can be seen in the narrow pulse width of channel O1 and also the servo angle, in the previous image.




Here is the Timer1 Output compare servo routine where it all happens -

// if we are at then end of our ten channels - pulse the reset pin to reset the counter to channel 0 
// else pulse the clock pin to advance to the next channel
// the setOutPutTimerForPulseDuration function sets the timer1 output compare register
// so that it will trigger this function again when we need to end the current pulse and 
// begin the next one, thats it, thats all we need to do to control 10 servos with 2 pins.

void CRCArduinoSerialServos::OCR1A_ISR()
{
  if(m_sCurrentOutputChannel >= RC_CHANNEL_OUT_COUNT)
  {
    m_sCurrentOutputChannel = 0;

    PORTB|=16;

    CRCArduinoSerialServos::setOutputTimerForPulseDuration();

    PORTB^=16;
 }
 else
 {
    PORTB|=2;

    CRCArduinoSerialServos::setOutputTimerForPulseDuration();

    PORTB^=2;
  }
    m_sCurrentOutputChannel++;
}

Looking at the generated assembly code, the whole thing takes only two millionths of a second thats only slightly little longer than a single call to digitalWrite.

Here is a sample sketch which you can use to try the library, it outputs 10 channels from a digital pin 9, reset is through pin 12.

For you to be able try things for yourself, it also reads the pulse width back in using an interrupt on pin2. Each channel is set to a pulse width 100 microseconds greater than the previous channel - ie. Channel 0 = 1000, Channel 1 = 1100, Channel 9 = 1900. You can use the interrupt on pin 2 to read the pulse width back in from each of your channels and show it in the serial monitor.

Don't forge that if you want to drive servos with your Arduino you will need to provide separate power to the servos, here's why -

http://rcarduino.blogspot.com/2012/04/servo-problems-with-arduino-part-1.html

http://rcarduino.blogspot.com/2012/04/servo-problems-part-2-demonstration.html

Install the CPP and .H files into a library folder named RCArduinoChannels

Here is a basic schematic, its that simple - total cost - about 30 cents.



The Sketch -

Note that if the code looks long, it isnt, its all comments, for each line of code there are many many lines of comments for you to read should you wish.

Duane B

// RCArduinoSerialServos by DuaneB is licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License.
// Based on a work at rcarduino.blogspot.com.

#include <RCArduinoSerialServos.h>

volatile uint32_t ulRiseTime;
volatile uint32_t ulPulseWidth;

void setup()
{
  Serial.begin(9600);
  Serial.println("RCArduinoSerialServos");
 
  // set the channels
  for(uint16_t nChannel = 0;nChannel < RC_CHANNEL_OUT_COUNT;nChannel++)
  {
    CRCArduinoSerialServos::writeMicroseconds(nChannel,1000+(nChannel*100));
  }

  CRCArduinoSerialServos::begin();
 
  attachInterrupt(0,calcPulse,CHANGE);
}

void loop()
{
  delay(10);

  if(ulPulseWidth != 0)
  {
    // disable interrupts so that our pulse value does not get overwritten while we try and read it
    uint8_t sReg = SREG;
    cli();
   
    // take a local copy of the pulse witdth so that we can reenable interrupts as soon as possible
    uint32_t ulLocalPulseWidth = ulPulseWidth;
   
    // clear the pulse width so that we will only pick up new values written by calcPulse rather
    // than keep printing old values.
    ulPulseWidth = 0;
   
    // turn interrupts back on
    SREG = sReg;
   
    // print the pulse width
    Serial.println(ulLocalPulseWidth);
  }
 
}

// Read pulse width applied to digital pin 2 (interrupt 0)
void calcPulse()
{
  if(digitalRead(2))
  {
    ulRiseTime = micros();
  }
  else
  {
    ulPulseWidth = micros()- ulRiseTime;
  }
}




The .H File -


/*****************************************************************************************************************************
// RCArduinoChannels by DuaneB is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
//
// http://rcarduino.blogspot.com
//
*****************************************************************************************************************************/


#include "Arduino.h"

// Dont change this,
// if you do not need ten channels, just leave some of the 4017 pins disconnected.
#define RC_CHANNEL_OUT_COUNT 10

// Minimum and Maximum servo pulse widths, you could change these,
// Check the servo library and use that range if you prefer
#define RCARDUINO_SERIAL_SERVO_MIN 1000
#define RCARDUINO_SERIAL_SERVO_MAX 2000

//////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// CRCArduinoSerialServos
//
// A class for generating signals in combination with a 4017 Counter
//
// Output upto 10 Servo channels using just digital pins 9 and 12
// 9 generates the clock signal and must be connected to the clock pin of the 4017
// 12 generates the reset pulse and must be connected to the master reset pin of the 4017
//
// The class uses Timer1, as this prevents use with the servo library
// The class uses pins 9 and 12
// The class does not adjust the servo frame to account for variations in pulse width,
// on the basis that many RC transmitters and receivers designed specifically to operate with servos
// output signals between 50 and 100hz, this is the same range as the library
//
// Use of an additional pin would provide for error detection, however using pin 12 to pulse master reset
// at the end of every frame means that the system is essentially self correcting
//
// Note
// This is a simplified derivative of the Arduino Servo Library created by Michael Margolis
// The simplification has been possible by moving some of the flexibility provided by the Servo library
// from software to hardware.
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////

class CRCArduinoSerialServos
{
public:
    CRCArduinoSerialServos();

    // configures timer1
    static void begin();

    // called by the timer interrupt service routine, see the cpp file for details.
    static void OCR1A_ISR();

    // called to set the pulse width for a specific channel, pulse widths are in microseconds - degrees are for wimps !
    static void writeMicroseconds(uint8_t nChannel,uint16_t nMicroseconds);

protected:
    // this sets the value of the timer1 output compare register to a point in the future
    // based on the required pulse with for the current servo
    static void setOutputTimerForPulseDuration();

    // Records the current output channel values in timer ticks
    // Manually set by calling writeChannel, the function adjusts from
    // user supplied micro seconds to timer ticks
    volatile static uint16_t m_unChannelSignalOut[RC_CHANNEL_OUT_COUNT];

    // current output channel, used by the timer ISR to track which channel is being generated
    static uint8_t m_sCurrentOutputChannel;

    // two helper functions to convert between timer values and microseconds
    static uint16_t ticksToMicroseconds(uint16_t unTicks);
    static uint16_t microsecondsToTicks(uint16_t unMicroseconds);
};



The CPP File - 


/*****************************************************************************************************************************
// RCArduinoSerialServos by DuaneB is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
//
// http://rcarduino.blogspot.com
//
*****************************************************************************************************************************/

#include "arduino.h"
#include "RCArduinoSerialServos.h"

/*----------------------------------------------------------------------------------------

This is essentially a derivative of the Arduino Servo Library created by Michael Margolis

As the technique is very similar to the Servo class, it can be useful to study in order
to understand the servo class.

What does the library do ? It uses a very inexpensive and common 4017 Counter IC
To generate pulses to independently drive up to 10 servos from two Arduino Pins

As previously mentioned, the library is based on the techniques used in the Arduino Servo
library created by Michael Margolis. This means that the library uses Timer1 and Timer1 output
compare register A.

OCR1A is linked to digital pin 9 and so we use digital pin 9 to generate the clock signal
for the 4017 counter.

Pin 12 is used as the reset pin.

*/

// Timer1 Output Compare A interrupt service routine
// call out class member function OCR1A_ISR so that we can
// access out member variables
ISR(TIMER1_COMPA_vect)
{
    CRCArduinoSerialServos::OCR1A_ISR();
}

void CRCArduinoSerialServos::OCR1A_ISR()
{
    // If the channel number is >= 10, we need to reset the counter
    // and start again from zero.
    // to do this we pulse the reset pin of the counter
    // this sets output 0 of the counter high, effectivley
    // starting the first pulse of our first channel
  if(m_sCurrentOutputChannel >= RC_CHANNEL_OUT_COUNT)
  {
    // reset our current servo/output channel to 0
    m_sCurrentOutputChannel = 0;

    // pulse reset on the counter - we set it high here
    PORTB|=16;

    // set the duration of the output pulse
    CRCArduinoSerialServos::setOutputTimerForPulseDuration();

    // finish the reset pulse - we set it low here
    PORTB^=16;
 }
 else
 {
  // pulse the clock pin high
    PORTB|=2;

    // set the duration of the output pulse
    CRCArduinoSerialServos::setOutputTimerForPulseDuration();

    // finish the clock pulse - set it back to low
    PORTB^=2;
  }
    // done with this channel so move on.
    m_sCurrentOutputChannel++;
}

// After we set an output pin high, we need to set the timer to comeback for the end of the pulse
void CRCArduinoSerialServos::setOutputTimerForPulseDuration()
{
  OCR1A = TCNT1 + m_unChannelSignalOut[m_sCurrentOutputChannel];
}

// updates a channel to a new value, the class will continue to pulse the channel
// with this value for the lifetime of the sketch or until writeChannel is called
// again to update the value
void CRCArduinoSerialServos::writeMicroseconds(uint8_t nChannel,uint16_t unMicroseconds)
{
    // dont allow a write to a non existent channel
    if(nChannel > RC_CHANNEL_OUT_COUNT)
        return;

  // constraint the value just in case
  unMicroseconds = constrain(unMicroseconds,RCARDUINO_SERIAL_SERVO_MIN,RCARDUINO_SERIAL_SERVO_MAX);

  // disable interrupts while we update the multi byte value output value
  uint8_t sreg = SREG;
  cli();
 
  m_unChannelSignalOut[nChannel] = microsecondsToTicks(unMicroseconds);

  // enable interrupts
  SREG = sreg;
}

uint16_t CRCArduinoSerialServos::ticksToMicroseconds(uint16_t unTicks)
{
    return unTicks / 2;
}

uint16_t CRCArduinoSerialServos::microsecondsToTicks(uint16_t unMicroseconds)
{
 return unMicroseconds * 2;
}

void CRCArduinoSerialServos::begin()
{
    // set the pins we will to outputs
    pinMode(9,OUTPUT);
    pinMode(12,OUTPUT);

    // pulse reset
    digitalWrite(12,HIGH);
    digitalWrite(12,LOW);

    TCNT1 = 0;              // clear the timer count  

    // Initilialise Timer1
    TCCR1A = 0;             // normal counting mode
    TCCR1B = 2;     // set prescaler of 64 = 1 tick = 4us

    // ENABLE TIMER1 OC1A INTERRUPT
    TIFR1 |= _BV(OCF1A);     // clear any pending interrupts;
    TIMSK1 |=  _BV(OCIE1A) ; // enable the output compare interrupt 

    OCR1A = TCNT1 + 4000; // Start in two milli seconds
}

// See the .h file
volatile uint16_t CRCArduinoSerialServos::m_unChannelSignalOut[RC_CHANNEL_OUT_COUNT];
uint8_t CRCArduinoSerialServos::m_sCurrentOutputChannel;









No comments:

Post a Comment