dual axis joystick/Reading it/07. A centre and a dead zone
Reading it · 07 of 10

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.

A centre and a dead zone
The centre is
Dead zone
Tilt0 %
Output now
-2
Let go
-3 to 0
Centre
50.0 % of scale
The sketch assumes the centre is half of full scale, but this stick rests 1.2 per cent below it. Let go, it reads -3 to 0 instead of 0, so whatever it steers creeps on its own. Measure the centre at start-up instead.

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 0

Tilted, 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_joystick_centre.ino
/*
  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.

When it does not work

It reads a steady 30 with the stick let go.

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.

At first it reads 100 before the end of the travel.

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.

Small movements do nothing.

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.

The numbers flicker by one or two even with a dead zone.

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.

Where this goes next

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

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

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.

Browse Modules and blocks on the forum