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
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 2The 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.
/*
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.11The same menu in MicroPython, for an ESP32, an ESP32-S3 or a Pico. time.ticks_ms() and ticks_diff() time the settle, which stays right when the millisecond counter wraps.
"""
Five-Direction Joystick - a menu, MicroPython 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 -> 3V3 (never 5V: a push to the right puts VCC on the pin)
NC -> nothing: it is connected to nothing on the board
SIGNAL -> 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.
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 SIGNAL is wired to.
# ESP32: 34. ESP32-S3: 4. Pico: 26.
SIGNAL_PIN = 4
NAME = ("right", "down", "left", "press", "up")
LIMIT = (722, 359, 227, 146, 56)
RIGHT, DOWN, LEFT, PRESS, UP = range(5)
# 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.
SETTLE_MS = 10
READ_MS = 5
ITEM = ("Speed", "Level", "Volume")
MAX_VALUE = 9
value = [0, 0, 0]
cursor = 0
ESP = sys.platform == "esp32" # ESP32 and ESP32-S3
adc = ADC(Pin(SIGNAL_PIN))
if ESP:
adc.atten(ADC.ATTN_11DB) # the full range, to about 3.1 V
def read_permille():
if ESP:
return adc.read_uv() // 3300
return adc.read_u16() * 1000 // 65535
def which_push(p):
for i, limit in enumerate(LIMIT):
if p > limit:
return i
return -1
def show():
print("> %s %d" % (ITEM[cursor], value[cursor]))
def act(d): # one push, acted on once
global cursor
if d == UP:
cursor = (cursor - 1) % len(ITEM)
elif d == DOWN:
cursor = (cursor + 1) % len(ITEM)
elif d == LEFT and value[cursor] > 0:
value[cursor] -= 1
elif d == RIGHT and value[cursor] < MAX_VALUE:
value[cursor] += 1
elif d == PRESS:
value[cursor] = 0
show()
candidate = -1 # what the readings say now
since = time.ticks_ms() # since when
believed = -1 # the push acted on
show()
while True:
d = which_push(read_permille())
now = time.ticks_ms()
if d != candidate: # changed: start timing it
candidate = d
since = now
elif time.ticks_diff(now, since) >= SETTLE_MS and d != believed:
believed = d # steady, and new
if d >= 0: # letting go is not a push
act(d)
time.sleep_ms(READ_MS)VCC goes to 3V3 on all three boards. The menu prints to the REPL; stop it with Ctrl-C.
View on GitHub · blocks/tk94-five-direction-joystick/micropython/joystick_menu.py @ v1.11When it does not work
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.
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 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.
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.
Edit this page — content/books/five-direction-joystick/one-step-per-push.mdx
Questions about this product
See what other owners have asked, and read their solutions.
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.