Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.

Joystick module with Raspberry Pi Pico W — MicroPython X/Y and button

Read an analog joystick on a Pico W: VRx/VRy on ADC0/ADC1 (GP26/GP27) and SW on GP22, powered from 3V3, with centre calibration and a dead zone.

MicroPythonVerified — micropython (mpy-cross v1.29.0), 3 Nov 2026Compile-only

Community draft — report a mistake

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 WOnly the pins used are shown · 3.3 V logicDual-axis joystick module (KY-023)GND3V3GP27GP26GP22GND+5VVRxVRySW
Every connection
Raspberry Pi Pico WDual-axis joystick module (KY-023)WireNotes
GNDGNDblack
3V3+5VredYes, 3V3 (pin 36) into the pin printed +5V — it is just the pot supply. At 5 V, VRx/VRy would exceed the ADC limit.
GP26VRxgreenGP26 = ADC0, physical pin 31.
GP27VRyblueGP27 = ADC1, physical pin 32.
GP22SWyellowGP22 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

main.pyMicroPython
# 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

REPL / Thonny shellOutput
Centre 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.

↑↓ to move · Enter to open