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
- Drag Raspberry Pi 4B onto the Canvas.
- 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.
- Drag the Analog Joystick Module onto the Canvas — a real 5-pin module that plugs directly into a breadboard.
- Hover over the joystick's pins to confirm the labels, left to right: +5V, VRy, VRx, SW, GND.
- Wire the MCP3008 to the Pi's fixed SPI pins (physical/BOARD numbering):
| MCP3008 pin | Pi pin |
|---|---|
| VDD | 3.3V |
| VREF | 3.3V |
| AGND | GND |
| DGND | GND |
| CLK | Physical pin 23 (SCLK) |
| DOUT | Physical pin 21 (MISO) |
| DIN | Physical pin 19 (MOSI) |
| CS/SHDN | Physical pin 24 (CE0) |
- Wire the joystick:
| Joystick pin | Connect to |
|---|---|
| +5V | Pi's 3.3V pin (see the note below) |
| GND | Pi's GND pin |
| VRx | MCP3008 CH0 |
| VRy | MCP3008 CH1 |
| SW | Pi'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.
- Go to the Code tab and write a script that reads both axes and the button:
import spidevimport asyncioimport 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)- 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.