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) |
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:
- 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_512or 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. - Check the SW Pull-Up Configuration: If your button always reads
HIGH(RELEASED) even when pressed to the click, you are likely missing theINPUT_PULLUPconfiguration. 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. - 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 theA0/A1definitions in the code.
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.






