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.
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 -
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.
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 -
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.
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().
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.
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.
to test, set and unset bit flags we can use -
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
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
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,
{
// 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
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);
}
}
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;
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.
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.
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;
// 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 ...
// 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;
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;
}
}
//
// 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