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
- 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
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.
You should also have a working knowledge of:
- The Arduino IDE
- Float, boolean, and int variables
- If loops
- For loops
- Arrays
- Servo library
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.
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.inoStep 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.
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 exampleI'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 takenIt 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.
LimitationsThe 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.
Comments