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
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

