Built-in project · ESP32-C3-DevKitM-1

ESP32-C3 thumb stick

A thumb stick on the ESP32-C3 ADC: the raw axes go to the serial monitor and how far the stick is pushed sets how brightly the LED on that side is lit. The whole build, an ESP32-C3 and 5 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.

IntermediateRuns in your browser. Free, and no account needed.

ESP32-C3 thumb stick · ESP32-C3-DevKitM-1live0.000 s 0.00x
Hold the button
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 · ESP32-C3-DevKitM-1
// Thumb stick: an ESP32-C3 reading a KY-023 on ADC1, out loud.
//
// Wiring, module to board:
//   VRX -> GPIO 0     VRY -> GPIO 1     SW -> GPIO 3
//   VCC -> 3V3        GND -> GND
// Two LEDs, each through 220 R to ground:
//   left -> GPIO 6    right -> GPIO 7
//
// Push the stick left or right and the LED on that side lights; push it far
// enough and it lights brightly rather than dimly, because the sketch bit-bangs
// a duty from how far the stick has gone. Press the stick down and both come
// on. Every time the reading changes, the raw pair and the offsets from centre
// are printed on the serial monitor.
//
// # This is what the ADC is for
//
// GPIO 0 to GPIO 4 are ADC1 channels 0 to 4 (docs/esp32c3.md section 3.5), and
// analogRead on one of them is a real conversion of the real net voltage: the
// two pots inside the module sit across the 3.3 V rail, so each axis is a
// voltage between the rails and comes back as 0..4095 at the default 11 dB
// attenuation. Nothing here is a threshold, which is the whole difference
// between a stick and two buttons.
//
// # Why the numbers are not 0, 2048, 4095
//
// A KY-023's pots do not reach the rails: the ends of the track are a little
// way inside them, and the spring does not centre perfectly either. So the
// sketch does not assume a center; mokxi_joystick.h samples the rest position
// in begin() and reports offsets from it, with a dead zone so a stick that is
// merely not perfectly centred does not creep. Move the stick before reset and
// the readings will be off, which is true of the hardware too.

#include "mokxi_joystick.h"

const uint8_t VRX_PIN = 0;
const uint8_t VRY_PIN = 1;
const uint8_t SW_PIN = 3;
const uint8_t LEFT_LED = 6;
const uint8_t RIGHT_LED = 7;

Joystick stick(VRX_PIN, VRY_PIN, SW_PIN);

// The PWM the two LEDs are lit at: 64 steps of 62 us is a 252 Hz frame, fast
// enough that the eye sees a brightness rather than a flicker, and the board
// has nothing else to do between conversions. 62 us clears the runtime's own
// 50 us park threshold (firmware/src/time.c): a step under that spins on real
// instructions to stay accurate at that scale, and this sketch has nothing
// that fine to be accurate about, so every step now parks the core in `wfi`
// between edges instead of burning cycles being briefly, needlessly precise.
const uint8_t STEPS = 64;
const uint16_t STEP_US = 62;

int16_t lastX = -1;
int16_t lastY = -1;

/** An offset from the rest position with the dead zone taken out of it. */
static int16_t deadzoned(int16_t offset)
{
  if (offset > JOYSTICK_DEADZONE) {
    return (int16_t)(offset - JOYSTICK_DEADZONE);
  }
  if (offset < -JOYSTICK_DEADZONE) {
    return (int16_t)(offset + JOYSTICK_DEADZONE);
  }
  return 0;
}

/** How brightly one side should be lit, 0..STEPS, from an axis offset. */
static uint8_t dutyOf(int16_t offset)
{
  if (offset <= 0) {
    return 0;
  }
  // Full scale less the dead zone is the travel that is left; map it onto the
  // steps with a shift rather than a divide, since the numbers are powers of
  // two either side of it.
  int32_t span = (JOYSTICK_FULL_SCALE / 2) - JOYSTICK_DEADZONE;
  int32_t duty = ((int32_t)offset * STEPS) / span;
  return (uint8_t)(duty > STEPS ? STEPS : duty);
}

/** One PWM frame with the two sides at their own duties. */
static void showFrame(uint8_t left, uint8_t right)
{
  for (uint8_t i = 0; i < STEPS; i++) {
    digitalWrite(LEFT_LED, i < left ? HIGH : LOW);
    digitalWrite(RIGHT_LED, i < right ? HIGH : LOW);
    delayMicroseconds(STEP_US);
  }
  digitalWrite(LEFT_LED, LOW);
  digitalWrite(RIGHT_LED, LOW);
}

void setup()
{
  Serial.begin(115200);
  pinMode(LEFT_LED, OUTPUT);
  pinMode(RIGHT_LED, OUTPUT);
  stick.begin();

  Serial.println("Mokxi ESP32-C3: a thumb stick on ADC1");
  Serial.print("resting at x=");
  Serial.print(stick.restX());
  Serial.print(" y=");
  Serial.print(stick.restY());
  Serial.print(" of ");
  Serial.println(JOYSTICK_FULL_SCALE);
}

void loop()
{
  int16_t x = stick.x();
  int16_t y = stick.y();
  int16_t dx = (int16_t)(x - stick.restX());
  int16_t dy = (int16_t)(y - stick.restY());

  // A conversion of a real net has no noise in it, so a changed reading means
  // the stick moved and every line printed is worth reading.
  if (x != lastX || y != lastY) {
    lastX = x;
    lastY = y;
    Serial.print("x=");
    Serial.print(x);
    Serial.print(" y=");
    Serial.print(y);
    Serial.print("  dx=");
    Serial.print(dx);
    Serial.print(" dy=");
    Serial.print(dy);
    if (stick.pressed()) {
      Serial.print("  pressed");
    }
    Serial.println();
  }

  if (stick.pressed()) {
    showFrame(STEPS, STEPS);
    return;
  }
  // The axes are already in hand: asking the Joystick again would be two more
  // conversions for numbers this loop has read once.
  int16_t swing = deadzoned(dx);
  showFrame(dutyOf((int16_t)(-swing)), dutyOf(swing));
}

Parts list

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

How it is wired

9 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.

  • ESP32-C3-DevKitM-1 pin 3V3; Thumb stick pin VCC
  • ESP32-C3-DevKitM-1 pin 3; Thumb stick pin SW
  • Ground: ESP32-C3-DevKitM-1 pin GND; Thumb stick pin GND; LED, green (1) pin C; LED, green (2) pin C
  • ESP32-C3-DevKitM-1 pin 0; Thumb stick pin VRX
  • ESP32-C3-DevKitM-1 pin 1; Thumb stick pin VRY
  • ESP32-C3-DevKitM-1 pin 6; Resistor, 220 Ω (1) pin 1
  • ESP32-C3-DevKitM-1 pin 7; Resistor, 220 Ω (2) pin 1
  • Resistor, 220 Ω (1) pin 2; LED, green (1) pin A
  • Resistor, 220 Ω (2) pin 2; LED, green (2) pin A

Change it and keep it

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