Built-in project · SparkFun Pro Micro

H-pattern shifter and handbrake on a SparkFun Pro Micro

Two pots read an H-pattern gate with hysteresis and push-down reverse, two microswitches make sequential paddles and a hall sensor is the handbrake, with the gear on a display. The whole build, a SparkFun Pro Micro and 7 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.

H-pattern shifter and handbrake · SparkFun Pro Microlive0.000 s 0.00x
Press Run, then drag both slide pots to put it in gear
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · SparkFun Pro Micro
// H-pattern shifter, sequential paddles and a hall handbrake on a Pro Micro.
//
// An H-pattern shifter is two potentiometers under the knob: one reads the
// gate (left to right), the other the throw (forward and back). The sketch
// turns the two readings into a gear, the way the shifter adapters people
// build for a spare wheel-set shifter do. A handbrake is one more axis, here
// a 49E hall sensor with a magnet on the lever, which wears nothing out.
//
//   gate pot (X)        wiper on A1       throw pot (Y)   wiper on A0
//   handbrake hall      OUT on A2         reverse button  on 2 (push the knob down)
//   sequential up       microswitch on 7  sequential down microswitch on 6
//   gear display        TM1637, CLK on 9 and DIO on 8
//
// What the PC sees, as one game controller:
//   buttons 0 to 5    gears 1 to 6, held while the gear is in
//   button 6          reverse
//   buttons 7 and 8   sequential up and down, held while the lever is
//   button 9          the handbrake past HANDBRAKE_BUTTON_AT, for games
//                     that only take the handbrake as a button
//   Z axis            the handbrake, 0 released to 1023 pulled
//
// In Mokxi: both slide pots start at the left end of their travel, which on
// the throw pot is the knob pulled back, so first slide the throw pot to the
// middle (neutral). Then drag the two slide pots to move the knob through the
// gate (the gate pot left to right, the throw pot right for 1, 3 and 5 and
// left for 2, 4 and 6), hold the black button and pull into the sixth slot for reverse,
// click the two microswitches for the paddles, and drag the magnet toward the
// sensor to pull the handbrake. The display shows the gear on the left and
// the handbrake in percent on the right; the Serial Monitor shows the report
// the shifter would send over USB.

#include <Joystick.h>
#include <TM1637Display.h>

// ---------------------------------------------------------------------------
// Your build. Calibrate with PRINT_RAW, then set the numbers below.
// ---------------------------------------------------------------------------

// true prints the raw pot and sensor readings twice a second, which is how
// you find the numbers for your own shifter and handbrake.
const bool PRINT_RAW = false;

const uint8_t GATE_PIN = A1;
const uint8_t THROW_PIN = A0;
const uint8_t HANDBRAKE_PIN = A2;
const uint8_t REVERSE_PIN = 2;
const uint8_t UP_PIN = 7;
const uint8_t DOWN_PIN = 6;
const uint8_t DISPLAY_CLK = 9;
const uint8_t DISPLAY_DIO = 8;

// The gate: below LEFT_EDGE is the 1-2 slot, above RIGHT_EDGE the 5-6 slot.
const int LEFT_EDGE = 340;
const int RIGHT_EDGE = 680;

// The throw, with hysteresis. A gear goes in past ENGAGE and only comes out
// again back past RELEASE, so a knob resting at the edge of a slot cannot
// chatter between a gear and neutral.
const int ODD_ENGAGE = 800;    // forward: 1, 3, 5
const int ODD_RELEASE = 700;
const int EVEN_ENGAGE = 220;   // back: 2, 4, 6 and reverse
const int EVEN_RELEASE = 320;

// The handbrake: the raw reading released and fully pulled, and how much of
// each end is dead, so a lever at rest reads exactly 0 and a hard pull 1023.
const int HANDBRAKE_RELEASED = 512;
const int HANDBRAKE_PULLED = 860;
const uint8_t DEADZONE_PERCENT = 4;
const uint8_t HANDBRAKE_BUTTON_AT = 50;  // percent

// ---------------------------------------------------------------------------

enum { REVERSE_BUTTON = 6, UP_BUTTON = 7, DOWN_BUTTON = 8, HANDBRAKE_BUTTON = 9 };

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 10, 0,
                   false, false, true,  // Z only
                   false, false, false, false, false, false, false, false);
TM1637Display display(DISPLAY_CLK, DISPLAY_DIO);

// The gear: 0 is neutral, 1 to 6, and -1 is reverse.
int gear = 0;

// No gear counts until the knob has been seen in neutral once, so a shifter
// plugged in while it is in gear does not drop a gear into the game.
bool seenNeutral = false;

int column(int x) {
  if (x < LEFT_EDGE) return 0;
  if (x > RIGHT_EDGE) return 2;
  return 1;
}

// Which gear the knob is in. A gear stays in until the throw comes back past
// its release line; only then can the knob pick a new slot.
int readGear(int x, int y, bool reverseHeld) {
  if (!seenNeutral) {
    seenNeutral = y > EVEN_RELEASE && y < ODD_RELEASE;
    return 0;
  }
  bool odd = gear > 0 && (gear & 1);
  bool even = gear == -1 || (gear > 0 && !(gear & 1));
  if (odd && y > ODD_RELEASE) return gear;
  if (even && y < EVEN_RELEASE) return gear;

  int slot = column(x);
  if (y > ODD_ENGAGE) return 1 + 2 * slot;
  if (y < EVEN_ENGAGE) {
    if (slot == 2 && reverseHeld) return -1;  // knob pushed down: reverse
    return 2 + 2 * slot;
  }
  return 0;
}

// The handbrake as 0 to 1023, with the dead ends cut off.
int handbrake(int raw) {
  long span = (long)HANDBRAKE_PULLED - HANDBRAKE_RELEASED;
  long at = (long)raw - HANDBRAKE_RELEASED;
  long dead = span * DEADZONE_PERCENT / 100;
  long value = (at - dead) * 1023L / (span - 2 * dead);
  if (value < 0) value = 0;
  if (value > 1023) value = 1023;
  return (int)value;
}

// Seven-segment letters for the display.
const uint8_t LETTER_N = SEG_C | SEG_E | SEG_G;
const uint8_t LETTER_R = SEG_E | SEG_G;
const uint8_t LETTER_U = SEG_B | SEG_C | SEG_D | SEG_E | SEG_F;
const uint8_t LETTER_D = SEG_B | SEG_C | SEG_D | SEG_E | SEG_G;

uint8_t shown[4] = {0xFF, 0xFF, 0xFF, 0xFF};

void show(bool up, bool down, int brake) {
  uint8_t digits[4];
  if (up) digits[0] = LETTER_U;
  else if (down) digits[0] = LETTER_D;
  else if (gear == 0) digits[0] = LETTER_N;
  else if (gear < 0) digits[0] = LETTER_R;
  else digits[0] = display.encodeDigit((uint8_t)gear);
  digits[1] = 0;
  int percent = (int)((long)brake * 99 / 1023);
  digits[2] = percent >= 10 ? display.encodeDigit((uint8_t)(percent / 10)) : 0;
  digits[3] = display.encodeDigit((uint8_t)(percent % 10));

  bool same = true;
  for (uint8_t i = 0; i < 4; i++) same = same && digits[i] == shown[i];
  if (same) return;  // the display bus is slow: only write what changed
  for (uint8_t i = 0; i < 4; i++) shown[i] = digits[i];
  display.setSegments(digits);
}

void setup() {
  pinMode(REVERSE_PIN, INPUT_PULLUP);
  pinMode(UP_PIN, INPUT_PULLUP);
  pinMode(DOWN_PIN, INPUT_PULLUP);
  Serial.begin(115200);
  Serial.println("Shifter ready: slide the pots through the gate, pull the handbrake.");
  display.setBrightness(5);
  Joystick.setZAxisRange(0, 1023);
  Joystick.begin(false);
}

unsigned long printedAt = 0;

void loop() {
  int x = analogRead(GATE_PIN);
  int y = analogRead(THROW_PIN);
  int raw = analogRead(HANDBRAKE_PIN);
  bool reverseHeld = digitalRead(REVERSE_PIN) == LOW;
  bool up = digitalRead(UP_PIN) == LOW;
  bool down = digitalRead(DOWN_PIN) == LOW;

  if (PRINT_RAW && millis() - printedAt >= 500) {
    printedAt = millis();
    Serial.print("raw gate ");
    Serial.print(x);
    Serial.print("  throw ");
    Serial.print(y);
    Serial.print("  handbrake ");
    Serial.println(raw);
  }

  int next = readGear(x, y, reverseHeld);
  if (next != gear) {
    gear = next;
    Serial.print("gear ");
    if (gear == 0) Serial.println("N");
    else if (gear < 0) Serial.println("R");
    else Serial.println(gear);
  }

  int brake = handbrake(raw);
  for (uint8_t g = 0; g < 6; g++) Joystick.setButton(g, gear == g + 1);
  Joystick.setButton(REVERSE_BUTTON, gear == -1);
  Joystick.setButton(UP_BUTTON, up);
  Joystick.setButton(DOWN_BUTTON, down);
  Joystick.setButton(HANDBRAKE_BUTTON, (long)brake * 100 >= (long)HANDBRAKE_BUTTON_AT * 1023);
  // Send the axis in steps of 8, so a hand resting on the lever does not
  // flood the PC with reports for the last bit of sensor noise.
  Joystick.setZAxis(brake & ~7);
  Joystick.sendState();

  show(up, down, brake);
  delay(2);
}

Parts list

9 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

10 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; Limit switch, lever microswitch (1) pin COM; Limit switch, lever microswitch (2) pin COM; Slide potentiometer (1) pin 1; Slide potentiometer (2) pin 1; Arcade button pin 1; Hall sensor, 49E linear pin GND
  • SparkFun Pro Micro pin 2; Arcade button pin 2
  • SparkFun Pro Micro pin 6; Limit switch, lever microswitch (2) pin NO
  • SparkFun Pro Micro pin 7; Limit switch, lever microswitch (1) pin NO
  • 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; Slide potentiometer (1) pin 3; Slide potentiometer (2) pin 3; Hall sensor, 49E linear pin VCC
  • SparkFun Pro Micro pin A2; Hall sensor, 49E linear pin OUT
  • SparkFun Pro Micro pin A1; Slide potentiometer (1) pin 2
  • SparkFun Pro Micro pin A0; Slide potentiometer (2) pin 2

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.