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.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// 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.
- 1 × ESP32-C3-DevKitM-1
- 1 × Full-size breadboard
- 1 × Thumb stick
- 2 × Resistor, 220 Ω
- 2 × LED, green
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.