If you need a standard 2-axis analog input for a 5V microcontroller project, buy the KY-023 breakout module. If you are using a 3.3V logic board like the ESP32 or Arduino Nano 33 IoT, buy the PS2 joystick module paired with a logic-level shifter or a dedicated 3.3V voltage regulator. Feeding a 5V KY-023 directly into a 3.3V ADC will not destroy the chip immediately if current is limited, but it will permanently peg your ADC readings at the maximum value (e.g., 4095 on a 12-bit ESP32) and cause long-term degradation of the GPIO pin's internal clamping diodes.
This guide cuts through the generic tutorials and gives you the exact hardware specs, a deadzone-calibrated C++ sketch, and a hardware-level debugging path for when your analog reads inevitably drift or lock up.
The Decision Matrix: Which Arduino Joystick to Buy
Not all thumbsticks are created equal. The physical form factor might look identical, but the internal potentiometer tapers and voltage tolerances dictate which module you should use. Use this decision path to select your hardware:
| Decision Criteria | KY-023 Breakout | PS2 Joystick Module | Adafruit Thumbstick (PID 512) |
|---|---|---|---|
| Target Logic Level | 5V (Uno R3, Mega) | 3.3V or 5V (ESP32, Pico) | 3.3V to 5V (Wide range) |
| Potentiometer Value | 10kΩ Linear | 10kΩ Linear | 10kΩ Linear |
| Mechanical Deadzone | Large (~15% center play) | Medium (~10% center play) | Small (~5% center play) |
| Average Price (2026) | $1.50 - $2.50 | $2.00 - $3.50 | $9.95 |
Hardware Spec Sheet & Parts List
Before wiring, verify you have the exact components. Substituting a 50kΩ potentiometer module will increase your ADC susceptibility to electromagnetic interference (EMI) from nearby servos or Wi-Fi antennas.
- Microcontroller: Arduino Uno R3 (ATmega328P, 5V logic, 10-bit ADC). Note: If using the newer Arduino Uno R4 Minima, the ADC is 14-bit (0-16383). You must adjust the code constants accordingly.
- Joystick Module: KY-023 (5V variant) with integrated 10kΩ dual-axis potentiometers and a momentary pushbutton (Z-axis).
- Wiring: 5x Male-to-Female or Male-to-Male 22 AWG jumper wires (keep them under 15cm to prevent voltage drop and capacitive coupling).
- Decoupling Capacitor (Optional but recommended): 0.1µF ceramic capacitor placed across the VCC and GND pins of the joystick if you are also driving inductive loads like motors on the same breadboard.
Pin Mapping and Wiring Procedure
The KY-023 uses a simple voltage divider circuit. The wiper pin of each potentiometer outputs a variable voltage between 0V and VCC. The pushbutton requires a pull-up resistor; we will use the microcontroller's internal pull-up to save a component.
| KY-023 Pin Label | Arduino Uno R3 Pin | Function & Electrical Notes |
|---|---|---|
| GND | GND | Common ground. Must share the same ground plane as the Arduino. |
| +5V (or VCC) | 5V | Do NOT use the 3.3V pin. The 10kΩ pots expect 5V to output the full 0-1023 ADC range. |
| VRx | A0 | X-axis analog wiper. Outputs 0V-5V. High impedance (~5kΩ at center). |
| VRy | A1 | Y-axis analog wiper. Outputs 0V-5V. |
| SW | D2 | Pushbutton. Active LOW. Connects to GND when pressed. |
- De-energize the board: Unplug the USB cable before inserting jumper wires into the Arduino headers.
- Wire Power: Connect the module GND to Arduino GND, and module +5V to Arduino 5V.
- Wire Analog: Route VRx to A0 and VRy to A1. Keep these wires away from the onboard voltage regulator to avoid thermal noise.
- Wire Digital: Connect the SW pin to Digital Pin 2.
- Verify: Use a multimeter in DC Voltage mode. Probe the +5V pin on the module relative to GND. It should read between 4.8V and 5.1V. If it reads lower, your USB port is browning out; use a powered hub.
Deadzone Calibration Code (Arduino Uno R3)
Raw joystick data is noisy. The mechanical springs rarely center the wiper exactly at 2.5V (ADC 512), and the carbon tracks generate static when moved. This sketch targets the Arduino Uno R3 and implements a software deadzone, clamping, and mapping to a clean -100 to +100 scale.
/*
* Arduino Joystick Deadzone Calibration Sketch
* Target Board: Arduino Uno R3 (ATmega328P, 10-bit ADC)
* Author: ElectricalFlux
*/
// --- Pin Definitions ---
#define PIN_JOY_X A0
#define PIN_JOY_Y A1
#define PIN_JOY_SW 2
// --- Calibration Constants ---
// Measure your specific joystick at rest and update these values
const int ADC_MAX = 1023;
const int ADC_CENTER_X = 512; // Ideal center, adjust if your hardware rests at 505, etc.
const int ADC_CENTER_Y = 512;
const int DEADZONE = 25; // Ignore ADC fluctuations within +/- 25 of center
void setup() {
Serial.begin(115200);
// Configure switch pin with internal pull-up resistor (approx 20kΩ-50kΩ)
pinMode(PIN_JOY_SW, INPUT_PULLUP);
// Allow ADC circuitry to stabilize
analogRead(PIN_JOY_X);
analogRead(PIN_JOY_Y);
delay(100);
}
void loop() {
// 1. Read Raw ADC Values
int rawX = analogRead(PIN_JOY_X);
int rawY = analogRead(PIN_JOY_Y);
bool isPressed = (digitalRead(PIN_JOY_SW) == LOW);
// 2. Apply Deadzone and Map to -100 to +100
int mappedX = applyDeadzoneAndMap(rawX, ADC_CENTER_X);
int mappedY = applyDeadzoneAndMap(rawY, ADC_CENTER_Y);
// 3. Error Handling / Bounds Clamping
// Protect against EMI spikes pushing values outside expected ranges
mappedX = constrain(mappedX, -100, 100);
mappedY = constrain(mappedY, -100, 100);
// 4. Output Telemetry
Serial.print("X:");
Serial.print(mappedX);
Serial.print(" | Y:");
Serial.print(mappedY);
Serial.print(" | SW:");
Serial.println(isPressed ? "PRESSED" : "RELEASED");
delay(50); // 20Hz polling rate is sufficient for human input
}
// Helper function to handle the math for deadzones
int applyDeadzoneAndMap(int rawVal, int centerVal) {
int delta = rawVal - centerVal;
// If within deadzone, return 0
if (abs(delta) <= DEADZONE) {
return 0;
}
// Map the remaining range to -100 to 100
if (delta > 0) {
return map(delta, DEADZONE, (ADC_MAX - centerVal), 0, 100);
} else {
return map(delta, -(centerVal), -DEADZONE, -100, 0);
}
}
Debugging: Output Stuck at "X: 1023, Y: 1023"
When working with analog sensors, you will eventually encounter a locked serial output. If your serial monitor displays the exact error string: X: 1023, Y: 1023, SW: 1 (Unresponsive to movement) or raw values maxed out, do not immediately assume the module is dead. Follow this ranked troubleshooting path.
The First Three Things to Check
- VCC at the Module Pins: Put your multimeter probes directly on the metal header pins of the KY-023. If you read 0V, your breadboard power rail is split or your jumper wire is broken internally. If you read 3.3V (because you plugged it into the wrong rail), the ADC will max out immediately.
- GND Continuity: Measure resistance between the Arduino GND pin and the Joystick GND pin. It must be less than 1Ω. A floating ground will cause the ADC to read random noise or peg to the maximum rail voltage.
- Wiper Voltage: With the joystick centered, probe the VRx and VRy pins. You should read exactly half of your VCC (e.g., 2.5V on a 5V system). If you read 5V regardless of how you move the stick, the internal carbon track is severed or the wiper is bent off the track.
Ranked Causes for Maxed-Out ADC Reads
| Probability | Cause | Fix |
|---|---|---|
| High | Wiring VRx/VRy to a Digital Pin instead of Analog. | Move wires to A0/A1. Digital pins read HIGH (1) which maps to max voltage in software. |
| Medium | Missing or broken GND connection. | Replace the GND jumper wire and verify continuity with a multimeter. |
| Low | Short circuit between +5V and VRx on the PCB. | Inspect the module PCB under magnification for solder bridges. Clean with flux and a solder wick. |
Note on Compiler Errors: If you copy the code above and see error: 'PIN_JOY_X' was not declared in this scope, it means you omitted the #define block at the top of the sketch. Always define hardware pins at the global scope before setup().
Extending and Simplifying the Build
Once your joystick is reading cleanly, you need to decide how to integrate it into your larger system. Here is how to adapt the build based on your project constraints.
How to Simplify (For Basic Menu Navigation)
If you are just building a simple LCD menu navigator and do not care about proportional speed control or mechanical drift, strip out the deadzone math. Replace the custom mapping function with a simple threshold check:
if (analogRead(PIN_JOY_X) > 700) { menu.moveRight(); }
if (analogRead(PIN_JOY_X) < 300) { menu.moveLeft(); }
This reduces CPU overhead and code complexity, though it will feel binary rather than analog.
How to Extend (For Precision Robotics or Drones)
The ATmega328P's internal 10-bit ADC is susceptible to noise, especially when you add servos or brushless ESCs to the same power bus. To extend this build for professional-grade RC transmitters or robotic arms:
- Add an External ADC: Wire an ADS1115 16-bit I2C ADC to the joystick. This gives you 65,536 steps of resolution instead of 1,024, completely eliminating the "steppy" feeling of cheap potentiometers.
- Implement Hardware Filtering: Solder a 10kΩ resistor in series with the VRx/VRy outputs, and place a 0.1µF capacitor from the analog input pin to GND. This creates a low-pass RC filter that physically blocks high-frequency EMI before it hits the microcontroller.
- Use Interrupts for the Switch: Instead of polling the SW pin in the
loop(), attach an interrupt usingattachInterrupt(digitalPinToInterrupt(PIN_JOY_SW), buttonISR, FALLING). This ensures you never miss a rapid button press, even if your main loop is bogged down calculating PID motor control loops.
For further reading on ADC quantization and noise reduction, refer to the official Arduino analogRead() documentation and the Arduino Uno R4 Minima Cheat Sheet if you decide to upgrade to a 14-bit architecture.






