dual axis joystick/Using it/09. Eight directions
Using it · 09 of 10

Eight directions

A menu, a robot or a game usually wants a direction, not two numbers. Measure how far the stick is from the centre, and if it is past the dead zone, cut the circle into eight 45° slices and name the one it is in. A round dead zone keeps a slight diagonal from snapping onto an axis.

From two numbers to one word

The last sketch turns the stick into two numbers from −100 to 100. Most projects then ask a simpler question: which way is it pushed? Two steps answer it. First, is the stick far enough from the centre to count? Second, if it is, which direction is it nearest to?

Eight directions
Dead zone
Directions
Angle20°
Distance40
Round zone
right
Square zone
right
Slice
45°
X 38, Y 14: 20° from the X axis, in the right slice. Each slice is 45° wide, centred on its direction, so a thumb has 22.5° of slack either side before the answer changes.

Move the stick round with the two sliders and watch which slice lights. Each slice is centred on its direction and 45° wide, so there is 22.5° of slack either side. Now switch the dead zone to square and push gently just off a diagonal: the two answers part company.

A round dead zone

The last article's dead zone works on each axis alone: anything under 10 on X is 0 on X. Drawn on the circle, that is a cross of two bands, and near the middle a slight diagonal loses its smaller half and snaps onto an axis. For a direction, the dead zone should be a distance instead: the stick counts once x * x + y * y is at least DEAD_ZONE * DEAD_ZONE, whichever way it points. Comparing squares saves a square root and gives the same answer.

Which slice

atan2(y, x) gives the stick's angle from the X axis, from −180° to 180° in radians. Dividing by 45° and rounding gives a whole number from −4 to 4, and adding 8 and taking the remainder turns it into 0 to 7, counting round from right. NAME[] is the same list in that order.

For four directions, divide by 90° instead and use every second name.

Which way is up

The sketch calls positive Y up and positive X right, and your stick may disagree. From the first read you know which number moved when you pushed towards the header: if it was X, as the drawing suggests, set SWAP_XY to true. Then push each way and flip X_SIGN or Y_SIGN for any direction that comes out backwards.

What you should see

Push the stick round a full circle and let go, then click:

right
up-right
up
up-left
left
down-left
down
down-right
centre
click

Each name prints once, when it changes, and the click once per press.

The code

The stick as one of eight directions or centre, printed only when it changes, and a click printed once per press. The mapping is the last article's; the dead zone is now a distance from the centre.

dual_joystick_directions.ino
/*
  Dual Axis Joystick - eight directions                  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;

#if defined(ARDUINO_ARCH_ESP32)
const float ADC_MAX = 4095;
#else
const float ADC_MAX = 1023;
#endif

const int DEAD_ZONE = 10;       // as a distance from the centre
const int EDGE_GUESS = 30;      // per cent of full scale, until learned
const unsigned long DEBOUNCE_MS = 20;

// From the first read: SWAP_XY true if X moved when you pushed towards
// and away from the header; a sign -1 if that direction came out wrong.
const bool SWAP_XY = false;
const int X_SIGN = 1;
const int Y_SIGN = 1;

const char* NAME[] = {"right", "up-right", "up", "up-left",
                      "left", "down-left", "down", "down-right"};

float centreX, loX, hiX, centreY, loY, hiY;
int lastDir = -2;               // -1 is centre; -2 is not yet known
int lastSw = LOW;
unsigned long quietUntil = 0;

float average(int pin) {
  float sum = 0;
  for (int i = 0; i < 16; i++) sum += analogRead(pin);
  return sum / 16;
}

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);
}

// 0..7 for the eight slices, counting round from right; -1 for centre.
int direction(int x, int y) {
  if (x * x + y * y < DEAD_ZONE * DEAD_ZONE) return -1;
  float a = atan2((float)y, (float)x);    // -pi..pi
  int i = (int)round(a / (PI / 4));
  return (i + 8) % 8;
}

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 a = axis(average(X_PIN), centreX, loX, hiX);
  int b = axis(average(Y_PIN), centreY, loY, hiY);
  int x = X_SIGN * (SWAP_XY ? b : a);
  int y = Y_SIGN * (SWAP_XY ? a : b);

  int d = direction(x, y);
  if (d != lastDir) {
    Serial.println(d < 0 ? "centre" : NAME[d]);
    lastDir = d;
  }

  unsigned long now = millis();
  if (now >= quietUntil) {
    int sw = digitalRead(SW_PIN);
    if (sw != lastSw) {
      if (sw == HIGH) Serial.println("click");
      lastSw = sw;
      quietUntil = now + DEBOUNCE_MS;
    }
  }
  delay(10);
}

After the first read, set SWAP_XY, X_SIGN and Y_SIGN so that pushing away from you prints up. The click uses the millis() wait from the last article, so the loop keeps reading the stick while SW settles.

When it does not work

Up and down come out as left and right.

X and Y are the pots' names, not directions, and on this board X probably follows the tilt towards and away from the header. Set SWAP_XY to true. If a direction then comes out backwards, set X_SIGN or Y_SIGN to -1.

It flickers between two directions.

The stick is sitting on the line between two slices, where a jitter of one count changes the answer. Hold it a little further in, or print a new direction only once it has held for two readings in a row.

A diagonal is hard to hit.

Eight slices are 45° each, so a diagonal is as wide as a straight direction, but a thumb tends to push along the axes. If your project only needs four, use four slices of 90°: the figure shows the difference.

It prints click twice for one press.

The switch chattered for longer than DEBOUNCE_MS. Raise it to 30 or 50 ms; a person cannot click fast enough for that to matter.

Where this goes next

Six symptoms, and the light or the wire to check first for each.

When a reading is wrong

Edit this page — content/books/dual-axis-joystick/eight-directions.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