Showing posts with label Radio Controlled Car. Show all posts
Showing posts with label Radio Controlled Car. Show all posts

Friday, October 19, 2012

Lap Timer Build Along Part 4 - Adding the IR Detector

The final part of the lap timer build along is also the easiest part, involving only the IR Detector and an optional LED with current limiting resistors.

The previous steps can be found in the project index section of RC Arduino -
http://rcarduino.blogspot.com/p/project-index.html

A good introduction to IR Detectors is provided on the Ada Fruit website here -

http://learn.adafruit.com/ir-sensor

To start our build we need a small section of strip board and a set of three wires for power, ground and IR Out.

These can be soldered to the strip board so each leg of the IR Detector is connected through the copper strips of the strip board to one strand of the cable. In the picture I have used a section of ribbon cable, the cable should be long enough to suite your application, for example long enough to mount the detector on the steering column support of your Kart and allow you to attach the main unit to the steering wheel - don't make the cable longer than necessary it will only get in the way and may pick up interference.

Notice that the IR Detector is facing away from the connecting wires and that there is room for some additional components between the detector and the connecting wires, this is to allow us to add an indicator ID.

Reverse Side View

Next we add a 10K current limiting resistor between the output of the IR Detector and the wire we will be connecting to our Arduino interrupt pin.

For this resistor to have any effect we need to cut the copper track underneath the resistor so that the current has to pass through the resistor, a 3mm or 3.5mm drill bit will do this nicely.

Reverse view showing the cut in the copper track beneath the 10K resistor

Next we need to add the current limiting resistor for our indicator LED, I am using 560, but anything from 500 to 800 Ohms should be fine. This resistor connects from the row with the VCC Pin of the detector to the row below the Vout pin. From here we can also add two short lengths of connecting wire for our indicator LED, on wire should come from the VOut track and another from the track below VOut where we have just added the resistor.

This is to allow use to add an indicator LED which will light whenever the unit receives an IR Signal, this is useful as it will let you know if there is environmental interference such as reflected sunlight or fluorescent lighting.

At this point you should have the following circuit -
You can now add an indicator LED of whatever colour will be most visible in your application. To connect the LED, the long leg should be connected to the length of wire which is soldered to the same row as the 560 Ohm resistor, the shorted leg should be connected to same the same row as the Vout pin of the IR Detector.

You can test this set up immediately by connecting 4 to 6 volts to the circuit, the positive voltage should be applied to the top row, the ground should be connected to the middle row. If you use a TV Remote or the Transponder from part 3 of the build along, you should see the LED Light, if not, try swapping the LED around incase it was soldered in reverse.

This is essentially the same circuit as shown in the Lady Ada tutorial -

http://learn.adafruit.com/ir-sensor/testing-an-ir-sensor

Assuming that you have tested your detector correctly, we can now connect it to the Lap Timer.

To do this we need to connect the Vcc wire (top pin/wire in the picture assuming the detector is facing away from the connecting wires) to the 5V supply of the Arduino. Next we need to connect the center wire to the ground of the Arduino. Finally connect the bottom wire to digital pin 2 of your Arduino.

Congratulations, you have now finished the electronics however to be able to use the lap timer you need to build a small enclosure for the detector, without this sunlight and many type of indoor lighting will saturate your detector so that it is unable to detect signals. As I am based in Dubai where the sun is always fierce I have gone as far as spraying the inside of my enclosure with ultra matt camouflage paint. You can see the enclosures I have used in the following clips of the timer in action, not that the indicator LED is on the outside of the enclosure where we can see it - you knew that already right ?

Build Along Lap Timer in action complete with IR Detector as built in this post


I have recently added a few extensions to the project including a count down mode and support for external audio, you can see the new menu options and see them in action at the track in the following two clips -


New Menu Options
At the track with external audio enabled


The external audio option uses an LM386 based amplifier to drive external speakers. You can use this IC to add big sound to any Arduino project, here is a link to the circuit as used in the Lap Timer -

http://rcarduino.blogspot.com/2012/08/adding-audio-to-arduino-projects.html

If you would like the latest code, contact me through the arduino forum for a zip file containing the full project.

Future Developments - I am considering adding support for three additional transponder types -

1) Magnetic - I am told that many Kart Tracks use a magnetic strip under the track which lapping Karts detect using a window sensor such as you would use for home security.
2) Commercial Beacons - The commercial beacons used at many auto racing tracks use a well know pattern of pulses, it would make sense to support this pattern allowing users to 'arrive and drive' without having to place your own transponder around the track.
3) User selected pulse length - Allow the user to choose a pulse length, this would allow two or three systems to be used alongside each other. All of the calculations needed to build your own unique transponder are linked in previous parts of build along.

Stay tuned.

Duane B

Sunday, May 20, 2012

RC Channels, L293D Motor Driver - Part 2 Calibration And Code

Creative Commons License
RCChannelsL293D by D Banks is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.


The following post builds on previous posts relating to interfacing RC Equipment with micro controller projects.


The post covers Arduino calibration with RC Equipment and the transformation of RC Steering and throttle channels into the pin logic levels and left and right PWM signals required to provide full proportional control of a tracked vehicle using the common L293D motor driver.

Why Calibration ?
In the very fist post dealing with RC Equipment we discussed how an RC Channel Signal is made up of a train of around 50 pulses per second and that each pulse is between 1000 and 2000 microseconds in length with neutral being 1500 microseconds.

http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html



In air applications the neutral point for the throttle channel is generally 1000 as braking and reverse are not available and so the full 1000 to 2000 microsecond range is used for forwards throttle control.

Many projects are based on the assumption that these values always apply, however this is not the case.

A variety of transmitter construction and environmental factors mean that we can rarely rely on neutral generating a pulse of exactly 1500 microseconds, nor should we expect full throttle, full braking or left and right end points to be exactly 1000 and 2000 microseconds. As an example, the values my transmitter provided by default are shown below -


Example minimum, center and maximum channel values from a Spectrum DX3S Transmitter/Receiver

Channel Value                      Actual Pulse Width             Percentage of Assumed Range
Full Left Steering108084.0
Full Right Steering190080.0
Center Steering14843.2
Full Brakes110878.4
Full Throttle191683.2
Neutral Throttle15244.8


Looking at the final percentage column we can see that the transmitter is sending only 80% of the expected range, this means our projects can only deliver 80% of their full performance.

A similar problem occurs with the throttle center point, the throttle channel is sending 1524 as the neutral point which corresponds to almost 5% throttle, this wastes power and causes unnecessary heat build up.

In order to overcome these problems I have built a simple 'one touch' calibration into the RCChannelsToL293 project.

One Touch Calibration

When the user presses the program button, the project enters PROGRAM_MODE for 10 seconds. During this time, the project examines all incoming signals to determine the highest and lowest value received.

All that is required is for the user to briefly apply full throttle and reverse then left and right steering for the code to record the end points.

The center points are captured immediately that program mode is entered, so it is important that the transmitter is in a neutral position when the program button is initially pressed.

Permanently Storing The Calibration

The project takes advantage of the ATMega microcontrollers onboard EEPROM memory to permanently store the end points. The user need only repeat the programing procedure if the transmitter/receiver is changed in the future.

The EEPROM (Electrically Erasable Programmable Read Only Memory) is a separate memory on the ATMega micro controller that can be used to store information between power cycles. To put it another way anything your store in EEPROM, is still going to be there when you reset the Arduino.

As we do not want to have to reprogram our RC Project every time we switch on, the EEPROM can be used to store settings and then read them in at power up.

The Code - Part 1 - Accessing the EEPROM

The Arduino provides a library for storing single byte values in EEPROM. To include the library in a project select Sketch/Import Library/EEPROM from the Arduino editor.

The library provides two functions EEPROM.read and EEPROM.write. These two functions work with a single byte at a time however the settings we want to store are held in two byte uint16_t variables. In order to read and write these variables we have to store and retrieve each byte separately.


// write a two byte channel setting to EEPROM
void writeChannelSetting(uint8_t nIndex,uint16_t unSetting)
{
  EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
  EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}
 
// read a two byte channel setting from EEPROM
uint16_t readChannelSetting(uint8_t nIndex)
{
  uint16_t unSetting = (EEPROM.read((nIndex*sizeof(uint16_t))+1)<<8);
  unSetting += EEPROM.read(nIndex*sizeof(uint16_t));

  return unSetting;
}


Part 2 - Implementing The Program Mode

To capture the settings we want to store I have added a program button, when this button is pushed the following steps are performed -

1) Set the mode variable gMode to PROGRAM_MODE
2) Set ulProgramModeExitTime to ten seconds from now
3) Set the throttle and steering channel center points to the current pulse width - for this reason it is important that all transmitter channels are at the neutral position when the program button is pressed.
4) Set all other end points to RC_NEUTRAL
5) Set gDirection to DIRECTION_STOP - this is used at a later point in the code to disable the L293D


  if(false == digitalRead(PROGRAM_PIN))
  {
    // give 10 seconds to program
    ulProgramModeExitTime = millis() + 10000;
    gMode = MODE_PROGRAM;
 
    unThrottleCenter = unThrottleIn;
    unSteeringCenter = unSteeringIn;
   
    unThrottleMin = RC_NEUTRAL;
    unThrottleMax = RC_NEUTRAL;
    unThrottleMin = RC_NEUTRAL;
    unThrottleMax = RC_NEUTRAL;
   
    gDirection = DIRECTION_STOP;
   
    delay(20);
  }


The rest of the loop function implements two modes 1) MODE_RUN and 2) MODE_PROGRAM. In MODE_PROGRAM, we monitor the incoming throttle and steering signals and test them to see whether they represent new end points, if so we update the current end points to the new values. The end points are recorded in unThrottleMin, unThrottleMax, unSteeringMin and unSteeringMax.

We also check whether we have reached ulProgramModeExitTime if so we store the new end points in EEPROM and set gMode back to MODE_RUN.


  if(gMode == MODE_PROGRAM)
  {
   if(ulProgramModeExitTime < millis())
   {
     // set to 0 and exit program mode
     ulProgramModeExitTime = 0;
     gMode = MODE_RUN;
   }
  
   if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
   {
     unThrottleMax = unThrottleIn;
   }
   else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
   {
     unThrottleMin = unThrottleIn;
   }
  
   if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
   {
     unSteeringMax = unSteeringIn;
   }
   else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
   {
     unSteeringMin = unSteeringIn;
   }
  }

Now that the calibration is covered we can move on to the fun part - controlling the motors.

Controlling Two Tracked Motors With An RC Transmitter.

In order to control a track vehicle with a standard RC Transmitter and receiver we need a way of converting from throttle and steering channels into two throttle channels and no steering channel.

In practice its very simple and can be outlined as follows -

1) Read the throttle channel and set the left and right motor speeds to the same value based on the level of input.
2) Read the steering channel, if the steering is outside of a central deadband, reduce the speed of the motor on the inside of the required direction in proportion to the level of steering input.

If the user turns the transmitter wheel 20% to the left, we will slow the left track down by 20% causing the truck to turn to the left.

It gets a little more complicated when we bring forwards and reverse into the mix and a little more complicated again when we introduce an ability to turn on the spot using counter rotation of the two motors  when steering is applied at idle throttle.

In order to manage these additional requirements I have added direction and gear variables. The direction variable is initially set based on throttle input - FORWARDS or REVERSE. The gear variable is also throttle dependent, if the throttle is within the central dead band, the gear is set to IDLE, if it is outside the deadband, the gear is set to FULL. 

Steering

The next section of code looks at the steering channel. If the gear has been set to IDLE and a steering angle is applied, the direction is overridden and set to ROTATE, this allows the truck to turn on the spot at idle. If the gear is FULL, the direction is not overridden, but the speed of the inside track is reduced in proportion to the level of steering.

In the case of 100% left or right steering input, the inside track will be stopped completely allowing the truck to pivot in a sharp turn around the inside track.

Rotation On The Spot At Idle -
// if at idle, set the direction to DIRECTION_ROTATE_RIGHT or DIRECTION_ROTATE_LEFT
// Speed of rotation is proportional to steering angle

     case GEAR_IDLE:
        if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
        {
          gDirection = DIRECTION_ROTATE_RIGHT;
          // use steering to set throttle
          throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
        }
        else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
        {
          gDirection = DIRECTION_ROTATE_LEFT;
          // use steering to set throttle
          throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
        }
        break;
      

Proportional Steering During Forwards Or Reverse Motion

      // if not at idle proportionally restrain inside track to turn vehicle around it
      case GEAR_FULL:
        if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
        {
          throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
        }
        else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
        {
          throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
        }
        break;
      

In the proportional steering mode, the map function is used to convert from steering input to a value determined by both the steering input and the throttle input. This is achieved in a single step by including the throttle input variable gThrottle in the output range passed to 'map'. This results in an output that is proportional to both the steering input unSteeringIn and the throttle input gThrottle.


In the next section of code, the L293 logic pins are set based on direction, this sets the required direction of rotation for each of the two motors.

At the end of this section analogWrite is used to set the individual PWM speeds or the two independent motors.


  if((gDirection != gOldDirection) || (gGear != gOldGear))
  {
    gOldDirection = gDirection;
    gOldGear = gGear;

    digitalWrite(LEFT1,LOW);
    digitalWrite(LEFT2,LOW);
    digitalWrite(RIGHT1,LOW);
    digitalWrite(RIGHT2,LOW);

    switch(gDirection)
    {
    case DIRECTION_FORWARD:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,HIGH);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,HIGH);
      break;
    case DIRECTION_REVERSE:
      digitalWrite(LEFT1,HIGH);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,HIGH);
      digitalWrite(RIGHT2,LOW);
      break;
    case DIRECTION_ROTATE_LEFT:
      digitalWrite(LEFT1,HIGH);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,HIGH);
      break;
    case DIRECTION_ROTATE_RIGHT:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,HIGH);
      digitalWrite(RIGHT1,HIGH);
      digitalWrite(RIGHT2,LOW);
      break;
    case DIRECTION_STOP:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,LOW);
      break;
    }
  }
 

The code presented provides us with full proportional forwards and reverse speed, left and right steering at any speed and in any direction and an extra bonus of on the spot rotation in either direction at idle.

The full code -

Enjoy

Duane B


#include <EEPROM.h>

// MultiChannel L293D
//
// rcarduino.blogspot.com
//
// A simple approach for reading two RC Channels from a hobby quality receiver
// and outputting to the common motor driver IC the L293D to drive a tracked vehicle
//
// We will use the Arduino to mix the channels to give car like steering using a standard two stick
// or pistol grip transmitter. The Aux channel will be used to switch and optional momentum mode on and off
//
// See related posts -
//
// Reading an RC Receiver - What does this signal look like and how do we read it -
// http://rcarduino.blogspot.co.uk/2012/01/how-to-read-rc-receiver-with.html
//
// The Arduino library only supports two interrupts, the Arduino pinChangeInt Library supports more than 20 -
// http://rcarduino.blogspot.co.uk/2012/03/need-more-interrupts-to-read-more.html
//
// The Arduino Servo Library supports upto 12 Servos on a single Arduino, read all about it here -
// http://rcarduino.blogspot.co.uk/2012/01/can-i-control-more-than-x-servos-with.html
//
// The wrong and then the right way to connect servos to Arduino
// 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
//
// Using pinChangeInt library and Servo library to read three RC Channels and drive 3 RC outputs (mix of Servos and ESCs)
// http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
//
// rcarduino.blogspot.com
//

// if stopped and turn
// rotate on spot
// if crawling
// rotate on one side
// if forward or backward
// map

#define RC_NEUTRAL 1500
#define RC_MAX 2000
#define RC_MIN 1000
#define RC_DEADBAND 40

uint16_t unSteeringMin = RC_MIN;
uint16_t unSteeringMax = RC_MAX;
uint16_t unSteeringCenter = RC_NEUTRAL;

uint16_t unThrottleMin = RC_MIN;
uint16_t unThrottleMax = RC_MAX;
uint16_t unThrottleCenter = RC_NEUTRAL;

#define PWM_MIN 0
#define PWM_MAX 255

#define GEAR_NONE 1
#define GEAR_IDLE 1
#define GEAR_FULL 2

#define PWM_SPEED_LEFT 10
#define PWM_SPEED_RIGHT 11
#define LEFT1 5
#define LEFT2 6
#define RIGHT1 7
#define RIGHT2 8

#define PROGRAM_PIN 9

// Assign your channel in pins
#define THROTTLE_IN_PIN 2
#define STEERING_IN_PIN 3

// These bit flags are set in bUpdateFlagsShared to indicate which
// channels have new signals
#define THROTTLE_FLAG 1
#define STEERING_FLAG 2

// holds the update flags defined above
volatile uint8_t bUpdateFlagsShared;

// shared variables are updated by the ISR and read by loop.
// In loop we immediatley take local copies so that the ISR can keep ownership of the
// shared ones. To access these in loop
// we first turn interrupts off with noInterrupts
// we take a copy to use in loop and the turn interrupts back on
// as quickly as possible, this ensures that we are always able to receive new signals
volatile uint16_t unThrottleInShared;
volatile uint16_t unSteeringInShared;

// These are used to record the rising edge of a pulse in the calcInput functions
// They do not need to be volatile as they are only used in the ISR. If we wanted
// to refer to these in loop and the ISR then they would need to be declared volatile
uint32_t ulThrottleStart;
uint32_t ulSteeringStart;

uint8_t gThrottle = 0;
uint8_t gGear = GEAR_NONE;
uint8_t gOldGear = GEAR_NONE;

#define DIRECTION_STOP 0
#define DIRECTION_FORWARD 1
#define DIRECTION_REVERSE 2
#define DIRECTION_ROTATE_RIGHT 3
#define DIRECTION_ROTATE_LEFT 4

uint8_t gThrottleDirection = DIRECTION_STOP;
uint8_t gDirection = DIRECTION_STOP;
uint8_t gOldDirection = DIRECTION_STOP;

#define IDLE_MAX 80

#define MODE_RUN 0
#define MODE_PROGRAM 1

uint8_t gMode = MODE_RUN;
uint32_t ulProgramModeExitTime = 0;

// Index into the EEPROM Storage assuming a 0 based array of uint16_t
// Data to be stored low byte, high byte
#define EEPROM_INDEX_STEERING_MIN 0
#define EEPROM_INDEX_STEERING_MAX 1
#define EEPROM_INDEX_STEERING_CENTER 2
#define EEPROM_INDEX_THROTTLE_MIN 3
#define EEPROM_INDEX_THROTTLE_MAX 4
#define EEPROM_INDEX_THROTTLE_CENTER 5

void setup()
{
  Serial.begin(9600);

  Serial.println("RCChannelsTo293");

  attachInterrupt(0 /* INT0 = THROTTLE_IN_PIN */,calcThrottle,CHANGE);
  attachInterrupt(1 /* INT1 = STEERING_IN_PIN */,calcSteering,CHANGE);

  pinMode(PWM_SPEED_LEFT,OUTPUT);
  pinMode(PWM_SPEED_RIGHT,OUTPUT);
  pinMode(LEFT1,OUTPUT);
  pinMode(LEFT2,OUTPUT);
  pinMode(RIGHT1,OUTPUT);
  pinMode(RIGHT2,OUTPUT);
 
  pinMode(PROGRAM_PIN,INPUT);
 
  readSettingsFromEEPROM();
}

void loop()
{
  // create local variables to hold a local copies of the channel inputs
  // these are declared static so that thier values will be retained
  // between calls to loop.
  static uint16_t unThrottleIn;
  static uint16_t unSteeringIn;
  // local copy of update flags
  static uint8_t bUpdateFlags;

  // check shared update flags to see if any channels have a new signal
  if(bUpdateFlagsShared)
  {
    noInterrupts(); // turn interrupts off quickly while we take local copies of the shared variables

      // take a local copy of which channels were updated in case we need to use this in the rest of loop
    bUpdateFlags = bUpdateFlagsShared;

    // in the current code, the shared values are always populated
    // so we could copy them without testing the flags
    // however in the future this could change, so lets
    // only copy when the flags tell us we can.

    if(bUpdateFlags & THROTTLE_FLAG)
    {
      unThrottleIn = unThrottleInShared;
    }

    if(bUpdateFlags & STEERING_FLAG)
    {
      unSteeringIn = unSteeringInShared;
    }

    // clear shared copy of updated flags as we have already taken the updates
    // we still have a local copy if we need to use it in bUpdateFlags
    bUpdateFlagsShared = 0;

    interrupts(); // we have local copies of the inputs, so now we can turn interrupts back on
    // as soon as interrupts are back on, we can no longer use the shared copies, the interrupt
    // service routines own these and could update them at any time. During the update, the
    // shared copies may contain junk. Luckily we have our local copies to work with :-)
  }

  if(false == digitalRead(PROGRAM_PIN))
  {
    // give 10 seconds to program
    ulProgramModeExitTime = millis() + 10000;
    gMode = MODE_PROGRAM;
   
    unThrottleMin = RC_NEUTRAL;
    unThrottleMax = RC_NEUTRAL;
    unSteeringMin = RC_NEUTRAL;
    unSteeringMax = RC_NEUTRAL;
   
    unThrottleCenter = unThrottleIn;
    unSteeringCenter = unSteeringIn;
   
    gDirection = DIRECTION_STOP;
   
    delay(20);
  }
 
  if(gMode == MODE_PROGRAM)
  {
   if(ulProgramModeExitTime < millis())
   {
     // set to 0 to exit program mode
     ulProgramModeExitTime = 0;
     gMode = MODE_RUN;
    
     writeSettingsToEEPROM();
   }
   else
   {
     if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
     {
       unThrottleMax = unThrottleIn;
     }
     else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
     {
       unThrottleMin = unThrottleIn;
     }
    
     if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
     {
       unSteeringMax = unSteeringIn;
     }
     else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
     {
       unSteeringMin = unSteeringIn;
     }
   }
  }

  // do any processing from here onwards
  // only use the local values unAuxIn, unThrottleIn and unSteeringIn, the shared
  // variables unAuxInShared, unThrottleInShared, unSteeringInShared are always owned by
  // the interrupt routines and should not be used in loop
 
  if(gMode == MODE_RUN)
  {
    // we are checking to see if the channel value has changed, this is indicated
    // by the flags. For the simple pass through we don't really need this check,
    // but for a more complex project where a new signal requires significant processing
    // this allows us to only calculate new values when we have new inputs, rather than
    // on every cycle.
    if(bUpdateFlags & THROTTLE_FLAG)
    {
      // A good idea would be to check the before and after value,
      // if they are not equal we are receiving out of range signals
      // this could be an error, interference or a transmitter setting change
      // in any case its a good idea to at least flag it to the user somehow
      unThrottleIn = constrain(unThrottleIn,unThrottleMin,unThrottleMax);
     
      if(unThrottleIn > unThrottleCenter)
      {
        gThrottle = map(unThrottleIn,unThrottleCenter,unThrottleMax,PWM_MIN,PWM_MAX);
        gThrottleDirection = DIRECTION_FORWARD;
      }
      else
      {
        gThrottle = map(unThrottleIn,unThrottleMin,unThrottleCenter,PWM_MAX,PWM_MIN);
        gThrottleDirection = DIRECTION_REVERSE;
      }
 
      if(gThrottle < IDLE_MAX)
      {
        gGear = GEAR_IDLE;
      }
      else
      {
        gGear = GEAR_FULL;
      }
    }
 
    if(bUpdateFlags & STEERING_FLAG)
    {
      uint8_t throttleLeft = gThrottle;
      uint8_t throttleRight = gThrottle;
 
      gDirection = gThrottleDirection;
     
      // see previous comments regarding trapping out of range errors
      // this is left for the user to decide how to handle and flag
      unSteeringIn = constrain(unSteeringIn,unSteeringMin,unSteeringMax);
 
      // if idle spin on spot
      switch(gGear)
      {
      case GEAR_IDLE:
        if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
        {
          gDirection = DIRECTION_ROTATE_RIGHT;
          // use steering to set throttle
          throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
        }
        else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
        {
          gDirection = DIRECTION_ROTATE_LEFT;
          // use steering to set throttle
          throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
        }
        break;
      // if not idle proportionally restrain inside track to turn vehicle around it
      case GEAR_FULL:
        if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
        {
          throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
        }
        else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
        {
          throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
        }
        break;
      }
      analogWrite(PWM_SPEED_LEFT,throttleLeft);
      analogWrite(PWM_SPEED_RIGHT,throttleRight);
    }
  }
 
  if((gDirection != gOldDirection) || (gGear != gOldGear))
  {
    gOldDirection = gDirection;
    gOldGear = gGear;

    digitalWrite(LEFT1,LOW);
    digitalWrite(LEFT2,LOW);
    digitalWrite(RIGHT1,LOW);
    digitalWrite(RIGHT2,LOW);

    switch(gDirection)
    {
    case DIRECTION_FORWARD:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,HIGH);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,HIGH);
      break;
    case DIRECTION_REVERSE:
      digitalWrite(LEFT1,HIGH);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,HIGH);
      digitalWrite(RIGHT2,LOW);
      break;
    case DIRECTION_ROTATE_LEFT:
      digitalWrite(LEFT1,HIGH);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,HIGH);
      break;
    case DIRECTION_ROTATE_RIGHT:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,HIGH);
      digitalWrite(RIGHT1,HIGH);
      digitalWrite(RIGHT2,LOW);
      break;
    case DIRECTION_STOP:
      digitalWrite(LEFT1,LOW);
      digitalWrite(LEFT2,LOW);
      digitalWrite(RIGHT1,LOW);
      digitalWrite(RIGHT2,LOW);
      break;
    }
  }

  bUpdateFlags = 0;
}


// simple interrupt service routine
void calcThrottle()
{
  // if the pin is high, its a rising edge of the signal pulse, so lets record its value
  if(digitalRead(THROTTLE_IN_PIN) == HIGH)
  {
    ulThrottleStart = micros();
  }
  else
  {
    // else it must be a falling edge, so lets get the time and subtract the time of the rising edge
    // this gives use the time between the rising and falling edges i.e. the pulse duration.
    unThrottleInShared = (uint16_t)(micros() - ulThrottleStart);
    // use set the throttle flag to indicate that a new throttle signal has been received
    bUpdateFlagsShared |= THROTTLE_FLAG;
  }
}

void calcSteering()
{
  if(digitalRead(STEERING_IN_PIN) == HIGH)
  {
    ulSteeringStart = micros();
  }
  else
  {
    unSteeringInShared = (uint16_t)(micros() - ulSteeringStart);
    bUpdateFlagsShared |= STEERING_FLAG;
  }
}




// Updated 04/06/2012 to use default values if no previous calibration is stored in EEPROM

void readSettingsFromEEPROM()
{
  unSteeringMin = readChannelSetting(EEPROM_INDEX_STEERING_MIN);
  if(unSteeringMin < RC_MIN || unSteeringMin > RC_NEUTRAL)
  {
    unSteeringMin = RC_MIN;
  }
  Serial.println(unSteeringMin);

  unSteeringMax = readChannelSetting(EEPROM_INDEX_STEERING_MAX);
  if(unSteeringMax > RC_MAX || unSteeringMax < RC_NEUTRAL)
  {
    unSteeringMax = RC_MAX;
  }
  Serial.println(unSteeringMax);
 
  unSteeringCenter = readChannelSetting(EEPROM_INDEX_STEERING_CENTER);
  if(unSteeringCenter < unSteeringMin || unSteeringCenter > unSteeringMax)
  {
    unSteeringCenter = RC_NEUTRAL;
  }
  Serial.println(unSteeringCenter);

  unThrottleMin = readChannelSetting(EEPROM_INDEX_THROTTLE_MIN);
  if(unThrottleMin < RC_MIN || unThrottleMin > RC_NEUTRAL)
  {
    unThrottleMin = RC_MIN;
  }
  Serial.println(unThrottleMin);

  unThrottleMax = readChannelSetting(EEPROM_INDEX_THROTTLE_MAX);
  if(unThrottleMax > RC_MAX || unThrottleMax < RC_NEUTRAL)
  {
    unThrottleMax = RC_MAX;
  }
  Serial.println(unThrottleMax);
 
  unThrottleCenter = readChannelSetting(EEPROM_INDEX_THROTTLE_CENTER);
  if(unThrottleCenter < unThrottleMin || unThrottleCenter > unThrottleMax)
  {
    unThrottleCenter = RC_NEUTRAL;
  }
  Serial.println(unThrottleCenter);
}

void writeSettingsToEEPROM()
{
  writeChannelSetting(EEPROM_INDEX_STEERING_MIN,unSteeringMin);
  writeChannelSetting(EEPROM_INDEX_STEERING_MAX,unSteeringMax);
  writeChannelSetting(EEPROM_INDEX_STEERING_CENTER,unSteeringCenter);
  writeChannelSetting(EEPROM_INDEX_THROTTLE_MIN,unThrottleMin);
  writeChannelSetting(EEPROM_INDEX_THROTTLE_MAX,unThrottleMax);
  writeChannelSetting(EEPROM_INDEX_THROTTLE_CENTER,unThrottleCenter);
           
  Serial.println(unSteeringMin);
  Serial.println(unSteeringMax);
  Serial.println(unSteeringCenter);
  Serial.println(unThrottleMin);
  Serial.println(unThrottleMax);
  Serial.println(unThrottleCenter);
}


uint16_t readChannelSetting(uint8_t nStart)
{
  uint16_t unSetting = (EEPROM.read((nStart*sizeof(uint16_t))+1)<<8);
  unSetting += EEPROM.read(nStart*sizeof(uint16_t));

  return unSetting;
}

void writeChannelSetting(uint8_t nIndex,uint16_t unSetting)
{
  EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
  EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}





Wednesday, May 16, 2012

RC Arduino Robot


A low cost remote controlled robot using a standalone Arduino to interface between a pistol grip RC Transmitter/Receiver and an L293DNE H-Bridge Motor driver. The project provides three mapping/mixing modes between the pistol grip transmitters channels and the forwards and reverse operation of the robot tracks. The mapping mode is dependent on the throttle input as follows - 

The Arduino provides three steering modes depending on the level of throttle input. The modes listed below are shown in the order of throttle input (low to high) and the order in which they appear in the video.

Mode 1 - Opposition
When the throttle is near the idle point, the tracks will operate in opposition to turn the robot on the spot.

Mode 2 - Braking
At low speed the robot turns by locking the inside track and pivoting around it.

Mode 3 - Proportional Control
At higher speeds the robot has full proportional control allowing for fine control over the steering at speed.

All modes are available in forwards and reverse, speed of rotation and forwards/reverse motion is also proportional in all modes.


The chassis consists of components from the the following Tamiya parts sets -


Double Independent Gearbox - 70168
http://www.etamiya.com/shop/tamiya-70168-tamiya-double-gear-leftriight-independent-speed-p-6477.html

Track and Wheel Set - 70100
http://www.etamiya.com/shop/tamiya-70100-tamiya-track-wheel-p-5767.html

Universal Plate Set - 70157
http://www.etamiya.com/shop/tamiya-70157-tamiya-universal-plate-2pcs-p-6911.html


Chassis underside view
Very few parts are used from 70157, any flat material could be substituted. 
For anyone planning to use 70157 or similar products I strongly recommend using 3mm machine screws and nylock nuts in place of the fixing pins provided in these sets. Nylon locking nuts are widely used in RC Cars to prevent parts unscrewing when subjected to vibration.

3mm locking nuts -
http://www.rcmart.com/traxxas-2745-nylon-locking-nuts-p-32138.html?cPath=595_739_331

Prototyping Platform
As I had not worked with an L293DNE motor driver before I wanted to build a circuit which I could use for prototyping. To do this I built a simple stripboard circuit with a standalone Arduino on the left and the L293DNE on the right.

To connect the two I soldered in several sets of headers allowing me to access the Arduino pins and the key L293DNE pins with jumper wires. While the initial result looked very promising as a prototyping platform, the end result is less appealing -

Initial Prototyping Platform
The End Result As Used In the Video
If I was to build the circuit again I would plan in advance for full forwards, full reverse and neutral throttle indicator LEDs. I have also added a programming button, this puts the robot into a programming mode where it will monitor the receiver for the maximum and minimum end points on the steering and throttle channels for ten seconds. These are used during normal operation to calculate the proportional output signal to the L293DNE motor driver based on the input from the receiver.

 The Robot next to the Spectrum DX3S Transmitter used to control it.

More about the transmitter here -  

I had originally intended to build a styrene Armoured Personnel Carrier type body for the project however my two year old and four year old were so dismissive of the paper prototype in the video that I may just build a Lego adaptor plate and let them build and rebuild as many bodies as they like.

Power
The Standalone Arduino is powered by a PP3 9 Volt battery mounted under the chassis plate. The motors and L293DNE are powered by 4AA batteries mounted above the chassis plate and just visible under the prototyping board in the picture.

Why two sets of batteries ? See here - 

The Code
The code builds on the preceding series of posts on this blog dealing with interface a micro controller to an RC Receiver. Most posts in the series have assumed that the Arduino will read the incoming RC Signals, transform them and output them as new RC Signals using the Servo library. In this case we are doing something slightly different.

The Spectrum DX3S transmitter is a pistol grip type with a trigger based throttle channel and a wheel based steering channel. These are not the most intuitive controls for a tracked vehicle having only forwards and backwards control over its two tracks. In this project, the Arduino is used to map from the transmitter input to independent instructions to the two motors to produce the desired movement.

In order to make the project a little more interesting I want to have three modes of movement 1) Turning on the spot, 2) Locking a track to pivot about it 3) Proportional power to each track for smooth steering. The mode is automatically selected based on the throttle input. At a standstill, the vehicle will operate both tracks in opposite directions to turn on the spot. At low speeds it will lock the inside track and pivot around it, at higher speeds the vehicle turns through proportionally slowing down the inside track depending on the degree of steering input.

Full code and explanation here -

Note - The final project has only modes 1) and 3), mode 2) has been removed as it provided no additional functionality, it is possible to lock the inside track for tight turns by applying full steering in mode 3.

http://rcarduino.blogspot.com/2012/05/interfacing-rc-channels-to-l293d-motor.html















Saturday, April 14, 2012

How To Read Multiple RC Channels

This post builds on previous posts to show a technique for reading multiple radio control receiver channels using an Arduino.

Update 27/03/2013 -  The Arduino Leonardo and Micro use the ATMega 32u4 chip which supports interrupts on fewer pins than the ATMega 328 used in Arduino UNOs and Minis. The pins used by the sketch have been rearranged so that the code can now be run the Leonardo and Micro as well.

If you have previously used the code, please ensure that you update your connecteds by swapping the input and output pins.


The following posts provide the necessary background to this post -

In the first post in this series we looked at the nature of an RC Receiver signal and provided a non blocking interrupt based approach for reading this signal with Arduino -

In the next post we looked at the Arduino servo library and how this allows us to generate the same signals that a radio control receiver generates. We can use this library to drive upto 12 servos or electronic speed controllers -
In the final post, we looked at the pinchangeint library which provides a convenient mechanism to access over twenty interrupts with a standard Arduino UNO. This overcomes the limitation of having only two external interrupts and allows us to use a standard Arduino to read more than twenty RC Channels -


Reading RC Channel Signals

Reading a single RC channel is relatively simple and can be outlined as -

1) Attach a pin change interrupt to the signal pin.
2) Create an interrupt service routine which will be called whenever the signal pin changes from high to low or low to high.

In the interrupt service routine we need to -

1) Check if the signal pin is high or low
2) If its high, this is the rising edge of the pulse, record the time using micros()
3) If its low, this is the falling edge. We are interested in the pulse duration, so if we call micros() to get the current time and then subtract from it the time we recorded in 2) for the rising edge we get the pulse duration.

The pulse duration calculated in 3) Above represents the input signal for the channel, refer to this post for a refresher - http://rcarduino.blogspot.co.uk/2012/01/how-to-read-rc-receiver-with.html

To attach the interrupt using the pin change interrupt library we call -

PCintPort::attachInterrupt(THROTTLE_IN_PIN, calcThrottle,CHANGE);

The interrupt service routine can be written as follows -

void calcThrottle()
{
  // if the pin is high, its a rising edge of the signal pulse,
  // so lets record its value
  if(digitalRead(THROTTLE_IN_PIN) == HIGH)
  {
    ulThrottleStart = micros();
  }
  else
  {
    // else it must be a falling edge, so lets get the time and subtract the time
    // of the rising edge this gives use the time between the rising and falling 
    // edges i.e. the pulse duration.
    unThrottleInShared = (uint16_t)(micros() - ulThrottleStart);

  }
}

So all we need to do is cut and paste the attachInterrupt and interrupt service routine code three times to read three channels ?

No. Its not that simple, but its really not very complicated either.

There are four considerations which apply when using interrupts -

1) Volatile Shared Variables
2) Variable Access
3) Fast Interrupt Service Routines
4) Timing

1) Volatile Shared Variables

When you upload a program with the Arduino IDE, your code will be compiled into an executable program that can be run on your Arduino. As part of this process the compiler will optimise the executable code to be smaller and/or faster. If we are not careful, this optimization can have unexpected results when using interrupts.

Example optimisations -

1) Replacing variables with constants
One of the optimisations a compiler will attempt is to look through your functions for any variables that are not being updated, the compiler can replace these with constant values.

2) Using registers to hold variables within functions
Another optimisation is to assign a variable to a register within a function rather than reading it from memory repeatedly.



The compiler makes optimisation decisions based on blocks of code, not entire projects. A common optimisation problem with interrupts occurs when a variable is updated in an interrupt service routine and read in the main code. Here the compiler may look at the main code and decide 'hey this variable never gets updated so I am going to replace it with a constant value'. In this case whatever the ISR does to the shared variable, your main code will never be effected because as far as the compiler could see, there was nothing that could update the variable within loop and so the executable code that was downloaded to you Arduino was generated to refer to a constant value - see 1) Above. 

A similar situation occurs when a variable is optimised to be held in a register rather than read from memory. One part of your executable code will be reading a value held in memory while another part will be updating a value held in a register. The end effect is that it appears as if your variable is not getting updated despite your ISR being called correctly and your code appearing correct - see 2) above.

Fortunately there is a keyword we can use in our code which tells the compiler 'never try and store this variable in a register, or replace it with a constant' the keyword is volatile. Any variables we wish to access from both our main code and an interrupt service routine must be declared volatile.

Really ?

Yes, I recently built an Audino, its an easy to build, fun to use synthesizer based on Arduino. The loop function reads five analogue inputs which control the sound. These variables are also used in a timer based interrupt service routine which actually creates the sound.

When I first built the project it didn't work. Reading through the source code I noticed that the analog inputs read in loop and used in the ISR were not declared as volatile. Adding the volatile declaration to these five variables fixed the problem.

Interestingly, the Audino has been around for years, it is likely that it worked perfectly with earlier versions of the Arduino compiler, but newer version may be optimizing differently. Whatever the case, the volatile keyword should always be used to tell the compiler not to optimize our shared variables.


In our code we must declare the channel input values as volatile in order that we can update them in the ISR and read them in loop -

volatile uint16_t unThrottleInShared;
volatile uint16_t unSteeringInShared;
volatile uint16_t unAuxInShared;


2) Variable Access

New Arduino programmers are often aware of the volatile keyword and assume that it is all that is needed to access shared variables. It isn't.

An interrupt service routine can be run at any point in your main code. Its called an interrupt because it interrupts your main code to do something else. This can lead to strange and difficult to trace errors if you do not control when your variables are being accessed.

How do these strange and difficult to trace errors happen ?

The simplest explanation is that if your main code is reading a variable when it is interrupted and the interrupting service routine changes the variable value, your main code will be left with part of the old value and part of the new value which is not what you intended or your code expected. This is often experienced as glitches blamed on hardware, but is in fact an easily solved software problem.

As a simple example a two byte integer changing from 256 to 255 in an interrupt service routine could be seen in loop as 511.

                                      Byte 1      Byte 2
Original value = 256 = 00000001 00000000
Updated value = 255 = 00000000 11111111

Loop reads byte 1 = 00000001

The ISR is called and updates the 2 byte integer from 256 to 255

Loop reads byte 2 = 11111111

Loop sees byte 1 + byte 2 as integer 0000000111111111 = 511

This unexpected value is a result of reading byte 1 before the interrupt and byte 2 after the interrupt. We must always ensure that our multi byte shared variables are read, updated or compared without any interrupts occuring.

To prevent this problem we need to ensure that whenever we are reading, writting or comparing a values larger than a single byte we cannot be interrupted until the operation is complete. The data types int, long and float are all multi byte data types ( int = 2 bytes, long = 4 bytes, float = 4 bytes) and therefore access must be controlled between the your main code and loop. The recommended approach to controlling access is to temporarily disable interrupts in order that your main code can perform a read, write or compare without being interrupted (remember the danger is that the interrupt will change part of the value that we have already read,written or compared in which case the results will be unpredictable and most likley lead to program errors - disabling interrupts for the read, write or compare prevents this).
  
To disable interrupts we call nointerrupts(), to enable then again we call interrupts().

// turn interrupts off 
noInterrupts();
    
    // Access your shared variables here ...

// finished with the shared variables, so turn interrupts back on
interrupts();

Note - While interrupts are disabled we will not be able to read incoming signals and this can effect our accuracy. See the example code where local variables as used to minimize the time that interrupts are disabled.
Using the status register directly vs interrupts() and noInterrupts()

Note - There is some criticism of noInterrupts and interrupts on the basis that the interrupts function reenables interrupts regardless of whether they were enabled or disabled before noInterrupts was called. This could cause problems where your code is called by a library which requires interrupts to be disabled. The preferred technique is to record the global status register, update the status register to disable interrupts, then restore it to its previous value - this maintains the status that was inplace before your code rather than blindly (and unsafely) restoring interrupts.

byte sregRestore = SREG;
cli() // clear the global interrupt enable flag

// Access your shared variables here ....

SREG = sregRestore; // restore the status register to its previous value


3) Fast Interrupt Service Routines

When an interrupt is triggered the microprocessor disables further interrupts before calling the interrupt service routine. While we are inside our interrupt service routine, we cannot process any new incoming events, therefore it is important for the accuracy of our input signal that we keep our interrupt service routines as short as possible.

As a guideline the interrupt service routine should record the occurrence of an event, but should not attempt to perform any extended processing in response to the event.

In our example of reading three receiver channels and outputting 3 servo signals we are processing 500 interrupts per second. The difference between an idle throttle and full throttle is only 0.5 thousandths of a second - timing is going to be important.

If we want our main code to process an event, we need some mechanism for the interrupt service routine to flag that an event has occurred, one option is the use of bit flags.

Bit Flags

We can use bit flags to signal to our main code that new information is available from an ISR. Bit flags take advantage of the fact that a micro processor can read a single byte without being interrupted.

How do but flags work ?

If we treat a byte as being made up of eight bits, b0 to b7, we can use each bit as a signal between our main code and ISRs.The bits have the decimal values 1,2,4,8,16,32,64,128 which we can use to set, test and clear the individual bit flags.

// These bit flags are set in bUpdateFlagsShared to indicate which
// channels have new signals
#define THROTTLE_FLAG 1
#define STEERING_FLAG 2
#define AUX_FLAG 4

// a single byte we can use to hold the update bit flags defined above
volatile uint8_t bUpdateFlagsShared;

to test, set and unset bit flags we can use -

// test to see if a flag has been set - use the bitwise AND operator &
if(bUpdateFlags & THROTTLE_FLAG)
{
   // flag was set ...
}

// set a bit flag - use the bitwise OR operator |
// set the throttle flag to indicate that a new throttle signal has been received
bUpdateFlagsShared |= THROTTLE_FLAG;

// clear a bit flag - use the bitwise XOR operator ^
bUpdateFlagsShared ^= THROTTLE_FLAG;


Bit flags provide us with a simple mechanism to communicate new events between the ISRs and loop. This is particularly important where we need to perform an extended calculation in response to the event. In the example code, we use the bit flag to tell loop that a new signal is available from a channel, loop then uses this information to updated the output for the channel.

4) Timing

We have already covered two of the issues which will effect our accuracy, we can add library functions for a more complete list as presented below -

1) Library functions that temporarily disable interrupts - some library functions will temporarily disable interrupts we cant control this but we can be careful about where we use these functions
2) Disabling interrupts in our own code - we will need to do this, but we can take care to minimise the time that interrupts are disabled.
3) Interrupt Service Routines - the microprocessor automatically disables interrupts before calling them and reenables them afterwards. Again we have to live with this but can be careful to minimize the time spent inside the ISR so that interrupts are enabled again as quickly as possible.

Putting it all together

The following code sample can be used to read three RC Channels through one set of Arduino pins and output those same channels on another set of pins. The code uses dedicated interrupt service routines for each channel, this is for clarity and could be changed to use a single interrupt service routine for all channels.

The code demonstrates the use of -

 - Bit flags to communicated events between the ISRs and Loop
 - Interrupt service routines to read rc channel signals
 - Enabling and disabling interrupts
 - Volatile shared variables
 - Taking local copies of variables to minimise the time that interrupts are disabled
 - Using the servo library to output RC Channels to servos or electronic speed controllers
 - Using the pin change interrupt library to access additional interrupts.

The sample code below the video was used in creating the video. A step by step guide to connecting the ESC, Steering servo, RC Receiver and Arduino will be provided in a followup post.


With two channels - throttle and steering - being read from the receiver and output to the car by Arduino


Sample Code - MultiChannels rcarduino.blogspot.com

// MultiChannels
//

// rcarduino.blogspot.com
//
// A simple approach for reading three RC Channels using pin change interrupts
//
// See related posts -
// http://rcarduino.blogspot.co.uk/2012/01/how-to-read-rc-receiver-with.html
// http://rcarduino.blogspot.co.uk/2012/03/need-more-interrupts-to-read-more.html
// http://rcarduino.blogspot.co.uk/2012/01/can-i-control-more-than-x-servos-with.html
//
// rcarduino.blogspot.com
//

// include the pinchangeint library - see the links in the related topics section above for details
#include <PinChangeInt.h>

#include <Servo.h>

// Assign your channel in pins
#define THROTTLE_IN_PIN 8
#define STEERING_IN_PIN 9
#define AUX_IN_PIN 10

// Assign your channel out pins
#define THROTTLE_OUT_PIN 5
#define STEERING_OUT_PIN 6
#define AUX_OUT_PIN 7

// Servo objects generate the signals expected by Electronic Speed Controllers and Servos
// We will use the objects to output the signals we read in
// this example code provides a straight pass through of the signal with no custom processing
Servo servoThrottle;
Servo servoSteering;
Servo servoAux;

// These bit flags are set in bUpdateFlagsShared to indicate which
// channels have new signals
#define THROTTLE_FLAG 1
#define STEERING_FLAG 2
#define AUX_FLAG 4

// holds the update flags defined above
volatile uint8_t bUpdateFlagsShared;

// shared variables are updated by the ISR and read by loop.
// In loop we immediatley take local copies so that the ISR can keep ownership of the
// shared ones. To access these in loop
// we first turn interrupts off with noInterrupts
// we take a copy to use in loop and the turn interrupts back on
// as quickly as possible, this ensures that we are always able to receive new signals
volatile uint16_t unThrottleInShared;
volatile uint16_t unSteeringInShared;
volatile uint16_t unAuxInShared;

// These are used to record the rising edge of a pulse in the calcInput functions
// They do not need to be volatile as they are only used in the ISR. If we wanted
// to refer to these in loop and the ISR then they would need to be declared volatile
uint32_t ulThrottleStart;
uint32_t ulSteeringStart;
uint32_t ulAuxStart;

void setup()
{
  Serial.begin(9600);
 
  Serial.println("multiChannels");

  // attach servo objects, these will generate the correct
  // pulses for driving Electronic speed controllers, servos or other devices
  // designed to interface directly with RC Receivers 
  servoThrottle.attach(THROTTLE_OUT_PIN);
  servoSteering.attach(STEERING_OUT_PIN);
  servoAux.attach(AUX_OUT_PIN);

  // using the PinChangeInt library, attach the interrupts
  // used to read the channels
  PCintPort::attachInterrupt(THROTTLE_IN_PIN, calcThrottle,CHANGE);
  PCintPort::attachInterrupt(STEERING_IN_PIN, calcSteering,CHANGE);
  PCintPort::attachInterrupt(AUX_IN_PIN, calcAux,CHANGE);
}

void loop()
{
  // create local variables to hold a local copies of the channel inputs
  // these are declared static so that thier values will be retained
  // between calls to loop.
  static uint16_t unThrottleIn;
  static uint16_t unSteeringIn;
  static uint16_t unAuxIn;
  // local copy of update flags
  static uint8_t bUpdateFlags;

  // check shared update flags to see if any channels have a new signal
  if(bUpdateFlagsShared)
  {
    noInterrupts(); // turn interrupts off quickly while we take local copies of the shared variables

    // take a local copy of which channels were updated in case we need to use this in the rest of loop
    bUpdateFlags = bUpdateFlagsShared;
   
    // in the current code, the shared values are always populated
    // so we could copy them without testing the flags
    // however in the future this could change, so lets
    // only copy when the flags tell us we can.
   
    if(bUpdateFlags & THROTTLE_FLAG)
    {
      unThrottleIn = unThrottleInShared;
    }
   
    if(bUpdateFlags & STEERING_FLAG)
    {
      unSteeringIn = unSteeringInShared;
    }
   
    if(bUpdateFlags & AUX_FLAG)
    {
      unAuxIn = unAuxInShared;
    }
    
    // clear shared copy of updated flags as we have already taken the updates
    // we still have a local copy if we need to use it in bUpdateFlags
    bUpdateFlagsShared = 0;
   
    interrupts(); // we have local copies of the inputs, so now we can turn interrupts back on
    // as soon as interrupts are back on, we can no longer use the shared copies, the interrupt
    // service routines own these and could update them at any time. During the update, the
    // shared copies may contain junk. Luckily we have our local copies to work with :-)
  }
 
  // do any processing from here onwards
  // only use the local values unAuxIn, unThrottleIn and unSteeringIn, the shared
  // variables unAuxInShared, unThrottleInShared, unSteeringInShared are always owned by
  // the interrupt routines and should not be used in loop
 
  // the following code provides simple pass through
  // this is a good initial test, the Arduino will pass through
  // receiver input as if the Arduino is not there.
  // This should be used to confirm the circuit and power
  // before attempting any custom processing in a project.
 
  // we are checking to see if the channel value has changed, this is indicated 
  // by the flags. For the simple pass through we don't really need this check,
  // but for a more complex project where a new signal requires significant processing
  // this allows us to only calculate new values when we have new inputs, rather than
  // on every cycle.
  if(bUpdateFlags & THROTTLE_FLAG)
  {
    if(servoThrottle.readMicroseconds() != unThrottleIn)
    {
      servoThrottle.writeMicroseconds(unThrottleIn);
    }
  }
 
  if(bUpdateFlags & STEERING_FLAG)
  {
    if(servoSteering.readMicroseconds() != unSteeringIn)
    {
      servoSteering.writeMicroseconds(unSteeringIn);
    }
  }
 
  if(bUpdateFlags & AUX_FLAG)
  {
    if(servoAux.readMicroseconds() != unAuxIn)
    {
      servoAux.writeMicroseconds(unAuxIn);
    }
  }
 
  bUpdateFlags = 0;
}


// simple interrupt service routine
void calcThrottle()
{
  // if the pin is high, its a rising edge of the signal pulse, so lets record its value
  if(digitalRead(THROTTLE_IN_PIN) == HIGH)
  {
    ulThrottleStart = micros();
  }
  else
  {
    // else it must be a falling edge, so lets get the time and subtract the time of the rising edge
    // this gives use the time between the rising and falling edges i.e. the pulse duration.
    unThrottleInShared = (uint16_t)(micros() - ulThrottleStart);
    // use set the throttle flag to indicate that a new throttle signal has been received
    bUpdateFlagsShared |= THROTTLE_FLAG;
  }
}

void calcSteering()
{
  if(digitalRead(STEERING_IN_PIN) == HIGH)
  {
    ulSteeringStart = micros();
  }
  else
  {
    unSteeringInShared = (uint16_t)(micros() - ulSteeringStart);
    bUpdateFlagsShared |= STEERING_FLAG;
  }
}

void calcAux()
{
  if(digitalRead(AUX_IN_PIN) == HIGH)
  {
    ulAuxStart = micros();
  }
  else
  {
    unAuxInShared = (uint16_t)(micros() - ulAuxStart);
    bUpdateFlagsShared |= AUX_FLAG;
  }
}

Coming soon - A walk through connecting the three radio channels to the Arduino and then the processed signals back out to a car, robot or other application.

Duane B