If you need reliable joystick code for Arduino, the fastest path to success is pairing a KY-023 analog joystick module with an Arduino Uno R3, wiring VRx to A0 and VRy to A1, and implementing a software deadzone of ±40 to eliminate center-drift. Cheap potentiometers wobble, USB power rails introduce noise, and floating digital pins will ruin your button logic. This guide gives you the exact pinout, production-ready C++ code with deadzone filtering, and a diagnostic tree to fix the three most common serial monitor failures.

The Decision Path: Which Joystick Module Should You Pick?

Before writing a single line of code, you must select the right hardware for your physical constraints. Makers often buy the wrong module and try to fix hardware limitations in software. Use this decision tree to lock in your part number.

If your project requires...Then choose this moduleWhy it wins
2-axis analog input on a strict budget (< $3)KY-023 (PS2-style)Direct 5V analog out, integrated tactile switch, ubiquitous availability.
Noise immunity, long cable runs, or I2C daisy-chainingAdafruit Thumbstick (I2C)Onboard ADC converts analog to digital before transmission; immune to voltage drop.
Strict 4-way or 8-way digital directional input (no proportional control)Arcade Digital StickMicroswitches provide crisp, debounced digital HIGH/LOW signals; no ADC needed.
Default Recommendation: For 90% of hobbyist builds (RC rovers, pan-tilt cameras, menu navigation), terminate your search and buy the KY-023. It operates natively on the Uno's 5V logic, requires no external libraries, and the code below is written specifically for its 10kΩ potentiometer taper.

Parts List and Pin Mapping Spec Sheet

This build targets the Arduino Uno R3 (ATmega328P). The Uno R3 features a 10-bit ADC, meaning analogRead() returns values from 0 to 1023. Note: If you are using the newer Arduino Uno R4 WiFi, the ADC is 14-bit natively, but defaults to 10-bit for backward compatibility. The code below will work on both without modification.

Bill of Materials

  • Microcontroller: Arduino Uno R3 (or compatible ATmega328P clone)
  • Joystick: KY-023 Dual-Axis Analog Joystick Module (includes onboard 10kΩ pots and tactile switch)
  • Wiring: 5x Male-to-Female Dupont jumper wires
  • Hardware Fix (Optional but recommended): 1x 0.1µF ceramic capacitor (for VCC noise filtering)

Pin Mapping Table

KY-023 PinArduino Uno R3 PinFunction & Configuration
GNDGNDCommon ground reference. Do not skip.
+5V (VCC)5VPower supply. Must be 5V for correct 1023 ADC scaling.
VRxA0X-axis analog wiper. Configured as INPUT.
VRyA1Y-axis analog wiper. Configured as INPUT.
SWD2Tactile button. Configured as INPUT_PULLUP.

Bench Tip: The KY-023 module's physical silkscreen sometimes swaps the 'X' and 'Y' labels depending on the manufacturer's batch. If your pan-tilt mechanism moves left when you push forward, simply swap the A0 and A1 wires rather than rewriting the code.

Complete, Compilable Arduino Joystick Code

The raw output of a KY-023 at rest is rarely a perfect 512. Mechanical tolerances mean your center point might be 508 or 518. Furthermore, cheap carbon-track potentiometers generate 'wiper noise'—rapid ADC fluctuations of ±5 units even when the stick is untouched.

The code below implements a software deadzone and state-change detection for the button. It is fully compilable, requires zero external libraries, and includes serial error handling to detect disconnected pins.

// =========================================================
// Bulletproof KY-023 Joystick Code for Arduino Uno R3
// Target Board: Arduino Uno R3 (ATmega328P, 10-bit ADC)
// =========================================================

// --- PIN DEFINITIONS ---
#define PIN_VRX A0
#define PIN_VRY A1
#define PIN_SW  2

// --- CALIBRATION & DEADZONE ---
// Theoretical center is 512. We define a deadzone to prevent drift.
#define ADC_CENTER 512
#define DEADZONE   40   // Ignores values between 472 and 552

// --- STATE VARIABLES ---
int rawX = 0;
int rawY = 0;
int mappedX = 0;
int mappedY = 0;
bool lastButtonState = HIGH;
bool currentButtonState = HIGH;

void setup() {
  Serial.begin(115200);
  
  // Configure button pin with internal 20k pull-up resistor
  // The KY-023 switch pulls to GND when pressed (Active LOW)
  pinMode(PIN_SW, INPUT_PULLUP);
  
  // Analog pins default to INPUT, but explicit is better for readability
  pinMode(PIN_VRX, INPUT);
  pinMode(PIN_VRY, INPUT);
  
  Serial.println(F("Joystick initialized. Move stick or press button."));
}

void loop() {
  // 1. Read Raw ADC Values
  rawX = analogRead(PIN_VRX);
  rawY = analogRead(PIN_VRY);
  
  // 2. Hardware Disconnect Error Handling
  // If a pin is floating or disconnected, it often reads erratic highs or solid 1023/0
  if (rawX == 0 && rawY == 0) {
    Serial.println(F("[ERR] Both axes reading 0. Check VCC and GND wiring."));
    delay(1000);
    return;
  }
  
  // 3. Apply Deadzone Filtering
  mappedX = applyDeadzone(rawX);
  mappedY = applyDeadzone(rawY);
  
  // 4. Button State-Change Detection (Debounce-free for simple presses)
  currentButtonState = digitalRead(PIN_SW);
  if (currentButtonState != lastButtonState) {
    if (currentButtonState == LOW) {
      Serial.println(F("[BTN] Pressed"));
    } else {
      Serial.println(F("[BTN] Released"));
    }
    lastButtonState = currentButtonState;
  }
  
  // 5. Output Telemetry
  Serial.print(F("X: ")); Serial.print(mappedX);
  Serial.print(F(" | Y: ")); Serial.print(mappedY);
  Serial.print(F(" | Raw(")); Serial.print(rawX);
  Serial.print(F(",")); Serial.print(rawY);
  Serial.println(F(")"));
  
  delay(50); // 20Hz update rate, prevents serial buffer flooding
}

// --- HELPER FUNCTIONS ---
int applyDeadzone(int rawValue) {
  if (rawValue > (ADC_CENTER - DEADZONE) && rawValue < (ADC_CENTER + DEADZONE)) {
    return 0; // Force to zero inside the deadzone
  }
  // Remap the active zones to a clean -100 to +100 scale
  if (rawValue <= (ADC_CENTER - DEADZONE)) {
    return map(rawValue, 0, ADC_CENTER - DEADZONE, -100, -1);
  } else {
    return map(rawValue, ADC_CENTER + DEADZONE, 1023, 1, 100);
  }
}

According to the Arduino analogRead() documentation, the ADC takes about 100 microseconds to sample. The 50ms delay in the loop yields a responsive 20Hz polling rate, which is ideal for human-interface devices without overwhelming the serial buffer.

Debugging: First Three Things to Check When It Fails

When your serial monitor output looks wrong, do not rewrite the code. The C++ above is mathematically sound; failures are almost always electrical. Here are the top three symptoms and their exact fixes.

Symptom 1: Serial monitor prints solid '1023' or '0' regardless of movement

  • Ranked Cause 1 (Most Likely): VCC and GND are swapped, or the ground wire is disconnected. The internal protection diodes clamp the floating analog pin to the supply rail.
  • Ranked Cause 2: You wired the digital 'SW' pin to an Analog pin (A0/A1) by mistake.
  • The Fix: Verify the KY-023 silkscreen. Use a multimeter in continuity mode to beep out the GND pin to the Arduino's USB shield ground. Ensure VCC is on the 5V rail, not 3.3V (which will cap your max reading at ~675).

Symptom 2: X and Y values jump wildly (e.g., 480 to 540) while the stick is untouched

  • Ranked Cause 1: USB 5V rail noise from your PC, or a failing carbon track inside the cheap potentiometer.
  • Ranked Cause 2: The ADC is sampling too quickly and picking up electromagnetic interference (EMI) from adjacent digital wires.
  • The Fix: Solder a 0.1µF ceramic capacitor directly across the VCC and GND header pins on the KY-023 module. This acts as a low-pass filter, smoothing out high-frequency power supply ripple before it hits the potentiometer wiper. As noted in Adafruit's analog joystick guide, physical wiper noise is a known limitation of budget modules, and hardware filtering is vastly superior to software averaging.

Symptom 3: Button reads random '1's and '0's when not pressed

  • Ranked Cause 1: You configured the pin as INPUT instead of INPUT_PULLUP. An unpressed tactile switch is an open circuit; without a pull-up resistor, the pin acts as an antenna, floating between HIGH and LOW.
  • Ranked Cause 2: You are using an external pull-down resistor wiring scheme but the code expects Active LOW.
  • The Fix: Ensure line 24 of the code reads exactly pinMode(PIN_SW, INPUT_PULLUP);. The ATmega328P has internal 20kΩ pull-up resistors that solve this without requiring external components.

How to Extend or Simplify the Build

Depending on your end application, you may not need proportional analog data, or you might need to drive heavy loads. Here is how to adapt the baseline build.

Simplify: Treat the Joystick as 5 Digital Buttons

If you are building a simple menu interface or a 4-way directional rover, proportional control (values 1 to 100) is unnecessary. You can simplify the logic by discarding the map() functions and using hard thresholds to register digital 'presses'.

// Simplified Digital-Only Logic
if (rawX < 200) Serial.println(F("LEFT"));
if (rawX > 800) Serial.println(F("RIGHT"));
if (rawY < 200) Serial.println(F("DOWN"));
if (rawY > 800) Serial.println(F("UP"));

This eliminates deadzone math entirely and reduces CPU cycles, which is useful if your loop is already bogged down by heavy sensor polling or NeoPixel rendering.

Extend: Add Exponential Response for RC Steering

Linear mapping (where 50% stick deflection = 50% motor power) feels twitchy in RC cars and drones. Human thumbs prefer an exponential curve where small center movements yield fine control, and outer deflections yield rapid acceleration.

To extend the build, replace the map() function in the helper block with a squared mathematical model:

// Exponential mapping for finer center control
float normalized = (rawValue - 512.0) / 512.0; // Range: -1.0 to 1.0
float exponential = normalized * abs(normalized); // Squares the curve, keeps sign
int finalOutput = exponential * 100; // Scale to -100 to 100

This requires changing the return type to handle floats temporarily but results in vastly superior physical handling for motorized projects.

Safety & Hardware Warning: Never wire a joystick module directly to mains-voltage relays or high-current motor drivers. The Arduino's 5V logic and the KY-023's low-current wipers are for signal generation only. Always pass the mapped -100 to 100 values into a motor driver library (like L298N or RoboClaw) that handles the high-current PWM switching and flyback diode protection.