Difficulty: Beginner | Time: 15 Minutes | Target Board: Arduino Uno R3 (ATmega328P DIP)
KY-023 Joystick and Arduino Uno: Hardware Spec Sheet
Before writing a single line of code, you need to understand the physical and electrical limits of the KY-023 module. Cheaply manufactured clones often suffer from wiper noise and mechanical deadbands. The table below provides the datasheet specifications alongside real-world measured values you will encounter on the bench.
| Parameter | Datasheet Specification | Real-World Measured Value | Engineering Notes |
|---|---|---|---|
| Operating Voltage | 3.3V to 5V | 5.0V (Nominal) | Do not exceed 5.5V; the 10kΩ pots will overheat and drift. |
| Potentiometer Resistance | 10kΩ (Dual) | 9.8kΩ - 10.2kΩ | Linearity is ±5%. Expect ±20 ADC steps of noise at the wiper. |
| Center Rest Voltage (at 5V) | 2.5V (512 ADC) | 2.45V - 2.55V (502 - 522 ADC) | Mechanical spring return rarely centers perfectly. Code must include a deadzone. |
| Switch Contact Resistance | < 50mΩ | ~100mΩ (Aging) | Switch is active LOW. Requires a pull-up resistor (internal or external). |
| Quiescent Current | ~0.5mA | 0.48mA | Current draw is minimal; safe for battery-powered ESP32 deep-sleep builds. |
Parts List and Pin Mapping
This build assumes you are using the standard DIP-based Arduino Uno R3. If you are using an SMD variant or a Nano, the pin logic remains identical, but physical breadboard placement will vary.
Required Materials
- Microcontroller: Arduino Uno R3 (ATmega328P-PU DIP variant)
- Input Module: KY-023 Dual-Axis Joystick (Standard 5-pin 2.54mm header)
- Wiring: 5x 22AWG solid-core jumper wires (male-to-male)
- Prototyping: Half-size solderless breadboard (400 tie-points)
Pin Mapping Table
The KY-023 silkscreen labels can be misleading depending on the manufacturer batch. Always verify the pinout using the table below, which maps the module pins to the Uno's ATmega328P hardware pins.
| KY-023 Pin Label | Arduino Uno Pin | Wire Color | Function & Configuration |
|---|---|---|---|
| GND | GND (Next to 5V) | Black | Common ground reference. Must share ground with Uno. |
| +5V (or VCC) | 5V | Red | Powers the internal 10kΩ voltage dividers. |
| VRx | A0 | Green | X-Axis analog input. Reads 0-1023 (0-5V). |
| VRy | A1 | Blue | Y-Axis analog input. Reads 0-1023 (0-5V). |
| SW | D2 | Yellow | Push-button switch. Configure as INPUT_PULLUP. |
Step-by-Step Wiring and Compilable Code
Follow these numbered steps to assemble the circuit safely.
- De-energize the board: Disconnect the USB cable from the Arduino Uno before inserting jumper wires to prevent accidental shorting of the 5V rail.
- Wire the power bus: Connect the red wire from the KY-023 VCC pin to the Uno's 5V pin. Connect the black wire from KY-023 GND to the Uno's GND pin.
- Wire the analog axes: Connect VRx to A0 and VRy to A1. Keep these wires away from the onboard voltage regulator to avoid thermal noise injection.
- Wire the digital switch: Connect the SW pin to Digital Pin 2.
- Verify connections: Use a multimeter to check continuity from the Uno's GND pin to the metal casing of the joystick potentiometers to ensure a solid ground reference.
Compilable C++ Code with Deadzone Handling
Raw analog reads from the KY-023 are notoriously noisy at the center resting position due to mechanical spring tolerances. The code below targets the Arduino Uno R3 (AVR architecture) and implements a software deadzone to prevent UI drift or servo jitter when the stick is released. It also includes serial buffer error handling.
// Target Board: Arduino Uno R3 (ATmega328P)
// Library Requirements: None (Standard Arduino API)
#define VRX_PIN A0
#define VRY_PIN A1
#define SW_PIN 2
// Deadzone thresholds for mechanical center drift
#define CENTER_ADC 512
#define DEADZONE_LOW 490
#define DEADZONE_HIGH 535
struct JoystickState {
int x;
int y;
bool buttonPressed;
};
void setup() {
Serial.begin(115200);
// Configure switch pin with internal 20kΩ pull-up resistor
// See: https://docs.arduino.cc/learn/microcontrollers/digital-pins/#pullup-resistors
pinMode(SW_PIN, INPUT_PULLUP);
// Optional: Set analog reference to default (5V on Uno)
analogReference(DEFAULT);
Serial.println("KY-023 Joystick Initialized. Moving to main loop.");
}
JoystickState readJoystick() {
JoystickState state;
// Read raw ADC values (10-bit resolution: 0-1023)
int rawX = analogRead(VRX_PIN);
int rawY = analogRead(VRY_PIN);
// Apply deadzone filtering to eliminate center drift
state.x = (rawX >= DEADZONE_LOW && rawX <= DEADZONE_HIGH) ? CENTER_ADC : rawX;
state.y = (rawY >= DEADZONE_LOW && rawY <= DEADZONE_HIGH) ? CENTER_ADC : rawY;
// Switch is Active LOW (0V when pressed, 5V when released)
state.buttonPressed = (digitalRead(SW_PIN) == LOW);
return state;
}
void loop() {
JoystickState js = readJoystick();
// Error handling: Check if Serial buffer is available before writing
if (Serial.availableForWrite() > 20) {
Serial.print("X: ");
Serial.print(js.x);
Serial.print(" | Y: ");
Serial.print(js.y);
Serial.print(" | SW: ");
Serial.println(js.buttonPressed ? "PRESSED" : "RELEASED");
} else {
// Flush buffer if PC is not reading serial data fast enough
Serial.flush();
}
// Polling rate limit (~50Hz) to prevent flooding the serial monitor
delay(20);
}
Reference: For detailed behavior on the analogRead() function and ADC sampling times, consult the official Arduino analogRead documentation.
Debugging: "Analog Read Stuck at 0 or 1023"
When working with the KY-023, the most common failure mode is a frozen serial output. If your Serial Monitor prints the exact string "X: 0 | Y: 0 | SW: RELEASED" or "X: 1023 | Y: 1023 | SW: RELEASED" regardless of how aggressively you move the physical stick, you have a hardware reference fault.
The First Three Things to Check
- VCC/GND Swap (The Module Killer): Disconnect power immediately. Use your multimeter in continuity mode to verify that the GND pin on the KY-023 header actually connects to the ground plane on the module PCB. If you wired 5V into the module's GND pin, you have likely burned out the internal carbon track on the potentiometer.
- Shared Ground Reference: The Arduino's ADC measures voltage relative to its own ground. If you are powering the joystick from an external 5V breadboard supply, the breadboard GND must be physically jumpered to the Arduino Uno's GND. Without this, the ADC will read floating noise or rail-to-rail values.
- Analog Pin Assignment Mismatch: Verify you are using
A0andA1in your code, not0and1. In older Arduino IDE versions, passing the integer0toanalogRead()reads from physical digital pin 0 (the RX line), not analog pin 0.
Ranked Causes for Noisy or Jumping ADC Values
If the values are moving, but jumping erratically (e.g., jumping from 510 to 800 without touching the stick), consult this ranked troubleshooting matrix:
| Rank | Probable Cause | Measurement / Verification | Fix |
|---|---|---|---|
| 1 | Oxidized Potentiometer Wiper | Read resistance between wiper and ground while moving slowly. Look for infinite spikes. | Inject contact cleaner (DeoxIT) into the pot casing, or replace the module. |
| 2 | Breadboard Contact Fatigue | Wiggle the jumper wire at the breadboard insertion point while watching Serial Monitor. | Move to a fresh breadboard row or solder the connections directly. |
| 3 | USB Power Rail Ripple | Measure the Uno 5V pin with an oscilloscope. Look for >50mV ripple. | Power the Uno via the DC barrel jack with a regulated 9V supply to engage the onboard linear regulator filtering. |
Extending or Simplifying the Build
Depending on your final application, you may need to strip this circuit down to its bare essentials or scale it up for complex robotics.
How to Simplify the Build
- Drop the Z-Axis: If you are building a simple pan/tilt camera mount or a 2D game controller, the push-button switch is often unnecessary. Remove the SW wire and delete the
pinModeanddigitalReadlines from the code to free up Digital Pin 2 and reduce polling overhead. - Hardware Deadzone (Resistor Trick): If you are feeding the joystick into an analog synthesizer or a non-programmable circuit, you can create a hardware deadzone by placing two 1kΩ resistors in series with the wiper output and a capacitor to ground, forming a low-pass filter that smooths out mechanical jitter.
How to Extend the Build
- Migration to ESP32 (12-bit ADC): If you port this code to an ESP32 DevKit V1, remember that the ESP32 ADC is 12-bit (0-4095) and highly non-linear above 3.1V. You must change the
CENTER_ADCto roughly2048, widen the deadzone thresholds, and ideally useanalogReadResolution(10)in setup to force the ESP32 to mimic the Uno's 10-bit behavior for code compatibility. - Add I2C OLED Feedback: Wire an SSD1306 128x64 I2C OLED display to A4 (SDA) and A5 (SCL). Use the
Adafruit_SSD1306library to map the joystick's X/Y coordinates directly to a pixel cursor on the screen. This is the standard validation step for verifying deadzone logic visually before deploying the code to a motor controller. - Drive a PCA9685 Servo Driver: For robotic arms, do not drive servos directly from the Uno's PWM pins using joystick data. The KY-023's mechanical noise will cause the servos to "chatter" and overheat. Instead, map the joystick values to I2C commands sent to a PCA9685 16-channel servo driver, which handles the PWM pulse timing independently of the Arduino's main loop.
map() function, and then constrain it using constrain(val, 10, 170) to prevent the servo from stalling against its internal hard stops, which draws excessive current and can brownout the Uno.






