A centre and a dead zone
A stick let go rests near the middle, not on it, and jitters by a few counts. 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.
Set the centre to half of full scale and the stick let go reads a few per cent off 0, and flickers; switch to a measured centre and it averages 0; add a dead zone and it holds 0. Everything in the figure is a share of full scale, so it looks the same on every board.
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 full scale instead and a stick resting a per cent or two off it reads a
few per cent with nobody touching it, and whatever it steers creeps.
The ends are learned. The sketch starts with a guess, 30 per cent of full scale either side of the centre, and each time a reading goes past an end, the end moves out to meet it. 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. Learning the ends also covers an ESP32, whose ADC runs out before the stick does.
The dead zone
Even with the centre measured, a stick let go jitters, 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 SW 0
X 0 Y 0 SW 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 eight-directions sketch 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, and the click. The centre is measured in setup(), the ends are learned as the stick reaches them, and a dead zone of 10 swallows the jitter.
/*
Dual Axis Joystick - a centre and a dead zone TK23 / /p/tk23
Wiring. Parts up, header along the bottom. Count from the square pad,
which is GND at the left-hand end, rightwards:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
NC -> nothing (in no net on the board)
X -> A0 on an Uno, GPIO 34 on an ESP32, GPIO 4 on an
ESP32-S3, GP26 on a Raspberry Pi Pico (analog)
Y -> A1, GPIO 35, GPIO 5, GP27 (same order, analog)
SW -> 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 and SW are wired to.
// Uno: A0 A1 2. ESP32: 34 35 25. ESP32-S3: 4 5 7. Pico: 26 27 15.
const int X_PIN = 4;
const int Y_PIN = 5;
const int SW_PIN = 7;
// The ADC's top count.
#if defined(ARDUINO_ARCH_ESP32)
const float ADC_MAX = 4095;
#else
const float ADC_MAX = 1023;
#endif
const int DEAD_ZONE = 10; // per cent either side of the centre
const int EDGE_GUESS = 30; // per cent of full scale, until learned
float centreX, loX, hiX; // measured, and the furthest seen
float centreY, loY, hiY;
// The mean of 16 readings: steadier than one.
float average(int pin) {
float sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(pin);
return sum / 16;
}
// -100..100 from the centre, widening the ends as the stick finds them.
int axis(float v, float centre, float &lo, float &hi) {
if (v < lo) lo = v;
if (v > hi) hi = v;
float p;
if (v >= centre) p = (v - centre) * 100.0 / (hi - centre);
else p = (v - centre) * 100.0 / (centre - lo);
return constrain((int)p, -100, 100);
}
void setup() {
Serial.begin(115200);
pinMode(SW_PIN, INPUT); // R10 on the board pulls it down
delay(200); // hands off the stick
float guess = ADC_MAX * EDGE_GUESS / 100.0;
centreX = average(X_PIN);
centreY = average(Y_PIN);
loX = centreX - guess;
hiX = centreX + guess;
loY = centreY - guess;
hiY = centreY + guess;
}
void loop() {
int x = axis(average(X_PIN), centreX, loX, hiX);
int y = axis(average(Y_PIN), centreY, loY, hiY);
if (abs(x) < DEAD_ZONE) x = 0; // the dead zone
if (abs(y) < DEAD_ZONE) y = 0;
Serial.print("X ");
Serial.print(x);
Serial.print(" Y ");
Serial.print(y);
Serial.print(" SW ");
Serial.println(digitalRead(SW_PIN));
delay(100);
}Keep your thumb off the stick while the board starts: the first readings are the centre. ADC_MAX is picked for you: 4095 on an ESP32 or ESP32-S3, 1023 on an Uno or a Pico. Move the stick round its whole circle once and the ends 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.
"""
Dual Axis Joystick - a centre and a dead zone, MicroPython
Wiring. Parts up, header along the bottom. Count from the square pad,
which is GND at the left-hand end, rightwards:
GND -> GND
VCC -> 3V3 (never 5V: X, Y and SW all reach VCC)
NC -> nothing (in no net on the board)
X -> GPIO 34 on an ESP32, GPIO 4 on an ESP32-S3,
GP26 on a Raspberry Pi Pico (analog)
Y -> GPIO 35, GPIO 5, GP27 (same order, analog)
SW -> 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 and SW are wired to.
# ESP32: 34 35 25. ESP32-S3: 4 5 7. Pico: 26 27 15.
X_PIN, Y_PIN, SW_PIN = 4, 5, 7
FULL = 65535 # read_u16 on every board
DEAD_ZONE = 10 # per cent either side of the centre
EDGE_GUESS = 30 # per cent of full scale, until learned
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 average(adc):
return sum(adc.read_u16() for _ in range(16)) / 16
class Axis:
def __init__(self, pin):
self.adc = analog(pin)
self.centre = average(self.adc) # hands off the stick
guess = FULL * EDGE_GUESS / 100
self.lo = self.centre - guess
self.hi = self.centre + guess
def read(self):
v = average(self.adc)
self.lo = min(self.lo, v)
self.hi = max(self.hi, v)
if v >= self.centre:
p = (v - self.centre) * 100 / (self.hi - self.centre)
else:
p = (v - 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
sw = Pin(SW_PIN, Pin.IN) # R10 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 SW %d" % (x_axis.read(), y_axis.read(),
sw.value()))
time.sleep_ms(100)There is no Uno here: an Uno cannot run MicroPython. read_u16 gives 0 to 65535 on every board, so FULL is the same everywhere. 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 30 per cent of full scale either side of the centre, well short of the real ends, 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.
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 average() or react only to changes of more than a few per cent.
Why one press can read as several, and the two lines that fix it.
A click that counts once →Edit this page — content/books/dual-axis-joystick/a-centre-and-a-dead-zone.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Dual Axis 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.