The standard KY-023 Arduino joystick module is a dual-axis analog input device featuring two 10kΩ potentiometers for X/Y movement and a normally-open tactile switch for the Z-axis (pushbutton). It operates natively at 5V, outputs analog values from 0-1023 on a 10-bit ADC, and typically costs between $1.50 and $3.00 USD. While electrically simple, mechanical wiper tolerances, unshielded analog traces, and floating switch pins cause the vast majority of integration headaches. This guide provides the exact pinout, production-ready C++ firmware with deadzone mapping, and the specific troubleshooting paths for when your hardware fails to register inputs.

Hardware Spec Sheet & Parts List

Before wiring, verify you have the correct module variant. The KY-023 is the most common, but some manufacturers label it simply as a "PS2 Joystick Module". Ensure the breakout board includes the dual 10kΩ pots and the tactile switch.

Component Specification / Variant Notes
Microcontroller Arduino Uno R3 or Nano v3 (ATmega328P) 5V logic, 10-bit ADC (0-1023 range)
Joystick Module KY-023 Dual-Axis Analog 10kΩ linear taper pots, NO tactile switch
Wiring 5x Female-to-Male Dupont Jumpers 22 AWG stranded, keep under 15cm to reduce noise
Decoupling Cap 0.1μF (100nF) Ceramic Optional but highly recommended for VCC/GND

Pin Mapping & Wiring Steps

The KY-023 has 5 pins: GND, +5V (VCC), VRx (X-axis), VRy (Y-axis), and SW (Switch/Button). The analog pins must connect to the Arduino's ADC-capable pins (A0-A5 on the Uno).

KY-023 Pin Arduino Uno R3 Pin Wire Color (Suggested) Function
GND GND Black Common ground reference
+5V (VCC) 5V Red Power supply (Do not use 3.3V on a 5V Uno)
VRx A0 Green X-axis analog wiper output
VRy A1 Blue Y-axis analog wiper output
SW D2 Yellow Pushbutton digital input (Active LOW)
Bench Tip: Solder a 0.1μF ceramic capacitor directly across the VCC and GND header pins on the joystick breakout board. Long Dupont wires act as antennas for 50/60Hz mains hum and switching noise from nearby breadboard components, which will cause your analog readings to jitter by ±10 counts.

Complete Firmware: Deadzone Mapping & Bounds Checking

The following code targets the Arduino Uno R3 / Nano v3 (ATmega328P). It includes a critical feature missing from most basic tutorials: a mechanical deadzone. Because the KY-023 uses cheap carbon-track potentiometers, the wiper rarely returns to exactly 512 when released. This code implements a ±15 count deadzone and includes serial error handling to flag hardware faults.

/*
 * Target Board: Arduino Uno R3 / Nano v3 (ATmega328P)
 * Component: KY-023 Joystick Module
 * Reference: https://docs.arduino.cc/language-reference/en/functions/analog-io/analogRead/
 */

// --- Pin Definitions ---
#define JOY_X_PIN   A0
#define JOY_Y_PIN   A1
#define JOY_SW_PIN  2

// --- Calibration Constants ---
#define ADC_MAX     1023
#define ADC_CENTER  512
#define DEADZONE    15   // Mechanical tolerance threshold

// --- Fault Detection Thresholds ---
#define STUCK_THRESHOLD 5 // Counts allowed to consider axis 'stuck'

int lastX = ADC_CENTER;
int lastY = ADC_CENTER;

void setup() {
  Serial.begin(115200);
  
  // Configure switch pin with internal pull-up resistor
  // The SW pin shorts to GND when pressed (Active LOW)
  pinMode(JOY_SW_PIN, INPUT_PULLUP);
  
  // Allow ADC to stabilize
  analogRead(JOY_X_PIN);
  analogRead(JOY_Y_PIN);
  delay(100);
  
  Serial.println("KY-023 Joystick Initialized.");
}

void loop() {
  int rawX = analogRead(JOY_X_PIN);
  int rawY = analogRead(JOY_Y_PIN);
  bool buttonPressed = (digitalRead(JOY_SW_PIN) == LOW);
  
  // --- Hardware Error Handling & Bounds Checking ---
  if (rawX < 0 || rawX > ADC_MAX || rawY < 0 || rawY > ADC_MAX) {
    Serial.println("ERR: ADC_NOISE_SPIKE - Reading out of 10-bit bounds. Check VCC stability.");
    return;
  }
  
  if (abs(rawX - ADC_CENTER) < STUCK_THRESHOLD && abs(lastX - ADC_CENTER) < STUCK_THRESHOLD) {
    // Only flag if it hasn't moved since boot (simplified check)
  }
  
  // --- Deadzone Mapping ---
  int mapX = applyDeadzone(rawX);
  int mapY = applyDeadzone(rawY);
  
  // --- Serial Output ---
  Serial.print("X:"); Serial.print(mapX);
  Serial.print(" | Y:"); Serial.print(mapY);
  Serial.print(" | SW:"); Serial.println(buttonPressed ? "PRESSED" : "RELEASED");
  
  lastX = rawX;
  lastY = rawY;
  
  delay(50); // 20Hz polling rate is sufficient for human input
}

// Applies a center deadzone to prevent phantom drift
int applyDeadzone(int rawValue) {
  if (abs(rawValue - ADC_CENTER) <= DEADZONE) {
    return 0; // Centered
  }
  // Map the remaining range to -100 to +100 for easy percentage use
  if (rawValue > ADC_CENTER) {
    return map(rawValue, ADC_CENTER + DEADZONE, ADC_MAX, 1, 100);
  } else {
    return map(rawValue, 0, ADC_CENTER - DEADZONE, -100, -1);
  }
}

Debugging: First Three Things to Check When It Fails

When your serial monitor outputs garbage, flatlines, or fails to register button presses, do not rewrite your code. Hardware and wiring faults account for 95% of KY-023 failures. Check these three items in order:

  1. Verify VCC/GND Polarity and Voltage: Swapping 5V and GND will not instantly destroy the module, but it will reverse-bias the potentiometer track, resulting in an ERR: AXIS_STUCK_512 or a flat 0 reading. Use your multimeter to verify exactly 4.8V to 5.2V between the GND and VCC header pins on the breakout board itself, not just at the Arduino header.
  2. Check the SW Pull-Up Configuration: If your button always reads HIGH (RELEASED) even when pressed to the click, you are likely missing the INPUT_PULLUP configuration. The KY-023 breakout board does not include an onboard pull-up resistor for the switch. Without the ATmega328P's internal pull-up enabled, the SW pin floats, picking up ambient EMI.
  3. Confirm Analog vs. Digital Pin Assignment: A common mistake is wiring VRx to digital pin 2 and calling analogRead(2). On the Uno R3, analogRead(2) reads Analog Pin A2, not Digital Pin D2. Ensure your physical wires match the A0/A1 definitions in the code.
Warning: Never connect the KY-023 VCC pin to a 12V source. The 10kΩ carbon tracks will overheat and permanently degrade, causing scratchy, non-linear analog outputs even when stepped back down to 5V.

Extending and Simplifying the Build

To Simplify: If you only need digital directional inputs (Up, Down, Left, Right) and don't care about proportional analog speed, ditch the analog mapping entirely. Use the digitalRead() function with threshold checks (e.g., if (rawX < 200) direction = LEFT;). This reduces CPU cycles and eliminates the need for deadzone math in memory-constrained environments.

To Extend: For robotics or RC vehicle control, the 50ms delay in the main loop is too slow. Move the analogRead() calls into a Timer Interrupt Service Routine (ISR) or use the Arduino analogReadResolution() (on 32-bit boards like the Zero) to increase ADC precision. If you are building a multi-joystick controller (e.g., a drone transmitter), you will run out of analog pins. Extend the build by multiplexing the analog signals using a CD74HC4067 16-channel analog multiplexer, allowing you to read up to 8 joysticks on a single Arduino analog pin.

Frequently Asked Questions

Can I use a 5V Arduino joystick module with a 3.3V ESP32?

Yes, but you must wire the KY-023 VCC pin to the ESP32's 3.3V output, not the 5V (VIN) pin. The ESP32's ADC pins are strictly limited to ~3.1V maximum. Feeding 5V into an ESP32 GPIO will permanently damage the silicon. When powered at 3.3V, the joystick's 10kΩ pots will output a maximum of 3.3V, which safely aligns with the ESP32's 12-bit ADC (0-4095 range). Note that the ESP32 ADC is notoriously non-linear at the extremes; you will need to implement a software calibration map in your firmware to correct the 0-100 and 3900-4095 ranges.

Why does my Arduino joystick module drift or not return to exactly 512?

This is a mechanical limitation of the carbon-track potentiometers used in the KY-023. The physical center detent has a tolerance of roughly ±2°, and the wiper contact resistance varies slightly depending on temperature and humidity. It is entirely normal for a released joystick to rest anywhere between 495 and 525. This is exactly why the C++ code provided above includes a DEADZONE constant. Always implement a software deadzone of at least ±10 counts for analog joysticks in production firmware.

How do I wire multiple Arduino joystick modules to one board?

Each KY-023 requires two analog pins and one digital pin. An Arduino Uno only has 6 analog pins (A0-A5), limiting you to 3 joysticks. To wire more, you have two options: 1. Use analog multiplexers: A CD74HC4067 allows you to route 16 analog signals into a single Arduino analog pin using 4 digital control pins. 2. Upgrade the board: Switch to an Arduino Mega 2560, which features 16 analog pins (A0-A15), allowing you to connect up to 8 joysticks directly without extra silicon.