If you are building a robotic arm controller, a retro gamepad, or a pan-tilt camera rig, the generic dual-axis analog joystick is your most cost-effective input device. But cheap potentiometers and unshielded jumper wires frequently lead to noisy ADC readings and phantom inputs. This guide gives you the exact wiring, deadzone-calibrated code, and bench-tested debugging steps to get a reliable joystick module Arduino integration working on the first try.
The Verdict: Which Joystick Module to Buy
Not all thumbsticks are created equal. Before you wire anything, use this decision matrix to pick the right hardware for your specific build constraint.
| Module Type | Best For | Pros & Cons | Typical Price |
|---|---|---|---|
| KY-023 Breakout | Breadboard prototyping & quick builds | Includes 5-pin header, 3.3V/5V tolerant. Bulky for permanent enclosures. | $1.50 - $3.00 |
| Bare PS2 Thumbstick | Custom PCB soldering & tight enclosures | Compact, low profile. Requires manual soldering of 5 tiny pads; easy to bridge. | $0.80 - $1.50 |
| I2C Rotary Encoder w/ Joystick | Boards with no ADC pins (e.g., ESP32-C3) | Digital output, no analog noise. Expensive, requires I2C library overhead. | $8.00 - $12.00 |
Hardware Spec Sheet & Pin Mapping
The KY-023 uses two 10kΩ B103 linear taper potentiometers (one for X, one for Y) and a tactile pushbutton for the Z-axis (press). Below are the exact specifications and pin mappings for an Arduino Nano V3 (ATmega328P, 16MHz). This code and wiring also apply directly to the Arduino Uno R3.
| KY-023 Pin | Arduino Nano V3 Pin | Function & Notes |
|---|---|---|
| GND | GND | Common ground. Do not leave floating. |
| +5V (VCC) | 5V | Powers the potentiometer voltage divider. Must match the board's ADC reference. |
| VRx | A0 | Analog input for the X-axis. Outputs 0-5V. |
| VRy | A1 | Analog input for the Y-axis. Outputs 0-5V. |
| SW | D2 | Digital input for the pushbutton. Active LOW. |
Wiring Steps and Compilable Code
Follow these numbered steps to wire the module. Keep your Dupont jumper wires under 15cm (6 inches) to minimize capacitive coupling and ADC noise.
- Insert the Arduino Nano V3 into your breadboard, ensuring the pins straddle the center trench.
- Connect the KY-023 GND to the Nano's GND pin.
- Connect the KY-023 +5V to the Nano's 5V pin. Warning: Do not use the 3.3V pin to power a KY-023 if your Nano is running on 5V logic; your maximum analog read will cap at ~675 instead of 1023.
- Connect VRx to A0 and VRy to A1.
- Connect SW to D2.
- Upload the following C++ code. This sketch includes hardware disconnect error handling and a software deadzone to eliminate resting drift.
/*
* KY-023 Joystick Module Arduino Code
* Target Board: Arduino Nano V3 (ATmega328P)
* Features: Deadzone calibration, disconnect error handling, button debounce.
*/
#define PIN_VRX A0
#define PIN_VRY A1
#define PIN_SW 2
// Calibration constants (measure these at rest with your specific module)
const int REST_X = 512;
const int REST_Y = 512;
const int DEADZONE = 25; // Ignore variations +/- 25 from rest
void setup() {
Serial.begin(115200);
pinMode(PIN_SW, INPUT_PULLUP); // Internal pull-up prevents floating pin noise
analogReference(DEFAULT); // Ensure 5V reference on 5V boards
}
void loop() {
int rawX = analogRead(PIN_VRX);
int rawY = analogRead(PIN_VRY);
bool buttonPressed = (digitalRead(PIN_SW) == LOW);
// Error Handling: Check for disconnected or shorted wires
if (rawX == 0 || rawX == 1023) {
Serial.println("[ERROR] VRx pin shorted to GND/VCC or disconnected.");
delay(500);
return;
}
if (rawY == 0 || rawY == 1023) {
Serial.println("[ERROR] VRy pin shorted to GND/VCC or disconnected.");
delay(500);
return;
}
// Apply Deadzone Filtering
int mappedX = applyDeadzone(rawX, REST_X);
int mappedY = applyDeadzone(rawY, REST_Y);
// Output formatted data
Serial.print("X:");
Serial.print(mappedX);
Serial.print(" | Y:");
Serial.print(mappedY);
Serial.print(" | BTN:");
Serial.println(buttonPressed ? "PRESSED" : "RELEASED");
delay(20); // 50Hz polling rate
}
int applyDeadzone(int raw, int rest) {
if (abs(raw - rest) < DEADZONE) {
return 0; // Force to zero inside the deadzone
}
// Remap the active zones to -100 to +100
if (raw > rest) {
return map(raw, rest + DEADZONE, 1023, 1, 100);
} else {
return map(raw, 0, rest - DEADZONE, -100, -1);
}
}
Debugging: Fixing ADC Noise and Drift
The most common issue builders face with the KY-023 is resting drift. You leave the joystick untouched, but the serial monitor outputs an exact error string like this:
Resting Value Drift: X=498, Y=522 (Fluctuating ±12)
This happens because the ATmega328P's internal ADC multiplexer is highly sensitive to impedance and electromagnetic interference. Here are the first three things to check when your joystick fails to hold a steady zero:
- Verify the VCC Rail with a DMM: Cheap USB cables cause voltage sag. If your Nano's 5V rail is actually outputting 4.6V under load, your joystick's resting voltage drops, shifting the ADC read. Measure the KY-023 VCC pin directly; it must be within 0.1V of the Nano's 5V pin.
- Check Jumper Wire Length and Routing: Unshielded Dupont wires longer than 15cm act as antennas, picking up 50/60Hz mains hum and high-frequency noise from nearby switching regulators. Keep analog wires short and away from digital clock lines.
- Add a 100nF Decoupling Capacitor: Solder or breadboard a 100nF (0.1µF) ceramic capacitor directly between the VRx and GND pins, and another between VRy and GND. This creates a hardware low-pass filter that shorts high-frequency noise to ground before it hits the Arduino's ADC sample-and-hold circuit. According to Texas Instruments application notes on ADC noise reduction, keeping the source impedance low and filtering high frequencies is mandatory for 10-bit ADC stability.
Ranked Causes for Persistent Drift
If the first three checks don't stabilize the reading within a ±2 margin, diagnose using this ranked list:
| Rank | Cause | Bench Test | Fix |
|---|---|---|---|
| 1 | Mechanical Potentiometer Wear | Rotate the stick 360° ten times. If the resting center shifts permanently, the carbon track is worn. | Replace the KY-023 module. They are $2; do not attempt to repair the carbon wiper. |
| 2 | ADC Multiplexer Crosstalk | Read only X-axis. If it stabilizes, but reading X then Y sequentially causes jumps, it's crosstalk. | Add a dummy analogRead() and discard it, or add a 10µs delayMicroseconds() between X and Y reads to let the ADC sample-and-hold capacitor settle. |
| 3 | Missing Internal Pull-Up on SW | Serial monitor shows random "PRESSED" events when not touching the stick. | Ensure pinMode(PIN_SW, INPUT_PULLUP); is in your setup. The KY-023 lacks an onboard pull-up resistor. |
Extending and Simplifying the Build
Depending on your end application, you rarely need the full 10-bit analog resolution. Here is how to adapt the hardware to your actual needs.
Simplify: Convert to a 4-Way Digital Switch
If you are building a simple menu navigator or a directional pad for an RC car, ignore the analog mapping entirely. Treat the joystick as four digital switches. In your code, simply check if the raw ADC value drops below 200 (Down/Left) or rises above 800 (Up/Right). This eliminates deadzone math, removes the need for decoupling capacitors, and frees up CPU cycles. You can even move the VRx/VRy pins to digital inputs with Schmitt triggers if your thresholds are clean.
Extend: USB HID Gamepad (Requires Board Swap)
If your goal is to use the joystick module to play PC games, the Arduino Nano V3 will not work out of the box because the ATmega328P lacks native USB HID (Human Interface Device) support.
By selecting the correct breakout board, keeping analog traces short, and implementing a software deadzone, your joystick module Arduino project will yield smooth, drift-free control straight from the bench to the enclosure.






