To read a standard KY-023 dual-axis joystick on an Arduino Nano V3, map VRx to A0, VRy to A1, and SW to D2, then use analogRead() with a ±50 deadzone threshold to eliminate center drift. Raw 10-bit ADC values (0–1023) must be calibrated to a -100 to 100 range for practical use in motor control or menu navigation.

Most basic tutorials stop at printing raw analog values to the Serial Monitor. In practice, uncalibrated joystick code leads to drifting servos, wandering RC cars, and noisy menu inputs. This guide provides a production-ready approach to Arduino joystick code, covering the exact ADC characteristics of the ATmega328P, a robust C++ implementation with deadzone logic, and a diagnostic framework for when your hardware misbehaves.

Project Overview & Parts List

Difficulty Rating: Beginner-Intermediate (2/5)
Estimated Time: 20 minutes for wiring and code upload; 10 minutes for physical calibration.
Target Board: Arduino Nano V3 (ATmega328P, 5V logic, 10-bit ADC).

Before writing a single line of code, verify your hardware. The KY-023 module contains two 10kΩ potentiometers and a momentary pushbutton. It requires a stable 5V reference to match the Nano's default ADC reference voltage.

  • Microcontroller: Arduino Nano V3 (Official or reputable clone with CH340/FTDI USB-to-Serial chip). Price: $12–$25.
  • Joystick Module: KY-023 Dual-Axis XY Joystick (Breakout board with VRx, VRy, SW, VCC, GND pins). Price: $2–$4.
  • Wiring: 22 AWG solid-core copper wire (pre-tinned) or standard 2.54mm pitch Dupont jumper wires.
  • Power: High-quality 5V USB power supply (avoid unbranded wall warts that introduce switching noise into the ADC).

ADC Specifications & Joystick Calibration Data

The ATmega328P on the Arduino Nano uses a 10-bit Analog-to-Digital Converter (ADC). This means it maps the 0–5V input range to integer values between 0 and 1023. Understanding this mapping is critical for writing reliable Arduino joystick code. If you power the joystick with 3.3V but leave the Nano's ADC reference at 5V (the default), your maximum raw value will cap at ~681, ruining your calibration.

The table below provides the expected raw ADC values and the calibrated ranges you should target in your code. These values assume a 5V VCC supply to the joystick and a physical center detent.

Parameter / State Expected Raw ADC (10-bit) Voltage Equivalent Calibrated Output Range Deadzone Threshold
Center Idle (X & Y) 512 (± 20) ~2.50V 0 ± 40 (Raw: 472 to 552)
X-Axis Minimum (Left) 0 to 10 0.00V - 0.05V -100 N/A
X-Axis Maximum (Right) 1013 to 1023 4.95V - 5.00V +100 N/A
Y-Axis Minimum (Down) 0 to 10 0.00V - 0.05V -100 N/A
Y-Axis Maximum (Up) 1013 to 1023 4.95V - 5.00V +100 N/A
Button Pressed (SW) 0 (Digital LOW) 0.00V TRUE / 1 Debounce: 50ms
Button Released (SW) 1 (Digital HIGH) 5.00V (Pull-up) FALSE / 0 N/A

Note: The physical center of cheap KY-023 modules rarely sits exactly at 512. Your code must sample the idle position on startup to establish a dynamic center offset.

Pin Mapping & Wiring Procedure

Follow this exact pinout. Do not use 3.3V for the joystick VCC pin unless you are using a 3.3V native board like an ESP32 or Arduino Due. For the 5V Nano V3, a 5V supply is mandatory to utilize the full 0-1023 ADC range.

KY-023 Joystick Pin Arduino Nano V3 Pin Function & Configuration Notes
GND GND Common ground. Ensure tight connection to prevent floating ADC noise.
+5V (VCC) 5V Powers the internal 10kΩ potentiometer voltage dividers.
VRx A0 X-axis analog input. Leave as default INPUT (do not use INPUT_PULLUP).
VRy A1 Y-axis analog input. Leave as default INPUT.
SW D2 Momentary pushbutton. Must be configured as INPUT_PULLUP in code.
  1. De-energize the board: Unplug the USB cable from the Nano before wiring.
  2. Connect Power: Run a wire from Nano 5V to KY-023 VCC, and Nano GND to KY-023 GND.
  3. Connect Axes: Wire VRx to A0 and VRy to A1. Keep these analog wires away from high-current paths or the onboard voltage regulator to avoid inductive noise.
  4. Connect Switch: Wire SW to D2. The KY-023 module does not have an onboard pull-up resistor for the switch, so the Nano's internal pull-up is required.
  5. Verify: Tug gently on all Dupont connectors. Loose ground wires are the #1 cause of erratic analog readings.

Complete Arduino Joystick Code

The following C++ code targets the Arduino Nano V3. It includes dynamic center calibration on boot, a mathematical deadzone to prevent axis drift, and software debouncing for the pushbutton. For more on how the analogRead() function samples the ADC, refer to the official Arduino documentation.

/*
 * Robust Arduino Joystick Code
 * Target Board: Arduino Nano V3 (ATmega328P)
 * Module: KY-023 Dual-Axis Joystick
 */

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

// --- CALIBRATION CONSTANTS ---
#define DEADZONE 40         // Raw ADC tolerance around center (±40)
#define DEBOUNCE_MS 50      // Button debounce delay

// --- GLOBAL VARIABLES ---
int xCenter = 512;
int yCenter = 512;
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;

void setup() {
  Serial.begin(115200);
  
  // Configure button pin with internal pull-up resistor
  // See: https://docs.arduino.cc/built-in-examples/digital/InputPullupSerial
  pinMode(PIN_SW, INPUT_PULLUP);
  
  // Allow analog reference to stabilize
  delay(100);
  
  // Calibrate center position on startup
  // Take multiple samples to average out initial ADC noise
  long xSum = 0, ySum = 0;
  for(int i = 0; i < 16; i++) {
    xSum += analogRead(PIN_VRX);
    ySum += analogRead(PIN_VRY);
    delay(5);
  }
  xCenter = xSum / 16;
  yCenter = ySum / 16;
  
  Serial.print("Calibrated Center -> X:");
  Serial.print(xCenter);
  Serial.print(" Y:");
  Serial.println(yCenter);
}

void loop() {
  // Read raw values
  int rawX = analogRead(PIN_VRX);
  int rawY = analogRead(PIN_VRY);
  
  // Apply deadzone and map to -100 to 100 range
  int mappedX = processAxis(rawX, xCenter);
  int mappedY = processAxis(rawY, yCenter);
  
  // Handle Button with Debounce
  bool currentButtonState = digitalRead(PIN_SW);
  bool buttonPressed = false;
  
  if (currentButtonState != lastButtonState) {
    lastDebounceTime = millis();
  }
  
  if ((millis() - lastDebounceTime) > DEBOUNCE_MS) {
    if (currentButtonState == LOW && lastButtonState == HIGH) {
      buttonPressed = true; // Triggered only on the falling edge
    }
  }
  lastButtonState = currentButtonState;
  
  // Output Data (CSV format for easy Serial Plotter viewing)
  Serial.print(mappedX);
  Serial.print(",");
  Serial.print(mappedY);
  Serial.print(",");
  Serial.println(buttonPressed ? 1 : 0);
  
  delay(20); // ~50Hz update rate
}

// Function to apply deadzone and map values
int processAxis(int rawValue, int centerOffset) {
  if (rawValue > centerOffset + DEADZONE) {
    // Map from (center+deadzone)...1023 to 1...100
    return map(rawValue, centerOffset + DEADZONE, 1023, 1, 100);
  } 
  else if (rawValue < centerOffset - DEADZONE) {
    // Map from 0...(center-deadzone) to -100...-1
    return map(rawValue, 0, centerOffset - DEADZONE, -100, -1);
  } 
  else {
    // Inside deadzone
    return 0;
  }
}

Debugging: First Three Things to Check When It Fails

When your Arduino joystick code behaves erratically, do not immediately rewrite your logic. Hardware and wiring faults mimic software bugs. If your Serial Monitor output is wrong, follow this diagnostic sequence.

1. Symptom: Output stuck at X: 1023 Y: 1023 or X: 0 Y: 0

Ranked Causes:

  1. Missing or Swapped Power: If VCC and GND are swapped, or if GND is disconnected, the ADC pins will float to the supply rail or ground. Verify 5V at the joystick VCC pin with a multimeter.
  2. Shorted Potentiometer: The internal wiper may be shorted to the end terminals due to physical damage or a manufacturing solder bridge.
  3. Wrong Pin Definition: Ensure your code uses A0 and A1, not 0 and 1 (which map to the hardware UART pins D0/D1 on the Nano).

2. Symptom: Wildly fluctuating center values (e.g., X: 480 to X: 540 at rest)

Ranked Causes:

  1. USB Power Noise: Cheap PC USB ports or unbranded wall adapters introduce high-frequency switching noise. The ATmega328P ADC is highly susceptible to VCC ripple. Fix: Add a 100nF ceramic capacitor between the joystick VCC and GND pins, or power the Nano via the VIN pin with a regulated 7-9V supply.
  2. Missing Deadzone Logic: If you are printing raw analogRead() values without the processAxis() deadzone function provided above, minor thermal drift and electrical noise will look like massive fluctuations.
  3. Long/Unshielded Wires: Analog signals over 12 inches act as antennas for EMI. Keep joystick wires short and twisted.

3. Symptom: Button reads inverted or outputs random 1s when not pressed

Ranked Causes:

  1. Missing INPUT_PULLUP: The KY-023 switch connects the SW pin to GND when pressed, but leaves it floating when released. If you use standard INPUT, the floating pin will pick up stray capacitance and trigger randomly. You must use pinMode(PIN_SW, INPUT_PULLUP);.
  2. Contact Bounce: If you see multiple rapid 1s for a single physical press, your debounce delay is too short. Increase DEBOUNCE_MS from 50 to 100.

Extending and Simplifying the Build

Depending on your end goal, the standard Nano + KY-023 setup can be modified to reduce complexity or unlock advanced features.

How to Simplify (Digital-Only Input)

If you only need to know if the joystick is being pushed in any direction (e.g., for a simple menu navigation or a wake-up trigger), abandon the ADC entirely. You can wire the VRx and VRy pins to digital input pins and rely on the internal logic thresholds, though this is unreliable due to the slow voltage ramp of potentiometers.

The better simplification: Use only the SW (button) pin and map it to a hardware interrupt using attachInterrupt(). This allows the microcontroller to sleep and only wake on a physical button press, saving massive amounts of power in battery-operated builds.

How to Extend (Native HID & I2C Multiplexing)

Native HID (Mouse/Keyboard): The ATmega328P on the Nano cannot natively emulate a USB mouse or keyboard. If your goal is to build a PC gaming controller or a mouse alternative, swap the Arduino Nano V3 for an Arduino Pro Micro (ATmega32U4). The 32U4 chip has native USB HID support, allowing you to include the Mouse.h and Keyboard.h libraries to send direct OS-level inputs without third-party serial bridging software.

Multiple Joysticks (I2C): The Nano only has 8 analog pins. If you are building a dual-stick RC transmitter, you will run out of ADC channels quickly. Instead of upgrading to a Mega2560, use an Adafruit ADS1115 16-bit external ADC via I2C. This provides 4 high-precision analog channels per chip, frees up the Nano's internal ADC, and offers 16-bit resolution (0–65535) for ultra-smooth gimbal control.