6-CH R/C TX Coder
by Harry Lythall - SM0VPO

Introduction

I am still building that 1 metre long Radio Controlled (RC) boat 😉. With home-made radio equipment and a 3D printer I do not need any serious money to build it, either. I have an E-Sky RC simulator (4 channels) that has the RC transmitter box and two joysticks. At $12 it is cheaper than buying ONE spare joystick. That is four channels, and by adding two 5K potentiometers I can have 6 channels. Time to take the old simulator box out of the attic.


My old E-Sky USB simulator will now have a new life.

In days of old, when knights were bold, and toilets were not invented, I would start laying out a PCB, spraying, curing, exposing, etching, drilling then spend an evening stuffing components on the board. This project deviates from that. If you have the joysticks and pots, all you need in an Arduino Nano, plus the radio transmitter. A $15 PMR walkie-Talkie pair from the toy shop is all you need for the radio side. One on permanent transmit and the other on permanent receive. I tested with a cheap pair that I borrowed. They are supposed to deliver only 0.5-Watts. The range quoted stated "Theoretically 10km" but I do not believe that.

This project describes the Arduino Nano board software and how it is used.

The Arduino Hardware

Download and install the Arduino IDE package on you computer, select your device and port number, then you can compile and upload this software. With the Arduino Nano the program is stored in flash RAM, so once it has been programmed, you can disconnect it from your computer. Apply 5-Volts and it will continue running. The Arduino Uno needs to have the script uploaded every time it is powered up, but this is NOT the case with the Nano.

If you have an Arduino Uno then you have six analogue input ports: A0 to A5. With these you can program the Uno for up to 6 proportional channels. The Arduino Nano, on the other hand, has 8 analogue input channels, A0 to A7, so you can use this in an 8-channel RC transmitter. The script in the Script section below is for the 6-channel version, so it will run on both the Uno and Nano. You can program the Arduino Mega for up to 16-Channels, but you will need to extend the encoder frame. See the timing data below.

Analogue RC Timing

This is rather important. For a commercial 6-Channel "Analogue Proportional RC" system, which is somewhat traditional, there are seven pulses of 100μs. The first pulse starts the RC decoder in the receiver. I will call this pulse P0. After a period of time, the encoder sends a second pulse, P1. The time between the rising edge of P0 and P1 determines the position of the servo in your RC model.

Futaba servos can cost typically $12 for a standard servo. The "traditional" system channel pulse varies from 500μs to 1500μs (0.5 to 1.5 milli-seconds). They have a rotation angle of something like 90° to control your model. Futaba, however, used 500μs to 2500μs (0.5 to 2.5 milli-seconds[ms]) and set a new "standard".

For simple general (27MHz) systems, and most cheaper 2-channel systems (rudder/elevators in aircraft or speed/rudder in boats), the timing was almost always 0.5 to 1.5ms.

Arduino development servos can cost typically $15 for FOUR miniature servos. The Arduino system channel pulse varies from 500μs to 2500μs (0.5 to 2.5ms), just the same as Futaba, and also have a 180° rotation angle.


An Arduino "development pack" of four servos - $15.

Both systems have a 20ms frame length, and there must be 5 milliseconds of idle time (sometimes more) for the analogue receiver to detect the end of frame. This is important.

If you have 20ms frame rate, and need a minimum of 5ms to detect the end of frame, then there is only 15ms remaining for channel information. With the longer Futaba and Arduino systens that use up to 2.5ms per channel, then there is only enough time for a maximum of 6-Channels.

The decoder is nothing more than a counter, like a CD4017B to seperate the channel pulses. Q1 to Q8 have a pulse length for each servo. Q0 is HIGH while waiting for the next frame to start. Every channel pulse keeps a capacitor charged, but the capacitor is discharged with a time-constant of about 4ms. If the capacitor discharges below a specific level then the CD4017B is reset in readiness for the next frame.


Simple "Channel Seperator" circuit from the RC receiver

This is just a counter. The CD4017B will also support up to 8 channels. Every channel pulse advances the counter to the next output. In this way each output is only HIGH for the duration of the channel timer. This signal is that which the servo expects. The circuit will accept a very low signal of just a few milli-volts of audio pulses. I will be updating this shortly to make the timing more accurate. It was developed in the days when there was more time between frames.

The input circuit is "self biasing", due to the input 47K resistor and the 1μf capacitor that follows the average DC level at the input. The capacitor marked "X" is charged very fast by the channel pulses (through the diode), and the 47K in parallel discharges it. The circuit was designed for a 4 and then 6-channel decoder. If you wish extend the 8-Channel encoder then you may have to reduce the 47K resistor a little (22K or 15K). To check this, set all 8 channels to +5-Volts to generate a "worst case" example. If it resets cleanly with all 8 channels having the longest possible pulse, then it should never give a problem.

In this project I have increased the frame time to 25ms, which means that you can experience the full 8 Analogue channels possible with the Arduino Nano. The servo rotation will be updated 40 times every second. Here are the actual waveforms in one frame:


One basic frame. 0-Volts on all 6 channels A0 to A5 (servo 0°)


One basic frame. 2.5-Volts on A0, CH-1. 1.5ms between P0 and P1 (servo 90°)


One basic frame. 5-Volts on A0, CH-1. 2.5ms between P0 and P1 (servo 180°)

My Arduino Script

With other people's Arduino projects it is customary to simply give the script and a wiring diagram. Since there are no components, other than the Arduino in this project, you do not really need a drawing. Instead I have given a connection table, see Pin Connections below. It is also customary to leave you in the dark to figure out for yourself just how the script works, but I will not do that. Here is the complete script for the Arduino:



// Analogue Proportional Radio Control of models - my first real Arduino project
// This code generate anything from 1 to 6-Channels, and can be extended to 8 proportional channels
// Harry lythall - SM0VPO - https://sm0vpo.com for more information

const int outputPin = 9; // Define digital output pin for pulses
const int syncPin = 10; // Define digital output for scope sync
const int joystickPins[] = {A0, A1, A2, A3, A4, A5}; // Define analog input pins for joysticks (up to 8 channels with A6 & A7)
const int minPulseWidth = 240; // Minimum joystick pulse width in microseconds (500μs minus time outside loops)
const int maxPulseWidth = 2220; // Maximum pulse width in microseconds (2500μs minus time outside loops)
const int startPulseDuration = 150; // Start/end pulse duration in microseconds
const int syncPulseDuration = 100; // Sync pulse duration in microseconds (for oscilloscope display)
int timerCalculation = 0;
int timerMilliseconds = 0;
int timerMicroseconds = 0;

void setup() {
  pinMode(outputPin, OUTPUT); // Initialize digital pin 9 as output
  pinMode(syncPin, OUTPUT); // Initialize digital pin 10 as output
  Serial.begin(9600); // Initialize serial communication (for debugging)
}

void loop() {
  // Start of one frame, read joysticks and generate channel pulses
  generateSyncPulse(); // Generate sync pulse (for oscilloscope)
  generateStartPulse(); // Generate frame start pulse
  timerCalculation = 23200; // (25000μs minus processing time outside timing loops - 28750 if 7 or 8 channels)

  // Loop through each joystick
  for (int i = 0; i < sizeof(joystickPins) / sizeof(joystickPins[0]); i++) {
    int joystickValue = analogRead(joystickPins[i]); // Read joystick value
    int pulseWidth = map(joystickValue, 0, 1023, minPulseWidth, maxPulseWidth); // Map joystick value to pulse width
    delayMicroseconds(pulseWidth); // Time delay based on joystick value
    (timerCalculation) = (timerCalculation) - (pulseWidth); // Subtract channel pulse from frame length counter
    generateStartPulse(); // Generate channel-end pulse (start of next channel, or end of frame)
  }
  
  (timerMilliseconds) = (timerCalculation / 1000); // 25000μs is too big for delay, convert to ms (integer)
  (timerMicroseconds) = (timerMilliseconds * 1000); // How much was lost generating the ms integer (in μs)?
  delay(timerMilliseconds); //Wait the whole milliseconds count
  delayMicroseconds(timerCalculation - timerMicroseconds); // Wait μs increments lost in integer - frame now 25ms (8CH = 30ms)
 }
void generateSyncPulse() {
  digitalWrite(syncPin, HIGH); // Generate sync pulse
  delayMicroseconds(syncPulseDuration); // Sync pulse duration
  digitalWrite(syncPin, LOW); // End sync pulse
}

void generateStartPulse() {
  digitalWrite(outputPin, HIGH); // Generate start/channel pulse
  delayMicroseconds(startPulseDuration); // Pulse duration
  digitalWrite(outputPin, LOW); // End start/channel pulse
}

}

Let us go through the code, step-by-step. With this knowledge you can adjust the code to suit your own system.


const int outputPin = 9; // Define digital output pin for pulses
const int syncPin = 10; // Define digital output for scope sync
const int joystickPins[] = {A0, A1, A2, A3, A4, A5}; // Define analog input pins for joysticks (up to 8 channels with A6 & A7)
const int minPulseWidth = 240; // Minimum joystick pulse width in microseconds (500μs minus time outside loops)
const int maxPulseWidth = 2220; // Maximum pulse width in microseconds (2500μs minus time outside loops)
const int startPulseDuration = 150; // Start/end pulse duration in microseconds
const int syncPulseDuration = 100; // Sync pulse duration in microseconds (for oscilloscopy display)
int timerCalculation = 0;
int timerMilliseconds = 0;
int timerMicroseconds = 0;

void setup() {
  pinMode(outputPin, OUTPUT); // Initialize digital pin 9 as output
  pinMode(syncPin, OUTPUT); // Initialize digital pin 10 as output
  Serial.begin(9600); // Initialize serial communication (for debugging)
}

These two bits of code define all the variables required for the code. I have been generous witht he comments in the code so they speak for themselves.
"const int joystickPins[] = {A0, A1, A2, A3, A4, A5}; // Define analog input pins for joysticks"
This defines the joystick potentiometer ports. The joystick potentiometers are connected across the Gnd and +5V line so that the analogue inputs receive a value between 0V and 5V, representing the joystick position. You can program only A0 for a single channel unit, or add as many channes as you like. I have fixed all the timing issues.

If you want to have a 7 or 8-Channel RC unit (Nano only) then replace "{A0, A1, A2, A3, A4, A5}" with "{A0, A1, A2, A3, A4, A5, A6, A7}". This will reduce the inter-frame rest to only 5ms, which is not very much for the decoder to detect a new frame. You can extend the frame 5ms by changing one parameter;
timerCalculation = 23200; // (25000μs minus processing time outside timing loops - 28750 if 7 or 8 channels)
Now the decoder will have a "fighting chance". The only downside of this is that the servo-refresh time at the receiver will change from 40 times a second to 33.33 time a second. Many older analogue RC systems used a 20ms to 25ms frame length, but this was only exact when all joysticks were centered. As joysticks are manipulated then the frame length varied a little +/- but this has no effect on operation. My code is now totally independent of the joystick movement. I have spent a lot of time on the timing.


  // Loop through each joystick
  for (int i = 0; i < sizeof(joystickPins) / sizeof(joystickPins[0]); i++) {
    int joystickValue = analogRead(joystickPins[i]); // Read joystick value
    int pulseWidth = map(joystickValue, 0, 1023, minPulseWidth, maxPulseWidth); // Map joystick value to pulse width
    delayMicroseconds(pulseWidth); // Time delay based on joystick value
    (timerCalculation) = (timerCalculation) - (pulseWidth); // Subtract channel pulse from frame length counter
    generateStartPulse(); // Generate channel-end pulse (start of next channel, or end of frame)
  }

This creates a loop that repeats for every channel configured. If you have 4 channels, then the joystick pins A0, A1, A2 and A3 will be read and the loop will only run four times. This routine does six things;

  1. Generates a loop to service all joysticks
  2. Reads the current joystick voltage [0 to 1023]
  3. Translates (Maps) the value in the range between "minPulseWidth" and "maxPulseWidth"
  4. Sends the value to the channel timer to execute the channel timer
  5. Takes the 25000μs frame length value and subtracts the time used by the current channel
  6. Generates a 200μs pulse [generateStartPulse()] - (end of channel pulse, but calls the StartPulse code)


  (timerMilliseconds) = (timerCalculation / 1000); // 25000μs is too big for delay, convert to ms (integer)
  (timerMicroseconds) = (timerMilliseconds * 1000); // How much was lost generating the ms integer (in μs)?
  delay(timerMilliseconds); //Wait the whole milliseconds count
  delayMicroseconds(timerCalculation - timerMicroseconds); // Wait μs increments lost in integer - frame now 25ms (8CH = 30ms)

I found that the Arduino is not easy using timers to generate interrupts, and then using timers in the code inside the interrupt timer. So I created a 25000μs variable (frame length) and then subtracted the time used for every analogue channel. This used as the final Frame-Timer value. I couldn't put the end result into a single timer because the Arduino becomes eratic with large values. So I divided it by 1000 (converted to Milliseconds) and used a millisecond timer. The problem with this is that the frame can have a +/-1ms error. So I took the ms integer, multiplied by 1000 (delay milliseconds integer now in microseconds) and subtracted this from the timerMicroseconds variable. So the missing microseconds (less than a ms) are now back in the timerMicroseconds and these are used in a microseconds timer. Yes, it is a bit of a "head scratcher", but it gives a lovely clean and acurate 25ms frame length. No jitter or jumps at all. It also works on both the Uno and the Nano.


void generateSyncPulse() {
  digitalWrite(syncPin, HIGH); // Generate sync pulse
  delayMicroseconds(syncPulseDuration); // Sync pulse duration
  digitalWrite(syncPin, LOW); // End sync pulse
}

void generateStartPulse() {
  digitalWrite(outputPin, HIGH); // Generate start pulse
  delayMicroseconds(startPulseDuration); // Pulse duration
  digitalWrite(outputPin, LOW);
}

The last bits of code are the sync-pulse and channel-pulse generator routines that are called at frame-start.

Joystick Notes

When you use a rotating potentiometer, it will travel from minimum to maximum in 300° of rotation. In this project, channels 5 upwards are therefore not a problem.

If you have a commercial joystick, you will see that they may only have a 90° rotational range, plus a bit extra for the "trim" function, or about 110° in total. You are not going to see the full 5-Volts range. There are a couple of solutions to this:

  1. Fit a 10K potentiometer and feed it with 12V. Just make sure that the voltage from the pot never exceeds +5-Volts. You can add a diode from the analogue data pin to the Vcc supply. This will clamp it within safe levels
  2. Use a Log-taper potentiometer. Connect one-end and wiper of the potentiometer, in series with a 1K0 resistor. This should get you 0 to 4-Volts.
  3. You can also change the "map(joystickValue, 0, 1023, minPulseWidth, maxPulseWidth)" to make adjustments. the value 1023 is the +5-Volt value. If you had 4.0-Volts then change the 1023 value to "1023 / 5 x 4 = 818"
  4. If you have space, then you can use two linear 20K potentiometers; Top of N°1, Bottom of N°2, and connect the two wipers together. When correctly adjusted you will have about 5K potentiometer that only has a 110° rotation


Option 4 - Using two ganged potentiometers connected to get 5K-Ohms with only 90° rotation.

Pin Connections

There are no components at all associated with this project, other than an Arduino Nano. All analogue joystick inputs need a voltage 0V to +5V to create the required chanel pulse. The connections to the Arduino are:

Device pinDevice pinDesignation
Arduino-Uno D9Arduino-nano D9 Pulses to Transmitter
Arduino-Uno D10Arduino-nano D10 Trigger to Oscilloscope
Arduino-Uno A0Arduino-nano A1 Channel-1 joystick input 0-5V
Arduino-Uno A1Arduino-nano A1 Channel-2 joystick input 0-5V
Arduino-Uno A2Arduino-nano A2 Channel-3 joystick input 0-5V
Arduino-Uno A3Arduino-nano A3 Channel-4 joystick input 0-5V
Arduino-Uno A4Arduino-nano A4 Channel-5 joystick input 0-5V
Arduino-Uno A5Arduino-nano A5 Channel-6 joystick input 0-5V
N/AArduino-nano A6 Channel-7 joystick input 0-5V
N/AArduino-nano A7 Channel-8 joystick input 0-5V
Arduino-Uno USB-AB or SocketArduino-nano USB-Mini-BPower input

Note - Arduino Uno can be powered by 5-Volts at the USB input. It can alternatively be powered through the power socket and an onboard voltage regulator that will accept 12-Volts.
Note - The Arduino Nano must be powered with 5-Volts through the USB connector.
There may be alternative power options, such as injecting to the 5V pins of the boards, but you should check your board data before playing with this. This I have no experience of.

Remember that whatever you do the analogue inputs MUST NEVER be set outside the Arduino power voltages. If you use, for example, Vcc = 3.3-Volts, then this is the maximum voltage that can be allowed at the analogue inputs.

Conclusion

In this project I have tried to give you a simple solution for a multi-channel analogue radio control unit using the Arduino micro-controller. Unlike Arduino projects published by other people I have explained the code so that you can edit or modify as you please to suit your own situation. I posted my prototype code, but now I have fixed all the variable timing issues you have a 100% functional code for up to 6.Channels. You can also extend it to 8-Channels if you change the two bits of code listed in the description.


Prototype plugged into a breadboard with scope probes

With this information you can make and program an RC unit for any number of channels, from 1 to 8. If you use the Arduino Mega then you can increase the frame length to 50ms and have up to 16 channels. When I was a kid it was always 2-Channels, unless you were really rich and could afford a 4-Channel unit. My parents would not get me any unit at all.

I hope that this project has given you some "food for thought". You can always e-mail me at harry.lythall@[my domain].com. You can even use oeieio@hotmail.com or hotmail@sm0vpo.com as they are both valid e-mail accounts for me 😉, although I would prefer that you visit my messageboard if you have any questions about this or any other project. I always look forward to receiving feedback, whether it be positive or negative ☺

Very best regards from Harry Lythall
SM0VPO (QRA = JO89WO), Märsta, Sweden.

Return to INFO page