Project showcase
Making Arduino-Based RC Transmitter of USB Flight Simulator

Making Arduino-Based RC Transmitter of USB Flight Simulator © MIT

My variant of converting a four-channel USB flight simulator into an RC transmitter using Arduino.

  • 1,139 views
  • 0 comments
  • 6 respects

Components and supplies

USB flight simulator
×1
11113 01
SparkFun Arduino Pro Mini 328 - 5V/16MHz
×1
USB-TTL adapter
×1
Breadboard 47х35mm (170 holes)
×1
Li-Ion battery 18650
×1
LI CHARGER microUSB TP4056-module
×1
Battery holder 18650 x1
×1
DC-DC StepUp 3v-to-5v 1A module
×1
Radio module NRF24L01-SMA
×1
Mfr 25frf52 10k sml
Resistor 10k ohm
×1
Adapter 5V-3,3V for NRF24L01
×1
Piezobuzzer LD-BZPN-2203
×1
Rotary encoder EC11 20mm
×1
Aluminum cap for encoder
×1

Necessary tools and machines

Hy gluegun
Hot glue gun (generic)
09507 01
Soldering iron (generic)
26w6260 40
Multitool, Screwdriver

Apps and online services

About this project

Components

USB flight simulators are used with special computer software (e. g. FMS, Fig. 1) for training or skill maintenance.

I had the simplest four-channel FS-SM020simulator with trimmers for all channels (Fig. 2).

Having disassembled it for the first time (Fig.3) I saw many holes under stickers for mounting additional controls, power socket; in addition there was a lot of empty space for all ideas described further.

All electronics had to be moved away except for potentiometers (Fig. 4).

Then I defined basic functionality of the device:

  • 2.4GHz operation frequency;
  • power supply from 18650 Li-Ion battery providing charging from USB-adapter without removing the battery from the transmitter;
  • battery charge control;
  • end point adjustments and reversal for each channel;
  • simple audible indication for each operating mode of the transmitter.

After that I collected all the necessary hardware:

  • ARDUINO Pro Mini board – «brain» of the transmitter, with small-sized (I had already had it in use);
  • solderless breadboard – as I planned to experiment and add functionality step by step I decided not to use soldering near ARDUINO board;
  • nRF24L01 module with external antenna for data transmitting;
  • 5V-3.3V adapter for nRF24L01 module since the main voltage is 5V but the module requires 3.3V;
  • piezobuzzer responsible for all sounds from the transmitter;;
  • encoder with a switch for channel selection and adjustment;
  • wires – both usual and ones for solderless setup;
  • power supply system (Fig. 5) which was the first thing to make because there was no sense to continue without it.

The transmitter is powered from Li-Ion 18650 battery put in battery holder. TP4056-based module is used to control charge/discharge of the battery.A step-up voltage stabilizer provides the needed voltage (5V) to power the transmitter. Between the TP4056-module and stabilizer I put a toggle switch to power off the transmitter.

The basic circuit of power supply system is shown on Fig. 6.

Having made a stable power source I could put and connect all components mentioned above inside simulator’s casing (Fig. 7–16).

Cable straps and His Majesty hot glue have been widely used in the assembly.

White wire that goes via 10K resistor to ARDUINO Pro Mini board for battery charge level control.

To provide antenna being plugged to or unplugged from the module I had to make the hole for USB cable bigger.

TP4056-module is placed in such a way that its LED can be seen through one of holes providing visual control of charge process (red – process goes on, green – battery is charged).

Circuit of the transmitter is shown on Fig. 17.

Free battery compartment of the simulator was used to store USB-cable and antenna when transporting (Fig. 18).

Control side is shown on Fig. 19.

Software

To make software development easier I split the code into several files (Fig. 20):

1. VMETER.h, VMETER.cpp – contains description of VMETER class for battery charge level control. Since there is a stable power source we can estimate charge level indirectly by measuring the battery voltage. Some constants and methods are given below:

SM_FACTOR - as voltage measurements are noisy I used exponential filter. This constant defines the filter ratio from 0 to 1;

V_RF - reference voltage for measurements which matches input voltage in this case. It can be measured by a multimeter directly between VCC and GND pins on ARDUINO board when the transmitter is turned on;

V_MIN - minimal battery voltage;

V_MAX - maximal battery voltage;

VMETER(byte pin); - constructor. pin – one of analog pins which is used to get measurements;

void init(void); - should be called in setup() for the first measurement;

boolean check(void); - called in loop() for each next measurement. Returns true if voltage gets lower compared to previous measurement;

byte getPercent(void); - returns «charge percent» from 0 to 100.

2. PIEZO.h, PIEZO.cpp – contains description of PIEZO class to control piezobuzzer. Some methods are listed below:

PIEZO(byte pin); - constructor. pin – digital pin (from 2 to 7 only) to which piezobuzzer is connected;

void enable(boolean enabled); - turns on (parameter is true) or off (parameter is false) piezobuzzer;

boolean isEnabled(void); - returns true if piezobuzzer is turned on;

void shortBeeps(byte n); - plays n short beeps;

void longBeep(void); - plays one long beep.

3. ENC.h, ENC.cpp – contains description of ENC class to handleencoder with switch. Constants and methods are given below:

DEBOUNCE_DELAY - debounce timeout in milliseconds;

LONG_PRESS - long press timeout (although this function is not used in the transmitter now but I left it for further improvements);

ENC(byte pinA, byte pinB, byte pinS); - constructor. pinA, pinB – pins to which signal contacts of encoder are connected; pinS – pin to which encoder’s switch is connected;

encState getState(void); - called in loop() to get the encoder’s state (encState type is defined in ENC.h) which possible values are:

  • esNone – encoder has not been rotated or pressed;
  • esRotated – encoder has been rotated;
  • esPressed – short press;
  • esLongPressed – long press;

int getPos(void); - returns the encoder’s position. In our case:

  • 1 – rotation in one direction;
  • –1 – rotation in other direction.

4. TR.ino – main program. In additionto VMETER.h, PIEZO.h, ENC.h standard libraries were also used:

RF24 – to work with radio module. There are no any outstanding features here so I will not consider it in detail.

EEPROM – to provide saving user’s settings into microcontroller’s memory.There is a feature here: just after the transmitter has been turned on the program checks a certain byte in memory (let’s call it the key). If its value differs from the one defined by user it means that settings have not been saved ever before and now it is time to write their default values as well as proper key. Providing healthy EEPROM (which has limited number of rewrite cycles) at next turning on the transmitter will find the key and restore user settings.

What has to be stores as “user settings”? According to requirement list above these are end points and reversals for each channel. In what way should they be stored? Here I have some math.

Let’s assume we have to control a servo on some channel and define end points of stick and servo as shown on Fig. 21.

Then the dependency between stick xin and servo xout position is

xout = aout + (xin – ain)× (bout – aout) / (bin– ain).

If we take minimal and maximal digital levels of voltage being measured on a potentiometer then ain = 0, bin = 1023, and we can simplify the formula

xout = aout + xin × (bout – aout) / 1023.

Let’s define a coefficient to adjust end points for the channel with servo

k = (mout – x’out) / (mout– xout),

where mout = 0.5 × (aout + bout) – servo’s middle position; x’out – desired servo position taking into account end points defined by user. Expressing that position from the formula above we get

x’out= 0.5 × (aout+ bout) × (1 – k) + k × xout.

Changing the sign of k we can reverse the control.

Thus microcontroller’s EEPROM will hold four coefficients according to the number of channels.

Encoder is used to adjust those coefficients. Pressing the switch selects the channel, rotating the shaft adjusts and points and reversal. When the next channel is selected the coefficient k for the previous one is being saved to EEPROM of microcontroller.

Some constants, variables and functions from TR.ino are given below:

K_MIN - Minimal value for coefficient k;

K_MAX - Maximal value for coefficient k;

K_STEP - Step for coefficient k;

EEPROM_KEY - Key for EEPROM;

EEPROM_KEY_ADDR - Address in EEPROM to store the key;

BATTERY_CRITICAL - «Charge percent». If voltage becomes less than this constant the transmitter alerts via piezobuzzer;

CHECK_INTERVAL - Timeout in milliseconds between voltage measurements.

typedef struct {
unsigned int Min;
unsigned int Max;
float k;
} ch;
ch chs[CH_COUNT] = {
{700, 2100, K_MAX},
{700, 2100, K_MAX},
{700, 2100, K_MAX},
{700, 2100, K_MAX}
};

This is an array of structures for each channel. Here you should define:

  • default end points aout and bout (see Fig. 20), for servos it is suitable to use values in milliseconds;
  • default value of coefficient k;

unsigned int convert(unsigned int v, int c) - implements formulas proposed above. v – digital level of voltage from potentiometer; с – index of channel from 0 to 3. Returns just the x’out.

void writeSettings(int c) - writes settings to EEPROM for channel с;

void readSettings(void) - reads settings from EEPROM for all channels.

Flashing process of ARDUINO Pro Mini board via USB-TTL adapter is shown on Fig. 22.

Operation

The power is switched on/off by the toggle. After one long beep the transmitter becomes active and the transmitter is ready to use.

To adjust the transmitter you should press the encoder’s switch. You will hear one short beep which means that you are able now to adjust the first channel(when adjusting the number of beeps corresponds to a channel index). Rotate the encoder’s shaft to adjust the end points and reversal. When it is done press switch again to move to the next channel. After all channels have been adjusted you will hear one long beep and the transmitter will return to its normal mode.

When the transmitter reaches the critical charge level it sends warning long beeps with timeout specified.

You can charge the transmitter via 2.1–5.5 socket with 5V power adapter (1A or higher recommended).

Example of adjusting end points and reversal for a servo

Operation check

Below you can find a sketch for a simple receiver to check if the transmitter works properly. I used ARDUINO Nano board and another nRF24L01 module with adapter (Fig. 23).

Circuit of the receiver is shown on Fig. 24.

Having turned on the transmitter and the receiver you can watch values from each channel coming via serial port (Fig. 24).

Code

Main programC/C++
Main ARDUINO code from TR.ino
// Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#include <RF24.h>
#include <EEPROM.h>
#include "PIEZO.h"
#include "VMETER.h"
#include "ENC.h"

RF24 radio(8, 9);
VMETER battery(A5);
PIEZO buzz(2);
ENC enc(4, 5, 3);

const float K_MIN = -1.0;
const float K_MAX = 1.0;
const float K_STEP = 0.1;

const byte EEPROM_KEY = 54;
const int EEPROM_KEY_ADDR = 0;

const byte BATTERY_CRITICAL = 20;
const unsigned long CHECK_INTERVAL = 4000;

const int CH_COUNT = 4;
const byte pin1 = A0;
const byte pin2 = A1;
const byte pin3 = A2;
const byte pin4 = A3;

typedef struct {
  unsigned int Min;
  unsigned int Max;
  float k;
} ch;

ch chs[CH_COUNT] = {
  {700, 2100, K_MAX},
  {700, 2100, K_MAX},
  {700, 2100, K_MAX},
  {700, 2100, K_MAX}
};

typedef struct {
  unsigned int val1 = 0;
  unsigned int val2 = 0;
  unsigned int val3 = 0;
  unsigned int val4 = 0;
} package;

package data;

unsigned long prevTime;
boolean isCritical = false;

int trMode = CH_COUNT;
encState eState;

unsigned int convert(unsigned int v, int c) {
  float val = chs[c].Min+v*float(chs[c].Max-chs[c].Min)/1023.0;
  val = val*chs[c].k+0.5*float(chs[c].Min+chs[c].Max)*(1.0-chs[c].k);
  return round(val);
}

void writeSettings(int c) {
  EEPROM.put(EEPROM_KEY_ADDR+c*sizeof(float)+1, chs[c].k);
}

void readSettings(void) {
  if (EEPROM.read(EEPROM_KEY_ADDR) != EEPROM_KEY) {
    EEPROM.write(EEPROM_KEY_ADDR, EEPROM_KEY);
    for (int i=0; i<CH_COUNT; i++) {
      writeSettings(i);
    }
  }
  else {
    for (int i=0; i<CH_COUNT; i++) {
      EEPROM.get(EEPROM_KEY_ADDR+i*sizeof(float)+1, chs[i].k);
    }
  }  
}

void setup() {
  battery.init();
  
  radio.begin();       
  radio.setChannel(100); 
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MAX);
  radio.openWritingPipe(0xF0F1F2F3F4LL);
  radio.powerUp();      
  radio.stopListening();

  readSettings();
  
  prevTime = millis();

  buzz.enable(true);
  buzz.longBeep();
}

void loop() {
  eState = enc.getState();
  
  if (eState != esNone) {
    if (eState == esPressed) {
      if (trMode < CH_COUNT) {
        writeSettings(trMode);
      }
      
      trMode = (++trMode) % (CH_COUNT+1);
      if (trMode < CH_COUNT) {
        buzz.shortBeeps(trMode+1);
      }
      else {
        buzz.longBeep();
      }
    }
    else {
      if (trMode < CH_COUNT) {
        if (enc.getPos() < 0) {
          if (chs[trMode].k > K_MIN) chs[trMode].k -= K_STEP;
        }
        else {
          if (chs[trMode].k < K_MAX) chs[trMode].k += K_STEP;
        }
      } 
    }
  }
  
  data.val1 = convert(analogRead(pin1), 0);
  data.val2 = convert(analogRead(pin2), 1);
  data.val3 = convert(analogRead(pin3), 2);
  data.val4 = convert(analogRead(pin4), 3);
  
  radio.write(&data, sizeof(data));

  if (millis()-prevTime > CHECK_INTERVAL) { 
    if (isCritical) {
      buzz.longBeep();
    }
    else {
      if ( battery.check() ) { 
        isCritical = (battery.getPercent() < BATTERY_CRITICAL);
      }  
    }

    prevTime = millis();  
  }
}
Code for VMETER class C/C++
Header file
// Arduino code for battery voltage meter
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#ifndef VMETER_H
#define VMETER_H

#include <Arduino.h>

const float SM_FACTOR = 0.1;
const float V_RF = 5.1;
const float V_MIN = 3.0;
const float V_MAX = 4.2;
const float V_DELTA = V_MAX-V_MIN;
const byte P_MIN = 0;
const byte P_MAX = 100;

class VMETER {
  private:
    byte _pin;
    unsigned int _v;
    unsigned int _vMin;
  public:
    VMETER(byte pin);
    void init(void);
    boolean check(void);
    byte getPercent(void);
};

#endif
Code for VMETER classC/C++
CPP-file
// Arduino code for battery voltage meter
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#include "VMETER.h"
#include <Arduino.h>

VMETER::VMETER(byte pin) {
  _pin = pin; 
}

boolean VMETER::check(void) {
  boolean res = false;
  
  _v = SM_FACTOR*analogRead(_pin)+(1-SM_FACTOR)*_v;

  if (_v < _vMin) {
    _vMin = _v;
    res = true;
  }

  return res;
}

void VMETER::init(void) {
  _v = analogRead(_pin);
  _vMin = _v;
}

byte VMETER::getPercent(void) {
  float res = 100*(float(_vMin)/1024*V_RF-V_MIN)/V_DELTA;
  if (res < P_MIN) res = P_MIN;
  if (res > P_MAX) res = P_MAX;

  return byte(res);
}
Code for receiverC/C++
ARDUINO code for receiver
// Arduino code to test RC transmitter via Serial port
// Tested: Arduino Nano (ATMega328P)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.


#include <RF24.h>

RF24 radio(8, 9);

//Data structure
typedef struct {
  unsigned int val1 = 0;
  unsigned int val2 = 0;
  unsigned int val3 = 0;
  unsigned int val4 = 0;
} package;

package data;

unsigned long prevTime, currTime;

void setup() {
  Serial.begin(9600);
  
  radio.begin();       
  radio.setChannel(100); 
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MAX);
  radio.openReadingPipe(1, 0xF0F1F2F3F4LL);
  radio.powerUp();
  radio.startListening();

  prevTime = millis();
}

void loop() {
  if (radio.available()) {
    radio.read(&data, sizeof(data));

    currTime = millis();
    if (currTime-prevTime >= 500) {
      Serial.print(data.val1);
      Serial.print('\t');
      Serial.print(data.val2);
      Serial.print('\t');
      Serial.print(data.val3);
      Serial.print('\t');
      Serial.println(data.val4);

      prevTime = currTime;
    }
  } 
}
Code for PIEZO classC/C++
Header file
// Arduino code for piezo buzzer
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#ifndef PIEZO_H
#define PIEZO_H

#include <Arduino.h>

const unsigned int PIEZO_P2 = 167;
const unsigned int PIEZO_N = 300;
const unsigned int PIEZO_DELAY = 20;

class PIEZO {
  private:
    byte _pin;
    boolean _enabled;

    void writePin(boolean isHigh);
    void beep(unsigned int n);
  public:
    PIEZO(byte pin);
    void enable(boolean enabled);
    boolean isEnabled(void);
    void shortBeeps(byte n);
    void longBeep(void);
};

#endif
Code for PIEZO classC/C++
CPP-file
// Arduino code for piezo buzzer
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#include "PIEZO.h"

PIEZO::PIEZO(byte pin) {
  _enabled = false;
  
  _pin = (1 << pin);
  DDRD |= _pin;
}

void PIEZO::writePin(boolean isHigh) {
  PORTD = (isHigh ? PORTD | _pin : PORTD & ~_pin);
}

void PIEZO::enable(boolean enabled) {
  _enabled = enabled;
}

boolean PIEZO::isEnabled(void) {
  return _enabled;
}

void PIEZO::beep(unsigned int n) {
  for (unsigned int j=0; j<n; j++) {
    writePin(HIGH);
    delayMicroseconds(PIEZO_P2);
    writePin(LOW);
    delayMicroseconds(PIEZO_P2);
  }
}

void PIEZO::shortBeeps(byte n) {
  if (_enabled) {
    for (byte i=1; i<=n; i++) {
      beep(PIEZO_N);

      if (i<n) delay(PIEZO_DELAY);
    }
  }
}

void PIEZO::longBeep(void) {
  if (_enabled) {
      beep(PIEZO_N << 1);
  }
}
Code for ENC classC/C++
Header file
// Arduino code for encoder
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#ifndef ENC_H
#define ENC_H

#include <Arduino.h>

const int DEBOUNCE_DELAY = 50;
const int LONG_PRESS = 500;

enum encState {esNone, esRotated, esPressed, esLongPressed};

class ENC {
  private:
    byte _pinA, _pinB, _pinS; 
    
    unsigned long _prevTime;
    boolean _prevA, _prevS, _prevSL;
    int _pos;
  public:
    ENC(byte pinA, byte pinB, byte pinS);
    
    encState getState(void);
    int getPos(void);
};

#endif
Code for ENC classC/C++
CPP-file
// Arduino code for encoder
// Part of Arduino code for RC transmitter
// Tested: Arduino Pro Mini (ATMega328P, 5V)
// Copyright: Anton Tsaritsynskyy, January 2020
// E-mail: tsaritsynskyy.a.a@gmail.com
//
// This software is provided "as is" without any warranties.
// Author is not responsible for any undesired effects caused by using this software.
// Commercial distribution of this software is not permitted.
// Third-party libraries and components are properties of their respective developers.

#include "ENC.h"

ENC::ENC(byte pinA, byte pinB, byte pinS) {
  _pinA = pinA;
  _pinB = pinB;
  _pinS = pinS;

  pinMode(_pinA, INPUT_PULLUP);
  pinMode(_pinB, INPUT_PULLUP);
  pinMode(_pinS, INPUT_PULLUP);

  _prevA = LOW;
  _prevS = HIGH;
  _prevSL = HIGH;
  _pos = 0;
  _prevTime = millis();
}

encState ENC::getState(void) {
  encState res = esNone;

  unsigned long currTime = millis();
    
  if (!digitalRead(_pinS)) {
    if (_prevS && ((currTime - _prevTime) > DEBOUNCE_DELAY)) {
       _prevTime = currTime;
       _prevS = LOW;
       _prevSL = HIGH;
    }

    if (_prevSL && ((currTime - _prevTime) > LONG_PRESS)) {
      _prevSL = LOW;
      _pos = 0;
      res = esLongPressed;
    }
  }
  else {
    if (!_prevS) {
      if ((currTime - _prevTime) > DEBOUNCE_DELAY) {
        _prevTime = currTime;
        _prevS = HIGH;
        if (_prevSL) res = esPressed; 
      }
    }
    else {
      byte A = digitalRead(_pinA);
      byte B = digitalRead(_pinB);
    
      if (!A && _prevA) { 
        if (B) _pos = 1;
        else _pos = -1;

        res = esRotated;
      }

      _prevA = A;
    } 
  }
  
  return res;
}

int ENC::getPos(void) {
  return _pos;
}

Schematics

Circuit of the transmitter
Circuit tr 1wsrurni4k

Comments

Similar projects you might like

Amazing 6WD Off-Road Robot | Arduino RC Robot

Project tutorial by Jithin Sanal

  • 10,000 views
  • 0 comments
  • 54 respects

Turn your RC Car to Bluetooth RC car

Project tutorial by Prajjwal Nag

  • 21,021 views
  • 2 comments
  • 25 respects

RC Car Hack With Android And Arduino

Project showcase by danionescu

  • 14,442 views
  • 2 comments
  • 16 respects

Renewing the Nikko Turbo 2 RC Car

Project tutorial by MarcoGPS

  • 3,493 views
  • 2 comments
  • 10 respects

LED RF Entryphone Coconut Build

Project in progress by javier muñoz sáez

  • 959 views
  • 0 comments
  • 2 respects

Arduino 4WD RC Car

Project tutorial by Andriy Baranov

  • 34,271 views
  • 9 comments
  • 74 respects
Add projectSign up / Login