A centre and a dead zone
A stick let go rests near the middle, not on it, and jitters by a few millivolts. So the sketch measures the centre at start-up, maps each axis to −100 to 100 from there, widens the ends as it sees the stick reach them, and calls anything within 10 of the centre 0: a dead zone.
Three numbers per axis
Raw counts are awkward to use. They rest somewhere near the middle, not on it, their range depends on the board, and they twitch by a few counts with the stick let go. What a program wants is simpler: 0 when the stick is let go, and −100 to 100 as it tilts. That takes three numbers per axis, the centre and the two ends.
The centre is measured, not assumed. setup() waits a moment and
averages sixteen readings of each axis with the stick let go. Assume half of
3V3 instead and a stick resting a few tens of millivolts off it reads a few
per cent with nobody touching it, and a character on a screen creeps.
The ends are learned. The sketch starts with a guess, 1000 mV either side of the centre, and each time a reading goes past an end, the end moves out to meet it. Push the stick round its full circle once and both ends are right for this stick on this board. Each side is scaled on its own, so a centre that is off the middle still maps to 0, and both ends still reach 100.
The dead zone
Even with the centre measured, a stick let go jitters by a few millivolts,
and the output flickers between −1 and 1. A dead zone treats anything within
DEAD_ZONE of the centre as exactly 0. Ten per cent is a common choice for a
thumb stick: small enough that you do not notice it, large enough to swallow
the jitter and the slightly different place the spring returns to each time.
The cost is the first 10 per cent of travel, which now does nothing. For a game that is invisible; for a camera slider that must creep very slowly, make it smaller.
What you should see
With the stick let go:
X 0 Y 0
X 0 Y 0Tilted, the numbers run out to ±100 at the ends of the travel. Which sign means which direction is still your stick's own; the steering sketch in the next article has a line to flip each axis.
The code
The stick as two numbers from −100 to 100, with 0 when it is let go. The centre is measured in setup(), the ends are learned as the stick reaches them, and a dead zone of 10 swallows the jitter.
/*
Slim Joystick - a centre and a dead zone TK21 / /p/tk21
Wiring. Parts up, header along the top. Count from the square pad,
which is GND at the right-hand end, leftwards:
GND -> GND
3V3 -> 3V3 on every board, the Uno included (it is printed 3V3)
BTNS -> A2 on an Uno, GPIO 32 on an ESP32, GPIO 6 on an
ESP32-S3, GP28 on a Raspberry Pi Pico (analog)
X -> A0, GPIO 34, GPIO 4, GP26 (same order, analog)
Y -> A1, GPIO 35, GPIO 5, GP27 (analog)
KEY -> D2, GPIO 25, GPIO 7, GP15 (digital)
Arduino IDE
Tools > Board your board, e.g. ESP32S3 Dev Module
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed.
*/
// The pins X, Y, BTNS and KEY are wired to.
// Uno: A0 A1 A2 2. ESP32: 34 35 32 25. ESP32-S3: 4 5 6 7. Pico: 26 27 28 15.
const int X_PIN = 4;
const int Y_PIN = 5;
const int BTNS_PIN = 6;
const int KEY_PIN = 7;
// Uno and Pico only: the ADC's full scale, in mV.
// Uno: 5000. Pico: 3300.
const float FULL_SCALE_MV = 5000.0;
const int DEAD_ZONE = 10; // per cent either side of the centre
const float EDGE_GUESS_MV = 1000; // the ends, until the stick shows them
float centreX, loX, hiX; // measured, and the furthest seen
float centreY, loY, hiY;
float readMv(int pin) {
#if defined(ARDUINO_ARCH_ESP32)
return analogReadMilliVolts(pin); // calibrated in the chip
#else
return analogRead(pin) * FULL_SCALE_MV / 1023.0;
#endif
}
// The mean of 16 readings: steadier than one.
float averageMv(int pin) {
float sum = 0;
for (int i = 0; i < 16; i++) sum += readMv(pin);
return sum / 16;
}
// -100..100 from the centre, widening the ends as the stick finds them.
int axis(float mv, float centre, float &lo, float &hi) {
if (mv < lo) lo = mv;
if (mv > hi) hi = mv;
float p;
if (mv >= centre) p = (mv - centre) * 100.0 / (hi - centre);
else p = (mv - centre) * 100.0 / (centre - lo);
int out = constrain((int)p, -100, 100);
if (abs(out) < DEAD_ZONE) out = 0; // the dead zone
return out;
}
void setup() {
Serial.begin(115200);
pinMode(KEY_PIN, INPUT); // R1 on the board pulls it down
delay(200); // hands off the stick
centreX = averageMv(X_PIN);
centreY = averageMv(Y_PIN);
loX = centreX - EDGE_GUESS_MV;
hiX = centreX + EDGE_GUESS_MV;
loY = centreY - EDGE_GUESS_MV;
hiY = centreY + EDGE_GUESS_MV;
}
void loop() {
int x = axis(averageMv(X_PIN), centreX, loX, hiX);
int y = axis(averageMv(Y_PIN), centreY, loY, hiY);
Serial.print("X ");
Serial.print(x);
Serial.print(" Y ");
Serial.println(y);
delay(100);
}Keep your thumb off the stick while the board starts: the first readings are the centre. The ends start as a guess, 1000 mV either side, and widen the first time you push the stick further; move it round its whole circle once and they are right.
The same mapping in MicroPython, for an ESP32, an ESP32-S3 or a Pico: the centre measured at start-up, the ends learned as the stick reaches them, and a dead zone of 10.
"""
Slim Joystick - a centre and a dead zone, MicroPython TK21 / /p/tk21
Wiring. Parts up, header along the top. Count from the square pad,
which is GND at the right-hand end, leftwards:
GND -> GND
3V3 -> 3V3 (the header is printed 3V3; never 5V)
BTNS -> GPIO 32 on an ESP32, GPIO 6 on an ESP32-S3,
GP28 on a Raspberry Pi Pico (analog)
X -> GPIO 34, GPIO 4, GP26 (same order, analog)
Y -> GPIO 35, GPIO 5, GP27 (analog)
KEY -> GPIO 25, GPIO 7, GP15 (digital)
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine, sys and time are built in.
"""
import sys
import time
from machine import ADC, Pin
# The GPIO numbers X, Y, BTNS and KEY are wired to.
# ESP32: 34 35 32 25. ESP32-S3: 4 5 6 7. Pico: 26 27 28 15.
X_PIN, Y_PIN, BTNS_PIN, KEY_PIN = 4, 5, 6, 7
DEAD_ZONE = 10 # per cent either side of the centre
EDGE_GUESS_MV = 1000 # the ends, until the stick shows them
ESP = sys.platform == "esp32" # ESP32 and ESP32-S3
def analog(pin):
adc = ADC(Pin(pin))
if ESP:
adc.atten(ADC.ATTN_11DB) # the full range, to about 3.1 V
return adc
def millivolts(adc):
if ESP:
return adc.read_uv() / 1000 # calibrated in the chip
return adc.read_u16() * 3300 / 65535
def average_mv(adc):
return sum(millivolts(adc) for _ in range(16)) / 16
class Axis:
def __init__(self, pin):
self.adc = analog(pin)
self.centre = average_mv(self.adc) # hands off the stick
self.lo = self.centre - EDGE_GUESS_MV
self.hi = self.centre + EDGE_GUESS_MV
def read(self):
mv = average_mv(self.adc)
self.lo = min(self.lo, mv)
self.hi = max(self.hi, mv)
if mv >= self.centre:
p = (mv - self.centre) * 100 / (self.hi - self.centre)
else:
p = (mv - self.centre) * 100 / (self.centre - self.lo)
p = max(-100, min(100, int(p)))
return 0 if abs(p) < DEAD_ZONE else p # the dead zone
key = Pin(KEY_PIN, Pin.IN) # R1 on the board pulls it down
time.sleep_ms(200)
x_axis = Axis(X_PIN)
y_axis = Axis(Y_PIN)
while True:
print("X %4d Y %4d" % (x_axis.read(), y_axis.read()))
time.sleep_ms(100)There is no Uno here: an Uno cannot run MicroPython. The Axis class keeps each axis's centre and ends together. Keep your thumb off the stick when you press Run, and stop it with Ctrl-C.
When it does not work
The stick was held over when the board started, so the centre was measured in the wrong place. Let go of the stick and press reset. The sketch reads the centre once, in setup(), a moment after power-up.
Until the stick has shown its ends, the sketch guesses they are 1000 mV either side of the centre, about two thirds of the real swing, so it reaches 100 early. Each time the stick goes past the guess, that end moves out to meet it. Move the stick round its full circle once after start-up and the ends are learned.
That is the dead zone: anything within 10 of the centre reads 0. Make DEAD_ZONE smaller for a finer touch, down to 3 or 4, but not 0, or the reading flickers with the stick let go.
Only near the edge of the dead zone, where a reading hovers either side of 10. That is normal. If it matters, average more readings in averageMv() or react only to changes of more than a few per cent.
Everything on the board at once: a dot the stick moves and the buttons nudge.
Steering a dot →Edit this page — content/books/slim-joystick/a-centre-and-a-dead-zone.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Slim Joystick
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.