Direct Answer: To connect a KY-023 joystick and Arduino Uno R3, wire the module's VRx to Analog Pin A0, VRy to A1, SW to Digital Pin 2, VCC to 5V, and GND to GND. The joystick uses two 10kΩ potentiometers for X/Y axes and a tactile switch for the Z-axis (button press).

Difficulty: Beginner | Time: 15 Minutes | Target Board: Arduino Uno R3 (ATmega328P DIP)

KY-023 Joystick and Arduino Uno: Hardware Spec Sheet

Before writing a single line of code, you need to understand the physical and electrical limits of the KY-023 module. Cheaply manufactured clones often suffer from wiper noise and mechanical deadbands. The table below provides the datasheet specifications alongside real-world measured values you will encounter on the bench.

Parameter Datasheet Specification Real-World Measured Value Engineering Notes
Operating Voltage 3.3V to 5V 5.0V (Nominal) Do not exceed 5.5V; the 10kΩ pots will overheat and drift.
Potentiometer Resistance 10kΩ (Dual) 9.8kΩ - 10.2kΩ Linearity is ±5%. Expect ±20 ADC steps of noise at the wiper.
Center Rest Voltage (at 5V) 2.5V (512 ADC) 2.45V - 2.55V (502 - 522 ADC) Mechanical spring return rarely centers perfectly. Code must include a deadzone.
Switch Contact Resistance < 50mΩ ~100mΩ (Aging) Switch is active LOW. Requires a pull-up resistor (internal or external).
Quiescent Current ~0.5mA 0.48mA Current draw is minimal; safe for battery-powered ESP32 deep-sleep builds.

Parts List and Pin Mapping

This build assumes you are using the standard DIP-based Arduino Uno R3. If you are using an SMD variant or a Nano, the pin logic remains identical, but physical breadboard placement will vary.

Required Materials

  • Microcontroller: Arduino Uno R3 (ATmega328P-PU DIP variant)
  • Input Module: KY-023 Dual-Axis Joystick (Standard 5-pin 2.54mm header)
  • Wiring: 5x 22AWG solid-core jumper wires (male-to-male)
  • Prototyping: Half-size solderless breadboard (400 tie-points)

Pin Mapping Table

The KY-023 silkscreen labels can be misleading depending on the manufacturer batch. Always verify the pinout using the table below, which maps the module pins to the Uno's ATmega328P hardware pins.

KY-023 Pin Label Arduino Uno Pin Wire Color Function & Configuration
GND GND (Next to 5V) Black Common ground reference. Must share ground with Uno.
+5V (or VCC) 5V Red Powers the internal 10kΩ voltage dividers.
VRx A0 Green X-Axis analog input. Reads 0-1023 (0-5V).
VRy A1 Blue Y-Axis analog input. Reads 0-1023 (0-5V).
SW D2 Yellow Push-button switch. Configure as INPUT_PULLUP.
Silkscreen Warning: On some cheap KY-023 clones, the VCC and GND pins are physically swapped on the PCB header compared to the original Sony PS2 joystick pinout. Always trace the ground plane on the back of the module with your multimeter in continuity mode before applying power. Wiring 5V into the GND pin will short the 5V rail through the 10kΩ pot, triggering the Uno's polyfuse or causing a brownout.

Step-by-Step Wiring and Compilable Code

Follow these numbered steps to assemble the circuit safely.

  1. De-energize the board: Disconnect the USB cable from the Arduino Uno before inserting jumper wires to prevent accidental shorting of the 5V rail.
  2. Wire the power bus: Connect the red wire from the KY-023 VCC pin to the Uno's 5V pin. Connect the black wire from KY-023 GND to the Uno's GND pin.
  3. Wire the analog axes: Connect VRx to A0 and VRy to A1. Keep these wires away from the onboard voltage regulator to avoid thermal noise injection.
  4. Wire the digital switch: Connect the SW pin to Digital Pin 2.
  5. Verify connections: Use a multimeter to check continuity from the Uno's GND pin to the metal casing of the joystick potentiometers to ensure a solid ground reference.

Compilable C++ Code with Deadzone Handling

Raw analog reads from the KY-023 are notoriously noisy at the center resting position due to mechanical spring tolerances. The code below targets the Arduino Uno R3 (AVR architecture) and implements a software deadzone to prevent UI drift or servo jitter when the stick is released. It also includes serial buffer error handling.

// Target Board: Arduino Uno R3 (ATmega328P)
// Library Requirements: None (Standard Arduino API)

#define VRX_PIN A0
#define VRY_PIN A1
#define SW_PIN  2

// Deadzone thresholds for mechanical center drift
#define CENTER_ADC 512
#define DEADZONE_LOW 490
#define DEADZONE_HIGH 535

struct JoystickState {
  int x;
  int y;
  bool buttonPressed;
};

void setup() {
  Serial.begin(115200);
  
  // Configure switch pin with internal 20kΩ pull-up resistor
  // See: https://docs.arduino.cc/learn/microcontrollers/digital-pins/#pullup-resistors
  pinMode(SW_PIN, INPUT_PULLUP);
  
  // Optional: Set analog reference to default (5V on Uno)
  analogReference(DEFAULT);
  
  Serial.println("KY-023 Joystick Initialized. Moving to main loop.");
}

JoystickState readJoystick() {
  JoystickState state;
  
  // Read raw ADC values (10-bit resolution: 0-1023)
  int rawX = analogRead(VRX_PIN);
  int rawY = analogRead(VRY_PIN);
  
  // Apply deadzone filtering to eliminate center drift
  state.x = (rawX >= DEADZONE_LOW && rawX <= DEADZONE_HIGH) ? CENTER_ADC : rawX;
  state.y = (rawY >= DEADZONE_LOW && rawY <= DEADZONE_HIGH) ? CENTER_ADC : rawY;
  
  // Switch is Active LOW (0V when pressed, 5V when released)
  state.buttonPressed = (digitalRead(SW_PIN) == LOW);
  
  return state;
}

void loop() {
  JoystickState js = readJoystick();
  
  // Error handling: Check if Serial buffer is available before writing
  if (Serial.availableForWrite() > 20) {
    Serial.print("X: ");
    Serial.print(js.x);
    Serial.print(" | Y: ");
    Serial.print(js.y);
    Serial.print(" | SW: ");
    Serial.println(js.buttonPressed ? "PRESSED" : "RELEASED");
  } else {
    // Flush buffer if PC is not reading serial data fast enough
    Serial.flush();
  }
  
  // Polling rate limit (~50Hz) to prevent flooding the serial monitor
  delay(20);
}

Reference: For detailed behavior on the analogRead() function and ADC sampling times, consult the official Arduino analogRead documentation.

Debugging: "Analog Read Stuck at 0 or 1023"

When working with the KY-023, the most common failure mode is a frozen serial output. If your Serial Monitor prints the exact string "X: 0 | Y: 0 | SW: RELEASED" or "X: 1023 | Y: 1023 | SW: RELEASED" regardless of how aggressively you move the physical stick, you have a hardware reference fault.

The First Three Things to Check

  1. VCC/GND Swap (The Module Killer): Disconnect power immediately. Use your multimeter in continuity mode to verify that the GND pin on the KY-023 header actually connects to the ground plane on the module PCB. If you wired 5V into the module's GND pin, you have likely burned out the internal carbon track on the potentiometer.
  2. Shared Ground Reference: The Arduino's ADC measures voltage relative to its own ground. If you are powering the joystick from an external 5V breadboard supply, the breadboard GND must be physically jumpered to the Arduino Uno's GND. Without this, the ADC will read floating noise or rail-to-rail values.
  3. Analog Pin Assignment Mismatch: Verify you are using A0 and A1 in your code, not 0 and 1. In older Arduino IDE versions, passing the integer 0 to analogRead() reads from physical digital pin 0 (the RX line), not analog pin 0.

Ranked Causes for Noisy or Jumping ADC Values

If the values are moving, but jumping erratically (e.g., jumping from 510 to 800 without touching the stick), consult this ranked troubleshooting matrix:

Rank Probable Cause Measurement / Verification Fix
1 Oxidized Potentiometer Wiper Read resistance between wiper and ground while moving slowly. Look for infinite spikes. Inject contact cleaner (DeoxIT) into the pot casing, or replace the module.
2 Breadboard Contact Fatigue Wiggle the jumper wire at the breadboard insertion point while watching Serial Monitor. Move to a fresh breadboard row or solder the connections directly.
3 USB Power Rail Ripple Measure the Uno 5V pin with an oscilloscope. Look for >50mV ripple. Power the Uno via the DC barrel jack with a regulated 9V supply to engage the onboard linear regulator filtering.

Extending or Simplifying the Build

Depending on your final application, you may need to strip this circuit down to its bare essentials or scale it up for complex robotics.

How to Simplify the Build

  • Drop the Z-Axis: If you are building a simple pan/tilt camera mount or a 2D game controller, the push-button switch is often unnecessary. Remove the SW wire and delete the pinMode and digitalRead lines from the code to free up Digital Pin 2 and reduce polling overhead.
  • Hardware Deadzone (Resistor Trick): If you are feeding the joystick into an analog synthesizer or a non-programmable circuit, you can create a hardware deadzone by placing two 1kΩ resistors in series with the wiper output and a capacitor to ground, forming a low-pass filter that smooths out mechanical jitter.

How to Extend the Build

  • Migration to ESP32 (12-bit ADC): If you port this code to an ESP32 DevKit V1, remember that the ESP32 ADC is 12-bit (0-4095) and highly non-linear above 3.1V. You must change the CENTER_ADC to roughly 2048, widen the deadzone thresholds, and ideally use analogReadResolution(10) in setup to force the ESP32 to mimic the Uno's 10-bit behavior for code compatibility.
  • Add I2C OLED Feedback: Wire an SSD1306 128x64 I2C OLED display to A4 (SDA) and A5 (SCL). Use the Adafruit_SSD1306 library to map the joystick's X/Y coordinates directly to a pixel cursor on the screen. This is the standard validation step for verifying deadzone logic visually before deploying the code to a motor controller.
  • Drive a PCA9685 Servo Driver: For robotic arms, do not drive servos directly from the Uno's PWM pins using joystick data. The KY-023's mechanical noise will cause the servos to "chatter" and overheat. Instead, map the joystick values to I2C commands sent to a PCA9685 16-channel servo driver, which handles the PWM pulse timing independently of the Arduino's main loop.
Bench Tip: When mapping the 0-1023 analog read to a servo's 0-180 degree range, never use the raw ADC value. Always pass it through the map() function, and then constrain it using constrain(val, 10, 170) to prevent the servo from stalling against its internal hard stops, which draws excessive current and can brownout the Uno.