Community project

ESP32 Bluetooth Gamepad

Photo of ESP32 Bluetooth Gamepad
Generated with AI

Sid2008-02

Published November 3, 2026

Build a wireless Bluetooth gamepad using an ESP32 microcontroller, a PS2 joystick module, and four push buttons. This project transforms the ESP32 into a fully functional game controller that connects to PCs, phones, and gaming devices over Bluetooth, with analog stick support and four action buttons.

This guide provides a complete wiring diagram showing how to connect the joystick's X and Y axes to the ESP32's analog inputs, wire the four buttons to digital pins, and set up power delivery. Assembly takes about 30 minutes, and the included firmware handles joystick calibration, dead zones, and Bluetooth HID gamepad communication so the controller works immediately with most games and applications.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

PS2 Joystick Module (KY-023)

Two-axis analog thumb joystick module (KY-023 / PS2 style) with a push switch under the stick. Two 10 kOhm potentiometers give X and Y voltages that sit near half the supply at rest, and pressing the stick operates a digital switch. It runs from 3.3 to 5 V and connects through a five-pin GND, +5V, VRx, VRy, SW right-angle header.

1

Push Button

A

Momentary push button switch

1

Push Button

B

Momentary push button switch

1

Push Button

X

Momentary push button switch

1

Push Button

Y

Momentary push button switch

Assemble it in 5 steps

1. Place the ESP32 and joystick

Put the ESP32 DevKit V1 and the KY-023 joystick module on the breadboard with their pins in separate rows. Connect the joystick GND pin to an ESP32 GND pin (ground), and connect its pin marked +5V or VCC to ESP32 3V3 (power).

  • The joystick header is usually printed GND, +5V, VRX, VRY, SW in order.
  • Use 3V3 even if the joystick pin says +5V; this makes its axis voltages safe for the ESP32.
  • Do not connect the joystick supply to 5V: its VRX and VRY pins can then output 5V and damage the ESP32 analog inputs.

2. Connect the joystick signals

Run a wire from VRX to GPIO34 (X-axis signal), VRY to GPIO35 (Y-axis signal), and SW to GPIO23 (stick-press signal).

  • GPIO34 and GPIO35 are input-only pins, which is exactly what the two joystick axis signals need.
  • Pressing the stick down becomes gamepad button 5.

3. Wire the A and B buttons

For the A button, connect one leg to GPIO16 (signal) and the other leg to GND (ground). For the B button, connect one leg to GPIO17 (signal) and the other leg to GND (ground).

  • On a four-leg breadboard button, use two legs on opposite sides of the button; the two legs on each same side are already connected together.
  • The ESP32 enables an internal pull-up resistor, so no separate resistor is needed for these buttons.

4. Wire the X and Y buttons

For the X button, connect one leg to GPIO18 (signal) and the other leg to GND (ground). For the Y button, connect one leg to GPIO19 (signal) and the other leg to GND (ground).

  • Keep each button's signal wire in a separate breadboard row so two button signals never touch each other.
  • A button connected to 3V3 instead of GND will read backwards and can make the controller appear to hold a button down.

5. Power and deploy the controller

Inspect that every button has one side connected to a GPIO and the other side connected to GND, then plug the ESP32 into your computer with USB. The USB cable powers the board and is also used when you press Deploy in Schematik.

  • Use a USB cable that carries data, not a charge-only cable.

Review all connections

1. Connections between "joystick_1" and "ESP32"

Functionjoystick_1ESP32
groundGND → ground returnGND
power+5V → 3.3 V supply; keeps analog outputs safe for ESP323V3
analogVRX → X-axis analog signalGPIO 34
analogVRY → Y-axis analog signalGPIO 35
digitalSW → stick press switch, active lowGPIO 23

2. Connections between "button_a" and "ESP32"

Functionbutton_aESP32
groundGND → ground returnGND
digitalSIGNAL → A button, active lowGPIO 16

3. Connections between "button_b" and "ESP32"

Functionbutton_bESP32
groundGND → ground returnGND
digitalSIGNAL → B button, active lowGPIO 17

4. Connections between "button_x" and "ESP32"

Functionbutton_xESP32
groundGND → ground returnGND
digitalSIGNAL → X button, active lowGPIO 18

5. Connections between "button_y" and "ESP32"

Functionbutton_yESP32
groundGND → ground returnGND
digitalSIGNAL → Y button, active lowGPIO 19

Deploy the firmware

#include <Arduino.h>
#include <BleGamepad.h>

constexpr int JOY_X_PIN = 34;
constexpr int JOY_Y_PIN = 35;
constexpr int JOY_SW_PIN = 23;
constexpr int BUTTON_A_PIN = 16;
constexpr int BUTTON_B_PIN = 17;
constexpr int BUTTON_X_PIN = 18;
constexpr int BUTTON_Y_PIN = 19;

constexpr int ADC_MAX = 4095;
constexpr int DEAD_ZONE = 110;
constexpr uint32_t REPORT_INTERVAL_MS = 20;

BleGamepad bleGamepad("ESP32 Simple Gamepad", "Schematik", 100);

int16_t previousX = 0;
int16_t previousY = 0;
bool previousButtons[5] = {false, false, false, false, false};
uint32_t lastReportMs = 0;

int16_t readAxis(int pin, bool invert) {
  const int raw = analogRead(pin);
  int centered = raw - (ADC_MAX / 2);

  if (abs(centered) < DEAD_ZONE) {
    centered = 0;
  }

  long value = map(centered, -(ADC_MAX / 2), ADC_MAX / 2, -32767, 32767);
  value = constrain(value, -32767, 32767);
  return invert ? -value : value;
}

bool readPressed(int pin) {
  return digitalRead(pin) == LOW;
}

void setButton(uint8_t number, bool pressed) {
  if (pressed) {
    bleGamepad.press(number);
  } else {
    bleGamepad.release(number);
  }
}

void setup() {
  pinMode(JOY_SW_PIN, INPUT_PULLUP);
  pinMode(BUTTON_A_PIN, INPUT_PULLUP);
  pinMode(BUTTON_B_PIN, INPUT_PULLUP);
  pinMode(BUTTON_X_PIN, INPUT_PULLUP);
  pinMode(BUTTON_Y_PIN, INPUT_PULLUP);

  analogReadResolution(12);
  bleGamepad.begin();
}

void loop() {
  const uint32_t now = millis();
  const uint32_t elapsed = now - lastReportMs;
  if (elapsed < REPORT_INTERVAL_MS) {
    delay(REPORT_INTERVAL_MS - elapsed);
    return;
  }
  lastReportMs = millis();

  const int16_t x = readAxis(JOY_X_PIN, false);
  const int16_t y = readAxis(JOY_Y_PIN, false);
  const bool buttons[5] = {
    readPressed(BUTTON_A_PIN),
    readPressed(BUTTON_B_PIN),
    readPressed(BUTTON_X_PIN),
    readPressed(BUTTON_Y_PIN),
    readPressed(JOY_SW_PIN)
  };

  if (!bleGamepad.isConnected()) {
    delay(1);
    return;
  }

  bool changed = (x != previousX) || (y != previousY);
  for (uint8_t i = 0; i < 5; ++i) {
    if (buttons[i] != previousButtons[i]) {
      changed = true;
    }
  }

  if (!changed) {
    delay(1);
    return;
  }

  bleGamepad.setLeftThumb(x, y);
  for (uint8_t i = 0; i < 5; ++i) {
    setButton(i + 1, buttons[i]);
    previousButtons[i] = buttons[i];
  }
  previousX = x;
  previousY = y;
  bleGamepad.sendReport();
  delay(1);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik