Built-in project · SparkFun Pro Micro

Load cell pedals on a Pro Micro

A load cell brake on an HX711 that responds to force, a hall sensor throttle and a pot clutch, each calibrated and sent as one Joystick.h report. The whole build, a SparkFun Pro Micro and 3 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.

Load cell pedals on a Pro Micro · SparkFun Pro Microlive0.000 s 0.00x
Click to open it in the editor
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
// Sim racing pedals on a Pro Micro: a load cell brake on an HX711, a hall
// sensor throttle and a potentiometer clutch, sent as one USB game controller
// report through the Arduino Joystick library's API (Joystick.h).
//
// Why a load cell for the brake. A pot or hall sensor measures how far a
// pedal has moved. A load cell measures how hard it is pushed, which is how a
// real brake pedal works: your leg remembers force far better than distance.
// That is why so many DIY pedal sets put a beam load cell under the brake and
// keep position sensors for the throttle and clutch.
//
// Wiring.
//
//   brake     HX711 DT on pin 4, SCK on pin 3, VCC 5 V, GND
//   throttle  SS49E hall sensor on A1 (a magnet on the pedal arm)
//   clutch    10k pot, wiper on A0
//
// In Mokxi: drag the HX711's load for the brake (in kilograms on the cell),
// slide the magnet towards the hall sensor for the throttle, and turn the
// knob for the clutch.
//
// The brake's numbers.
//   The HX711 is a 24-bit converter made for load cells. At start-up the
//   sketch reads it with no foot on the pedal and calls that zero (tare).
//   COUNTS_PER_KG turns counts into kilograms. Find it on your own cell: hang
//   or stand a known weight on it, take the reading minus the tare, and divide
//   by the kilograms. BRAKE_MAX_KG is the push that means full brake; a
//   higher number makes a stiffer, more progressive pedal. The HX711 runs at
//   80 readings a second here (its RATE pin high), not the 10 it starts at.
//
// 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. Flash the same
// sketch to a real Pro Micro and a racing game sees three pedal axes.

#include <HX711.h>
#include <Joystick.h>

// ---------------------------------------------------------------------------
// Make it yours
// ---------------------------------------------------------------------------

const uint8_t BRAKE_DT_PIN = 4;
const uint8_t BRAKE_SCK_PIN = 3;
const uint8_t THROTTLE_PIN = A1;
const uint8_t CLUTCH_PIN = A0;

// Counts per kilogram for this cell: a 100 kg, 1 mV/V beam at gain 128.
const long COUNTS_PER_KG = 21475;
// The push, in tenths of a kilogram, that is full brake (400 = 40 kg).
const long BRAKE_MAX_TENTHS = 400;
// Pushes under this count as no brake, so a resting foot does not drag it.
const long BRAKE_DEADZONE_TENTHS = 10;
// 0 is a straight line; 100 makes the first half of the travel gentle and
// the end sharp, the progressive feel many drivers like for trail braking.
const int BRAKE_CURVE = 50;

// The throttle's hall sensor reading at rest and floored.
const int THROTTLE_REST_RAW = 530;
const int THROTTLE_FLOOR_RAW = 820;
// The clutch pot's reading at rest and pushed in.
const int CLUTCH_REST_RAW = 0;
const int CLUTCH_FLOOR_RAW = 1023;
// A little dead travel at each end of the throttle and clutch, in percent.
const int END_DEADZONE = 3;

// Print the brake in kilograms each time it changes by half a kilogram.
const bool SHOW_KG = true;

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

HX711 brakeCell;
long tare = 0;

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_MULTI_AXIS, 0, 0,
                   false, false, true, false, false, false,
                   false, false, true, true, false);

int lastBrake = -10, lastThrottle = -10, lastClutch = -10;
long lastShownTenths = -100;

// A reading between rest and floor as 0..1023, with dead travel at each end.
int travel(int raw, int rest, int floorRaw) {
  long span = (long)floorRaw - rest;
  long at = (long)raw - rest;
  if (span < 0) {
    span = -span;
    at = -at;
  }
  long dead = span * END_DEADZONE / 100;
  long out = (at - dead) * 1023 / (span - 2 * dead);
  if (out < 0) out = 0;
  if (out > 1023) out = 1023;
  return (int)out;
}

// Tenths of a kilogram on the cell as 0..1023, with the deadzone and curve.
int brakeAxis(long tenths) {
  if (tenths <= BRAKE_DEADZONE_TENTHS) return 0;
  long x = (tenths - BRAKE_DEADZONE_TENTHS) * 1023 / (BRAKE_MAX_TENTHS - BRAKE_DEADZONE_TENTHS);
  if (x > 1023) x = 1023;
  long square = x * x / 1023;
  return (int)((x * (100 - BRAKE_CURVE) + square * BRAKE_CURVE) / 100);
}

// Report an axis when it has moved more than a few counts, or reached an end,
// so the load cell's last-digit noise does not send a report every reading.
bool moved(int now, int &last) {
  if (now > last + 6 || now < last - 6 || ((now == 0 || now == 1023) && now != last)) {
    last = now;
    return true;
  }
  return false;
}

void setup() {
  Serial.begin(115200);
  brakeCell.begin(BRAKE_DT_PIN, BRAKE_SCK_PIN);
  tare = brakeCell.read_average(8);  // no foot on the pedal yet
  Joystick.setZAxisRange(0, 1023);
  Joystick.setAcceleratorRange(0, 1023);
  Joystick.setBrakeRange(0, 1023);
  Joystick.begin(false);
  Serial.println("Pedals ready: brake tared.");
}

void loop() {
  bool changed = false;

  if (brakeCell.is_ready()) {
    long counts = brakeCell.read() - tare;
    long tenths = counts * 10 / COUNTS_PER_KG;
    if (tenths < 0) tenths = 0;
    changed |= moved(brakeAxis(tenths), lastBrake);
    if (SHOW_KG && (tenths > lastShownTenths + 5 || tenths < lastShownTenths - 5)) {
      lastShownTenths = tenths;
      Serial.print("Brake ");
      Serial.print(tenths / 10);
      Serial.print('.');
      Serial.print(tenths % 10);
      Serial.println(" kg");
    }
  }

  changed |= moved(travel(analogRead(THROTTLE_PIN), THROTTLE_REST_RAW, THROTTLE_FLOOR_RAW), lastThrottle);
  changed |= moved(travel(analogRead(CLUTCH_PIN), CLUTCH_REST_RAW, CLUTCH_FLOOR_RAW), lastClutch);

  if (changed) {
    Joystick.setAccelerator(lastThrottle);
    Joystick.setBrake(lastBrake < 0 ? 0 : lastBrake);
    Joystick.setZAxis(lastClutch);
    Joystick.sendState();
  }
}

Parts list

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

How it is wired

7 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; Load cell with HX711 pin GND
  • SparkFun Pro Micro pin 3; Load cell with HX711 pin SCK
  • SparkFun Pro Micro pin 4; Load cell with HX711 pin DT
  • Ground: SparkFun Pro Micro pin GND; Hall sensor, 49E linear pin GND; Potentiometer pin 1
  • SparkFun Pro Micro pin VCC; Load cell with HX711 pin VCC; Hall sensor, 49E linear pin VCC; Potentiometer pin 3
  • SparkFun Pro Micro pin A1; Hall sensor, 49E linear pin OUT
  • SparkFun Pro Micro pin A0; Potentiometer 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.