five-direction joystick/Using it/09. One step per push
Using it · 09 of 10

One step per push

A menu that moves one step per push. Let go of a push and SIGNAL takes about 3 ms to fall to 0, through every lower level, because C3 drains through R14; a sketch that believes every reading acts on each. The menu sketch believes a push only after 10 ms of the same reading, and acts only when it changes.

Letting go is not instant

Letting go
Let go of
The sketch
Prints
0
Pushes that never happened
0
Time constant
1 ms
Let go of right and SIGNAL does not drop at once: C3 holds its charge and drains through R14, falling by about two thirds every 1 ms. Press Play and watch what each sketch prints on the way down.

C3, 100 nF, sits from SIGNAL to GND. While a push is held it charges to that push's level. Let go and every contact opens, and the only way out for the charge is R14, 10 kΩ. SIGNAL falls with a time constant of 10 kΩ × 100 nF, 1 ms: about two thirds of the way to 0 in each millisecond.

On its way down it passes through every lower level. Let go of right and SIGNAL stays in right's own band for a third of a millisecond, spends about 0.7 ms in down's, then crosses left's, the press's and up's, reaching nothing after about 3 ms. Press Play with "Believes every reading": a sketch that acts on every change moves down, left, selects and moves up before it notices you let go.

Pushing is quicker. Right joins SIGNAL to VCC through the contact at once; the others charge C3 through the ladder in about half a millisecond. And the contacts bounce as they close, like every switch's.

Believe a steady reading

The menu sketch reads every 5 ms and keeps two things: what the readings say now, and since when. A push is believed only when the readings have said the same thing for SETTLE_MS, 10 ms. The fall lasts 3 ms, so no level on the way down is ever read twice in a row: it changes before it can be believed.

Then it acts only when the believed push changes. Holding down is one step, not a step every 5 ms. Letting go makes the believed push "nothing", which does nothing, and the next push counts again.

if (d != candidate) {           // changed: start timing it
  candidate = d;
  since = now;
} else if (now - since >= SETTLE_MS && d != believed) {
  believed = d;                 // steady, and new
  if (d >= 0) act(d);           // letting go is not a push
}

What you should see

Up and down move the > between the settings; left and right step the value from 0 to 9; a press sets it back to 0. The serial monitor at 115200 prints one line per push:

> Speed 0
> Speed 1
> Speed 2
> Level 0
> Level 1
> Speed 2

The code

Three settings, Speed, Level and Volume, each 0 to 9. Up and down move the cursor, left and right change the value under it, and a press sets it to 0. loop() reads every 5 ms; a push is believed after 10 ms of the same reading, and act() runs once each time the believed push changes to a new one. Change SIGNAL_PIN to the pin you wired.

joystick_menu.ino
/*
  Five-Direction Joystick - a menu                     TK94 / /p/tk94

  Up and down move through three settings, left and right change
  the one you are on, and a press sets it back to 0. Each push
  counts once, however long you hold it.

  Wiring. Parts up, header at the bottom. Count from the square pad,
  which is GND at the left-hand end:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
    NC     -> nothing: it is connected to nothing on the board
    SIGNAL -> A0 on an Uno, GPIO 34 on an ESP32, GPIO 4 on an
              ESP32-S3, GP26 on a Raspberry Pi Pico       (analog)

  The names are for the block held with the header on the left,
  the way the words on its front read.

  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 analog pin SIGNAL is wired to.
// Uno: A0. ESP32: 34. ESP32-S3: 4. Pico: 26.
const int SIGNAL_PIN = 4;

const char* NAME[] = {"right", "down", "left", "press", "up"};
const int LIMIT[] = {722, 359, 227, 146, 56};
enum { RIGHT, DOWN, LEFT, PRESS, UP };

// A push is believed once it has read the same for SETTLE_MS.
// Letting go takes about 3 ms to fall through the lower levels.
const unsigned long SETTLE_MS = 10;
const unsigned long READ_MS = 5;

const char* ITEM[] = {"Speed", "Level", "Volume"};
const int ITEMS = 3;
const int MAX_VALUE = 9;
int value[ITEMS] = {0, 0, 0};
int cursor = 0;

int candidate = -1;             // what the readings say now
unsigned long since = 0;        // since when
int believed = -1;              // the push the sketch acted on

int readPermille() {
#if defined(ARDUINO_ARCH_ESP32)
  return analogReadMilliVolts(SIGNAL_PIN) * 1000L / 3300;
#else
  return analogRead(SIGNAL_PIN) * 1000L / 1023;
#endif
}

int whichPush(int permille) {
  for (int i = 0; i < 5; i++) {
    if (permille > LIMIT[i]) return i;
  }
  return -1;
}

void show() {
  Serial.print("> ");
  Serial.print(ITEM[cursor]);
  Serial.print(" ");
  Serial.println(value[cursor]);
}

// One push, acted on once.
void act(int d) {
  if (d == UP) cursor = (cursor + ITEMS - 1) % ITEMS;
  if (d == DOWN) cursor = (cursor + 1) % ITEMS;
  if (d == LEFT && value[cursor] > 0) value[cursor]--;
  if (d == RIGHT && value[cursor] < MAX_VALUE) value[cursor]++;
  if (d == PRESS) value[cursor] = 0;
  show();
}

void setup() {
  Serial.begin(115200);
  show();
}

void loop() {
  int d = whichPush(readPermille());
  unsigned long now = millis();
  if (d != candidate) {           // changed: start timing it
    candidate = d;
    since = now;
  } else if (now - since >= SETTLE_MS && d != believed) {
    believed = d;                 // steady, and new
    if (d >= 0) act(d);           // letting go is not a push
  }
  delay(READ_MS);
}

Letting go makes the believed push -1, nothing, and runs nothing, so the next push counts again. The same readPermille() and lines as the first read. The sketch compiles for an Uno, an ESP32 and an ESP32-S3.

View on GitHub · blocks/tk94-five-direction-joystick/arduino/joystick_menu/joystick_menu.ino @ v1.11

When it does not work

Letting go of right moves the cursor down as well.

The sketch is acting on the fall. After a release SIGNAL passes through down, left, the press and up on its way to 0, about 3 ms in all. Believe a reading only once it has held for 10 ms, as SETTLE_MS does here, and the fall never counts.

Holding a direction does not repeat.

On purpose: the sketch acts when the believed push changes, and a held push does not change. For auto-repeat, keep a time of the last action and act again every 300 ms or so while the same push is still believed.

A quick tap is sometimes missed.

A push has to read the same for SETTLE_MS, 10 ms, and the sketch reads every 5 ms, so a tap under about 15 ms can slip through. A thumb's tap is several times longer. Lower SETTLE_MS if yours are faster; do not go under the 3 ms fall.

Rolling from left to up presses select.

Up and left together read as a press, and a roll that holds both for 10 ms is believed. Make the press ask for a longer hold, or put select somewhere a diagonal cannot reach, as two at once explains.

Where this goes next

Six symptoms, and where to look for each.

When a reading is wrong →

Edit this page — content/books/five-direction-joystick/one-step-per-push.mdx

Community

Questions about this product

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

Ask a question ↗

Five-Direction 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 →