Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / IoT / Arduino

Radio Control Transmitter - Joystick

4.93/5 (31 votes)
11 Oct 2010CPOL4 min read 114.4K   927  
Home built joystick type radio control transmitter based on Arduino

Introduction

Traditionally, RC plane/heli/quadcopter pilots use a dual thumb stick type transmitter, however and especially quadcopter pilots, it's not a very intuitive way to fly especially for the beginner.
We've all seen the movies and at least have a vague idea that a pilot of a real aircraft/spaceship uses a joystick. So why not with RC aircraft.

The basic idea of my project was to implement a joystick solution in the easiest way possible, but one that fulfills my exact requirements.

Background

Arduino is an open-source electronics prototyping platform based on flexible, easy-to-use hardware and software.

For this project, I chose the Arduino NANO for its small size and I/O requirements. It interfaces with the joystick potentiometers (0-5vdc), switches and Serial LCD. A serial data stream (PPM) is then output and interfaced to an off-the-shelf 430Mhz transmitter module/pcb.

Photos

External - This is a hybrid of two different PC gaming joysticks, the base from one and the hand grip from another. The hand grip is a 3D type, i.e. provides rudder control by twisting the grip as well as the usual aeleron/elevator controls (forward/back/left/right).

RC_Joystick_Tx/RCJoystickTx.jpg

Internal - Home projects aren't supposed to be neat & tidy (leave that to the day job!).
Here you'll see the Arduino NANO pcb, 430Mhz Tx pcb, Lipo battery in the base, and the LCD module & switches in the lid.

RC_Joystick_Tx/ianjtx6b.jpg

PPM Data stream - The whole project revolves around providing a PPM stream compatible with the input on the 430Mhz Tx board. It's an industry standard (more or less!) amongst traditional RC Transmitter manufacturers who use this interface to hook up 2 transmitters together in a Master/Buddy scenario for new pilots. Ref. Futaba.
The manufacturer of the 430MHz transmitter pcb designed his board to fit in with this scenario, so it was relatively well understood what I needed to send to the transmitter in order for the servo's etc on the aircraft to react accordingly.

PPM is a type of PWM. In the photo below, there are 6 channels which make up the PPM stream and which is repeated every 22ms. So, starting with the long sync pulse, you'll then get a fixed LO pulse of 300uS followed by the channel pulse HI (range 700 to 1700 uS), then the next fixed LO pulse again and so on.
P.S. Ignore the red trace, it was only used to sync my scope.

RC_Joystick_Tx/PPMscope.jpg

Video

Using the Code

There are two parts to the code, the main PDE and one include (LCD routines):

  • RCJoystick_IanJ_V2.pde
  • LCD0821.pde

Most of the VAR setup is done at the top of the main code, however, there is additional setup required for joystick rates selection near the bottom.

Main routine = RCJoystick_IanJ_V2.pde.

C++
// RC Joystick 430mHz
// For use with Arduino Nano V3.0
// Ian Johnston 28/04/2010
//
// Ver 1.6 - Now using Timer1 to set 22mS refresh for PPM output.
//           Dual rates are implemented but need custom setup.
//           Trimming is not working.
//           Battery monitor sub tweaked
//           Adjusted timing off various subs
//           Various tweaks to LCD & battery monitor
//
// LCD type = LCD0821 Matrix Orbital (serial TTL)

// Make sure LCD0821.pde is in the same dir as RCJoystick_IanJ_V2.pde

ISR which calls the ppmoutput routine at the required 22mS intervals, also updates the ticks used for timing elsewhere.

C++
    ISR(TIMER1_COMPA_vect) {
    ppmoutput(); // Jump to ppmoutput subroutine
    tick = tick + 1;  // update timing tick for subs
    tick2 = tick2 + 1;  // update tick
}

Timer setup. This is the heart of the whole thing, it defines a 22ms timer which creates the recurring PPM stream. Some RC Transmitters are very sensitive to the 22ms timing so using an accurate timer as opposed to on the fly timing is crucial.

C++
 // Setup timer
  TCCR1A = B00110001; // Compare register B used in mode '3'
  TCCR1B = B00010010; // WGM13 and CS11 set to 1
  TCCR1C = B00000000; // All set to 0
  TIMSK1 = B00000010; // Interrupt on compare B
  TIFR1  = B00000010; // Interrupt on compare B
  OCR1A = 22000; // 22mS PPM output refresh
  OCR1B = 1000;
}

Part of the main loop, this is where the ticks are used to generate the LCD refresh and timing for the calling of the panel switches and battery monitor subroutines.

C++
  if (tick >= 11) {    // only run certain subs every 1/4 sec or so (22mS * 11 = 242mS)
      tick = 0;
      switchesRates();    // Run sub - read panel switches
      batterymonitor();   // Run sub - check battery
  }
  
  // generate slow changing flag, about 2sec on/off
  if (tick2 <= 50) {
      slowflag = 0;
  }
  if (tick2 >= 50 && tick2 <=100) {
      slowflag = 1;
  }
  if (tick2 >= 100) {
      slowflag = 0;
      tick2 = 0;
  }   
}

After reading in the raw values from the joystock potentiometers, they are now scaled using Y=MX+C to values suitable for direct output on the PPM stream.
Using MAP, it's easy to reverse the direction in cases where the physical potentiometer is reversed. There is no elaborate/easy calibration routine, it all has to be hard coded in, compiled and uploaded to the Arduino every time a change is made.

C++
// Compensate for discrepancy in pot inputs including centering offset.
// Also use this to invert inputs if necessary (swap x1 & y1)
// y=mx+c, x to y scales to x1 to y1
  AI_TIpot = map(AI_Raw_TIpot, 0, 1023, 1023, 0) + 0; // TI pot
  AI_Throt = map(AI_Raw_Throt, 0, 1023, -25, 1150) - 0; // Throttle
  AI_Rudde = map(AI_Raw_Rudde, 0, 1023, 0, 1023) + 0; // Rudder
  AI_Eleva = map(AI_Raw_Eleva, 0, 1023, 0, 1023) + 0; // Elevator
  AI_Aeler = map(AI_Raw_Aeler, 0, 1023, 0, 1023) + 0; // Aeleron
  AI_Batte = AI_Raw_Batte / 36.2; // Battery

Add in offsets and additionally scaling. This is the "rates" select which gives the joystick mild to coarse functionality.

C++
 // Map analogue inputs to PPM rates for each of the channels
   Aeleron_uS = (AI_Aeler * DualrateMultAel) + pulseMin + DualrateAdjAel;
   Elevator_uS = (AI_Eleva * DualrateMultEle) + pulseMin + DualrateAdjEle;
   Throttle_uS = (AI_Throt * DualrateMultThr) + pulseMin + DualrateAdjThr;
   Rudder_uS = (AI_Rudde * DualrateMultRud) + pulseMin + DualrateAdjRud;
   TI_uS = (AI_TIpot * DualrateMultTI) + pulseMin + DualrateAdjTI;
}

This is part of the PPM output sub. Simply build up the output stream sequentially. Of course, there are much more elaborate ways of coding such a data stream, such as using arrays, however as per my brief, the idea was to get this up & running in a short period of time.

C++
void ppmoutput() { // PPM output sub

 // Channel 1 - Aeleron
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(Aeleron_uS);  // Hold for Aeleron_uS microseconds      

 // Channel 2 - Elevator 
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(Elevator_uS); // Hold for Elevator_uS microseconds      

 // Channel 3 - Throttle
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(Throttle_uS); // Hold for Throttle_uS microseconds      

 // Channel 4 - Rudder
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(Rudder_uS);   // Hold for Rudder_uS microseconds

 // Channel 5 - TI Switch
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(TIsw_uS);     // Hold for TIsw_uS microseconds        

 // Channel 6 - TI pot
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);
  delayMicroseconds(TI_uS);       // Hold for TI_uS microseconds 

The final part of building the PPM stream is to generate the synchro pulse which makes up the time from the last of the PPM channels (which will vary in timing) and to the start of the 1st one again next time round.

So, start the pulse but don't finish it yet because this is where the housekeeping can now be done such as update the LCD, read the panel switches, etc.

C++
 // Synchro pulse
  digitalWrite(outPinPPM, LOW);
  delayMicroseconds(Fixed_uS);    // Hold
  digitalWrite(outPinPPM, HIGH);  // Start Synchro pulse
}

Points of Interest

Flying with the joystick is so much easier for us RC newbies, and to add an extra dimension, just try it in the dark at night with some high power LEDs fitted. Great fun!

History

  • V1.6 Current

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)