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
{codecitation}
// PPM Encoder Joystick to Futaba Trainer Port
// For use with Arduino Nano V3.0
// Ian Johnston 29/04/2010
// Version 1.1
int AI_Pin_AEL = 6; // Ana In - Aeleron potentiometer (Ana In Ch.0 playing up?)
int AI_Pin_ELE = 1; // Ana In - Elevator potentiometer
int AI_Pin_THR = 2; // Ana In - Throttle potentiometer
int AI_Pin_RUD = 3; // Ana In - Rudder potentiometer
int AI_Pin_TIpot = 4; // Ana In - TI potentiometer
int AI_Raw_AEL; // Ana In raw var - 0->1023
int AI_Raw_ELE; // Ana In raw var - 0->1023
int AI_Raw_THR; // Ana In raw var - 0->1023
int AI_Raw_RUD; // Ana In raw var - 0->1023
int AI_Raw_TIpot; // Ana In raw var - 0->1023
int AI_AEL; // Ana In var - 0->1023 compensated
int AI_ELE; // Ana In var - 0->1023 compensated
int AI_THR; // Ana In var - 0->1023 compensated
int AI_RUD; // Ana In var - 0->1023 compensated
int AI_TIpot; // Ana In var - 0->1023 compensated
int Aeleron_uS = 750; // Ana In Ch.0 uS var - Aeleron
int Elevator_uS = 750; // Ana In Ch.1 uS var - Elevator
int Throttle_uS = 750; // Ana In Ch.2 uS var - Throttle
int Rudder_uS = 750; // Ana In Ch.3 uS var - Rudder
int TI_uS = 750; // Ana In Ch.4 uS var - TI
int TIsw_uS = 750; // Ana In Ch.4 uS var - TI Switch
int Fixed_uS = 300; // PPM frame fixed LOW phase
int pulseMin = 750; // pulse minimum width minus start in uS
int pulseMax = 1700; // pulse maximum width in uS
float DualrateMultAel = 0.9; // Dual rate mult
float DualrateMultEle = 0.9; // Dual rate mult
float DualrateMultThr = 0.9; // Dual rate mult
float DualrateMultRud = 0.9; // Dual rate mult
float DualrateMultTI = 0.9; // Dual rate mult
int DualrateAdjAel = 0; // Dual rate mid adjustment
int DualrateAdjEle = 0; // Dual rate mid adjustment
int DualrateAdjThr = 0; // Dual rate mid adjustment
int DualrateAdjRud = 0; // Dual rate mid adjustment
int DualrateAdjTI = 0; // Dual rate mid adjustment
int outPinPPM = 10; // digital pin 10
int outPinTEST = 8; // digital pin 8
int inPinD6 = 6; // digital pin 6
int inPinD11 = 11; // digital pin 11
ISR(TIMER1_COMPA_vect) {
ppmoutput(); // Jump to ppmoutput subroutine
}
void setup() {
// Serial.begin(9600) ; // Test
pinMode(outPinPPM, OUTPUT); // sets the digital pin as output
pinMode(inPinD6, INPUT); // sets the digital pin as input
digitalWrite(inPinD6, HIGH); // turn on pull-up resistor
pinMode(inPinD11, INPUT); // sets the digital pin as input
digitalWrite(inPinD11, HIGH); // turn on pull-up resistor
// 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;
}
void ppmoutput() { // PPM output sub
// test pulse - used to trigger scope
// digitalWrite(outPinTEST, LOW);
// delayMicroseconds(100); // Hold
// digitalWrite(outPinTEST, HIGH);
// 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
// Synchro pulse
digitalWrite(outPinPPM, LOW);
delayMicroseconds(Fixed_uS); // Hold
digitalWrite(outPinPPM, HIGH); // Start Synchro pulse
}
void loop() { // Main loop
// Read analogue ports
AI_Raw_AEL = analogRead(AI_Pin_AEL);
AI_Raw_ELE = analogRead(AI_Pin_ELE);
AI_Raw_THR = analogRead(AI_Pin_THR);
AI_Raw_RUD = analogRead(AI_Pin_RUD);
AI_Raw_TIpot = analogRead(AI_Pin_TIpot);
// 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_AEL = map(AI_Raw_AEL, 0, 1023, 1200, 0) - 100; // Invert Aeleron pot and slight centre offset
AI_ELE = map(AI_Raw_ELE, 0, 1023, 1200, 0) - 120; // Invert Elevator pot and slight centre offset
AI_THR = map(AI_Raw_THR, 0, 1023, 0, 1023) + 0; // Throttle
AI_RUD = map(AI_Raw_RUD, 0, 1023, 0, 1023) + 0; // Rudder
AI_TIpot = map(AI_Raw_TIpot, 0, 1023, 1023, 0) + 0; // Thermal Intelligence pot (TI)
// Map analogue inputs to PPM rates for each of the channels
Aeleron_uS = (AI_AEL * DualrateMultAel) + pulseMin + DualrateAdjAel;
Elevator_uS = (AI_ELE * DualrateMultEle) + pulseMin + DualrateAdjEle;
Throttle_uS = (AI_THR * DualrateMultThr) + pulseMin + DualrateAdjThr;
Rudder_uS = (AI_RUD * DualrateMultRud) + pulseMin + DualrateAdjRud;
TI_uS = (AI_TIpot * DualrateMultTI) + pulseMin + DualrateAdjTI;
// Check limits
if (Aeleron_uS <= 750) Aeleron_uS = 750; // Min
if (Aeleron_uS >= 1700) Aeleron_uS = 1700; // Max
if (Elevator_uS <= 750) Elevator_uS = 750; // Min
if (Elevator_uS >= 1700) Elevator_uS = 1700; // Max
if (Throttle_uS <= 750) Throttle_uS = 750; // Min
if (Throttle_uS >= 1700) Throttle_uS = 1700; // Max
if (Rudder_uS <= 750) Rudder_uS = 750; // Min
if (Rudder_uS >= 1700) Rudder_uS = 1700; // Max
if (TI_uS <= 750) TI_uS = 750; // Min
if (TI_uS >= 1700) TI_uS = 1700; // Max
if (digitalRead(inPinD6) == 0) { // Low rate
DualrateMultAel = 0.5;
DualrateMultEle = 0.5;
DualrateMultThr = 0.9;
DualrateMultRud = 0.7;
DualrateMultTI = 0.9;
DualrateAdjAel = 200;
DualrateAdjEle = 200;
DualrateAdjThr = 0;
DualrateAdjRud = 100;
DualrateAdjTI = 0;
}
if (digitalRead(inPinD6) == 1) { // Normal/high rate
DualrateMultAel = 0.9;
DualrateMultEle = 0.9;
DualrateMultThr = 0.9;
DualrateMultRud = 0.9;
DualrateMultTI = 0.9;
DualrateAdjAel = 0;
DualrateAdjEle = 0;
DualrateAdjThr = 0;
DualrateAdjRud = 0;
DualrateAdjTI = 0;
}
if (digitalRead(inPinD11) == 1) { // TI Switch
TIsw_uS = 1700;
} else {
TIsw_uS = 750;
}
}
{/codecitation}