Its a very common question, 'How do I read an RC Receiver with my micro controller' and the answer is often very simple however the simple answer is close to useless in a real world application.
The approach outlined in this series of posts has been tested in an RC Race car running at 40+ Kmh at a range of 100 meters.
The approach is reliable, resilient, easy to understand and easy to modify. It has been tested using using 27Mhz AM radio equipment and entry level electronics. Use of better quality electronics and radio equipment will provide improvements in range and signal quality however as the development process has demonstrated, even low end equipment can be interfaced with Arduino for control of an RC Race car.
Update 05/11/12 Read multiple RC channels with a smoother, faster library -
http://rcarduino.blogspot.com/2012/11/how-to-read-rc-channels-rcarduinofastlib.html
For the background to this and the original sketch before optimisation see -
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
Update 04/11/12 - For those of you looking to read a PPM Stream instead of individual channels, I will have a small fast library up this week - will post an update with the link here -
http://rcarduino.blogspot.ae/2012/11/how-to-read-rc-receiver-ppm-stream.html
The Simple Approach and Why Its Wrong
A common suggestion is to connect the receiver to the microcontroler such that there is a common ground (GND) between the two devices and then attach the white or orange signal wire from the receiver to one of the digital input microcontroller pins.
From here there are a variety of approaches to reading the values from the pins -
PulseIn (a blocking polling approach)
PulseIn is a function available with the Arduino that implements an approach know as 'poling'. It essentially sits around waiting for something to happen, until something happens the rest of your code is blocked. This is okay for a simple lab exercise to read and print values from a receiver but it is a hopeless approach for a real world application. Fortunately there are better approaches that do not require a major learning curve.
Timers
There is an example in the Arduino Playground which uses timers ReadReceiver. It may offer greater resolution than the method I am using, but it is also considerably more complicated and as I will show in a follow up post, the source receiver signal degrades rapidly as you move outside the lab rendering the increased accuracy less valuable.
Interrupts
The Timer example and my own approach both use Interrupts. Interrupts allow you to declare your interest in an event and then have the micro controller 'interrupt' your code whenever the event occurs.
Lets take a closer look at the channel signal and determine which bits we should be interested in -
If all we are interested in is the pulse duration, why not use pulseIn ? after all there is nothing else of interest in the signal.
The Arduino UNO is able to perform 16 million operations in one second. In the 2 milli second duration of a full throttle pulse, the Arduino could have performed 32,000 operations ! Thats a lot of wasted power. But it gets worse, what if you call pulseIn immediately after a pulse, you will have to wait a whole 20 milliseconds for the next pulse to arrive and complete. Thats a full 320,000 operations useful operations your code could have completed.
On average you can expect to call pulseIn in the center between two pulses, this means that over an extended period, half or your available processing power is wasted just waiting for the next pulse to arrive and complete. If you add more inputs, the approach becomes quickly unsustainable as you can only give your attention to a single input at a time.
Don't Use Pulse In !
Here are some updates showing the code in action -
Active Yaw Control Of An RC Race Car
http://rcarduino.blogspot.com/2012/07/rcarduino-yaw-control-part-2.html
Mapping RC Car Controls To A Tank Tracked Robot
http://rcarduino.blogspot.com/2012/05/interfacing-rc-channels-to-l293d-motor.html
Next up - Part 2 including what does the signal really look like ? and more of the code to deal with it.
For reading multiple channels using an interrupt driven technique see -
http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
The approach outlined in this series of posts has been tested in an RC Race car running at 40+ Kmh at a range of 100 meters.
The approach is reliable, resilient, easy to understand and easy to modify. It has been tested using using 27Mhz AM radio equipment and entry level electronics. Use of better quality electronics and radio equipment will provide improvements in range and signal quality however as the development process has demonstrated, even low end equipment can be interfaced with Arduino for control of an RC Race car.
Update 05/11/12 Read multiple RC channels with a smoother, faster library -
http://rcarduino.blogspot.com/2012/11/how-to-read-rc-channels-rcarduinofastlib.html
For the background to this and the original sketch before optimisation see -
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
Update 04/11/12 - For those of you looking to read a PPM Stream instead of individual channels, I will have a small fast library up this week - will post an update with the link here -
http://rcarduino.blogspot.ae/2012/11/how-to-read-rc-receiver-ppm-stream.html
Background Information
What are we looking for when we read from an RC Receiver ? Its not what I originally expected, my guess was that it was an analogue signal that would be amplified by the speed controller or servo controller to drive the motors. Its not analogue at all, its actually mostly empty space.
Each channel decoded by your receiver is sent to your ESCs and Servos as a train of pulses, these pulses are sent about 50 times a second but the suprising part is, each pulse only lasts between one and two milliseconds (1/1000 to 2/1000 of a second). Before the next pulse arrives, there is a gap of 10 times the length of the even longest pulse.
In an Electric RC Car a pulse width of 1000 is full reverse or brake, a pulse width of 2000 is full throttle and a pulse of 1500 is neutral. Note that these may be reversed, but the nature and range of the signal does not change.
This pulse train as it comes from the receiver could not be used to drive anything, not even a toy motor. The ESC or Servo control circuitry takes the pulse train as an input and generates a very different output, not just in power, but also in profile and even polarity in the case of reversing a motor.
What are we looking for when we read from an RC Receiver ? Its not what I originally expected, my guess was that it was an analogue signal that would be amplified by the speed controller or servo controller to drive the motors. Its not analogue at all, its actually mostly empty space.
Each channel decoded by your receiver is sent to your ESCs and Servos as a train of pulses, these pulses are sent about 50 times a second but the suprising part is, each pulse only lasts between one and two milliseconds (1/1000 to 2/1000 of a second). Before the next pulse arrives, there is a gap of 10 times the length of the even longest pulse.
In an Electric RC Car a pulse width of 1000 is full reverse or brake, a pulse width of 2000 is full throttle and a pulse of 1500 is neutral. Note that these may be reversed, but the nature and range of the signal does not change.
This pulse train as it comes from the receiver could not be used to drive anything, not even a toy motor. The ESC or Servo control circuitry takes the pulse train as an input and generates a very different output, not just in power, but also in profile and even polarity in the case of reversing a motor.
The Simple Approach and Why Its Wrong
A common suggestion is to connect the receiver to the microcontroler such that there is a common ground (GND) between the two devices and then attach the white or orange signal wire from the receiver to one of the digital input microcontroller pins.
From here there are a variety of approaches to reading the values from the pins -
PulseIn (a blocking polling approach)
PulseIn is a function available with the Arduino that implements an approach know as 'poling'. It essentially sits around waiting for something to happen, until something happens the rest of your code is blocked. This is okay for a simple lab exercise to read and print values from a receiver but it is a hopeless approach for a real world application. Fortunately there are better approaches that do not require a major learning curve.
Timers
There is an example in the Arduino Playground which uses timers ReadReceiver. It may offer greater resolution than the method I am using, but it is also considerably more complicated and as I will show in a follow up post, the source receiver signal degrades rapidly as you move outside the lab rendering the increased accuracy less valuable.
Interrupts
The Timer example and my own approach both use Interrupts. Interrupts allow you to declare your interest in an event and then have the micro controller 'interrupt' your code whenever the event occurs.
Lets take a closer look at the channel signal and determine which bits we should be interested in -
If all we are interested in is the pulse duration, why not use pulseIn ? after all there is nothing else of interest in the signal.
The Arduino UNO is able to perform 16 million operations in one second. In the 2 milli second duration of a full throttle pulse, the Arduino could have performed 32,000 operations ! Thats a lot of wasted power. But it gets worse, what if you call pulseIn immediately after a pulse, you will have to wait a whole 20 milliseconds for the next pulse to arrive and complete. Thats a full 320,000 operations useful operations your code could have completed.
On average you can expect to call pulseIn in the center between two pulses, this means that over an extended period, half or your available processing power is wasted just waiting for the next pulse to arrive and complete. If you add more inputs, the approach becomes quickly unsustainable as you can only give your attention to a single input at a time.
Don't Use Pulse In !
Here are some updates showing the code in action -
Active Yaw Control Of An RC Race Car
http://rcarduino.blogspot.com/2012/07/rcarduino-yaw-control-part-2.html
Mapping RC Car Controls To A Tank Tracked Robot
http://rcarduino.blogspot.com/2012/05/interfacing-rc-channels-to-l293d-motor.html
Using an interrupt to efficiently detect new pulses and output to serial
For reading multiple RC Channels see -
http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
For reading multiple RC Channels see -
http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
// First Example in a series of posts illustrating reading an RC Receiver with
// micro controller interrupts.
//
// Subsequent posts will provide enhancements required for real world operation
// in high speed applications with multiple inputs.
//
// http://rcarduino.blogspot.com/
//
// Posts in the series will be titled - How To Read an RC Receiver With A Microcontroller
// See also http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
#define THROTTLE_SIGNAL_IN 0 // INTERRUPT 0 = DIGITAL PIN 2 - use the interrupt number in attachInterrupt
#define THROTTLE_SIGNAL_IN_PIN 2 // INTERRUPT 0 = DIGITAL PIN 2 - use the PIN number in digitalRead
#define NEUTRAL_THROTTLE 1500 // this is the duration in microseconds of neutral throttle on an electric RC Car
volatile int nThrottleIn = NEUTRAL_THROTTLE; // volatile, we set this in the Interrupt and read it in loop so it must be declared volatile
volatile unsigned long ulStartPeriod = 0; // set in the interrupt
volatile boolean bNewThrottleSignal = false; // set in the interrupt and read in the loop
// we could use nThrottleIn = 0 in loop instead of a separate variable, but using bNewThrottleSignal to indicate we have a new signal
// is clearer for this first example
void setup()
{
// tell the Arduino we want the function calcInput to be called whenever INT0 (digital pin 2) changes from HIGH to LOW or LOW to HIGH
// catching these changes will allow us to calculate how long the input pulse is
attachInterrupt(THROTTLE_SIGNAL_IN,calcInput,CHANGE);
Serial.begin(9600);
}
void loop()
{
// if a new throttle signal has been measured, lets print the value to serial, if not our code could carry on with some other processing
if(bNewThrottleSignal)
{
// micro controller interrupts.
//
// Subsequent posts will provide enhancements required for real world operation
// in high speed applications with multiple inputs.
//
// http://rcarduino.blogspot.com/
//
// Posts in the series will be titled - How To Read an RC Receiver With A Microcontroller
// See also http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
#define THROTTLE_SIGNAL_IN 0 // INTERRUPT 0 = DIGITAL PIN 2 - use the interrupt number in attachInterrupt
#define THROTTLE_SIGNAL_IN_PIN 2 // INTERRUPT 0 = DIGITAL PIN 2 - use the PIN number in digitalRead
#define NEUTRAL_THROTTLE 1500 // this is the duration in microseconds of neutral throttle on an electric RC Car
volatile int nThrottleIn = NEUTRAL_THROTTLE; // volatile, we set this in the Interrupt and read it in loop so it must be declared volatile
volatile unsigned long ulStartPeriod = 0; // set in the interrupt
volatile boolean bNewThrottleSignal = false; // set in the interrupt and read in the loop
// we could use nThrottleIn = 0 in loop instead of a separate variable, but using bNewThrottleSignal to indicate we have a new signal
// is clearer for this first example
void setup()
{
// tell the Arduino we want the function calcInput to be called whenever INT0 (digital pin 2) changes from HIGH to LOW or LOW to HIGH
// catching these changes will allow us to calculate how long the input pulse is
attachInterrupt(THROTTLE_SIGNAL_IN,calcInput,CHANGE);
Serial.begin(9600);
}
void loop()
{
// if a new throttle signal has been measured, lets print the value to serial, if not our code could carry on with some other processing
if(bNewThrottleSignal)
{
Serial.println(nThrottleIn);
// set this back to false when we have finished
// with nThrottleIn, while true, calcInput will not update
// nThrottleIn
bNewThrottleSignal = false;
}
// other processing ...
}
void calcInput()
{
// if the pin is high, its the start of an interrupt
if(digitalRead(THROTTLE_SIGNAL_IN_PIN) == HIGH)
{
// get the time using micros - when our code gets really busy this will become inaccurate, but for the current application its
// easy to understand and works very well
ulStartPeriod = micros();
}
else
{
// if the pin is low, its the falling edge of the pulse so now we can calculate the pulse duration by subtracting the
// start time ulStartPeriod from the current time returned by micros()
if(ulStartPeriod && (bNewThrottleSignal == false))
{
nThrottleIn = (int)(micros() - ulStartPeriod);
ulStartPeriod = 0;
// tell loop we have a new signal on the throttle channel
// we will not update nThrottleIn until loop sets
// bNewThrottleSignal back to false
bNewThrottleSignal = true;
}
}
}
// set this back to false when we have finished
// with nThrottleIn, while true, calcInput will not update
// nThrottleIn
bNewThrottleSignal = false;
}
// other processing ...
}
void calcInput()
{
// if the pin is high, its the start of an interrupt
if(digitalRead(THROTTLE_SIGNAL_IN_PIN) == HIGH)
{
// get the time using micros - when our code gets really busy this will become inaccurate, but for the current application its
// easy to understand and works very well
ulStartPeriod = micros();
}
else
{
// if the pin is low, its the falling edge of the pulse so now we can calculate the pulse duration by subtracting the
// start time ulStartPeriod from the current time returned by micros()
if(ulStartPeriod && (bNewThrottleSignal == false))
{
nThrottleIn = (int)(micros() - ulStartPeriod);
ulStartPeriod = 0;
// tell loop we have a new signal on the throttle channel
// we will not update nThrottleIn until loop sets
// bNewThrottleSignal back to false
bNewThrottleSignal = true;
}
}
}
Next up - Part 2 including what does the signal really look like ? and more of the code to deal with it.
For reading multiple channels using an interrupt driven technique see -
http://rcarduino.blogspot.co.uk/2012/04/how-to-read-multiple-rc-channels-draft.html
Really Good Post!!
ReplyDeleteSan Francisco Bay Area 220/240 Volt 50 Hz Appliance & 110/220 Volt Transformer/Adapter Specialists.220 Volt 50 HZ appliance
Thanks, its always nice to get some feedback.
ReplyDeleteI've just finished reading though pretty much everything on this blog. Awesome stuff!
ReplyDeleteHow many channels can be read using your method? As many as the used Arduino has interrupts?
Hi, I am in the process of migrating my projects to use an interrupt library which will allow me to read as many interrupts as the Arduino has IO PINs. Without this library the Arduino firmware limits you to two.
ReplyDeleteLibrary details here - http://arduino.cc/playground/Main/PinChangeInt
Its also very easy to drive multiple servos and ESCs through the Arduino.
Duane B
Thank you for your quick reply! I've got a few rc projects I'd like to use an arduino in. One is a tugboat for which I'd like an adjustable propeller/rudder mixer (only requires 2 rc channels so can be done with current code).
DeleteI'll be following your blog with great interest to see what you come up with!
I like your code but have some questions born out of ignorance.
ReplyDeleteIt would appear that, due to noise (my guess), the interrupt is constantly triggered. In that case doesn't if effectively block the code?
Any pointers on how to use this approach with both interrupts? I want to drive a robot with the arduino and need forward/reverse as well as right/left capability. I am not driving servos, I am drivng a motor controller.
I'd sure like to communicate with you directly about some of these things.
Thanks,
Jim
Hi,
ReplyDeleteI have tested similar code with 700 interrupts per second, this used less than one percent of the Arduino processing power, see here - http://rcarduino.blogspot.com/2012/03/need-more-interrupts-to-read-more.html
If you try to do any processing inside your interrupts you will block your main code from running, but with a simple ISR you should be able to get much higher frequencies than I have tested.
What sensors are you using and why do you think you have so much noise ?
Duane
This isn’t simple. In fact there is a lot of technicality involved-Though the steps here make it pretty comprehensible.
ReplyDeleteNeo Poly Dex
Hey. I finally received my Servo extension cables in the post and got this working straight away! My target project (not for any particular reason, other than to see if i can do it) is to take two inputs, a vertical axis, and a horizontal axis on a single stick, and translate them into "tank" movement. (equatable to two vertical axis)
ReplyDeleteI figure if I can get the Arduino to map the two axis to integers, giving a range of say, -10 to 10, representing stick position for each axis, I can then get it to add the value for the horizontal stick, onto the value for the vertical axis, causing the theoretical vehicle to turn.
maybe :s
This uses RC signals but does more or less the same thing using the map function to translate from separate forward/reverse left/right signals into left and right track speeds -
ReplyDeletehttp://rcarduino.blogspot.com/2012/05/rc-arduino-robot.html
http://rcarduino.blogspot.com/2012/05/interfacing-rc-channels-to-l293d-motor.html
Duane B
Cant get this to work, serial monitor just shows random numbers from 10 to 10000 and moving the stick doesnt make any difference
ReplyDeleteHere you go then
ReplyDelete1) Does your receiver work at all - can it drive a servo directly ?
2) How are you connecting the receiver to Arduino, which pins where ?
3) How are you powering the receiver ?
Duane B
I have tested the reciever with external battery pack and servos, its working fine
ReplyDeleteI have the reciever connected from one channels signal to pin 2 on arduino and gnd and + to recievers battery channel and I know which pins are which on tje reciever.
Also sorry for my bad english
Try powering the receiver with its own battery, you will still need to connect the Arduino ground to the battery/receiver ground.
ReplyDeleteDo you have anywhere you can post a picture of your set up ?
Thanks for help I got it working, and used the tank code on a small chinese rc tank toy, and an sn754410 motor controller.
DeleteMy serialPPM signal from the receiver is inverted (the synch and channel data is high and the interchannel pulses are low). Is there anything in the code I need to change for it to work correctly - I notice that the INT0 is set for execution on a FALLING edge.
ReplyDeleteregards Peter
Hi, a very simple solution would be to use a transistor or opamp to invert the signal - in this case two wrongs should make a right.
ReplyDeleteDuane B
Thanks for the info!
ReplyDeleteI think you may have mixed up your units in the graph
Am I correct in saying that the SPI library uses one of the two (UNO board) interrupts
ReplyDeleteand therefore I will not be able to use this for throttle and direction without using the pinchangeint library?
hi,
ReplyDeleteI don't think that the SPI Library uses any of the external interrupts. It might have its own dedicated interrupt to indicate when a transfer is complete but that will not clash with anything in the example sketch. try it and let me know how you get on.
Duane B
Hi.
ReplyDeleteJust getting round to testing this code for my own application. If it works, it'll be awesome, and I'll for sure send some of the credit your way.
one thing confuses me though. (doesn't matter all that much because it seems to work, but..)
in your code, you define two pins:
#define THROTTLE_SIGNAL_IN 0 // INTERRUPT 0 = DIGITAL PIN 2 - use the interrupt number in attachInterrupt
#define THROTTLE_SIGNAL_IN_PIN 2 // INTERRUPT 0 = DIGITAL PIN 2 - use the PIN number in digitalRead
one with a zero, the other with a two. then you say they are both interupt 1, corresponding to digital pin 2. Correspondingly, in my setup, I only have a physical connection to pin 2. if I define the throttle_signal_in as 0:
#define THROTTLE_SIGNAL_IN 0
the code works. but if I define it as anything else:
#define THROTTLE_SIGNAL_IN 1 or
#define THROTTLE_SIGNAL_IN 2 or
#define THROTTLE_SIGNAL_IN 3 ,
it doesn't. the serial port doesn't display anything. I'm obviously missing something. indeed, when the throttle signal in is set to one, the interupt routine never gets called. I tested this by putting a println statement inside the interrupt code. Interestingly, this causes the code to crash at random spots during the printing of the line (you get fractional lines), but at least there is proof that it is called at least once, where it isn't called at all if throttle signal in is 1, and presumably if it is 2 or 3, or anything else.
-mike
Hi,
DeleteIts slightly confusing, but on and Arduino UNO interrupt number 0 is on pin 2 and interrupt 1 is on pin 3.
Its more confusing if your using a Leonardo because its the other way around, INT0 is on pin 3 and INT1 on pin2.
Thats why I have the two separate definitions, one for the interrupt number used in attachInterrupt and one for the pin number which we need to read with digitalRead.
Theres a bit more covering some other Arduino variants here -
http://rcarduino.blogspot.com/2013/04/the-problem-and-solutions-with-arduino.html
Duane B
Duane,
ReplyDeleteI have this code working well for operating a robot that my students have built. Thank you very much for this entry by the way. But the issue we are having now is that we would like to have a sweeping ultrasonic sensor on the front to avoid obstacles. I have the coding seeming all well and good to make that work, but the problem is that I am getting an error that I have posted below in trying to include the standard Servo.h library. Do you know how I would get one servo working on the standard angle system and the others working on you writemicrosecond commands? Any help would be greatly appreciated to help us get our project rolling again before school is out for the year.
Thanks,
Derek
Error:
Servo/Servo.cpp.o: In function `__vector_11':
/Users/wcsstaff/Documents/WHS/Course Files/Principles of Tech II/Arduino Info/Arduino Programming/Arduino.app/Contents/Resources/Java/libraries/Servo/Servo.cpp:103: multiple definition of `__vector_11'
RCArduinoFastLib/RCArduinoFastLib.cpp.o:/Users/wcsstaff/Documents/Arduino 1.0/libraries/RCArduinoFastLib/RCArduinoFastLib.cpp:51: first defined here
Hi,
ReplyDeleteI am going to respond under the FAQ because your using code from a few different posts.
See here - http://rcarduino.blogspot.ae/2013/02/rcarduino-libraries-faq.html
Duane B
Hi I almost have this working.
ReplyDeleteI have 6 pulses in my ppm signal. Therefore, if my calculations are correct.
RC_CHANNEL_OUT_COUNT 6+1
RC_CHANNEL_IN_COUNT 6
SERVO_FRAME_SPACE,8000=6*2000-20000
I don't seem to all the channels accounted for, and I'm not sure why.
I think my ppm signal may be inverted. If so would I have this partial success of 3 channels working?
I thought it was the time between pulses that givs the channel position data. If I have 6 Pulses that means I only have 5 between puls positions to measure. What is the relation between PPM pulses and number of channels.?