Project 018 - RC PPM Trainer Port Joystick V1

DISCLAIMER: This design is experimental, so if you decide to build one yourself then you are on your own, I can't be held responsible for any problems/issues/damage/injury that may occur if you decide to follow this build and make one yourself.

INTRO

My FPV interest is in QuadCopters mainly, and I really feel that flying with a PC style joystick would make it easier & more enjoyable than using the normal joystick controls on the Radio Tx.

Some folks have managed a similar feat of tying a PC joystick to a Radio Tx but with a laptop connecting the two together. With my project however I wanted to tie the joystick directly to the Radio Tx, and what better to use than an Arduino Nano V3.0 mounted inside the Joystick.

Arduino Nano V3.0:-



So I ripped out the electronics from a Saitek AVR8 PC Joystick (flightsim) and replaced with the Nano V3.0 and my own code. There's a single cable direct from the joystick to the trainer port (PPM) of my Futaba Tx. The Tx also provides power for the joystick.

2 days later, i.e. 1 day for the hardware, and another for the code and it's job done. I have 3-axis control i.e. Aelerons, Elevators, Rudder & also Throttle via the T-bar at the front of the Joystick, and in addition I'm reading one of the front panel switches to allow for selecting of dual rates on the controls. Lastly, I'm also reading a pot and further switch for control over my QuadCopter's "TI" (Thermal Intelligence).

The Nano has lots of analogue & digital inputs spare, so there's scope in the future for interfacing to more of the joystick switches/buttons for the likes of camera pan/tilt channels etc.

Below you'll find a quick video of it in operation on the bench where you can see the servo test mode of the Futaba Tx (servo's 1-8), and there's also a back garden flight test with the Quadcopter. There's also some photos and my Arduino code.

There's a few threads over at RCGroups.com where folks are using my code, here's some:
Joystick to Transmitter: once more

Flying with the stick:
I have to say I like it a lot more than using the Tx for flying my Quadcopter. I feel a lot more "attached" to the Quad, i.e. more one with it. Mind you, I kept throwing the throttle the wrong way! I think my head was thinking pull = up.
I just held it against my chest, left hand holding the base and throttle at the same time, right hand on the stick.

PHOTOS

 

TECHNICAL

Here's a wee wiring diagram.

Here's the connections required to the Futaba Trainer Conn (square type).
Asterix denotes connections required including the switched supply from the Radio Tx.

Regarding the Arduino Nano code, basically the output has to drive the PPM input port of the host Radio Tx, commonly knows as the Trainer Port. Here is an example of a Futaba compatible PPM stream. The spec seems to vary for this depending on where you look, however, there does seem to be a bit of leeway in this respect.
My code has 6 channels, but the following shows 8off, again demonstrating the flexibility of the standard PPM stream.

There are a couple of challenges not least of which the entire stream must be repeated every 20mS. So, as each channel pulse width varies with the user joystick input then the time left for the synchronize pulse will also vary. On top of this the Arduino code also has to go off and do other things so a fairly reliable method to compute the required size of the Sync pulse is required.

I have implemented 6 channels, however, it's possible to implement more as it's just a case of trying to squeeze as much as possible into the available 20mS total. Future updates may include a wee LCD and also to allow user control over rates & trim etc.

Other than that, the code is fairly simple. There's one main loop with just a wee subroutune for reading the dual rate switch. I did think about adding structure but seeing as how it's fairly timing critical given the 20mS frame requirement the easiest way was to just use one big main loop.

The other thing to note is that I never used the built in Arduino timers....again simple is best and the "delayMicroseconds" ain't half an easy way to built up the PPM frame especially since there is no requirement for the code to do anything else at the same time.

Update 29/4/10 - Fixed jitter on PPM output by incorporating an ISR timer. Much more stable output now.

ARDUINO CODE 


Arduino C++
1// PPM Encoder Joystick to Futaba Trainer Port2// For use with Arduino Nano V3.03// Ian Johnston 29/04/20104// Version 1.15 6int AI_Pin_AEL = 6;    // Ana In - Aeleron potentiometer (Ana In Ch.0 playing up?)7int AI_Pin_ELE = 1;    // Ana In - Elevator potentiometer8int AI_Pin_THR = 2;    // Ana In - Throttle potentiometer9int AI_Pin_RUD = 3;    // Ana In - Rudder potentiometer10int AI_Pin_TIpot = 4;    // Ana In - TI potentiometer11int AI_Raw_AEL;        // Ana In raw var - 0->102312int AI_Raw_ELE;        // Ana In raw var - 0->102313int AI_Raw_THR;        // Ana In raw var - 0->102314int AI_Raw_RUD;        // Ana In raw var - 0->102315int AI_Raw_TIpot;        // Ana In raw var - 0->102316int AI_AEL;           // Ana In var - 0->1023 compensated17int AI_ELE;           // Ana In var - 0->1023 compensated18int AI_THR;           // Ana In var - 0->1023 compensated19int AI_RUD;           // Ana In var - 0->1023 compensated20int AI_TIpot;           // Ana In var - 0->1023 compensated21int Aeleron_uS = 750;     // Ana In Ch.0 uS var - Aeleron22int Elevator_uS = 750;    // Ana In Ch.1 uS var - Elevator23int Throttle_uS = 750;    // Ana In Ch.2 uS var - Throttle24int Rudder_uS = 750;      // Ana In Ch.3 uS var - Rudder25int TI_uS = 750;          // Ana In Ch.4 uS var - TI26int TIsw_uS = 750;        // Ana In Ch.4 uS var - TI Switch27 28int Fixed_uS = 300;       // PPM frame fixed LOW phase29int pulseMin = 750;          // pulse minimum width minus start in uS30int pulseMax = 1700;      // pulse maximum width in uS31 32float DualrateMultAel = 0.9; // Dual rate mult33float DualrateMultEle = 0.9; // Dual rate mult34float DualrateMultThr = 0.9; // Dual rate mult35float DualrateMultRud = 0.9; // Dual rate mult36float DualrateMultTI = 0.9; // Dual rate mult37int DualrateAdjAel = 0;   // Dual rate mid adjustment38int DualrateAdjEle = 0;      // Dual rate mid adjustment39int DualrateAdjThr = 0;      // Dual rate mid adjustment40int DualrateAdjRud = 0;      // Dual rate mid adjustment41int DualrateAdjTI = 0;      // Dual rate mid adjustment42 43int outPinPPM = 10;       // digital pin 1044int outPinTEST = 8;       // digital pin 845int inPinD6 = 6;          // digital pin 646int inPinD11 = 11;        // digital pin 1147 48ISR(TIMER1_COMPA_vect) {49    ppmoutput(); // Jump to ppmoutput subroutine50}51 52void setup() {53 54  // Serial.begin(9600) ; // Test55 56  pinMode(outPinPPM, OUTPUT);   // sets the digital pin as output57  pinMode(inPinD6, INPUT);      // sets the digital pin as input58  digitalWrite(inPinD6, HIGH);  // turn on pull-up resistor59  pinMode(inPinD11, INPUT);     // sets the digital pin as input60  digitalWrite(inPinD11, HIGH); // turn on pull-up resistor61 62   // Setup timer63  TCCR1A = B00110001; // Compare register B used in mode '3'64  TCCR1B = B00010010; // WGM13 and CS11 set to 165  TCCR1C = B00000000; // All set to 066  TIMSK1 = B00000010; // Interrupt on compare B67  TIFR1  = B00000010; // Interrupt on compare B68  OCR1A = 22000; // 22mS PPM output refresh69  OCR1B = 1000;70 71}72 73void ppmoutput() { // PPM output sub74 75 // test pulse - used to trigger scope76 // digitalWrite(outPinTEST, LOW);77 // delayMicroseconds(100);    // Hold78 // digitalWrite(outPinTEST, HIGH);79 80 // Channel 1 - Aeleron81  digitalWrite(outPinPPM, LOW);82  delayMicroseconds(Fixed_uS);    // Hold83  digitalWrite(outPinPPM, HIGH);84  delayMicroseconds(Aeleron_uS);  // Hold for Aeleron_uS microseconds     85 86 // Channel 2 - Elevator87  digitalWrite(outPinPPM, LOW);88  delayMicroseconds(Fixed_uS);    // Hold89  digitalWrite(outPinPPM, HIGH);90  delayMicroseconds(Elevator_uS); // Hold for Elevator_uS microseconds     91 92 // Channel 3 - Throttle93  digitalWrite(outPinPPM, LOW);94  delayMicroseconds(Fixed_uS);    // Hold95  digitalWrite(outPinPPM, HIGH);96  delayMicroseconds(Throttle_uS); // Hold for Throttle_uS microseconds     97 98 // Channel 4 - Rudder99  digitalWrite(outPinPPM, LOW);100  delayMicroseconds(Fixed_uS);    // Hold101  digitalWrite(outPinPPM, HIGH);102  delayMicroseconds(Rudder_uS);   // Hold for Rudder_uS microseconds103 104 // Channel 5 - TI Switch105  digitalWrite(outPinPPM, LOW);106  delayMicroseconds(Fixed_uS);    // Hold107  digitalWrite(outPinPPM, HIGH);108  delayMicroseconds(TIsw_uS);     // Hold for TIsw_uS microseconds       109 110 // Channel 6 - TI pot111  digitalWrite(outPinPPM, LOW);112  delayMicroseconds(Fixed_uS);    // Hold113  digitalWrite(outPinPPM, HIGH);114  delayMicroseconds(TI_uS);       // Hold for TI_uS microseconds 115 116 // Synchro pulse117  digitalWrite(outPinPPM, LOW);118  delayMicroseconds(Fixed_uS);    // Hold119  digitalWrite(outPinPPM, HIGH);  // Start Synchro pulse120 121}122 123void loop() { // Main loop124 125 // Read analogue ports126   AI_Raw_AEL = analogRead(AI_Pin_AEL);127   AI_Raw_ELE = analogRead(AI_Pin_ELE);128   AI_Raw_THR = analogRead(AI_Pin_THR);129   AI_Raw_RUD = analogRead(AI_Pin_RUD);130   AI_Raw_TIpot = analogRead(AI_Pin_TIpot);131 132 // Compensate for discrepancy in pot inputs including centering offset.133 // Also use this to invert inputs if necessary (swap x1 & y1)134 // y=mx+c, x to y scales to x1 to y1135   AI_AEL = map(AI_Raw_AEL, 0, 1023, 1200, 0) - 100; // Invert Aeleron pot and slight centre offset136   AI_ELE = map(AI_Raw_ELE, 0, 1023, 1200, 0) - 120; // Invert Elevator pot and slight centre offset137   AI_THR = map(AI_Raw_THR, 0, 1023, 0, 1023) + 0;  // Throttle138   AI_RUD = map(AI_Raw_RUD, 0, 1023, 0, 1023) + 0;  // Rudder139   AI_TIpot = map(AI_Raw_TIpot, 0, 1023, 1023, 0) + 0;  // Thermal Intelligence pot (TI)140  141 // Map analogue inputs to PPM rates for each of the channels142   Aeleron_uS = (AI_AEL * DualrateMultAel) + pulseMin + DualrateAdjAel;143   Elevator_uS = (AI_ELE * DualrateMultEle) + pulseMin + DualrateAdjEle;144   Throttle_uS = (AI_THR * DualrateMultThr) + pulseMin + DualrateAdjThr;145   Rudder_uS = (AI_RUD * DualrateMultRud) + pulseMin + DualrateAdjRud;146   TI_uS = (AI_TIpot * DualrateMultTI) + pulseMin + DualrateAdjTI;147 148 // Check limits149  if (Aeleron_uS <= 750) Aeleron_uS = 750;     // Min150  if (Aeleron_uS >= 1700) Aeleron_uS = 1700;   // Max  151  if (Elevator_uS <= 750) Elevator_uS = 750;   // Min152  if (Elevator_uS >= 1700) Elevator_uS = 1700; // Max153  if (Throttle_uS <= 750) Throttle_uS = 750;   // Min154  if (Throttle_uS >= 1700) Throttle_uS = 1700; // Max155  if (Rudder_uS <= 750) Rudder_uS = 750;       // Min156  if (Rudder_uS >= 1700) Rudder_uS = 1700;     // Max  157  if (TI_uS <= 750) TI_uS = 750;               // Min158  if (TI_uS >= 1700) TI_uS = 1700;             // Max159  160  if (digitalRead(inPinD6) == 0) { // Low rate161         DualrateMultAel = 0.5;162        DualrateMultEle = 0.5;163        DualrateMultThr = 0.9;164        DualrateMultRud = 0.7;165        DualrateMultTI = 0.9;166        DualrateAdjAel = 200;167        DualrateAdjEle = 200;168        DualrateAdjThr = 0;169        DualrateAdjRud = 100;170        DualrateAdjTI = 0;171  }172 173  if (digitalRead(inPinD6) == 1) { // Normal/high rate174        DualrateMultAel = 0.9;175        DualrateMultEle = 0.9;176        DualrateMultThr = 0.9;177        DualrateMultRud = 0.9;178        DualrateMultTI = 0.9;179        DualrateAdjAel = 0;180        DualrateAdjEle = 0;181        DualrateAdjThr = 0;182        DualrateAdjRud = 0;183        DualrateAdjTI = 0;184  }185 186  if (digitalRead(inPinD11) == 1) { // TI Switch   187        TIsw_uS = 1700;188  } else {189        TIsw_uS = 750;190  }191 192}Codequote by Ian Johnston