Reading radio control signals into the Arduino

Greg Wilson
5 min readJan 13, 2018

--

This article, part of the Arduino Radio Control Model Plane Altitude Hold series, describes reading radio control signals using the Arduino.

Normally the on-board radio receiver would be directly connected to the servos that operate the control surfaces, or to an electronic speed control that controls the motor. We’re going to use the Arduino to intercept the control signals.

Background

The receiver sends pulse-width modulated signals to the servos and ESC. The signal voltage is generally zero. Every 20 milliseconds or so there will be a pulse. The duration of the pulse is the important thing. A short pulse is roughly 1100 micro-seconds long, and a long pulse is roughly 1900 micro-seconds long.

Ben Ripley has a wonderful guide to the main options fo reading pulse width modulated signals using the Arduino.

It is generally best to use Arduino interrupts to monitor the signal voltages.

Main idea

The following program uses two interrupt subroutines for each control that we’re monitoring.

  • The ‘rising’ interrupt subroutines fires automatically when the Arduino detects the voltage rising from the usual background level. Our rising interrupt routine records the time that this occurs, disables the rising interrupt, and replaces it with a ‘falling’ interrupt.
  • Similarly, the falling interrupt routine is fired when the Arduino detects the signal voltage falling back to the background level. The routine records the time that this occurs, calculates the time gap since the rising event, and updates a global variable with the calculated pulse width. It then disables the falling interrupt and re-activates the rising interrupt.
  • The process repeats.

We need to monitor three channels: elevator, throttle and a switch (like the landing gear channel or the flaps channel). With rising and falling interrupts for each channel, we need six interrupt routines altogether.

Hardware setup

We connect one (or more) of the servo positive wires to +5V, one (or more) of the servo negative wires to GROUND, and each of the three servo signal wires (often coloured white, yellow or orange) to an Arduino input pin (to match the details in the program).

For this prototype, the Arduino and the receiver can be powered via USB.

In this prototype, I’m using an inexpensive and obsolete OrangeRx DSM2 6-channel receiver, but virtually any receiver ought to work fine.

Program

Here’s the program I used:

/* Demonstrates one way to read signal data from a
* radio control receiver. We use the EnableInterrupt
* library.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
* OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <EnableInterrupt.h>// Switch
volatile int rc_switch_pulsewidth_in = -100;
volatile long rc_switch_pulse_start;
// The pin ID connected to the switch signal wire.
#define rc_switch_pin_in 8
// Elevator
volatile int rc_elevator_pulsewidth_in = -100;
volatile long rc_elevator_pulse_start;
// The pin ID connected to the elevator input signal wire.
#define rc_elevator_pin_in 9
// Throttle
volatile int rc_throttle_pulsewidth_in = -100;
int rc_throttle_pulsewidth_out;
volatile long rc_throttle_pulse_start;
// The pin ID connected to the throttle input signal wire.
#define rc_throttle_pin_in 10
// Timing
long time_start;
// This item toggles debugging behaviour on or off.
// Comment the item out to disable debugging messages.
#define debug
// Set up debugging behaviours
#ifdef debug
#define debug_print(x) Serial.print (x)
#define debug_println(x) Serial.println (x)
#else
#define debug_print(x)
#define debug_println(x)
#endif
void setup() {
// Set the pin modes
pinMode(rc_switch_pin_in, INPUT);
pinMode(rc_elevator_pin_in, INPUT);
pinMode(rc_throttle_pin_in, INPUT);
// Set the initial interrupts
enableInterrupt(rc_switch_pin_in,
isrSwitchRising, RISING);
enableInterrupt(rc_elevator_pin_in,
isrElevatorRising, RISING);
enableInterrupt(rc_throttle_pin_in,
isrThrottleRising, RISING);
// Note the start time
time_start = micros();
#ifdef debug
// Start serial communication so we can see the results
Serial.begin(57600);
#endif
}
void loop() {
static long counter = 0;
long time_loop_start;
float cps;
static long time_of_last_message = micros();
time_loop_start = micros(); #ifdef debug
// Print some details, but not every iteration.
// Just every half-second or so.
if (time_loop_start - time_of_last_message > 500000) {
Serial.print("Counter: ");
Serial.print(counter);
Serial.print("\tSwitch: ");
Serial.print(rc_switch_pulsewidth_in);
Serial.print("\tElevator: ");
Serial.print(rc_elevator_pulsewidth_in);
Serial.print("\tThrottle: ");
Serial.print(rc_throttle_pulsewidth_in);
Serial.print("\tCycles/sec: ");
cps = (1000000.0 * counter) /
(time_loop_start - time_start);
Serial.println(cps);
time_of_last_message = micros();
}
#endif
counter++;
}
/* This function is called by an interrupt event when
* the signal voltage on the elevator channel falls.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrElevatorFalling() {
rc_elevator_pulsewidth_in = micros() -
rc_elevator_pulse_start;
disableInterrupt(rc_elevator_pin_in);
enableInterrupt(rc_elevator_pin_in,
isrElevatorRising, RISING);
}
/* This function is called by an interrupt event when
* the signal voltage on the elevator channel rises.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrElevatorRising() {
rc_elevator_pulse_start = micros();
disableInterrupt(rc_elevator_pin_in);
enableInterrupt(rc_elevator_pin_in,
isrElevatorFalling, FALLING);
}
/* This function is called by an interrupt event when
* the signal voltage on the switch channel falls.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrSwitchFalling() {
rc_switch_pulsewidth_in = micros() - rc_switch_pulse_start;
disableInterrupt(rc_switch_pin_in);
enableInterrupt(rc_switch_pin_in, isrSwitchRising, RISING);
}
/* This function is called by an interrupt event when
* the signal voltage on the switch channel rises.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrSwitchRising() {
rc_switch_pulse_start = micros();
disableInterrupt(rc_switch_pin_in);
enableInterrupt(rc_switch_pin_in,
isrSwitchFalling, FALLING);
}
/* This function is called by an interrupt event when
* the signal voltage on the throttle channel falls.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrThrottleFalling() {
rc_throttle_pulsewidth_in = micros() -
rc_throttle_pulse_start;
disableInterrupt(rc_throttle_pin_in);
enableInterrupt(rc_throttle_pin_in,
isrThrottleRising, RISING);
}
/* This function is called by an interrupt event when
* the signal voltage on the throttle channel rises.
* 'isr' denotes Interrupt Sub-Routine.
*/
void isrThrottleRising() {
rc_throttle_pulse_start = micros();
disableInterrupt(rc_throttle_pin_in);
enableInterrupt(rc_throttle_pin_in,
isrThrottleFalling, FALLING);
}

The variables that are to be altered by the interrupt subroutines are declared as ‘volatile’.

Output

Here are some selected lines of output from the program.

Counter: 1057662 Switch: 1092 Elevator: 1512 Throttle: 1096 Cycles/sec: 140259.32Counter: 16780225 Switch: 1092 Elevator: 1112 Throttle: 1100 Cycles/sec: 140130.71Counter: 2115231 Switch: 1092 Elevator: 1512 Throttle: 1908 Cycles/sec: 140200.56Counter: 3031796 Switch: 1092 Elevator: 1816 Throttle: 1100 Cycles/sec: 140183.23Counter: 5358465 Switch: 1928 Elevator: 1512 Throttle: 1100 Cycles/sec: 140165.79

The range of pulse-widths is from ~1100 microseconds to ~1900 microseconds. The results are consistent from line to line, and well-matched to stick inputs.

We can see that the processing loop runs at more than 100,000 cycles per second. That’s ample for our needs. The normal rate of pulses from the receiver to the servos is usually around 50 per second, so that figure is our target rate.

Conclusion

We have established a method to read the radio control signals that pass from the receiver to the servos and ESC. The method seems reliable and can operate quickly.

--

--

Greg Wilson

Hopeless at orienteering, rubbish at flying radio controlled planes, but enjoys both activities anyway.