Arduino Tutorial: KY-023 Joystick Module (XY + Push Button)

Advanced Tutorial Views: 824

This tutorial teaches you how to use the KY-023 Joystick Module with Arduino projects. You will learn the electrical behavior (two analog axes + one digital switch), correct wiring, stable reading, calibration (center + range), deadzone filtering, and how to drive real outputs like servos, motors, and menus.

Arduino Joystick Tutorial: KY-023 (2-Axis + Push Button) — Wiring, Code, Calibration & Projects

This tutorial teaches you how to use the KY-023 Joystick Module with Arduino projects. You will learn the electrical behavior (two analog axes + one digital switch), correct wiring, stable reading, calibration (center + range), deadzone filtering, and how to drive real outputs like servos, motors, and menus.

Tutorial Beginner ? Advanced Arduino User Interface Analog Inputs Robotics
What you get: 2 analog voltages (X and Y) from two 5K potentiometers, plus a momentary push button that closes when you press the joystick down. The module exposes 5 pins: VCC, GND, X, Y, Button.

1) What the KY-023 is and how it works

The KY-023 joystick module is essentially two potentiometers arranged at right angles (X and Y), plus a push button. As you move the stick, each potentiometer outputs an analog voltage proportional to position. When you press the stick down, a normally-open switch closes.

  • X axis: analog voltage for left/right movement
  • Y axis: analog voltage for up/down movement
  • Button: digital input (pressed or not pressed)
Important: Analog sticks are not perfect. The “center” will drift slightly and the extremes will rarely be exactly 0V and VCC. This is why calibration and a deadzone are essential for smooth projects.

2) Pins, voltages, and expected readings

The module provides 5 pins: VCC, GND, X, Y and Button. When powered at 5V, the joystick outputs around 2.5V at rest (center position) on both X and Y. Moving the stick pushes the axis voltage toward 0V in one direction and toward 5V in the opposite direction.

What Arduino “sees”

  • On UNO/Nano/Mega (10-bit ADC): analogRead() returns 0–1023
  • At center (5V supply): typically around ~512 (not always exactly)
  • Button is a switch: use a digital pin with pull-up or pull-down logic
Specification note: The module uses 2 × 5K potentiometers and a normally-open momentary switch that closes with downward pressure.

3) Wiring to Arduino (UNO/Nano/Mega) and 3.3V boards

3.1 Arduino UNO / Nano / Mega wiring

KY-023 Pin Arduino Pin Notes
VCC 5V Power the module from Arduino 5V
GND GND Common ground is required
X A0 Analog input
Y A1 Analog input
Button D2 Digital input with internal pull-up enabled

3.2 Using 3.3V microcontrollers (ESP32 / ESP8266 / Raspberry Pi Pico)

Power the joystick from 3.3V if your board’s analog pins are not 5V-tolerant. The joystick outputs scale with VCC, so “center” becomes ~1.65V and extremes become 0–3.3V. The concept is identical; your analog range changes.

Do not feed 5V analog outputs into a 3.3V-only ADC. If you must power the joystick at 5V, use a resistor divider or level shifting for X/Y (and consider the button input level as well).

4) Basic Arduino code (serial monitor test)

This sketch prints X, Y and Button values to the Serial Monitor so you can confirm wiring and observe real ranges.


/*
  KY-023 Joystick Module Test
  - X axis  -> A0
  - Y axis  -> A1
  - Button  -> D2 (uses INPUT_PULLUP)
*/

const int PIN_X = A0;
const int PIN_Y = A1;
const int PIN_BTN = 2;

void setup() {
  Serial.begin(115200);

  pinMode(PIN_BTN, INPUT_PULLUP); // Button closes to GND (common wiring approach)
}

void loop() {
  int x = analogRead(PIN_X);     // 0..1023 on UNO/Nano/Mega
  int y = analogRead(PIN_Y);
  bool pressed = (digitalRead(PIN_BTN) == LOW); // LOW when pressed (because pull-up)

  Serial.print("X=");
  Serial.print(x);
  Serial.print("  Y=");
  Serial.print(y);
  Serial.print("  BTN=");
  Serial.println(pressed ? "PRESSED" : "released");

  delay(50);
}
    
Expected behavior: At rest you will usually see X and Y near the middle of the ADC range. As you push to extremes, values move toward the ends. The button reads “pressed” only when you click the stick down.

5) Calibration (center, min/max) and deadzone filtering

For real projects, you want stable “centered” behavior and predictable full-range mapping. Two practical steps solve most joystick problems:

5.1 Determine center values

  • Read X/Y at rest for ~1–2 seconds and average them to get centerX and centerY.
  • Save these as constants or perform the average on startup.

5.2 Apply a deadzone

Deadzone means: if X or Y is “close enough” to the center, treat it as exactly centered. This prevents drift and jitter.


int applyDeadzone(int value, int center, int deadzone) {
  if (abs(value - center) <= deadzone) return center;
  return value;
}
    
Typical deadzone values: 10–40 counts on a 10-bit ADC (UNO/Nano/Mega), depending on module variance and wiring noise.

6) Mapping joystick values to useful ranges

Most projects want values like -100..+100, or servo angles 0..180, or motor PWM 0..255. After calibration, map relative to center:

6.1 Normalize to -1.0 .. +1.0 (conceptually clean)


// For UNO/Nano/Mega where analogRead is 0..1023
float normalizeAxis(int value, int center) {
  // Pick a conservative span; you can also store measured min/max.
  const float span = 512.0; // approx half-scale
  float v = (value - center) / span;
  if (v > 1.0) v = 1.0;
  if (v < -1.0) v = -1.0;
  return v;
}
    

6.2 Map to servo angle 0..180


int axisToServoAngle(float axis) {
  // axis in [-1..+1] ? 0..180
  int angle = (int)(90 + axis * 90);
  if (angle < 0) angle = 0;
  if (angle > 180) angle = 180;
  return angle;
}
    

6.3 Map to motor throttle -255..+255


int axisToMotor(float axis) {
  int pwm = (int)(axis * 255);
  if (pwm > 255) pwm = 255;
  if (pwm < -255) pwm = -255;
  return pwm;
}
    

7) Project examples (Arduino-focused)

Example A: Pan/tilt servo control (2 servos)

Use X to pan and Y to tilt. This is a classic joystick project.


#include <Servo.h>

const int PIN_X = A0;
const int PIN_Y = A1;
const int PIN_BTN = 2;

Servo servoPan;
Servo servoTilt;

int centerX = 512;
int centerY = 512;
const int deadzone = 20;

int applyDeadzone(int value, int center, int dz) {
  if (abs(value - center) <= dz) return center;
  return value;
}

float normalizeAxis(int value, int center) {
  const float span = 512.0;
  float v = (value - center) / span;
  if (v > 1.0) v = 1.0;
  if (v < -1.0) v = -1.0;
  return v;
}

int axisToAngle(float axis) {
  int angle = (int)(90 + axis * 90);
  if (angle < 0) angle = 0;
  if (angle > 180) angle = 180;
  return angle;
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_BTN, INPUT_PULLUP);

  servoPan.attach(9);
  servoTilt.attach(10);

  // Optional: quick center calibration on startup
  long sx = 0, sy = 0;
  for (int i = 0; i < 50; i++) {
    sx += analogRead(PIN_X);
    sy += analogRead(PIN_Y);
    delay(5);
  }
  centerX = (int)(sx / 50);
  centerY = (int)(sy / 50);
}

void loop() {
  int x = applyDeadzone(analogRead(PIN_X), centerX, deadzone);
  int y = applyDeadzone(analogRead(PIN_Y), centerY, deadzone);

  float nx = normalizeAxis(x, centerX);
  float ny = normalizeAxis(y, centerY);

  servoPan.write(axisToAngle(nx));
  servoTilt.write(axisToAngle(ny));

  // Button could reset to center, or toggle “hold position”
  if (digitalRead(PIN_BTN) == LOW) {
    servoPan.write(90);
    servoTilt.write(90);
    delay(200);
  }

  delay(15);
}
    

Example B: Motor speed and steering (robot car)

Use Y as throttle and X as steering. You can feed the result into a motor driver (L298N, TB6612FNG, etc.) by converting to PWM and direction pins.

Implementation note: Most motor drivers want two signals per motor: direction (IN1/IN2) and PWM speed (ENA). Convert joystick axis to signed throttle values.

Example C: Menu navigation (OLED/LCD projects)

Use X or Y to move selection and the button to “select”. Add a larger deadzone and a “repeat delay” so it behaves like a D-pad.


8) Noise, jitter, and stability best practices

  • Use short wires for X/Y analog lines. Long wires pick up noise.
  • Add a deadzone so the system does not drift.
  • Use averaging or a light low-pass filter if you need smoother control: average 4–16 samples per read.
  • Ensure the Arduino GND is shared with the joystick module.
  • Do not power noisy loads (motors/relays) from the same rail without proper decoupling.
Common reason for jitter: powering motors from the same 5V rail as the Arduino + joystick, causing voltage dip and ADC noise. Use separate motor power where possible, and keep grounds properly tied.

9) Troubleshooting: common problems and fixes

Problem: X and Y always read ~1023 or ~0

  • Likely cause: X/Y wired to the wrong pins (not analog) or missing ground reference.
  • Fix: verify GND, verify X?A0, Y?A1, verify VCC.

Problem: Center is not ~512 and drifts

  • Normal: the resting point can vary.
  • Fix: implement startup calibration + deadzone.

Problem: Button reads “pressed” all the time

  • Likely cause: no pull-up/pull-down, or wiring expects pull-up logic.
  • Fix: use pinMode(BTN, INPUT_PULLUP) and interpret LOW as pressed.

Problem: Values jump when motors run

  • Cause: electrical noise and ground bounce.
  • Fix: separate power rails, add decoupling, shorten analog wires, average readings.

10) Quick checklist (copy/paste)


KY-023 + Arduino Checklist
-------------------------
? VCC -> 5V (UNO/Nano/Mega) or 3.3V (3.3V-only ADC boards)
? GND -> GND (common ground required)
? X -> A0 (analog)
? Y -> A1 (analog)
? Button -> D2 (digital) with INPUT_PULLUP enabled
? Confirm center values in Serial Monitor
? Implement deadzone (10–40 counts typical on 10-bit ADC)
? Calibrate center on startup (optional but recommended)
? Average samples if jitter is visible
? Keep analog wires short; avoid motor noise on same rail
    

Products that this may apply to