Learn

Reading an Analog Joystick Module (X, Y and Button) on a Raspberry Pi 4B

Wire an analog joystick to a Raspberry Pi 4B: read its X and Y axes through an MCP3008 ADC on two separate channels, and read its push-button directly on a GPIO pin.

Note: the Analog Joystick Module is a VIP-tier component in the simulator — you’ll need a VIP plan to use it there.

Try this directly in the free Raspberry Pi 4B simulator — no hardware or signup required. New to the GPIO header? Start with the interactive pinout guide.

What you’ll need

  • Raspberry Pi 4B
  • MCP3008 ADC
  • Analog Joystick Module

Step by step

  1. Drag Raspberry Pi 4B onto the Canvas.
  2. Drag the MCP3008 ADC onto the Canvas. The Pi can't read analog voltages, so the joystick's two analog axes have to go through this chip.
  3. Drag the Analog Joystick Module onto the Canvas — a real 5-pin module that plugs directly into a breadboard.
  4. Hover over the joystick's pins to confirm the labels, left to right: +5V, VRy, VRx, SW, GND.
  5. Wire the MCP3008 to the Pi's fixed SPI pins (physical/BOARD numbering):
MCP3008 pinPi pin
VDD3.3V
VREF3.3V
AGNDGND
DGNDGND
CLKPhysical pin 23 (SCLK)
DOUTPhysical pin 21 (MISO)
DINPhysical pin 19 (MOSI)
CS/SHDNPhysical pin 24 (CE0)
  1. Wire the joystick:
Joystick pinConnect to
+5VPi's 3.3V pin (see the note below)
GNDPi's GND pin
VRxMCP3008 CH0
VRyMCP3008 CH1
SWPi's physical pin 37 (a GPIO pin)

The pin is labelled +5V because most real modules accept 3.3V–5V. Power it from 3.3V here: the MCP3008 measures against its 3.3V VREF, so a 5V joystick would push the axes above what the chip can read (the reading would flatten at 1023 and the Console shows a warning). VRx and VRy are two separate potentiometers, so each axis goes to its own MCP3008 channel, and SW is a separate push-button that goes straight to the Pi.

  1. Go to the Code tab and write a script that reads both axes and the button:
import spidev
import asyncio
import RPi.GPIO as GPIO
 
GPIO.setmode(GPIO.BOARD)
GPIO.setup(37, GPIO.IN, pull_up_down=GPIO.PUD_UP) # the module has no pull-up of its own
 
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1350000
 
def read_channel(channel):
adc = spi.xfer2([1, (8 + channel) << 4, 0])
return ((adc[1] & 3) << 8) + adc[2] # returns 0-1023
 
while True:
x = read_channel(0) # VRx
y = read_channel(1) # VRy
pressed = GPIO.input(37) == 0 # LOW = pressed
print(f"X={x} Y={y} Button={'PRESSED' if pressed else 'released'}")
await asyncio.sleep(0.3)
  1. Click Start.

Drag the thumbstick (the big black cap) with the mouse to tilt it — it springs back to the center when you let go. A quick click on the cap without moving presses the button. To hold the stick at a position, select the module and untick "Auto-center on release" in its settings panel (while stopped).

What “working correctly” looks like

  • With the stick untouched, X and Y both read about 512 (the middle of 0–1023).
  • Dragging right raises X toward 1023 and left lowers it toward 0; dragging down raises Y toward 1023 and up lowers it toward 0. The two axes move independently.
  • A quick click on the cap prints Button=PRESSED while it's held, whatever position the stick is in, and returns to released when you let go.

If something’s wrong

  • Both axes always read 0 → check the MCP3008's VDD, VREF, AGND and DGND are all wired, and that CS goes to physical pin 24 (CE0), the pin the script opens with spi.open(0, 0).
  • Both axes seem stuck at the same value, or X and Y look swapped → confirm VRx goes to CH0 and VRy to CH1, matching read_channel(0) and read_channel(1) in the script.
  • An axis flattens at 1023 and the Console shows a warning → the joystick is powered from 5V; move its +5V wire to a 3.3V pin.
  • The button always reads released → make sure SW goes to the pin you set up (physical pin 37) and that the script uses pull_up_down=GPIO.PUD_UP, and remember the press is a click on the cap without dragging.
  • The stick doesn't return to center → the "Auto-center on release" setting is off; turn it back on while the simulation is stopped.