A thumb joystick is two potentiometers and a push switch. On a Pico W the pots go to two of the three ADC pins, GP26 and GP27, and the switch to GP22 with the internal pull-up.
Power the module from 3V3 even though the pin says +5V. The script calibrates the centre at start-up and prints X and Y from −100 % to +100 % with a direction label.
Wiring
| Raspberry Pi Pico W | Dual-axis joystick module (KY-023) | Wire | Notes |
|---|---|---|---|
| GND | GND | black | |
| 3V3 | +5V | red | Yes, 3V3 (pin 36) into the pin printed +5V — it is just the pot supply. At 5 V, VRx/VRy would exceed the ADC limit. |
| GP26 | VRx | green | GP26 = ADC0, physical pin 31. |
| GP27 | VRy | blue | GP27 = ADC1, physical pin 32. |
| GP22 | SW | yellow | GP22 is physical pin 29. The internal pull-up is enabled in code. |
3.3–5 V part, 3.3 V board. VRx and VRy swing between 0 V and the module’s supply. On ESP32 and Pico, power the joystick from 3.3 V (the pin labelled +5V is just the supply input) so the analog outputs never exceed 3.3 V.
Board quirks
- The Pico W has exactly three ADC pins on the header (GP26–GP28), so one joystick uses two of them. A second joystick needs an external ADC such as an ADS1115.
- Sticks rarely rest at exact mid-scale; the script measures the rest position at start-up and adds a 6 % dead zone so it reads 0 when you let go.
- SW shorts to GND when you press the stick, so it reads 0 when pressed (pull-up enabled with
Pin.PULL_UP).
Code
# Analog joystick module (KY-023 style) on a Raspberry Pi Pico W (MicroPython).
# Wiring: +5V -> 3V3 (pin 36, yes, 3.3 V), GND -> GND, VRx -> GP26 / ADC0 (pin 31),
# VRy -> GP27 / ADC1 (pin 32), SW -> GP22 (pin 29) with the internal pull-up.
from machine import ADC, Pin
import time
DEADZONE = 6 # percent around the centre treated as "no movement"
vrx = ADC(26)
vry = ADC(27)
sw = Pin(22, Pin.IN, Pin.PULL_UP) # the button pulls SW to GND when pressed
# Each stick rests slightly off-centre: measure the rest position at start-up.
cx = sum(vrx.read_u16() for _ in range(16)) // 16
cy = sum(vry.read_u16() for _ in range(16)) // 16
print("Centre calibrated: x={} y={}".format(cx, cy))
def axis(raw, centre):
"""Map 0..centre..65535 to -100..0..+100 %."""
if raw >= centre:
v = (raw - centre) * 100 // max(65535 - centre, 1)
else:
v = -((centre - raw) * 100 // max(centre, 1))
return 0 if abs(v) < DEADZONE else v
def direction(x, y):
if x == 0 and y == 0:
return "centre"
parts = []
if y > 0:
parts.append("down")
elif y < 0:
parts.append("up")
if x > 0:
parts.append("right")
elif x < 0:
parts.append("left")
return "-".join(parts)
while True:
x = axis(vrx.read_u16(), cx)
y = axis(vry.read_u16(), cy)
pressed = sw.value() == 0
print("X {:+4d} % Y {:+4d} % {:12s} {}".format(x, y, direction(x, y), "PRESSED" if pressed else ""))
time.sleep_ms(150)# Analog joystick module (KY-023 style) on a Raspberry Pi Pico W (MicroPython).
# Wiring: +5V -> 3V3 (pin 36, yes, 3.3 V), GND -> GND, VRx -> GP26 / ADC0 (pin 31),
# VRy -> GP27 / ADC1 (pin 32), SW -> GP22 (pin 29) with the internal pull-up.
from machine import ADC, Pin
import time
DEADZONE = 6 # percent around the centre treated as "no movement"
vrx = ADC(26)
vry = ADC(27)
sw = Pin(22, Pin.IN, Pin.PULL_UP) # the button pulls SW to GND when pressed
# Each stick rests slightly off-centre: measure the rest position at start-up.
cx = sum(vrx.read_u16() for _ in range(16)) // 16
cy = sum(vry.read_u16() for _ in range(16)) // 16
print("Centre calibrated: x={} y={}".format(cx, cy))
def axis(raw, centre):
"""Map 0..centre..65535 to -100..0..+100 %."""
if raw >= centre:
v = (raw - centre) * 100 // max(65535 - centre, 1)
else:
v = -((centre - raw) * 100 // max(centre, 1))
return 0 if abs(v) < DEADZONE else v
def direction(x, y):
if x == 0 and y == 0:
return "centre"
parts = []
if y > 0:
parts.append("down")
elif y < 0:
parts.append("up")
if x > 0:
parts.append("right")
elif x < 0:
parts.append("left")
return "-".join(parts)
while True:
x = axis(vrx.read_u16(), cx)
y = axis(vry.read_u16(), cy)
pressed = sw.value() == 0
print("X {:+4d} % Y {:+4d} % {:12s} {}".format(x, y, direction(x, y), "PRESSED" if pressed else ""))
time.sleep_ms(150)Expected serial output
Centre calibrated: x=32418 y=33102
X +0 % Y +0 % centre
X +87 % Y -4 % right
X -52 % Y -63 % up-left PRESSEDCentre calibrated: x=32418 y=33102 X +0 % Y +0 % centre X +87 % Y -4 % right X -52 % Y -63 % up-left PRESSED
Common errors
Axes swapped or reversed
Depends how the module is mounted. Swap VRx/VRy in code, or negate a value.
Button always shows PRESSED
SW is on the wrong pin or the pull-up is missing. Keep Pin.PULL_UP on GP22.