Documentation

ShillehTek KY-023 Dual Axis Joystick Module PS2 Analog Sensor for Arduino | ShillehTek Product Manual
Documentation / ShillehTek KY-023 Dual Axis Joystick Module PS2 Analog Sensor for Arduino | ShillehTek Product Manual

ShillehTek KY-023 Dual Axis Joystick Module PS2 Analog Sensor for Arduino | ShillehTek Product Manual

manualshillehtek

Overview

The KY-023 is a dual-axis analog joystick module — basically the same thumbstick mechanism used in PlayStation controllers, broken out onto a small breadboard-friendly PCB. It gives you two independent potentiometers (X and Y axes) plus a momentary push-button switch under the stick, so a single module covers everything from cursor control and motor steering to menu navigation and game controllers.

The two axes output an analog voltage between GND and Vcc that maps directly to the stick's position, so any microcontroller with an ADC can read it. The switch (SW) is just a normally-open contact to ground when you press straight down on the stick — handy for "select" or "fire" actions.

It works with both 3.3V and 5V systems, draws almost no current (it's just a passive divider plus a button), and is the go-to part for robot control, PTZ camera rigs, retro game projects, and any UI that needs an analog input.

At a Glance

Type
Dual-axis analog joystick + button
Operating Voltage
3.3V - 5V
Operating Current
< 5 mA
Output
2 x analog (X, Y) + 1 x digital (SW)
Pin Count
5 pins
Pins
GND, +5V, VRX, VRY, SW

Specifications

Parameter Value
Module Type KY-023 PS2-style dual-axis analog joystick
Operating Voltage 3.3V - 5V DC
Operating Current < 5 mA (passive resistive divider)
X / Y Axis Output Analog 0V - Vcc, ~Vcc/2 at center
Switch (SW) Momentary push-button, normally open, to GND when pressed
Internal Resistance ~10 k-ohm per axis potentiometer
Pin Count 5 (GND, +5V, VRX, VRY, SW)
Mounting 4 x M3 holes
Operating Temperature -10 degC to +70 degC
Dimensions ~40 x 27 mm

Pinout Diagram

KY-023 dual-axis analog joystick module pinout diagram showing the five pins along the bottom edge: GND, +5V, VRX (X-axis analog), VRY (Y-axis analog), and SW (push-button switch)

Wiring Guide

Arduino Wiring

VRX and VRY go to any two analog inputs (A0 - A5 on UNO/Nano). SW is a digital pull-up input — enable INPUT_PULLUP in code so you don't need an external resistor.

KY-023 Pin Arduino Pin
GND GND
+5V 5V
VRX A0
VRY A1
SW D2 (with INPUT_PULLUP)
Tip: The center reading is rarely exactly 512. Sample your stick at rest and store that as the "deadzone center" so small wobbles don't trigger movement.

ESP32 Wiring

Power from 3.3V (do NOT use 5V into a 3.3V ADC). Use any two ADC1 channels for the axes — ADC2 is shared with Wi-Fi and may give bad readings when Wi-Fi is active.

KY-023 Pin ESP32 Pin
GND GND
+5V 3V3
VRX GPIO 34 (ADC1_CH6)
VRY GPIO 35 (ADC1_CH7)
SW GPIO 32 (with INPUT_PULLUP)
Info: Powering the joystick at 3.3V means full-scale ADC values reach ~4095 (12-bit). Don't run the module at 5V into the ESP32 — it can damage the analog input.

Raspberry Pi Wiring

The Pi has no built-in ADC, so you need an external chip such as the MCP3008 (10-bit SPI ADC) to read VRX and VRY. SW can connect directly to a GPIO with internal pull-up.

KY-023 Pin Raspberry Pi Pin
GND Pin 6 (GND)
+5V Pin 1 (3.3V) - power MCP3008 from same rail
VRX MCP3008 CH0
VRY MCP3008 CH1
SW Pin 11 (GPIO 17, internal pull-up)
Tip: The gpiozero library has a built-in MCP3008 class that returns axis values as 0.0 - 1.0 floats — much easier than raw SPI.

Raspberry Pi Pico Wiring

The Pico has three usable ADC inputs: GP26, GP27, and GP28. Power the joystick from 3V3(OUT) — never from VBUS.

KY-023 Pin Pico Pin
GND GND
+5V 3V3 (OUT)
VRX GP26 (ADC0)
VRY GP27 (ADC1)
SW GP15 (with internal pull-up)

Code Examples

Arduino - Read X, Y and Button

ky023_arduino.ino
// KY-023 joystick on Arduino
// VRX -> A0, VRY -> A1, SW -> D2, +5V, GND

const int xPin = A0;
const int yPin = A1;
const int swPin = 2;

void setup() {
  pinMode(swPin, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int x = analogRead(xPin);     // 0 - 1023
  int y = analogRead(yPin);     // 0 - 1023
  int sw = digitalRead(swPin);  // 0 = pressed

  Serial.print("X: "); Serial.print(x);
  Serial.print(" Y: "); Serial.print(y);
  Serial.print(" SW: "); Serial.println(sw == LOW ? "PRESSED" : "released");

  delay(100);
}

Arduino - Direction with Deadzone

ky023_direction.ino
// Convert joystick to discrete directions with a center deadzone

const int xPin = A0;
const int yPin = A1;
const int center = 512;
const int deadzone = 80;

void setup() { Serial.begin(9600); }

void loop() {
  int x = analogRead(xPin) - center;
  int y = analogRead(yPin) - center;

  if (abs(x) < deadzone && abs(y) < deadzone) {
    Serial.println("CENTER");
  } else if (abs(x) > abs(y)) {
    Serial.println(x > 0 ? "RIGHT" : "LEFT");
  } else {
    Serial.println(y > 0 ? "DOWN" : "UP");
  }
  delay(150);
}

Raspberry Pi Pico - MicroPython

ky023_pico.py
# KY-023 on Raspberry Pi Pico (MicroPython)
# VRX -> GP26, VRY -> GP27, SW -> GP15, +5V -> 3V3, GND -> GND

from machine import ADC, Pin
import time

x_axis = ADC(Pin(26))
y_axis = ADC(Pin(27))
sw     = Pin(15, Pin.IN, Pin.PULL_UP)

while True:
    # 16-bit values (0 - 65535)
    x = x_axis.read_u16()
    y = y_axis.read_u16()
    btn = "PRESSED" if sw.value() == 0 else "released"
    print("X:", x, " Y:", y, " SW:", btn)
    time.sleep(0.1)

Raspberry Pi (Python with gpiozero + MCP3008)

ky023_rpi.py
#!/usr/bin/env python3
# KY-023 on Raspberry Pi via MCP3008 ADC
# pip install gpiozero

from gpiozero import MCP3008, Button
import time

x_axis = MCP3008(channel=0)   # 0.0 - 1.0
y_axis = MCP3008(channel=1)
button = Button(17)           # GPIO 17

while True:
    x = x_axis.value
    y = y_axis.value
    pressed = button.is_pressed
    print(f"X: {x:.2f}  Y: {y:.2f}  SW: {'PRESSED' if pressed else 'released'}")
    time.sleep(0.1)

Frequently Asked Questions

Why doesn't my joystick read exactly 512 at center?
That's normal — these thumbsticks are mechanical potentiometers and the center is approximate. Real centers usually fall between 480 and 540 on a 10-bit ADC. Read the resting value once at startup and use it as your zero reference, then apply a small deadzone (~30-80 counts) so light wobbles don't register.
My X and Y feel swapped or reversed.
Module orientation isn't standardized — the labels VRX and VRY refer to the potentiometers, not your physical "left/right" or "up/down". If they're swapped, just swap the pins in code. If an axis is reversed, change value to 1023 - value.
Why does my SW pin read random/floating?
SW is a normally-open switch to GND, so it needs a pull-up. Either enable the microcontroller's internal pull-up with INPUT_PULLUP / Pin.PULL_UP, or add an external 10 k-ohm resistor between SW and Vcc. With pull-up enabled, "released" reads HIGH and "pressed" reads LOW.
Can I use it with a 3.3V microcontroller?
Yes — power the +5V pin from 3.3V instead of 5V. The output range will then be 0 to 3.3V, which is exactly what 3.3V ADCs (ESP32, Pico, STM32) expect. Don't power at 5V if you're feeding the analog outputs into a 3.3V-only ADC; you'll exceed the input range.
How do I use it on a Raspberry Pi (no analog input)?
The Pi has no built-in ADC, so you'll need an external one such as the MCP3008 (8-channel SPI, ~$3). Wire VRX and VRY into two MCP3008 channels and read them over SPI. SW can go directly to a Pi GPIO since it's a digital signal.
Can I daisy-chain or use multiple joysticks?
Yes. Each joystick just needs two ADC inputs and one digital input. On Arduino UNO you have 6 ADCs, so up to 3 joysticks fit comfortably. On ESP32 use ADC1 channels (avoid ADC2 with Wi-Fi). For more, multiplex with a 4051/4067 analog mux or use multiple MCP3008 chips.
Can I use it as a USB game controller?
Yes — combine the KY-023 with a board that supports HID, such as an Arduino Pro Micro, Raspberry Pi Pico, or any ESP32-S2/S3. Read the analog axes and button, then send them as standard USB HID gamepad reports. The Arduino Joystick library and CircuitPython's usb_hid module both make this straightforward.