Friday, January 20, 2012

How To Read an RC Receiver With A Microcontroller - Part 1

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


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.


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


// 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)
 {

   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;
    }
  }
}



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

48 comments:

  1. Really Good Post!!

    San Francisco Bay Area 220/240 Volt 50 Hz Appliance & 110/220 Volt Transformer/Adapter Specialists.220 Volt 50 HZ appliance

    ReplyDelete
    Replies
    1. hi

      Im a Quadriplegic and after 20 years in a wheelchair Im going crazy so I need a hobby

      I watched these videos and I was thinking we could use either a raspberry pi or Beaglebone PC with a Bluetooth chip and 2.4ghz mounted on a shield, that way we can eliminate the PC and Wii remote. Mind you I'm no hacker or electronics guy either.

      I saw one video where the guy used a PC based oscilloscope to map the wave form generated by the R/C cars transmitter only to replicate it on an Arduino board, could this be accomplished using the Beaglebone a bluetooth chip and 2.4ghz transmitter

      Are the 2.4ghz transmitters and ZigBee chips available designed to send and receive this type of data, the new remote control car transmitters and receivers use a 2.4ghz system that utilises automatic frequency hoping digital system (AFHDS) without crystals like the old radios, they can use 60 or more randomly picked channels to operate on, I'm not sure if this type of technology is used in the Arduino world.

      Here's a few links on how it works

      http://www.powershow.com/view1/20586b-ZDc1Z/NIBLUM_Non-invasive_Bluetooth_Mouse_for_Wheelchair_Users_powerpoint_ppt_presentation

      http://www.youtube.com/playlist?list=PL41C821BE875FDE9B

      I'm guessing we would need their USB Bluetooth dongle if we build ours with its own would you know anyone you can forward my message and email deauzie@gmail.com to who could help.

      To test it until my new wheelchair arrives we could use a cheap analogue arcade joystick and mini wireless expansion board

      As I said Im no technician

      Paul

      Delete
  2. Thanks, its always nice to get some feedback.

    ReplyDelete
  3. I've just finished reading though pretty much everything on this blog. Awesome stuff!
    How many channels can be read using your method? As many as the used Arduino has interrupts?

    ReplyDelete
  4. 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.

    Library details here - http://arduino.cc/playground/Main/PinChangeInt

    Its also very easy to drive multiple servos and ESCs through the Arduino.

    Duane B

    ReplyDelete
    Replies
    1. 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).
      I'll be following your blog with great interest to see what you come up with!

      Delete
  5. I like your code but have some questions born out of ignorance.

    It 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

    ReplyDelete
  6. Hi,
    I 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

    ReplyDelete
  7. This isn’t simple. In fact there is a lot of technicality involved-Though the steps here make it pretty comprehensible.

    Neo Poly Dex

    ReplyDelete
  8. 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)

    I 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

    ReplyDelete
  9. 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 -

    http://rcarduino.blogspot.com/2012/05/rc-arduino-robot.html

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

    Duane B

    ReplyDelete
  10. Cant get this to work, serial monitor just shows random numbers from 10 to 10000 and moving the stick doesnt make any difference

    ReplyDelete
    Replies
    1. I think you should check your Serial Monitor speed. It must be the same, that Arduino uses(in this case 9600).

      Delete
  11. Here you go then

    1) 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

    ReplyDelete
  12. I have tested the reciever with external battery pack and servos, its working fine
    I 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

    ReplyDelete
  13. Try powering the receiver with its own battery, you will still need to connect the Arduino ground to the battery/receiver ground.

    Do you have anywhere you can post a picture of your set up ?

    ReplyDelete
    Replies
    1. Thanks for help I got it working, and used the tank code on a small chinese rc tank toy, and an sn754410 motor controller.

      Delete
  14. My 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.

    regards Peter

    ReplyDelete
  15. 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.

    Duane B

    ReplyDelete
  16. Thanks for the info!

    I think you may have mixed up your units in the graph

    ReplyDelete
  17. Am I correct in saying that the SPI library uses one of the two (UNO board) interrupts
    and therefore I will not be able to use this for throttle and direction without using the pinchangeint library?

    ReplyDelete
  18. hi,
    I 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

    ReplyDelete
  19. Hi.

    Just 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



    ReplyDelete
    Replies
    1. Hi,
      Its 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

      Delete
  20. Duane,

    I 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

    ReplyDelete
  21. Hi,
    I 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

    ReplyDelete
  22. Hi I almost have this working.

    I 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.?

    ReplyDelete
  23. One quick test you can do is to invert your PPM signal in hardware between the Arduino and the receiver. You can use a transistor as a switch to do this or an op-amp if you have one.

    Let me know if you don't have the hardware and I will suggest something else

    Duane.

    ReplyDelete
  24. Hi Duane,

    As a model pilot (R/C airplanes) and IT guy, this blog is very interesting to me. Tried it today and .... 'tada' : never so happy to see numbers between 980 and 1932 flashing by. Thanks a lot. This is cool stuff ! My next step will be to try and find the PPM signal on my receiver :-)

    Patrick.

    ReplyDelete
  25. Hi Duane,
    This looks like it might be just what I need to control a set of landing lights on an RC aircraft but I don't have space for an Arduino.
    My question is can your code be run on an Attiny85?
    I only need to monitor one channel (gear) to set lights on / off.
    I also use this MCU to run a simple blink and fade sketch to control the strobes and anti collision lights.

    gizmoDave

    ReplyDelete
  26. Great Article, but where is part 2?

    ReplyDelete
  27. Here is the link for part 2. You have to do some digging around as Duane's site is organized like my work bench, what you need is here its just not likely in plain view :)

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

    ReplyDelete
    Replies
    1. Got it Dave. Thanks very much old Chap. Keep up the good work

      Delete
  28. Hi, great article but I have a question; I can't understand the first condition before the boolean AND in this line: "if(ulStartPeriod && (bNewThrottleSignal == false)". Could you explain it to me?
    Thank You
    Fabio

    ReplyDelete
  29. Hi,
    It might have been more clear if i used this instead if((ulStartPeriod > 0) && (bNewThrottleSignal == false))

    if(ulStartPeriod) just means if ulStartPeriod is not false. The value zero represents false so in the original code we are basically saying if ulStartPeriod is not zero

    Its worth having a look at the multichannel code as well, there are a few posts that build in a series of improvements over the code in this first post in the series.

    Duane B

    ReplyDelete
  30. Thank for the code! its really helpful. very clean and neat explanation

    ReplyDelete
  31. The graphic in Decoded Single Channel Signal from RC Receiver shows pulse widths in microseconds. I believe these should be milliseconds.

    ReplyDelete
  32. I am unable to get it working. I have connected Pin 2 of Arduino (uno) to the receiver pin on my RC car. My car uses following circuit for receiving and decoding the signal from remote.
    http://www.circuitstoday.com/5-channel-radio-remote-control
    I am using Pin 14 of the IC (RXB) to drive the pin 2 of Arduino.

    thanks,
    ravi

    ReplyDelete
  33. Hi Ravi, The code and circuits throughout rcarduino assume you are using a proportional radio. The chips you are using are not proportional, they provide simple on off signals. You should try reading them using digitalRead(pinNumber);

    Duane B.

    ReplyDelete
  34. Hi, I am into RC and want to control two motors proportionally for a tank. Which Arduino board do I need, UNO? In the simple case of proportionally controlling (with programe mode and reverse capability) a single motor, which is the Arduino board needed? Thanks

    ReplyDelete
  35. Why not to use pulsein? It will not sit forever waiting for new pulses as the RC transmitter is always sending pulse trains no matter the joystick position. I have written a simpler code that uses pulsein to read two channels with the same results.

    ReplyDelete
  36. If I use pulseIn to read channel1, by the time the pulseIn function returns the channel2 pulse will have already started, pulseIn will miss this as it hasn't been called yet so when is called its going to sit there for the entire frame waiting for a new pulse and the whole time your project cannot do anything else but sit and wait. Multiply this by 3,4,5,6,7 or 8 channels and it just keeps getting worse.

    While the output might look similar, an interrupt driven approach will use about 1% of Arduino processing power, where as a pulseIn approach will use 100%, this does not become obvious until you try and do some processing or realtime control - this would not work with pulseIn - http://www.youtube.com/watch?v=TXTCxaamZFU See here for more - http://rcarduino.blogspot.ae/2012/07/rcarduino-yaw-control-part-2.html

    Duane

    ReplyDelete
  37. you code is really good for standalone applications. However, I think your code donot use all the available resources in an efficient manner. You can use hardware interrupts instead and try to minimize the delay as you are trying to measure the pulse high time. Have a look at this post http://hobbylogs.me.pn/?p=110 , it shows a very simple way of using the hardware interrupts for read RC signals

    ReplyDelete
  38. Hi Omer, Why do you think this code does not use hardware interrupts ?

    Duane B

    ReplyDelete
  39. Hi, I have tried your code without success :/ I think it is mainly my fault, cause for a lot of people it is working perfectly.
    I have a Duemilanove, but i am not sure about it, it could be a chinese replica.
    I have connected to Digital PIN 3 (as far as i know it is also an external interrupt pin).
    So i changed the code for the reason that the signal could change during the pulse, so i have written a procedure for a RISING edge and an other for the FALLING edge, and in the rising edge procedure i counted the interrupts but not an even interrupt happened :(
    I have tested my reciever and i use it day to day.

    I would be very pleased if you can help me.
    Here is my code:

    #define THROTTLE_PIN 3

    volatile unsigned long timer_start;
    volatile unsigned long IRQs;
    volatile int pulse_time;

    void startThrottlecalc(){
    timer_start = micros();
    IRQs++;
    }

    void endThrottlecalc(){
    pulse_time =((volatile int)micros() - timer_start);
    timer_start = 0;
    }

    void setup(){
    Serial.begin(19200);
    timer_start = 0;
    IRQs = 0;
    pinMode(THROTTLE_PIN,INPUT);
    attachInterrupt(THROTTLE_PIN,startThrottlecalc,RISING);
    attachInterrupt(THROTTLE_PIN,endThrottlecalc,FALLING);
    }

    void loop(){
    Serial.print(pulse_time);
    Serial.print('\t');
    Serial.print(IRQs);
    Serial.print('\n');
    delay(20);
    }

    ReplyDelete
    Replies
    1. Your code is not going to work but before I get into the reasons why, have you successfully tried the example code given in the blog post ?

      Duane B

      Delete
    2. The reason you code won't work is that pin numbers and interrupt numbers are different. So to attach an interrupt to INT0 you use 0, but the pin number is digital pin 2, for int1, its digital pin 3. Its confusing and different on all the different Arduino models, there is a small table here that gives the interrupt numbers and corresponding pin numbers for each board - http://rcarduino.blogspot.ae/2013/04/the-problem-and-solutions-with-arduino.html

      In your case, change #define THROTTLE_PIN 3 to 2 and it should work. If it still doesnt work, go back and try the test sketch in the blog post and make sure you receiver has a common ground with the Arduino.

      Delete
  40. Hi,

    I tried to control several servos with this code. I copied it 3x and adapted it. Now I can read 3 channels from my receiver. But then i attached 'servo.h' and 2 of the channels hadn't a stable signal. every 1 to 2 seconds or so the value of the read channel falls and raise again. Do you have any idea what the problem is?

    greetings from Germany
    Thomas

    ReplyDelete