Having not used a remote control transmitter/receiver before, how they work was a bit of mystery to me. This post will walk through a description of how the receiver sends its data, and how to read that data with an Arduino.
Hardware
For this, I am using a Chinese clone Arduino Uno and a Hobbyking transmitter/receiver pair. Your radio might be slightly different, but they mostly operate the same. Higher end radios may have different means of communicating (CPPM, SBus, etc), but we’ll stick with using the standard PPM signals for each channel.
Looking at the Receiver
Since I hadn’t use an RC receiver before this project, I wasn’t sure about how the pins of the receiver are wired. Doing a bit of research, I found that the rightmost row of pins are ground pins, the middle row are V+ pins, and the leftmost row of pins (closest to the labels) are the signal pins.
Using a multimeter, I found that the V+ and GND pins are all connected together:
What this means is that we need only the following connections:
- a wire connecting arduino GND to any of the pins in the GND row of the receiver
- a wire connecting arduino V+ to any of the pins in the V+ row of the receiver
- individual wires for each channel connecting the arduino to the receiver
How the receiver sends data
The receiver transmits data through Pulse Position Modulation (PPM). Roughly every 20ms, the signal pins become high (5v) for between 1ms (1000μs) and 2ms (2000μs). The duration of this pulse indicates the position of the channel’s stick. Check out this article on reading an RC receiver for further explaination.
When the stick is centered, this pulse will be 1500μs in length. When it is at maximum, the pulse will be 2000μs, and when it is a minimum the pulse will be 1000μs. Note that these values can vary slightly depending on the manufacturer of your RC receiver, and how it is configured.
In order to check my understanding of how everything was working, I connected 5V and GND to the receiver, and connected the channel 3 (which I know to be the throttle) signal pin to my oscilloscope.
With the throttle stick all the way at the bottom, I saw that the pulse was approximately 1ms (or 1000μs) in length. Note, each major division on the graph is 5ms.
When I raised the throttle stick to the maximum position, I saw that the pulses lengthed to approximately 2ms (or 2000μs) in length:
It seems that the signals look just as expected!
Connecting the Arduino and using EnableInterrupt
to trigger interrupts
The general approach that we’ll be using is to set up a CHANGE
interrupt on each of the
pins that we connect the signal wires to. This way, functions will be called when the signal
goes high or low. We can use these change interrupts to calculate the time between the
signal going high and the signal going low, and thus get the value.
Because we’re using the EnableInterrupt library, we can use almost any pins that we want. To get an idea of which pins you can use with your particular Arduino, take a look at the EnableInterrupt wiki. For my demo sketch, I connected the Arduino to the receiver as follows:
The Code
Here is the full code for reading the values from the RC receiver. Scroll down for the breakdown and walkthrough.
#include <EnableInterrupt.h>
#define SERIAL_PORT_SPEED 57600
#define RC_NUM_CHANNELS 4
#define RC_CH1 0
#define RC_CH2 1
#define RC_CH3 2
#define RC_CH4 3
#define RC_CH1_INPUT A0
#define RC_CH2_INPUT A1
#define RC_CH3_INPUT A2
#define RC_CH4_INPUT A3
uint16_t rc_values[RC_NUM_CHANNELS];
uint32_t rc_start[RC_NUM_CHANNELS];
volatile uint16_t rc_shared[RC_NUM_CHANNELS];
void rc_read_values() {
noInterrupts();
memcpy(rc_values, (const void *)rc_shared, sizeof(rc_shared));
interrupts();
}
void calc_input(uint8_t channel, uint8_t input_pin) {
if (digitalRead(input_pin) == HIGH) {
rc_start[channel] = micros();
} else {
uint16_t rc_compare = (uint16_t)(micros() - rc_start[channel]);
rc_shared[channel] = rc_compare;
}
}
void calc_ch1() { calc_input(RC_CH1, RC_CH1_INPUT); }
void calc_ch2() { calc_input(RC_CH2, RC_CH2_INPUT); }
void calc_ch3() { calc_input(RC_CH3, RC_CH3_INPUT); }
void calc_ch4() { calc_input(RC_CH4, RC_CH4_INPUT); }
void setup() {
Serial.begin(SERIAL_PORT_SPEED);
pinMode(RC_CH1_INPUT, INPUT);
pinMode(RC_CH2_INPUT, INPUT);
pinMode(RC_CH3_INPUT, INPUT);
pinMode(RC_CH4_INPUT, INPUT);
enableInterrupt(RC_CH1_INPUT, calc_ch1, CHANGE);
enableInterrupt(RC_CH2_INPUT, calc_ch2, CHANGE);
enableInterrupt(RC_CH3_INPUT, calc_ch3, CHANGE);
enableInterrupt(RC_CH4_INPUT, calc_ch4, CHANGE);
}
void loop() {
rc_read_values();
Serial.print("CH1:"); Serial.print(rc_values[RC_CH1]); Serial.print("\t");
Serial.print("CH2:"); Serial.print(rc_values[RC_CH2]); Serial.print("\t");
Serial.print("CH3:"); Serial.print(rc_values[RC_CH3]); Serial.print("\t");
Serial.print("CH4:"); Serial.println(rc_values[RC_CH4]);
delay(200);
}
You should be able to add additional channels if your receiver supports more than 4.
Note, I probably could have refactored the code to make it a bit more concise, but I wanted to keep it as understandable as possible.
Code walkthrough
To get started with the code, we include the EnableInterrupt
library
(you could also use PinChangeInt
). We add macros for the number of channels
we have, as well as RC_CH1
- RC_CH2
which will allow us to access the specific
channels in arrays. Finally, we have RC_CH1_INPUT
- RC_CH4_INPUT
which
define the pins that the signals are connected to.
#include <EnableInterrupt.h>
#define SERIAL_PORT_SPEED 57600
#define RC_NUM_CHANNELS 4
#define RC_CH1 0
#define RC_CH2 1
#define RC_CH3 2
#define RC_CH4 3
#define RC_CH1_INPUT A0
#define RC_CH2_INPUT A1
#define RC_CH3_INPUT A2
#define RC_CH4_INPUT A3
Next, we declare some arrays. rc_values[]
will contain the final values that
we have read, and is intended to be used by our sketch outside of the
RC code. rc_start[]
keeps track of the time (from micros()
) that the pulses
start.
rc_shared[]
holds the values for each channel until they can be copied into
rc_values[]
. It is marked volatile because it can be updated from the interrupt
service routines at any time. We don’t want to use this outside of the RC
code exactly because it can be updated at any time.
uint16_t rc_values[RC_NUM_CHANNELS];
uint32_t rc_start[RC_NUM_CHANNELS];
volatile uint16_t rc_shared[RC_NUM_CHANNELS];
Next, we define our interrupts and our setup()
function. For
the rest of the walkthrough, I’ll only be showing CH_1 functions. for brevity.
We start by opening a serial port so we can look at the RC values after we’ve read
them, and set the RC_CH*_INPUT
pins to INPUT.
We enable an interrupt on the RC_CH1_INPUT
pin, so that the calc_ch1
function will be called any time that pin’s value changes from high to low, or vice versa.
void calc_ch1() { calc_input(RC_CH1, RC_CH1_INPUT); }
void setup() {
Serial.begin(SERIAL_PORT_SPEED);
pinMode(RC_CH1_INPUT, INPUT);
enableInterrupt(RC_CH1_INPUT, calc_ch1, CHANGE);
}
Next is the calc_input
function, which is where most of the logic resides.
We start out by checking if the pin that changed is HIGH
or not. If it is
HIGH
, we know that the pulse has just started (e.g. we are on the leading edge of
the pulse). If it is LOW
, we know we are at the end of the pulse.
If we are at the beginning of the pulse, we need to store the micros()
time at which
the pulse is starting, for later comparison.
If we are at the trailing edge of the pulse, we compare the current micros()
to the
value that we previously stored in rc_start[]
. The result of the computation indicates
the position of our transmitter stick! We store the value in the rc_shared[]
array
for reading later.
void calc_input(uint8_t channel, uint8_t input_pin) {
if (digitalRead(input_pin) == HIGH) {
rc_start[channel] = micros();
} else {
uint16_t rc_compare = (uint16_t)(micros() - rc_start[channel]);
rc_shared[channel] = rc_compare;
}
}
Finally, we have our main loop, which periodically calls the rc_read_values()
function, and then prints out the values for viewing on the serial monitor.
The rc_read_values()
function simply copies the volatile rc_shared[]
values into
the rc_values[]
array. Prior to copying, we disable interrupts to ensure that
the interrupts do not trigger and alter the memory as we’re trying to read it.
Again, the rc_values[]
array should be used throughout
the rest of our program (the non remote control related code).
void loop() {
rc_read_values();
Serial.print("CH1:"); Serial.print(rc_values[RC_CH1]); Serial.print("\t");
/// ...
delay(200);
}
void rc_read_values() {
noInterrupts();
memcpy(rc_values, (const void *)rc_shared, sizeof(rc_shared));
interrupts();
}
If you’ve wired everything, and the program is set up properly, you should see something like the following in the serial monitor:
CH1:1572 CH2:1248 CH3:1076 CH4:1436
CH1:1708 CH2:1096 CH3:1076 CH4:1436
CH1:1704 CH2:1108 CH3:1084 CH4:1424
CH1:1668 CH2:1160 CH3:1372 CH4:1472
CH1:1572 CH2:1352 CH3:1440 CH4:1448
CH1:1528 CH2:1480 CH3:1248 CH4:1364
CH1:1504 CH2:1548 CH3:1072 CH4:1296
CH1:1444 CH2:1420 CH3:1156 CH4:1268
CH1:1496 CH2:1092 CH3:1652 CH4:1456
CH1:1544 CH2:1096 CH3:1772 CH4:1536
CH1:1760 CH2:1204 CH3:1776 CH4:1900
CH1:1840 CH2:1472 CH3:1768 CH4:1912
CH1:1732 CH2:1856 CH3:1776 CH4:1680
CH1:1592 CH2:1852 CH3:1776 CH4:1520
CH1:1496 CH2:1476 CH3:1752 CH4:1436
CH1:1528 CH2:1096 CH3:1088 CH4:1212
Further improvements
That’s it for now, however here are some ideas of how you might further improve the code:
- Add a
last_update_time
variable which gets updated every time an interrupt fires. Iflast_update_time
has not been updated in 2 seconds, consider the receiver disconnected and reset the values to 1500. - Add a constrain call to ensure that the values are within the expected range of 1000μs - 2000μs.
- If the values that are sent by your receiver are not exactly in the range of 1000μs - 2000μs, you may want to add some sort of calibration, perhaps with the arduino map function.
In addition to code improvements, you may want to configure the transmitter itself. Check out the resources below.
Resources
- How To Read an RC Receiver With A Microcontroller - Part 1
- HOW TO: Make your own USB cable for HK-T6A calibration
- Turborix/HobbyKing/Flysky Configurator for Mac OSX
- Transmitter/receiver data sheet
Thanks for reading!
If you have any questions or comments tweet me @bolandrm!
I plan to write some more in-depth posts about building my quadcopter flight controller from scratch. Be sure to follow me if you’re interested!
Thanks for the post, really useful!
Will this code work with a Nano? The library documentation doesn’t specify.
Yep, it should work.
Will effect the operation of the code to change the code as follows?
pinMode(RC CH1 INPUT, INPUT PULLUP);
(underscores will not display)
I like to protect my Nano’s from stray over voltage using the INPUT_PULLUP command to engage the internal resistors.
DOH! After looking at the source code more carefully I realized the actual inputs from the RC receiver are analog and the internal resistors don’t apply. I was confused about the #define RC_CH4 3 thinking the interrupts were read on digital pins. Just didn’t examine the code carefully.
Thanks! One step closer to an RC lawn mower.
Thanks dude you helped me a lot. but i just don't understand uint8_t, uint16_t and uint32_t. Can you explained it ?
Short, int, long, etc types can be different length depending on the system. However, the std int types always use the number of bits that is specified in the type name. meaning, for example, uint32_t always has 32 bits (4 bytes).
http://pubs.opengroup.org/o...
Works great at first time even after modifying for 6 channels (I use it with a Flysky FS-i6). Good time response, I will try with 10 channels after updating my transmitter.
Hi Ryan,Thanks a lot for this code. I've expanded it to read 6 channels from my 14 channel receiver and it's working. I've now added an i/o shield (Velleman KA05) to my Uno and I'd like to control the 6 relais on this board. Can you please help with some output lines to control a relais switch based on the RC signal either going high or low from the neutral state (1500). The shiield uses pins 8-13 for output.
Being a newby to Arduino code writing, you help is greatly appreciated,
Thanks,
Jos
@bolandrm:disqus Hi, I'm trying to use your code but when viewing in the serial monitor i just get random characters instead of: Ch 1: .... Ch 2: .... What may be the reason for this. I'm using an Arduino Nano with an exactly copy of your code only using different analog ports.
Holly junk, dude. Just set the right baud rate in serial monitor :D
Thanks for breaking down RC receiver values in such an understandable way! Your insights really clarified things for me as a beginner in RC hobbies. Looking forward to more informative posts like this!
Thank you for this post, It help me a lot. But I have problem with my Arduino Uno (Chinese version) that used CH340G chip and I couldn't get RX signal by using interrupt. I tried to use your code with the library enableInterrupt.h but the signal get the difference value (from 40-12000) when the board get vibration instead of 0 value (I don't plug signal Pins to Arduino board). Have you ever had this problem?
No, I haven't had that problem. I am also using a Chinese Arduino Uno with no problems. You may want to use an oscilloscope to verify that the signal looks correct before trying to connect to the Arduino.
Where i can add the last_update_time variable?
Each time you interrupt on a channel. In the loop code, read current time with micros(); and check if this value exceed a define maximum value. In this case set all values to 0 and stop the code (run into an infinite loop and wait for a physical button push for example).
Great post, Ryan! The step-by-step guide on reading RC receiver values with Arduino is super helpful, especially for beginners. I appreciate how clearly you explained PWM signals and the code examples—makes it easy to follow along. Looking forward to trying this out in my own project. Thanks for sharing!
Hi to all !
I am new to arduino and struggeling to get a code, to read RC reciever signal and convert it to analog voltage output. Ryan's code seemed to be a good start. Unfortunately when running it, all my channels show 0. Not responding on joystick movement. RC input is OK, checked with scope ... Any ideas ?
What I would advice if you are new to Arduino should be to start with easier steps. If you really want to run that code maybe by adding some more serial monitor outputs to check if interrupts à lanched, on which channel, what data is read/filled, etc...
int ch1; int ch2;int ch3;
void setup() {
pinMode(5, INPUT); pinMode(6, INPUT);pinMode(7, INPUT);
Serial.begin(9600);
}
void loop() {
ch1 = pulseIn(5, HIGH, 25000); // Read the pulse width of
ch2 = pulseIn(6, HIGH, 25000); // each channel
ch3 = pulseIn(7, HIGH, 25000);
Serial.print("Channel 1:"); // Print the value of
Serial.println(ch1); // each channel
Serial.print("Channel 2:");
Serial.println(ch2);
Serial.print("Channel 3:");
Serial.println(ch3);
delay(100); // I put this here just to make the terminal
}
we r getting velu from this simple code ... what banefit do we can get from ur Code...Only Using Interrupt ... Im Hobbyist ..i dont have much Knowledge of Oscilloscope and μs ... I Want To Benifeted from Ur Code Plz Im Wating for ur Advice ...
values read b Arduino UNO from channel 3 of my RX receiver are inconsistent(they are random varying from 200 to 3k,4k)
I have tried ur code & another code pulsein function, can u tell me what should I do resolve this problem , I don't have any idea on how to solve this problem
Work fine on my uno board but not on my Mega 2560 (all value are "0") don't really understand why
Dear, I think it's because the A0-A7 pins on Mega are not external interrupt pins. Tryin with A8 (PCINT16), A9 (PCINT17)... A15 (PCINT23) should solve your issue.
How in the world is this PPM, when the thing that varies in the signal isn't the position, but the width?? This is PWM, pulse width modulation. The big difference is also that each channel needs a separate signal wire for PWM, while PPM has all channels on one single signal wire. Look here: http://www.instructables.co...
Since I hadn’t use an RC receiver before this project, I wasn’t sure about how the pins of the receiver are wired. Doing a bit of research, I found that the rightmost row of pins are ground pins, the middle row are V+ pins, and the leftmost row of pins (closest to the labels) are the signal pins.
No, this is PPM
PPM has a CONSTANT pulse WIDTH, that does not change, what changes is the DISTANCE BETWEEN the pulses. Look at the oscilloscope screenshot, he is MEASURING WIDTH of the pulses. That's why it is Pulse Width Modulation, and not Pulse Position Modulation. Have you ever studied electronics?
Thank you for the clear, concise code and explanation.
I am using the position of a switch on the transmitter to control some lighting effects on a model boat. My loop() has delays in it. Acting on the position of the control is delayed as well if rc_read_values() is in the loop(). How do I make rc_read_values() as real-time as possible in this application?
Wonderfully written. I assume in the loop() method, rather than printing the output, the value and channel would be passed to another method for processing in order to set an output pin to high or low?
Yep, that's correct! You could convert it to a more reasonable scale for use, like -100 to 100.