Autopilot and landing gear panel on a SparkFun Pro Micro
An autopilot knob for heading, altitude and vertical speed on a four-digit display, an AP button, and a gear lever with three green lights and a red one in transit. The whole build, a SparkFun Pro Micro and 14 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 sim autopilot and landing gear panel on a Pro Micro.
//
// One knob sets the autopilot: push it to pick heading, altitude or vertical
// speed, turn it to change that value, and the four-digit display shows it.
// A gear lever drives three green "down and locked" lights and a red "in
// transit" light the way a real gear panel does, and the AP button latches
// its own light.
//
// gear lever toggle on 7 (a slide switch here)
// AP button momentary on 6 AP light A0
// knob encoder, A on 3, B on 4, push on 5
// display TM1637, CLK on 9 and DIO on 8
// gear lights nose 10, left 16, right 14 (green), in transit 15 (red)
//
// What the PC sees, as one game controller. Every knob click and every flip
// of the lever is a short press of its own button, which is how autopilot
// knobs and gear levers are bound in flight sims:
//
// button 0 / 1 gear up / gear down
// button 2 AP master, held while the button is
// button 3 / 4 heading + / - button 5 / 6 altitude + / -
// button 7 / 8 vertical speed + / -
//
// Honest about the display. On a real panel the value shown comes back from
// the sim, through a tool such as MobiFlight, SimConnect or DCS-BIOS, so it
// always agrees with the cockpit. This sketch keeps its own count instead,
// starting from the values below, so it stays in step only while every click
// reaches the sim.
//
// In Mokxi: turn the knob by dragging or scrolling, click it to change mode,
// click the slide switch to move the gear lever and hold the round button for
// the autopilot.
#include <Joystick.h>
#include <TM1637Display.h>
// ---------------------------------------------------------------------------
// Your build.
// ---------------------------------------------------------------------------
const uint8_t GEAR_PIN = 7;
const uint8_t AP_PIN = 6;
const uint8_t AP_LIGHT = A0;
const uint8_t KNOB_A = 3;
const uint8_t KNOB_B = 4;
const uint8_t KNOB_PUSH = 5;
const uint8_t DISPLAY_CLK = 9;
const uint8_t DISPLAY_DIO = 8;
const uint8_t GREEN[3] = {10, 16, 14}; // nose, left, right
const uint8_t RED = 15;
// How long the gear takes, and when each leg locks after the lever moves.
const unsigned long GEAR_TRAVEL_MS = 4000;
const unsigned long LEG_LOCKS_MS[3] = {3000, 3400, 3800};
const unsigned long PULSE_MS = 100;
const unsigned long GAP_MS = 40;
const unsigned long LOCK_MS = 10;
// Where the values start, and how far one click moves them. Turn the knob
// faster than FAST_MS a click and each click moves FAST_STEP instead, the way
// a real autopilot knob speeds up.
struct Mode {
char letter;
int value;
int minimum;
int maximum;
int step;
int fastStep;
bool wraps;
uint8_t upButton;
uint8_t downButton;
};
Mode modes[] = {
{'H', 360, 1, 360, 1, 10, true, 3, 4}, // heading, degrees
{'A', 50, 0, 450, 1, 10, false, 5, 6}, // altitude, hundreds of feet
{'V', 0, -60, 60, 1, 5, false, 7, 8}, // vertical speed, hundreds of feet a minute
};
const unsigned long FAST_MS = 60;
// ---------------------------------------------------------------------------
const uint8_t MODE_COUNT = sizeof(modes) / sizeof(modes[0]);
const uint8_t BUTTON_COUNT = 9;
enum { GEAR_UP_BUTTON = 0, GEAR_DOWN_BUTTON = 1, AP_BUTTON = 2 };
Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD, BUTTON_COUNT, 0,
false, false, false, false, false, false, false, false, false, false, false);
TM1637Display display(DISPLAY_CLK, DISPLAY_DIO);
// ---- pulses, as on the button box -----------------------------------------
uint8_t waiting[BUTTON_COUNT];
uint8_t phase[BUTTON_COUNT]; // 0 idle, 1 held, 2 gap
unsigned long phaseAt[BUTTON_COUNT];
void pulse(uint8_t b) {
if (waiting[b] < 20) waiting[b]++;
}
void runPulses(unsigned long now) {
for (uint8_t b = 0; b < BUTTON_COUNT; b++) {
if (phase[b] == 0 && waiting[b] > 0) {
waiting[b]--;
phase[b] = 1;
phaseAt[b] = now;
} else if (phase[b] == 1 && now - phaseAt[b] >= PULSE_MS) {
phase[b] = 2;
phaseAt[b] = now;
} else if (phase[b] == 2 && now - phaseAt[b] >= GAP_MS) {
phase[b] = 0;
}
}
}
// ---- the knob -------------------------------------------------------------
const int8_t QUARTER[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
uint8_t knobState;
int8_t knobSteps;
unsigned long lastClickAt;
uint8_t mode = 0;
bool pushed = false;
unsigned long pushedAt = 0;
void click(int direction, unsigned long now) {
Mode &m = modes[mode];
int step = (now - lastClickAt < FAST_MS) ? m.fastStep : m.step;
lastClickAt = now;
int v = m.value + direction * step;
if (m.wraps) {
int span = m.maximum - m.minimum + 1;
while (v < m.minimum) v += span;
while (v > m.maximum) v -= span;
} else {
if (v < m.minimum) v = m.minimum;
if (v > m.maximum) v = m.maximum;
}
m.value = v;
// One press per click whatever the step: the sim does its own speeding up.
pulse(direction > 0 ? m.upButton : m.downButton);
Serial.print(m.letter);
Serial.print(' ');
Serial.println(m.value);
}
void readKnob(unsigned long now) {
uint8_t state = (uint8_t)((digitalRead(KNOB_A) == HIGH ? 2 : 0) | (digitalRead(KNOB_B) == HIGH ? 1 : 0));
if (state != knobState) {
knobSteps += QUARTER[(knobState << 2) | state];
knobState = state;
if (state == 3) {
if (knobSteps >= 2) click(1, now);
else if (knobSteps <= -2) click(-1, now);
knobSteps = 0;
}
}
bool down = digitalRead(KNOB_PUSH) == LOW;
if (down != pushed && now - pushedAt >= LOCK_MS) {
pushed = down;
pushedAt = now;
if (down) {
mode = (uint8_t)((mode + 1) % MODE_COUNT);
Serial.print("mode ");
Serial.println(modes[mode].letter);
}
}
}
// ---- the gear -------------------------------------------------------------
bool leverDown = false;
unsigned long leverAt = 0;
unsigned long leverChangedAt = 0;
bool travelling = false;
void readLever(unsigned long now) {
bool down = digitalRead(GEAR_PIN) == LOW;
if (down == leverDown || now - leverChangedAt < LOCK_MS) return;
leverDown = down;
leverChangedAt = now;
leverAt = now;
travelling = true;
pulse(down ? GEAR_DOWN_BUTTON : GEAR_UP_BUTTON);
Serial.println(down ? "gear down" : "gear up");
}
void showGear(unsigned long now) {
unsigned long t = now - leverAt;
if (travelling && t >= GEAR_TRAVEL_MS) {
travelling = false;
Serial.println(leverDown ? "three green" : "gear up and locked");
}
for (uint8_t leg = 0; leg < 3; leg++) {
bool locked = leverDown && (!travelling || t >= LEG_LOCKS_MS[leg]);
digitalWrite(GREEN[leg], locked ? HIGH : LOW);
}
digitalWrite(RED, travelling ? HIGH : LOW);
}
// ---- the autopilot button -------------------------------------------------
bool apHeld = false;
bool apOn = false;
unsigned long apAt = 0;
void readAp(unsigned long now) {
bool down = digitalRead(AP_PIN) == LOW;
if (down == apHeld || now - apAt < LOCK_MS) return;
apHeld = down;
apAt = now;
if (down) {
// The light is the panel's own latch: the sim's AP state is the truth,
// and a real panel would take the light from it.
apOn = !apOn;
Serial.println(apOn ? "AP on" : "AP off");
}
digitalWrite(AP_LIGHT, apOn ? HIGH : LOW);
}
// ---- the display ----------------------------------------------------------
uint8_t shown[4] = {0xFF, 0xFF, 0xFF, 0xFF};
uint8_t letter(char c) {
switch (c) {
case 'H': return SEG_B | SEG_C | SEG_E | SEG_F | SEG_G;
case 'A': return SEG_A | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G;
case 'V': return SEG_C | SEG_D | SEG_E; // a lower-case u, the nearest a 7-segment gets
default: return 0;
}
}
void showValue() {
const Mode &m = modes[mode];
uint8_t digits[4];
digits[0] = letter(m.letter);
// The value right-aligned in the last three digits, a minus sign just left
// of it when it is negative.
int v = m.value < 0 ? -m.value : m.value;
int8_t at = 3;
digits[1] = digits[2] = 0;
do {
digits[at--] = display.encodeDigit((uint8_t)(v % 10));
v /= 10;
} while (v > 0 && at >= 1);
if (m.value < 0 && at >= 1) digits[at] = SEG_G;
bool same = true;
for (uint8_t i = 0; i < 4; i++) same = same && digits[i] == shown[i];
if (same) return;
for (uint8_t i = 0; i < 4; i++) shown[i] = digits[i];
display.setSegments(digits);
}
void setup() {
pinMode(GEAR_PIN, INPUT_PULLUP);
pinMode(AP_PIN, INPUT_PULLUP);
pinMode(KNOB_A, INPUT_PULLUP);
pinMode(KNOB_B, INPUT_PULLUP);
pinMode(KNOB_PUSH, INPUT_PULLUP);
pinMode(AP_LIGHT, OUTPUT);
pinMode(RED, OUTPUT);
for (uint8_t leg = 0; leg < 3; leg++) pinMode(GREEN[leg], OUTPUT);
knobState = (uint8_t)((digitalRead(KNOB_A) == HIGH ? 2 : 0) | (digitalRead(KNOB_B) == HIGH ? 1 : 0));
Serial.begin(115200);
Serial.println("Autopilot panel ready: push the knob for HDG, ALT or VS.");
display.setBrightness(5);
// The lever as it is at power-up is where the gear already is: no pulse,
// no transit, just the lights for that state.
delay(20);
leverDown = digitalRead(GEAR_PIN) == LOW;
Joystick.begin(false);
}
void loop() {
unsigned long now = millis();
readKnob(now);
readLever(now);
readAp(now);
runPulses(now);
showGear(now);
showValue();
for (uint8_t b = 0; b < BUTTON_COUNT; b++) Joystick.setButton(b, phase[b] == 1);
Joystick.setButton(AP_BUTTON, apHeld);
Joystick.sendState();
}
Parts list
16 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × SparkFun Pro Micro
- 1 × Full-size breadboard
- 1 × Four-digit display, TM1637
- 1 × Rotary encoder, KY-040
- 1 × Slide switch
- 1 × Arcade button
- 3 × LED, green
- 5 × Resistor, 220 Ω
- 1 × LED, yellow
- 1 × LED, red
How it is wired
19 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; Four-digit display, TM1637 pin GND; Rotary encoder, KY-040 pin GND; Slide switch pin 3; Arcade button pin 1; Resistor, 220 Ω (1) pin 2; Resistor, 220 Ω (2) pin 2; Resistor, 220 Ω (3) pin 2; Resistor, 220 Ω (4) pin 2; Resistor, 220 Ω (5) pin 2
- SparkFun Pro Micro pin 3; Rotary encoder, KY-040 pin CLK
- SparkFun Pro Micro pin 4; Rotary encoder, KY-040 pin DT
- SparkFun Pro Micro pin 5; Rotary encoder, KY-040 pin SW
- SparkFun Pro Micro pin 6; Arcade button pin 2
- SparkFun Pro Micro pin 7; Slide switch pin 2
- SparkFun Pro Micro pin 8; Four-digit display, TM1637 pin DIO
- SparkFun Pro Micro pin 9; Four-digit display, TM1637 pin CLK
- SparkFun Pro Micro pin VCC; Four-digit display, TM1637 pin VCC; Rotary encoder, KY-040 pin VCC
- SparkFun Pro Micro pin A0; LED, yellow pin A
- SparkFun Pro Micro pin 15; LED, red pin A
- SparkFun Pro Micro pin 14; LED, green (3) pin A
- SparkFun Pro Micro pin 16; LED, green (2) pin A
- SparkFun Pro Micro pin 10; LED, green (1) pin A
- LED, green (1) pin C; Resistor, 220 Ω (1) pin 1
- LED, green (2) pin C; Resistor, 220 Ω (2) pin 1
- LED, green (3) pin C; Resistor, 220 Ω (3) pin 1
- LED, yellow pin C; Resistor, 220 Ω (4) pin 1
- LED, red pin C; Resistor, 220 Ω (5) pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.