Flight stick and throttle on a Pro Micro
A two-axis stick with a center deadzone and an expo curve, a twist rudder, a hall sensor throttle, a hat switch and two buttons, sent as one Joystick.h report. The whole build, a SparkFun Pro Micro and 4 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Flight stick and throttle on a Pro Micro: two stick axes, a twist rudder,
// a hall sensor throttle, an eight-way hat and three buttons, sent as one USB
// joystick report through the Arduino Joystick library's API (Joystick.h).
//
// Wiring.
//
// stick X A1, Y A0, press 15 (a thumbstick module: two 10k pots)
// twist rudder A2 (a 10k pot)
// throttle A3 (an SS49E linear hall sensor, a magnet on the lever)
// hat up 7, down 6, left 5, right 4 trigger 3 pinky 2
//
// Every switch has one leg on GND and its pin pulled up inside the chip, so
// a pressed switch reads LOW.
//
// In Mokxi: drag the thumbstick for the stick, turn the knob for the twist,
// slide the magnet towards the hall sensor to open the throttle, and use the
// pad (arrow keys, Z and X) for the hat, the trigger and the pinky button.
// The thumbstick module stands in for a gimbal: a full-size stick reads its
// two axes the same way, from two pots or two hall sensors.
//
// Shaping the stick. Two numbers per axis do most of the work:
// DEADZONE readings this close to the center count as centered, so a stick
// that does not return to exactly 512 does not drift the plane.
// EXPO how much the response curves. 0 is a straight line; 100 is a
// pure cube, gentle near the center for fine corrections and
// still full travel at the edge.
//
// The throttle. A hall sensor's output does not run rail to rail and is not a
// straight line with distance, so the sketch maps two calibrated readings,
// idle and full, onto 0 to 1023. Turn on SHOW_RAW, move the lever to each end
// and type the two numbers in.
//
// Not simulated: the USB connection to a PC. Mokxi models this chip's USB as
// a serial pipe, so each report prints as a [Joystick] line on the Serial
// Monitor. Flash the same sketch to a real Pro Micro and a flight simulator
// sees a game controller with these axes and buttons.
#include <Joystick.h>
// ---------------------------------------------------------------------------
// Make it yours
// ---------------------------------------------------------------------------
const uint8_t STICK_X_PIN = A1;
const uint8_t STICK_Y_PIN = A0;
const uint8_t TWIST_PIN = A2;
const uint8_t THROTTLE_PIN = A3;
const uint8_t HAT_PINS[4] = {7, 6, 5, 4}; // up, down, left, right
const uint8_t BUTTON_PINS[3] = {3, 2, 15}; // trigger, pinky, stick press
// Flip an axis that moves the wrong way in the sim.
const bool INVERT_X = false;
const bool INVERT_Y = true; // pull back to climb: back is up on most sticks
const bool INVERT_TWIST = false;
const int DEADZONE = 24; // counts either side of center, out of 512
const int EXPO = 40; // percent: 0 straight, 100 a pure cube
const int TWIST_DEADZONE = 40;
// Throttle calibration: the hall sensor's reading at idle and at full.
const int THROTTLE_IDLE_RAW = 530;
const int THROTTLE_FULL_RAW = 820;
const bool SHOW_RAW = false;
// ---------------------------------------------------------------------------
Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 3, 1,
true, true, false, false, false, false,
true, true, false, false, false);
// -512..511 around the center, with a deadzone and expo, back to 0..1023.
int shape(int raw, int deadzone, bool invert) {
long v = (long)raw - 512;
if (invert) v = -v;
long sign = v < 0 ? -1 : 1;
long mag = v * sign;
if (mag <= deadzone) return 512;
// rescale what is left after the deadzone to 0..511
long x = (mag - deadzone) * 511 / (511 - deadzone);
if (x > 511) x = 511;
// expo: blend the line x with the cube x^3 / 511^2
long cube = x * x / 511 * x / 511;
long out = (x * (100 - EXPO) + cube * EXPO) / 100;
return (int)(512 + sign * out);
}
int throttle() {
long raw = analogRead(THROTTLE_PIN);
long out = (raw - THROTTLE_IDLE_RAW) * 1023 / (THROTTLE_FULL_RAW - THROTTLE_IDLE_RAW);
if (out < 0) out = 0;
if (out > 1023) out = 1023;
return (int)out;
}
// The hat from four switches: -1 released, else 0, 45 ... 315 degrees.
int hat() {
bool up = digitalRead(HAT_PINS[0]) == LOW;
bool dn = digitalRead(HAT_PINS[1]) == LOW;
bool lf = digitalRead(HAT_PINS[2]) == LOW;
bool rt = digitalRead(HAT_PINS[3]) == LOW;
if (up && rt) return 45;
if (dn && rt) return 135;
if (dn && lf) return 225;
if (up && lf) return 315;
if (up) return 0;
if (rt) return 90;
if (dn) return 180;
if (lf) return 270;
return JOYSTICK_HATSWITCH_RELEASE;
}
// Only report an axis when it has moved more than a count or two, so ADC
// noise on a real board does not send a report every pass.
bool moved(int now, int &last) {
if (now > last + 2 || now < last - 2 || ((now == 512 || now == 0) && now != last)) {
last = now;
return true;
}
return false;
}
int lastX = -10, lastY = -10, lastTwist = -10, lastThrottle = -10, lastHat = -2;
uint8_t lastButtons = 0xFF;
unsigned long lastShown = 0;
void setup() {
for (uint8_t i = 0; i < 4; i++) pinMode(HAT_PINS[i], INPUT_PULLUP);
for (uint8_t i = 0; i < 3; i++) pinMode(BUTTON_PINS[i], INPUT_PULLUP);
Serial.begin(115200);
Joystick.setXAxisRange(0, 1023);
Joystick.setYAxisRange(0, 1023);
Joystick.setRudderRange(0, 1023);
Joystick.setThrottleRange(0, 1023);
Joystick.begin(false);
Serial.println("Flight stick ready.");
}
void loop() {
bool changed = false;
int x = shape(analogRead(STICK_X_PIN), DEADZONE, INVERT_X);
int y = shape(analogRead(STICK_Y_PIN), DEADZONE, INVERT_Y);
int twist = shape(analogRead(TWIST_PIN), TWIST_DEADZONE, INVERT_TWIST);
int thr = throttle();
changed |= moved(x, lastX);
changed |= moved(y, lastY);
changed |= moved(twist, lastTwist);
changed |= moved(thr, lastThrottle);
int h = hat();
if (h != lastHat) {
lastHat = h;
changed = true;
}
uint8_t buttons = 0;
for (uint8_t i = 0; i < 3; i++) {
if (digitalRead(BUTTON_PINS[i]) == LOW) buttons |= (uint8_t)(1u << i);
}
if (buttons != lastButtons) {
if ((buttons & 1) && !(lastButtons & 1) && lastButtons != 0xFF) Serial.println("Trigger: fire");
lastButtons = buttons;
changed = true;
}
if (changed) {
Joystick.setXAxis(lastX);
Joystick.setYAxis(lastY);
Joystick.setRudder(lastTwist);
Joystick.setThrottle(lastThrottle);
Joystick.setHatSwitch(0, lastHat);
for (uint8_t i = 0; i < 3; i++) Joystick.setButton(i, (buttons >> i) & 1);
Joystick.sendState();
}
if (SHOW_RAW && millis() - lastShown >= 500) {
lastShown = millis();
Serial.print("raw x ");
Serial.print(analogRead(STICK_X_PIN));
Serial.print(" y ");
Serial.print(analogRead(STICK_Y_PIN));
Serial.print(" twist ");
Serial.print(analogRead(TWIST_PIN));
Serial.print(" throttle ");
Serial.println(analogRead(THROTTLE_PIN));
}
}
Parts list
6 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
14 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: SparkFun Pro Micro pin GND; Game console buttons pin GND
- SparkFun Pro Micro pin 2; Game console buttons pin B
- SparkFun Pro Micro pin 3; Game console buttons pin A
- SparkFun Pro Micro pin 4; Game console buttons pin RIGHT
- SparkFun Pro Micro pin 5; Game console buttons pin LEFT
- SparkFun Pro Micro pin 6; Game console buttons pin DOWN
- SparkFun Pro Micro pin 7; Game console buttons pin UP
- Ground: SparkFun Pro Micro pin GND; Hall sensor, 49E linear pin GND; Potentiometer pin 1; Thumb stick pin GND
- SparkFun Pro Micro pin VCC; Hall sensor, 49E linear pin VCC; Potentiometer pin 3; Thumb stick pin VCC
- SparkFun Pro Micro pin A3; Hall sensor, 49E linear pin OUT
- SparkFun Pro Micro pin A2; Potentiometer pin 2
- SparkFun Pro Micro pin A1; Thumb stick pin VRX
- SparkFun Pro Micro pin A0; Thumb stick pin VRY
- SparkFun Pro Micro pin 15; Thumb stick pin SW
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.