Project tutorial
Read PWM, Decode RC Receiver Input, and Apply Fail-Safe

Read PWM, Decode RC Receiver Input, and Apply Fail-Safe © GPL3+

Easy to use code to measure PWM signals (<2.5Khz) plus a dedicated function to calibrate the input from an RC receiver, including fail-safe.

  • 1,246 views
  • 0 comments
  • 6 respects

Components and supplies

About this project

This project contains generic but efficient code that can be used to simply read an RC receiver (or any other PWM signal) on any Arduino input pin, and also apply a fail-safe in the case of the loss of the transmitter signal.

Below is a video showing an Arduino uno acting as a servo mixer using the code PWMread_RCfailsafe.ino available at the bottom of this page.

Helpfully the functions in PWMread_RCfailsafe.ino look after the interrupt registers for you, and can be moved easily between different projects that use different pins.

In the video example below:

  • The fail-safe activates when the receiver has no signal from the transmitter. elevator channel is set to full up, and aileron channel is set to neutral
  • The direction of the blue servo is set by the throttle position
  • The range of movement (rates) of the blue servo is set by the slider on the side of the transmitter
  • The mixing is turned on an off using the gear switch on the transmitter

Contents

  • How are servos controlled by PWM
  • Example uses of Arduino in RC models / robots
  • Code Overview: Decode PWM from RC receiver with fail-safe
  • How to use PWMread_RCfailsafe.ino
  • Display the receiver frame rate and frequency
  • Servo mixing example
  • Rationale for the approach taken
  • Limitations

How are servos and speed controllers controlled by PWM?

It is assumed in the rest of this project that you have an understanding of the PWM signals used to control servos and speed controllers. Here is a good video explaining how these pulse width modulation (PWM) signals work.

Servo City Explanation of PWM control of Servos

You should also have a working knowledge of:

  • The Arduino IDE
  • Float, boolean, and int variables
  • If loops
  • For loops
  • Arrays
  • Servo library

Example uses of Arduino in RC models / robots

You will only be limited by your imagination:

  • Apply servo mixing, switch lights on/off, control pumps/valves, set bespoke sequences...
  • Create a controller (i.e. flight stabilization/ autopilot, heading hold, altitude/depth hold, auto-leveler, sense and avoid, return home...)
  • Have your RC model respond to a loss of signal or low battery voltage...
  • Use the same transmitter for multiple models/ projects without having to change any settings or use a model memory feature.

Code overview: Decode PWM from RC receiver with fail-safe

This code measures PWM (Pulse Width Modulation) signals using pin change interrupts. The functions used automate the set-up of the interrupts and the extraction of data from any digital or analog pin (excluding A6 and A7), on the Arduino Uno, Nano or Pro Mini. This makes the code easy to use even for beginners.

The primary aim of this project was to create a generic RC receiver with fail-safe "module" that can be quickly moved between projects. As such the example code shown in the "how to use" section can just be used as a means to an end.

Note: this code will not work with the software serial or any other library which uses pin change interrupts.

For those interested in how the code works:

  • The input pins are identified in an array. This array can be any length.
  • A setup function enables pin change interrupts by setting the appropriate registers for each pin listed in the pin array.
  • A voltage change on any of the selected pins will trigger one of three Interrupt Service Routes (ISR) depending on which port register the pin belongs to ISR(PCINT0_vect) -> Port B, ISR(PCINT1_vect) -> Port C or ISR(PCINT2_vect) -> Port D.
  • Within each ISR a FOR loop and IF statements are used to determine which pin has changed, and which RC channel it belongs to. The time of the interrupt is noted via the use of micros() before returning back to the main loop().
  • The time intervals between pin changes are used to calculate pulse width, and repetition period.
  • Flags are set in each ISR to indicate when new pulses have been received
  • The flags are then used by the remaining functions to extract and process the data collected by the ISRs

The following YouTube videomadebyJoop Brokking talks about a different projectthat uses the same method for connecting an RC receiver to arduino. During the first 8 minutes Joopclearlyexplains howto use pin change interrupts to measure PWM signals from an RC receiver.

All of this detail is looked after by PWMread_RCfailsafe.ino which can be downloaded at the bottom of this page.

Some useful information on port manipulation can also also be found here:https://tronixstuff.com/2011/10/22/tutorial-arduino-port-manipulation/

In addition to the pin change interrupt handling, a dedicated function RC_decode() has been written to convert a pulse width (1000-2000uS) into a +-100% control signal from a transmitter. A fail-safe checks for a valid transmitter signal using the signal tolerances of 10-330Hz, and 500-2500uS. If the signal is lost then RC_decode() returns a predetermine fail-safe value.

The calibration values for a specific transmitter, and fail-safe positions can be set for each channel in PWMread_RCfailsafe.ino

How to use PWMread_RCfailsafe.ino

Step 1: Example hardware setup with Arduino Uno

If following the example set-up with an Arduino Uno connect your receiver as follows: (otherwise if your using your own project jump straight to step 2)

  • Power the receiver using the 5v and GND pins
  • Connect the signal pins from the receiver to pins 2 to 7 on the Arduino using female to male jumper wires. (If you have a 2 channel receiver connect to only pins 2 and 3)
  • For the servo mixer example attach one servo signal wire to pin 9 and the other to pin 10.

Step 2: Copy PWMread_RCfailsafe.ino into the sketch folder

An example sketch RC_Read_Example has been included for download at the bottom of page. You can use this as you main sketch when following the steps below.

Copy and past the PWMread_RCfailsafe.ino file into the folder containing your main sketch. When you next open the sketch in the IDE, a second tab will appear containing the code within PWMread_RCfailsafe.ino.

Step 3: Specify the input pins

Open or re-open the main sketch in the Arduino IDE.

Click on the PWMread_RCfailsafe tab, scroll down to the "USER DEFINED VARIABLES" title and enter the input pins in the array pwmPIN[].

Note: Any number of pins can be used, and in any order. Just be aware that the more inputs you have the more time the code will spend addressing the interrupt routine. Note A6 and A7 are analog only pins and cannot be used.

The Arduino MEGA is not currently supported, however this could be easily remedied if there was the appetite for it.

Note: the first element in pwmPIN[] is channel 1, the second element channel 2, etc... if your using all of the channels from the receiver it would be a good idea to make sure the receiver channels 1 corresponds to channel 1 in pwmPIN[]...

Step 4: Review the available functions in PWMread_RCfailsafe.ino

Step 5: Print the pulse width data to serial

Upload the RC_Read_Example code, turn on your transmitter and print the raw pulse width data to serial.

The RC_avail() function should be used to check when new data has been received on all channels, and then use print_RCpwm() to send the pulse width data to serial.

Step 6: Calibrate the transmitter

Using the pulse width data printed to serial via print_RCpwm() to manually modify the values in the arrays RC_min[], RC_mid[], and RC_max[] in order to calibrate each channel into the range +-100%.

Step 7: Print the calibrated channels to serial

Comment out the print_RCpwm() function

Use the RC_decode(channel) function to calibrate each channel into the range +-1.

Then print each of the calibrated channels to serial using the decimal2percentage() function followed by Serial.println("")

Step 8: Set the fail-safe

Adjust the fail-safe positions in the RC_failsafe[] array for each channel (in the range +-1).

Turn the transmitter on and off to check that the fail-safe operates as desired.

The RC input can now be used in your sketch.

Note: you may have to deactivate any fail-safe feature in the receiver, otherwise the arduino will notbe able to respond to the loss of transmitter signal.

Display the receiver frame rate and frequency

The receiver pulse repetition period, and frequency can be printed to serial. Check that new data is available on the chosen channel by using the function PWM_read(channel number), before using PWM_period() and PWM_freq() to extract the data for printing. Example code is available in RC_FrameRate.ino.

It's best to use the first channel as this will be the first pulse sent in each receiver frame. PWM_read() uses the same flags as RC_decode(CH) so make sure PWM_read() is called first.

See the screen shot below:

The receiver period can be useful to know as it tells you how much time the code has before the next set of data arrives. If RC_avail() does not detect new RC data after a predetermined time i.e. 21ms then run RC_decode() in order to trigger the fail-safe and or to continue to run the program (which could be a PID controller) at a steady frequency.

This is achieved in the RC_Read_Example.ino by the following if statement.

now = millis();
if(RC_avail() || now - rc_update > 21)
  rc_update = now;
  // update RC input data using RC_decode()
  // run a PID controller  
  // apply servo mixing
  // position the servos
}

Servo mixing example

I've included RC_ServoMixer_Example.ino to show how you could mix two receiver channels (in this case channels 2 and 3, elevator and aileron). The sketch also shows a method for setting servo direction, rate, and sub trim. The servo library is used to control the servos via pins 9 and 10.

Below is a screen shot of the servo mixing section of the code:

The mix is achieved by simply adding and subtracting the two channels together, and limiting the output to the range -1 to +1. When applying elevator and aileron mixing you create two outputs one for each servo.

mix1 = channel 2 - channel3 (elv - ail)

mix2 = channel 2 + channel3 (elv - ail)

Before positioning the servos you will need to convert the +-100% (+-1) signal to an equivalent pulse width in microseconds for the servo. In the RC_ServoMixer_Example.ino I use a function calc_uS() to do this. This function is placed at the bottom of the sketch and is shown in the screen shot below.

The direction, rate, and sub trim specified for each servo is used to calculate an appropriate pulse width for the servo.

The standard neutral pulse is 1500uS, and the normal range either side of neutral is +-500uS. This gives a min pulse width of 1000uS (-100%) and max of 2000uS (+100%). The pulse with rates, direction and sub trim applied can therefore be calculated as follows.

pulse, uS = 1500 + (servo_position_% * rates * direction + sub trim) * 500

The servo direction, rate and sub trim can be static or modified dynamically by the sketch in response to an input from another receiver channel, or by some other means.

Rationale for the approach taken

It is possible to read an RC receiver using the pulseIn(PIN, HIGH) function, however pulseIn() blocks the code in loop() while it waits for a pulse to start and then to finish, wasting precious processing time. If there is more than one input data could also be lost.

For speed it is best to use the pin change interrupt feature of the Arduino along with direct port manipulation to allow the code in loop() to run with the minimum of delay. This however is more involved and time consuming than simply calling pulseIn(PIN, HIGH).

Therefore I wanted to get the advantages of both worlds by writing some generic code that I can move between projects. All that is needed is to copy and paste an.ino file (containing the functions and interrupt routines) into the main sketch folder, specify the input pins, and then use the functions in the sketch.

Limitations

The micros()function

The microsecond timing on the arduino is carried out using the micros() function. This function counts in 4uS steps. This means we have a 4 microsecond level of precision when we measure the 1000-2000uS pulses. From a practical point of view this is more than adequate.

If desired It is possible to improve this resolution to 0.5uS by using timer interrupts. see link below:

https://www.instructables.com/id/How-to-get-an-Arduino-micros-function-with-05us-pr/

Efficiency of PWMread_RCfailsafe.ino

If your using PWMread_RCfailsafe.ino to read a 6 or 9 channel receiver 1.4-2.0% of the processing time is spent running the pin change interrupt routines, which I would argue is more than acceptable.

However it's always to good to understand the limitations of the code, and how it could be sped up if needed.

Below is a list of the time it takes to run each ISR depending on the number of selected input channels.

1 channel < 8uS

2 channels < 12uS

3 channels < 16uS

4 channels < 20uS

5 channels < 20uS

6 channels < 24uS

Note: the more channels used the longer each ISR takes to run. This is becausea for loopruns through each channel every time the ISR is called.

This extra time (inefficiency) is negligible when measuring low frequency (i.e.50hz) RCsignals.

On top of the above it takes ~4uS to enter and exit an ISR. For one pulse the ISR runs twice, once at the start of a pulse (LOW to HIGH) and then again at the end (HIGH to LOW).

The time taken to measure 1 pulse when using 6 RC inputs is

2 * (4us to enter ISR + 24uS to run ISR) = 2 * 28 = 48uS.

Note: this is the minimum pulse width than can be measured.

The time taken to read all 6 channels is 288uS (6 * 48uS)

Assuming that the receiver repetition period is 20 milliseconds, then the interrupt will be running for 1.44% (0.000288/0.02) of the time. This is significantly better than using the pulseIn()function. pulseIn() would block the code for up to 20 milliseconds for each pin.

FYI: if the arduino had only 2 RC inputs then the ISR will run for just 0.16% of the time (0.000032/0.02)

Maximum practical frequency (Hz)

If using this code for any other purpose I would suggest that a the maximum practical frequency is 2.5kHz. This give 100 steps of resolution from the micros() function (+- 0.025kHz).

If using one input pin at this frequency 3% of the time is spent in the interrupt, which means that the minimum duty that can be measured is 0.03. This equates to a minimum pulse with of 12uS.

For higher frequencies rewrite the ISR to suit your application.

Code

PWMread_RCfailsafeArduino
This .ino file contains the functions and pin change interrupt routines (ISR) used to decode an RC Receiver and apply a fail safe if the transmitter signal is lost. Copy and paste this file into the same folder as the main sketch (when you open the sketch this code will appear as a second tab in the arduino IDE). Then follow the instructions in the file.
/*  Kelvin Nelson 24/07/2019
 *  
 *  Pulse Width Modulation (PWM) decoding of RC Receiver with failsafe
 *  
 *  This code contains easy to use functions to measure square wave signals on any arduiuno pro mini, nano or uno pins, excluding A6 and A7.
 *  The code is intended to be used with RC receivers, but could also be used in most other PWM measurement applications as a direct replacement for pulseIn(PIN, HIGH). 
 *  (to date it hasn't been tested at a frequency greater than 1khz or on an arduino mega)
 *  
 *  An RC signal pulse can be converted from a pulse width duration (1000-2000uS) at each input pin into an -+100% (-+1.0) output for use in a sketch.
 *  The calibration for this conversion plus a failsafe setting can be set for each channel. (fail safe tolerances 10-330Hz and 500-2500uS). 
 *  
 *  The raw data for each pin can also be extracted, i.e. time of pulse, pulse width, frame length, duty and frequency.
 *  
 *  Set-up is quick, the organisation of this file is as follows:
 *  
 *    - Overview of the code
 *    - List of functions
 *    - How to use, including example sketches
 *    - User defined variables -> specify input pins, transmitter calibration, and failsafe.
 *    - Global variables and functions
 *  
 *  OVERVIEW OF THE CODE:
 *  
 *  The code enables pin change interrupts on the selected pins by setting the appropriate registers.
 *  
 *  A voltage change on any of the selected pins will trigger one of three Interrupt Service Routines depending on which register the pin belongs to.
 *    - ISR(PCINT0_vect), ISR(PCINT1_vect) or ISR(PCINT2_vect)
 *    
 *  Within each ISR the code determines which pin has changed, and makes a note of the time before returning back to the main loop().
 *  
 *  The time intervals between pin changes are used to calculate pulse width and frame length. 
 *  
 *  Flags are set by the ISR to indicate when new pulses are received.
 *  
 *  The Flags are then used to extract and process the data collected by each ISR.
 * 
 *  Although it's not exactly the same, this code follows similar principles to those explained in this video: https://youtu.be/bENjl1KQbvo
 * 
 */
// LIST OF FUNCTIONS:
// OUTPUT TYPE    NAME OF FUNCTION           NOTES

// void           setup_pwmRead()            initialise the PWM measurement using pin change interrupts

// RC RECEIVER DECODING
// boolean        RC_avail()                 returns a HIGH when new RC data is available
// float          RC_decode(channel number)  decodes the selected RC channel into the range +-100%, and applies a failsafe.
// void           print_RCpwm()              Prints the RC channel raw data to serial port (used for calibration).

// GENERIC PWM MEASUREMENTS
// boolean        PWM_read(channel number)   returns a HIGH when a new pulse has been detected on a particular channel. 
//                                           The function saves the pulse data to variables outside the interrupt routines
//                                           and must be called just before using the rest of PWM functions.
// unsigned long  PWM_time()                 returns the time at the start of pulse 
// float          PWM()                      returns the pulse width
// float          PWM_period()               returns the time between pulses
// float          PWM_freq()                 calculates the frequency
// float          PWM_duty()                 calculates the duty

// NOTE: PWM_read(CH) and RC_decode(CH) use the same flags to detect when new data is available, meaning data could be lost if both are used on the same channel at the same time.
// SUGESTION: if you want to use PWM_read(CH) to find the frame rate of an RC channel call it before RC_decode(CH). The output from RC_decode(CH) will then default to the failsafe.

// HOW TO USE, including example sketches

// under the "USER DEFINED VARIABLES" title in the code below:
//
//    Step 1: enter the input pins into the array pwmPIN[] = {}. 
//
//            - Any number of pins can be entered into pwmPIN[] (pins available 0 - 13 and A0 - A5)
//            - The pins do not need to be in numerical order, for example pwmPIN[] = {A0,5,6,10,8} for 5 channels, or pwmPIN[] = {A0,5} for 2 channels
//            - The first element in the array is the pin number for  "channel 1", and the second is the pin number for "channel 2"... etc.
//            - All pins connected to the RC receiver need to be at the start of the array. i.e. the first 2 channels could be RC inputs and the 3rd channel could be connected to another device like the echo pin of an ultrasonic sensor.
//
//    Step 2: if an RC receiver is connected to all of the inputs then set RC_inputs to 0, if not specify the number of channels connected to the receiver i.e. RC_inputs = 2;
//
//    Step 3: calibrate your transmitter by uploading a simple sketch with this .ino file included in the sketch folder, and print the raw PWM values to serial (alternatively copy and paste the functions needed into the sketch).
//            Using the info from the serial monitor manually update the values in arrays RC_min[], RC_mid[], RC_max[] to suit your transmitter (use full rates to get the best resolution).
        
//            an example sketch for printing the RC channel PWM data to serial. 
              /* 
              void setup()  {
                  setup_pwmRead();
                  Serial.begin(9600);
              }
              void loop() {
                  if(RC_avail()) print_RCpwm();
              }
               */

//    Step 4: Choose a failsafe position for each channel, in the range -1.0 to +1.0, and enter it into the array RC_failsafe[] = {}
//            Note: if you would like the arduino to respond to the loss of transmitter signal you may need to disable the failsafe feature on your receiver (if it has one).
//            an example sketch to check the operation of the failsafe, and for printing the calibrated channels to serial:
/* 
              unsigned long now;                        // timing variables to update data at a regular interval                  
              unsigned long rc_update;
              const int channels = 6;                   // specify the number of receiver channels
              float RC_in[channels];                    // an array to store the calibrated input from receiver 
              
              void setup()  {
                  setup_pwmRead();                      
                  Serial.begin(9600);
              }
              
              void loop()  {
                  now = millis();
                  
                  if(RC_avail() || now - rc_update > 25){   // if RC data is available or 25ms has passed since last update (adjust to suit frame rate of receiver)
                    
                    rc_update = now;                           
                    
                    //print_RCpwm();                        // uncommment to print raw data from receiver to serial
                    
                    for (int i = 0; i<channels; i++){       // run through each RC channel
                      int CH = i+1;
                      
                      RC_in[i] = RC_decode(CH);             // decode receiver channel and apply failsafe
                      
                      print_decimal2percentage(RC_in[i]);   // uncomment to print calibrated receiver input (+-100%) to serial       
                    }
                    Serial.println();                       // uncomment when printing calibrated receiver input to serial.
                  }
              }
               */

// EXAMPLE USE OF GENERIC PWM FUNCTIONS:
  /*
 // Print the pulse width of channel 1 to the serial monitor. 
 // This is equivelant to the using standard arduino pulseIn(pin, HIGH) function, but without blocking the code.
 
 if (PWM_read(1)){          // if a new pulse is detected on channel 1, print the pulse width to serial monitor.
   Serial.println(PWM());
 } 
  */
 // Or 
 /*
 // Print RC receiver frame length and frame rate
 
 if (PWM_read(1)){                                      // if a new pulse is detected on channel 1
   Serial.print(PWM_period(),0);Serial.print("uS ");     
   Serial.print(PWM_freq());Serial.println("Hz");
 }

 */

/*
 *  USER DEFINED VARIABLES (MODIFY TO SUIT YOUR APPLICATION)
 */
 
// PWM input pins, any of the following pins can be used: digital 0 - 13 or analog A0 - A5 

const int pwmPIN[]={2,3,4,5,6,7}; // an array to identify the PWM input pins (the array can be any length) 
                                  // first pin is channel 1, second is channel 2...etc

int RC_inputs = 0;                // The number of pins in pwmPIN that are connected to an RC receiver. Addition pins not connected to an RC receiver could be used for any other purpose i.e. detecting the echo pulse on an HC-SR04 ultrasonic distance sensor
                                  // When 0, it will automatically update to the number of pins specified in pwmPIN[] after calling setup_pwmRead().                                                
// Calibration of each RC channel:
 
// The arrays below are used to calibrate each RC channel into the range -1 to +1 so that servo direction, mixing, rates, sub trims...etc can be applied in sketch depending on application. 
// The arrays should be modified in order to calibrate the min, middle and max pulse durations to suit your transmitter (use max rates to get the best resolution). 
// FYI: the function print_PWM() will print the raw pulse width data for all the RC channels to the serial port.
// if the RC_min[], RC_mid[], RC_max[] are empty or have missing data the calibration will default to min 1000us, mid 1500us and max 2000us.

//SANWA 6CH 40MHz with corona RP6D1  
//                THR     RUD     PIT     BAL     SWITCH  SLIDER
int RC_min[6] = { 988,    1060,   976,    960,    1056,   1116};
int RC_mid[6] = { 1472,   1446,   1424,   1398,   1374,   1460};
int RC_max[6] = { 1800,   1816,   1796,   1764,   1876,   1796};

// fail safe positions

float RC_failsafe[] = {0.00, 0.00, 1, 0.00, -0.25, 0.00};
   
// enter a failsafe position (in the range of -+1) for each RC channel in case radio signal is lost
// if the array is the incorrect length for the number of RC channels, the failsafe will default to neutral i.e. 0. 
// The failsafe tolerances are: 10-330Hz & 500-2500us

/*
 *    GLOBAL PWM DECODE VARIABLES
 */

const int num_ch = sizeof(pwmPIN)/sizeof(int);  // calculate the number of input pins (or channels)
volatile int PW[num_ch];                        // an array to store pulsewidth measurements
volatile boolean prev_pinState[num_ch];         // an array used to determine whether a pin has gone low-high or high-low
volatile unsigned long pciTime;                 // the time of the current pin change interrupt
volatile unsigned long pwmTimer[num_ch];        // an array to store the start time of each PWM pulse

volatile boolean pwmFlag[num_ch];               // flag whenever new data is available on each pin
volatile boolean RC_data_rdy;                   // flag when all RC receiver channels have received a new pulse
unsigned long pwmPeriod[num_ch];                 // period, mirco sec, between two pulses on each pin

byte pwmPIN_reg[num_ch];                        // each of the input pins expressed as a position on it's associated port register
byte pwmPIN_port[num_ch];                       // identify which port each input pin belongs to (0 = PORTB, 1 = PORTC, 2 = PORTD)

const int size_RC_min = sizeof(RC_min) / sizeof(int);           // measure the size of the calibration and failsafe arrays
const int size_RC_mid = sizeof(RC_mid) / sizeof(int);
const int size_RC_max = sizeof(RC_max) / sizeof(int);
const int size_RC_failsafe = sizeof(RC_failsafe) / sizeof(float);

// FUNCTION USED TO TURN ON THE INTERRUPTS ON THE RELEVANT PINS
// code from http://playground.arduino.cc/Main/PinChangeInterrupt

void pciSetup(byte pin){
    *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin));  // enable pin
    PCIFR  |= bit (digitalPinToPCICRbit(pin));                   // clear any outstanding interrupt
    PCICR  |= bit (digitalPinToPCICRbit(pin));                   // enable interrupt for the group
}

// FUNCTION USED TO FIND THE PIN POSITION ON EACH PORT REGISTER: helps the interrupt service routines, ISR, run faster

void pwmPIN_to_port(){
  for (int i = 0; i < num_ch; i++){

    // determine which port and therefore ISR (PCINT0_vect, PCINT1_vect or PCINT2_vect) each pwmPIN belongs to.
                                                                  pwmPIN_port[i] = 1;    // pin belongs to PCINT1_vect (PORT C)
    if (pwmPIN[i] >= 0 && pwmPIN[i] <= 7)                         pwmPIN_port[i] = 2;    // pin belongs to PCINT2_vect (PORT D)
    else if (pwmPIN[i] >= 8 && pwmPIN[i] <= 13)                   pwmPIN_port[i] = 0;    // pin belongs to PCINT0_vect (PORT B)

    // covert the pin number (i.e. pin 11 or pin A0) to the pin position in the port register. There is most likely a better way of doing this using a macro...
    // (Reading the pin state directly from the port registers speeds up the code in the ISR)
    
    if(pwmPIN[i] == 0 || pwmPIN[i] == A0 || pwmPIN[i] == 8)         pwmPIN_reg[i] = 0b00000001;
    else if(pwmPIN[i] == 1 || pwmPIN[i] == A1 || pwmPIN[i] == 9)    pwmPIN_reg[i] = 0b00000010;
    else if(pwmPIN[i] == 2 || pwmPIN[i] == A2 || pwmPIN[i] == 10)   pwmPIN_reg[i] = 0b00000100;
    else if(pwmPIN[i] == 3 || pwmPIN[i] == A3 || pwmPIN[i] == 11)   pwmPIN_reg[i] = 0b00001000;
    else if(pwmPIN[i] == 4 || pwmPIN[i] == A4 || pwmPIN[i] == 12)   pwmPIN_reg[i] = 0b00010000;
    else if(pwmPIN[i] == 5 || pwmPIN[i] == A5 || pwmPIN[i] == 13)   pwmPIN_reg[i] = 0b00100000;
    else if(pwmPIN[i] == 6)                                         pwmPIN_reg[i] = 0b01000000;
    else if(pwmPIN[i] == 7)                                         pwmPIN_reg[i] = 0b10000000;
    
  }
}

// SETUP OF PIN CHANGE INTERRUPTS

void setup_pwmRead(){
  
  for(int i = 0; i < num_ch; i++){              // run through each input pin
    pciSetup(pwmPIN[i]);                        // enable pinchange interrupt for pin
  }
  pwmPIN_to_port();                             // determines the port for each input pin
                                                // pwmPIN_to_port() also coverts the pin number in pwmPIN[] (i.e. pin 11 or pin A0) to the pin position in the port register (i.e. 0b00000001) for use in the ISR.
  
  if(RC_inputs == 0 || RC_inputs > num_ch) RC_inputs = num_ch;    // define the number of pins connected to an RC receiver.                                          
} 

// INTERRUPT SERVICE ROUTINES (ISR) USED TO READ PWM INPUT

// the PCINT0_vect (B port register) reacts to any changes on pins D8-13.
// the PCINT1_vect (C port register)          ""        ""         A0-A5.
// the PCINT2_vect (D port register)          ""        ""         D0-7.

// port registers are used to speed up if statements in ISR code:
// https://www.arduino.cc/en/Reference/PortManipulation http://tronixstuff.com/2011/10/22/tutorial-arduino-port-manipulation/
// http://harperjiangnew.blogspot.co.uk/2013/05/arduino-port-manipulation-on-mega-2560.html


// READ INTERRUPTS ON PINS D8-D13: ISR routine detects which pin has changed, and returns PWM pulse width, and pulse repetition period.

ISR(PCINT0_vect){                                                 // this function will run if a pin change is detected on portB
  
  pciTime = micros();                                             // Record the time of the PIN change in microseconds

  for (int i = 0; i < num_ch; i++){                               // run through each of the channels
    if (pwmPIN_port[i] == 0){                                     // if the current channel belongs to portB
      
      if(prev_pinState[i] == 0 && PINB & pwmPIN_reg[i]){          // and the pin state has changed from LOW to HIGH (start of pulse)
        prev_pinState[i] = 1;                                     // record pin state
        pwmPeriod[i] = pciTime - pwmTimer[i];                     // calculate the time period, micro sec, between the current and previous pulse
        pwmTimer[i] = pciTime;                                    // record the start time of the current pulse
      }
      else if (prev_pinState[i] == 1 && !(PINB & pwmPIN_reg[i])){ // or the pin state has changed from HIGH to LOW (end of pulse)
        prev_pinState[i] = 0;                                     // record pin state
        PW[i] = pciTime - pwmTimer[i];                            // calculate the duration of the current pulse
        pwmFlag[i] = HIGH;                                        // flag that new data is available
        if(i+1 == RC_inputs) RC_data_rdy = HIGH;                  
      }
    }
  }
}

// READ INTERRUPTS ON PINS A0-A5: ISR routine detects which pin has changed, and returns PWM pulse width, and pulse repetition period.

ISR(PCINT1_vect){                                                 // this function will run if a pin change is detected on portC

  pciTime = micros();                                             // Record the time of the PIN change in microseconds

  for (int i = 0; i < num_ch; i++){                               // run through each of the channels
    if (pwmPIN_port[i] == 1){                                     // if the current channel belongs to portC
      
      if(prev_pinState[i] == 0 && PINC & pwmPIN_reg[i]){          // and the pin state has changed from LOW to HIGH (start of pulse)
        prev_pinState[i] = 1;                                     // record pin state
        pwmPeriod[i] = pciTime - pwmTimer[i];                     // calculate the time period, micro sec, between the current and previous pulse
        pwmTimer[i] = pciTime;                                    // record the start time of the current pulse
      }
      else if (prev_pinState[i] == 1 && !(PINC & pwmPIN_reg[i])){ // or the pin state has changed from HIGH to LOW (end of pulse)
        prev_pinState[i] = 0;                                     // record pin state
        PW[i] = pciTime - pwmTimer[i];                             // calculate the duration of the current pulse
        pwmFlag[i] = HIGH;                                         // flag that new data is available
        if(i+1 == RC_inputs) RC_data_rdy = HIGH;
      }
    }
  }
}

// READ INTERRUPTS ON PINS D0-7: ISR routine detects which pin has changed, and returns PWM pulse width, and pulse repetition period.

ISR(PCINT2_vect){                                                 // this function will run if a pin change is detected on portD

  pciTime = micros();                                             // Record the time of the PIN change in microseconds

  for (int i = 0; i < num_ch; i++){                               // run through each of the channels
    if (pwmPIN_port[i] == 2){                                     // if the current channel belongs to portD
      
      if(prev_pinState[i] == 0 && PIND & pwmPIN_reg[i]){          // and the pin state has changed from LOW to HIGH (start of pulse)
        prev_pinState[i] = 1;                                     // record pin state
        pwmPeriod[i] = pciTime - pwmTimer[i];                     // calculate the time period, micro sec, between the current and previous pulse
        pwmTimer[i] = pciTime;                                    // record the start time of the current pulse
      }
      else if (prev_pinState[i] == 1 && !(PIND & pwmPIN_reg[i])){ // or the pin state has changed from HIGH to LOW (end of pulse)
        prev_pinState[i] = 0;                                     // record pin state
        PW[i] = pciTime - pwmTimer[i];                            // calculate the duration of the current pulse
        pwmFlag[i] = HIGH;                                        // flag that new data is available
        if(i+1 == RC_inputs) RC_data_rdy = HIGH;
      }
    }
  }
}

/*
 *  RC OUTPUT FUNCTIONS
 */

 boolean RC_avail(){
    boolean avail = RC_data_rdy;
    RC_data_rdy = LOW;                          // reset the flag
    return avail;
    }

  float RC_decode(int CH){
  
  if(CH < 1 || CH > RC_inputs) return 0;     // if channel number is out of bounds return zero.
  
  int i = CH - 1;                     

  // determine the pulse width calibration for the RC channel. The default is 1000, 1500 and 2000us.
  
  int Min;
  if(CH <= size_RC_min) Min = RC_min[CH-1]; else Min = 1000;
  
  int Mid;
  if(CH <= size_RC_mid) Mid = RC_mid[CH-1]; else Mid = 1500;
  
  int Max;
  if(CH <= size_RC_max) Max = RC_max[CH-1]; else Max = 2000;

  float CH_output;
      
  if(FAILSAFE(CH) == HIGH){                         // If the RC channel is outside of failsafe tolerances (10-330hz and 500-2500uS)
      if(CH > size_RC_failsafe) CH_output = 0;      // and if no failsafe position has been defined, set output to neutral
      else CH_output = RC_failsafe[i];              // or if defined set the failsafe position 
  }
  else{                                             // If the RC signal is valid
    CH_output = calibrate(PW[i],Min,Mid,Max);       // calibrate the pulse width to the range -1 to 1.
  }
  return CH_output;                                 

  // The signal is mapped from a pulsewidth into the range of -1 to +1, using the user defined calibrate() function in this code. 

  // 0 represents neutral or center stick on the transmitter
  // 1 is full displacement of a control input is one direction (i.e full left rudder)
  // -1 is full displacement of the control input in the other direction (i.e. full right rudder)
}

/*
 *  Receiver Calibration
 */

 // NEED TO SPEED UP

float calibrate(float Rx, int Min, int Mid, int Max){
   float calibrated;
   if (Rx >= Mid)
   {
    calibrated = map(Rx, Mid, Max, 0, 1000);  // map from 0% to 100% in one direction
   }
   else if (Rx == 0)
   {
    calibrated = 0;                           // neutral
   }
   else
   {
    calibrated = map(Rx, Min, Mid, -1000, 0); // map from 0% to -100% in the other direction
   }
  return calibrated * 0.001;
}

// Basic Receiver FAIL SAFE
// check for 500-2500us and 10-330Hz (same limits as pololu)

boolean FAILSAFE(int CH){

   int i = CH-1;
   boolean failsafe_flag = LOW;
        
       if(pwmFlag[i] == 1)                             // if a new pulse has been measured.
         {
            pwmFlag[i] = 0;                            // set flag to zero
      
            if(pwmPeriod[i] > 100000)                  // if time between pulses indicates a pulse rate of less than 10Hz   
            {
              failsafe_flag = HIGH;                       
            }
            else if(pwmPeriod[i] < 3000)               // or if time between pulses indicates a pulse rate greater than 330Hz   
            {
              failsafe_flag = HIGH;                             
            }

            if(PW[i] < 500 || PW[i] > 2500)           // if pulswidth is outside of the range 500-2500ms
            {
              failsafe_flag = HIGH;                        
            }   
         }
        else if (micros() - pwmTimer[i] > 100000)     // if there is no new pulswidth measurement within 100ms (10hz)
        {
          failsafe_flag = HIGH;                      
        }

    return failsafe_flag;   
}

/*
 *  Quick print function of Rx channel input
 */

void print_RCpwm(){                             // display the raw RC Channel PWM Inputs
  for (int i = 0; i < RC_inputs; i++){
    //Serial.print(" ch");Serial.print(i+1);
    Serial.print("  ");
    if(PW[i] < 1000) Serial.print(" ");
    Serial.print(PW[i]);
  }
  Serial.println("");
}

void print_decimal2percentage(float dec){
  int pc = dec*100;
  // the number and text will take up 6 charactors i.e ___3%_ or -100%_
  if (pc >= 0) Serial.print(" ");
  if (abs(pc) < 100) Serial.print(" ");
  if (abs(pc) < 10) Serial.print(" ");
  Serial.print(" ");Serial.print(pc);Serial.print("% ");
}

/*
 * GENERIC PWM FUNCTIONS
 */

unsigned long pin_time;
float pin_pwm;
float pin_period;

boolean PWM_read(int CH){
  if(CH < 1 && CH > num_ch) return false;
  int i = CH-1;
  boolean avail = pwmFlag[i];
  if (avail == HIGH){
    pwmFlag[i] = LOW;
    noInterrupts();
    pin_time = pwmTimer[i];
    pin_pwm = PW[i];
    pin_period = pwmPeriod[i];
    interrupts();
  }
  return avail;
}

unsigned long PWM_time(){return pin_time;}
float PWM_period(){return pin_period;}
float PWM(){return pin_pwm;}

float PWM_freq(){
  float freq;
  return freq = 1000000 / pin_period;  // frequency Hz
}

float PWM_duty(){
  float duty;
  duty = pin_pwm/pin_period;
  return duty;
}
RC_Read_ExampleArduino
An example sketch used to display raw data in order to calibrate your RC receiver and set your the fail safe. The PWMread_RCfailsafe.ino file should be copied into the same folder in order for the functions to be available.
unsigned long now;                        // timing variables to update data at a regular interval                  
unsigned long rc_update;
const int channels = 6;                   // specify the number of receiver channels
float RC_in[channels];                    // an array to store the calibrated input from receiver 

void setup() {
    setup_pwmRead();                      
    Serial.begin(9600);
}

void loop() {  
    
    now = millis();
    
    if(RC_avail() || now - rc_update > 25){   // if RC data is available or 25ms has passed since last update (adjust to be equal or greater than the frame rate of receiver)
      
      rc_update = now;                           
      
      print_RCpwm();                        // uncommment to print raw data from receiver to serial
      
      for (int i = 0; i<channels; i++){       // run through each RC channel
        int CH = i+1;
        
        RC_in[i] = RC_decode(CH);             // decode receiver channel and apply failsafe
        
        //print_decimal2percentage(RC_in[i]);   // uncomment to print calibrated receiver input (+-100%) to serial       
      }
      //Serial.println();                       // uncomment when printing calibrated receiver input to serial.
    }
}
RC_FrameRateArduino
Example sketch that prints the frame rate and frequency of an RC Receiver. The PWMread_RCfailsafe.ino file should be copied into the same folder in order for the functions to be available.
void setup() {
    setup_pwmRead();                      
    Serial.begin(9600);
}

void loop() {  
    
    // Print RC receiver frame length and frame rate
 
    if (PWM_read(1)){                                      // if a new pulse is detected on channel 1
      Serial.print(PWM_period(),0);Serial.print("uS ");     
      Serial.print(PWM_freq());Serial.println("Hz");
    }

}
RC_ServoMixer_ExampleArduino
An servo mixing example. Two channels from a 6 channel are receiver are mixed and sent to two servos controlled using the servo library. The PWMread_RCfailsafe.ino file should be copied into the same folder in order for the functions to be available.
// servo variables

#include <Servo.h> // include the servo library to control the servos

Servo servo1;   // name each servo output for use with the servo library    
Servo servo2;      

// Each servo must be attached to a pin that has a PWM output
// on the arduino uno, nano and pro mini these pins are 3, 5, 6, 9, 10 and 11

const int servo1_pin = 9;  // identify the pins that each servo signal wire is connected to
const int servo2_pin = 10;

// Select Servo Direction, Rates and Sub-trim (the size of each array must match the number of servos)

boolean servo_dir[] = {0,1};     // Direction: 0 is normal, 1 is reverse
float servo_rates[] = {1,0.5};  // Rates: range 0 to 2 (1 = +-500us (NORMAL), 2 = +-1000us (MAX)): The amount of servo deflection in both directions
float servo_subtrim[] = {0.0,0.0};  // Subtrimrange -1 to +1 (-1 = 1000us, 0 = 1500us, 1 = 2000us): The neutral position of the servo
boolean servo_mix_on = true;

unsigned long now;                  // timing variables to update data at a regular interval                        
unsigned long rc_update;

// Receiver variables

const int channels = 6;                   // specify the number of receiver channels
float RC_in[channels];                    // an array to store the calibrated input from receiver 

void setup() {
    
    servo1.attach(servo1_pin, 500, 2500); // attach the servo library to each servo pin, and define min and max uS values
    servo2.attach(servo2_pin, 500, 2500);
    
    setup_pwmRead();                      
    Serial.begin(9600);
}

void loop() {  
    
    now = millis();
    
    if(RC_avail() || now - rc_update > 25){   // if RC data is available or 25ms has passed since last update (adjust to > frame rate of receiver)
      
      rc_update = now;                           
      
      print_RCpwm();                        // uncommment to print raw data from receiver to serial
      
      for (int i = 0; i<channels; i++){       // run through each RC channel
        int CH = i+1;
        
        RC_in[i] = RC_decode(CH);             // decode receiver channel and apply failsafe
        
        //print_decimal2percentage(RC_in[i]);   // uncomment to print calibrated receiver input (+-100%) to serial       
      }
      //Serial.println();                       // uncomment when printing calibrated receiver input to serial.

      
      
      int servo1_uS;      // variables to store the pulse widths to be sent to the servo
      int servo2_uS;      
      
      if (servo_mix_on == true){              // MIXING ON
        
        float mix1 = RC_in[1] - RC_in[2];     // Channel 2 (ELV) - Channel 3 (AIL)
        float mix2 = RC_in[1] + RC_in[2];     // Channel 2 (ELV) + Channel 3 (AIL)
  
        if(mix1 > 1) mix1 = 1;                // limit mixer output to +-1
        else if(mix1 < -1) mix1 = -1;
  
        if(mix2 > 1) mix2 = 1;                // limit mixer output to +-1
        else if(mix2 < -1) mix2 = -1;  
  
        // Calculate the pulse widths for the servos
      
        servo1_uS = calc_uS(mix1, 1);         // apply the servo rates, direction and sub_trim for servo 1, and convert to a RC pulsewidth (microseconds, uS)
        servo2_uS = calc_uS(mix2, 2);         // apply the servo rates, direction and sub_trim for servo 2, and convert to a RC pulsewidth (microseconds, uS)
            
      }
      else{                                   // MIXING OFF
        servo1_uS = calc_uS(RC_in[1],1);      // apply the servo rates, direction and sub_trim for servo 1, and convert to a RC pulsewidth (microseconds, uS)
        servo2_uS = calc_uS(RC_in[2],2);      // apply the servo rates, direction and sub_trim for servo 1, and convert to a RC pulsewidth (microseconds, uS)
      }

      servo1.writeMicroseconds(servo1_uS);   // write the pulsewidth to the servo.
      servo2.writeMicroseconds(servo2_uS);   // write the pulsewidth to the servo. 
    }
}

int calc_uS(float cmd, int servo){                                // cmd = commanded position +-100% 
                                                                  // servo = servo num (to apply correct direction, rates and trim)
  int i = servo-1;
  float dir;
  if(servo_dir[i] == 0) dir = -1; else dir = 1;                   // set the direction of servo travel
  
  cmd = 1500 + (cmd*servo_rates[i]*dir + servo_subtrim[i])*500;   // apply servo rates and sub trim, then convert to a uS value

  if(cmd > 2500) cmd = 2500;                                      // limit pulsewidth to the range 500 to 2500us
  else if(cmd < 500) cmd = 500;

  return cmd;
}

Schematics

Arduino Uno Setup
This RC Receiver is powered by 5v and ground from the ICSP pins with the 6 signal outputs connected to pins 2-7
Micro servo 1 is powered by 5v pin and ground, with signal wire connected to pin 9
Micro servo 2 powered by 3.3v pin and ground, with signal wired connected to pin 10
Img 4469 5dzt0ofg87

Comments

Similar projects you might like

Joystick Controlled RC Car

Project tutorial by Arduino_Scuola

  • 11,246 views
  • 4 comments
  • 15 respects

DIY RC Plane 4 Channel Transmitter – Receiver Using Arduino

Project showcase by Team RootSaid

  • 19,727 views
  • 0 comments
  • 26 respects

Flying Drone Mjolnir with Hacked RC Transmitter

Project in progress by Allen Pan

  • 15,607 views
  • 1 comment
  • 57 respects

Control Dual Axis FPV Camera Cradle with Joystick Module

Project tutorial by SurtrTech

  • 8,069 views
  • 9 comments
  • 43 respects

Simple Object avoider robot the using Actobotics Runt

Project tutorial by Scott Beasley

  • 7,519 views
  • 4 comments
  • 17 respects

Arduino Marble Maze Labyrinth

Project tutorial by AhmedAzouz

  • 6,894 views
  • 9 comments
  • 33 respects
Add projectSign up / Login