The two-axis joystick module is a pair of 10 kΩ potentiometers and a push button under one thumb stick — the input for robot-car remotes and menu navigation.
On the ESP32 DevKit V1 there is one wiring surprise: the pin labelled +5V goes to 3V3. The axes go to the ADC1 input-only pins D34 and D35, and the button to D32, which has the internal pull-up those two lack. The sketch calibrates the centre at start-up and prints each axis as −100 … +100 % plus a direction.
Wiring
| ESP32 DevKit V1 | Dual-axis joystick module (KY-023) | Wire | Notes |
|---|---|---|---|
| 3V3 | +5V | red | Yes, 3V3 into the pin labelled +5V: it is only the supply for the two pots. At 5 V the axes would swing to 5 V. |
| GND | GND | black | |
| D34 | VRx | yellow | ADC1, input-only. |
| D35 | VRy | blue | ADC1, input-only. |
| D32 | SW | green | Button to GND when pressed. D32 has an internal pull-up; D34/D35 don’t. |
3.3–5 V part, 3.3 V board. VRx and VRy swing between 0 V and the module’s supply. On ESP32 and Pico, power the joystick from 3.3 V (the pin labelled +5V is just the supply input) so the analog outputs never exceed 3.3 V.
Board quirks
- The pin printed +5V is just the top of both potentiometers. On a 3.3 V board it must go to 3V3, otherwise VRx/VRy reach 5 V.
- Both axes go on input-only ADC1 pins (34 and 35), which keep working when you add Wi-Fi or ESP-NOW for a remote control. The button can’t share them: 34–39 have no internal pull-ups, so SW goes on D32 with
INPUT_PULLUP. - The resting position is rarely 2048. The cheap pots sit off-centre and the ESP32 ADC is non-linear, so the sketch measures the centre at boot and uses a dead zone of ±150 counts.
- Near both ends the ESP32 ADC flattens (below about 0.15 V and above about 3.1 V with the default attenuation), so the stick hits ±100 % a little before its mechanical limit. That’s normal.
Code
// Analog joystick module (KY-023 style) on an ESP32 DevKit V1.
// +5V pin → 3V3 (!), GND → GND, VRx → D34, VRy → D35 (ADC1, input-only), SW → D32 (internal pull-up).
// Calibrates the centre at start-up — don't touch the stick while it boots.
constexpr uint8_t X_PIN = 34;
constexpr uint8_t Y_PIN = 35;
constexpr uint8_t SW_PIN = 32; // 34/35 have no pull-ups, so the button goes on a full GPIO
constexpr int DEADZONE = 150; // counts around the centre treated as "no movement"
int cx, cy;
int axis(uint8_t pin, int centre) {
int v = analogRead(pin) - centre; // −centre … +(4095 − centre)
if (abs(v) < DEADZONE) return 0;
int span = v > 0 ? 4095 - centre : centre;
return constrain(v * 100 / span, -100, 100); // −100 … +100 %
}
void setup() {
Serial.begin(115200);
pinMode(SW_PIN, INPUT_PULLUP);
long sx = 0, sy = 0;
for (int i = 0; i < 32; i++) {
sx += analogRead(X_PIN);
sy += analogRead(Y_PIN);
delay(5);
}
cx = sx / 32;
cy = sy / 32;
Serial.printf("Joystick centre: X %d Y %d (ideal 2048 — ESP32 ADC and pots vary)\n", cx, cy);
}
void loop() {
int x = axis(X_PIN, cx);
int y = axis(Y_PIN, cy);
bool pressed = digitalRead(SW_PIN) == LOW;
const char *dir = (x == 0 && y == 0) ? "centre"
: abs(x) > abs(y) ? (x > 0 ? "right" : "left")
: (y > 0 ? "down" : "up");
Serial.printf("X %4d %% Y %4d %% %-6s %s\n", x, y, dir, pressed ? "[pressed]" : "");
delay(100);
}// Analog joystick module (KY-023 style) on an ESP32 DevKit V1.
// +5V pin → 3V3 (!), GND → GND, VRx → D34, VRy → D35 (ADC1, input-only), SW → D32 (internal pull-up).
// Calibrates the centre at start-up — don't touch the stick while it boots.
constexpr uint8_t X_PIN = 34;
constexpr uint8_t Y_PIN = 35;
constexpr uint8_t SW_PIN = 32; // 34/35 have no pull-ups, so the button goes on a full GPIO
constexpr int DEADZONE = 150; // counts around the centre treated as "no movement"
int cx, cy;
int axis(uint8_t pin, int centre) {
int v = analogRead(pin) - centre; // −centre … +(4095 − centre)
if (abs(v) < DEADZONE) return 0;
int span = v > 0 ? 4095 - centre : centre;
return constrain(v * 100 / span, -100, 100); // −100 … +100 %
}
void setup() {
Serial.begin(115200);
pinMode(SW_PIN, INPUT_PULLUP);
long sx = 0, sy = 0;
for (int i = 0; i < 32; i++) {
sx += analogRead(X_PIN);
sy += analogRead(Y_PIN);
delay(5);
}
cx = sx / 32;
cy = sy / 32;
Serial.printf("Joystick centre: X %d Y %d (ideal 2048 — ESP32 ADC and pots vary)\n", cx, cy);
}
void loop() {
int x = axis(X_PIN, cx);
int y = axis(Y_PIN, cy);
bool pressed = digitalRead(SW_PIN) == LOW;
const char *dir = (x == 0 && y == 0) ? "centre"
: abs(x) > abs(y) ? (x > 0 ? "right" : "left")
: (y > 0 ? "down" : "up");
Serial.printf("X %4d %% Y %4d %% %-6s %s\n", x, y, dir, pressed ? "[pressed]" : "");
delay(100);
}Expected serial output
Joystick centre: X 1873 Y 1912 (ideal 2048 — ESP32 ADC and pots vary)
X 0 % Y 0 % centre
X 100 % Y 3 % right
X 0 % Y -87 % up [pressed]Joystick centre: X 1873 Y 1912 (ideal 2048 — ESP32 ADC and pots vary) X 0 % Y 0 % centre X 100 % Y 3 % right X 0 % Y -87 % up [pressed]
Common errors
One direction reaches 100 % at half travel
The module is on 5 V (VIN) instead of 3V3, so the axis passes 3.3 V halfway. Move +5V to 3V3.
Button reads pressed all the time
SW is on D34/D35/VP/VN, which have no pull-up and float. Move it to D32 (or any output-capable pin) and keep INPUT_PULLUP.
Stick drifts when centred
You touched it during start-up calibration, or the dead zone is too small for your module. Press EN with the stick released, or raise DEADZONE.