slim joystick/Reading it/09. A centre and a dead zone
Reading it · 09 of 11

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.

A centre and a dead zone
The centre is
Dead zone
Tilt0 %
Output now
-3
Let go
-4 to -2
Centre
1650 mV
The sketch assumes the centre is half of 3V3, 1650 mV, but this stick rests at about 1601 mV. Let go, it reads -4 to -2 instead of 0, so a character creeps across the screen on its own. Measure the centre at start-up instead.

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

joystick_centre.ino
/*
  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.

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

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 averageMv() or react only to changes of more than a few per cent.

Where this goes next

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

Community

Questions about this product

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

Ask a question ↗

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.

Browse Modules and blocks on the forum