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.
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)
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
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.
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);
}
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;
}
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.
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.
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