Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Worldwide shipping via DHL
Skip to main content

ESP32 KY-023 Joystick: Build a BLE Gamepad

October 02, 2026 8 views

ESP32 KY-023 Joystick: Build a BLE Gamepad | ShillehTek
Project

Build an ESP32 KY-023 joystick BLE gamepad with four buttons that pairs natively to Android and Windows using Bluetooth HID, using parts from ShillehTek.

1 hr Beginner to Intermediate6 parts

Project Overview

ESP32 Bluetooth Gamepad: Build a wireless BLE HID controller using an ESP32 dev board, a KY-023 dual-axis joystick module, and four tactile buttons so it pairs natively with Android phones and Windows PCs (no app or driver required).

Every phone, laptop, and single-board computer already speaks Bluetooth HID, the same protocol a store-bought wireless controller uses. An ESP32 can speak it too, which means a joystick module, a handful of tactile buttons, and about a hundred lines of code become a controller that pairs like any other.

This guide builds a one-stick, five-button gamepad on a breadboard, pairs it with an Android phone and a Windows PC, calibrates the stick so the cursor does not drift, and shows how to grow it into a two-stick pad, a battery-powered handheld, or the transmitter for your own robot.

  • Time: ~1 hour
  • Skill level: Beginner to Intermediate
  • What you will build: A BLE HID gamepad (X/Y stick + 5 buttons) recognized natively by Android, Windows, macOS, Linux, and Raspberry Pi, with a center-calibrated dead zone and about 100 reports a second.
ESP32 BLE gamepad on a breadboard with KY-023 joystick and buttons paired to a phone
A joystick, two buttons, and an ESP32 paired to a phone like any Bluetooth controller.

Parts List

From ShillehTek

External

  • An Android phone or a Bluetooth-equipped PC to pair with; a free gamepad tester app or a web gamepad tester in Chrome
  • Optional: an 18650 cell and holder for the handheld version

Note: the ESP32's second ADC (GPIO0, 2, 4, 12-15, 25-27) is unusable while the radio is on. Bluetooth is friendlier than Wi-Fi here, but put the joystick on ADC1 pins (GPIO32-39) anyway so the same board works if you ever add Wi-Fi. GPIO34-39 are input-only, which is exactly what an analog stick needs.

Step-by-Step Guide

Step 1 - Wire the stick and buttons

ESP32 wired to a KY-023 joystick module and four tactile buttons for a BLE gamepad build
Two axes, a stick click, and four buttons for a simple gamepad input layout.

Goal: Two analog axes and five digital inputs.

What to do: KY-023: +5V to 3V3 (the ESP32 ADC must never see 5 V), GND to GND, VRx to GPIO34, VRy to GPIO35, SW to GPIO32.

What to do: Buttons: connect one leg of each to GPIO25, GPIO26, GPIO27, and GPIO14, and connect the other legs to GND. No resistors are needed because the sketch enables internal pull-ups, so pressed reads LOW. Lay the buttons out as a diamond (A/B/X/Y) to the right of the stick.

Expected result: A controller-shaped breadboard. Nothing happens yet.

Step 2 - Install the libraries

Goal: HID without writing descriptors by hand.

What to do: Install the ESP32 board package (Boards Manager, search for "esp32" by Espressif, version 3.x). Then in the Library Manager install NimBLE-Arduino and ESP32-BLE-Gamepad (by lemmingDev). Select your ESP32 Dev Module and the correct port.

Expected result: The sketch below compiles.

Step 3 - Upload the sketch

#include <BleGamepad.h>          // lemmingDev ESP32-BLE-Gamepad (needs NimBLE-Arduino)

const int JOY_X = 34, JOY_Y = 35, JOY_SW = 32;          // KY-023: VRx, VRy, SW
const int BTN[4] = {25, 26, 27, 14};                    // A, B, X, Y  (other leg to GND)
const int DEADZONE = 60;                                // raw ADC counts around centre that count as "still"
int cx = 2048, cy = 2048;                               // stick centre, measured at boot

BleGamepad pad("ShillehTek Pad", "ShillehTek", 100);    // name, manufacturer, battery %

int toAxis(int raw, int centre) {                       // 12-bit ADC reading -> 0..32767 with the centre at 16384
  int d = raw - centre;
  if (abs(d) < DEADZONE) d = 0;
  long v = 16384L + (long)d * 16384L / 2048L;
  return constrain(v, 0, 32767);
}

void setup() {
  pinMode(JOY_SW, INPUT_PULLUP);
  for (int i = 0; i < 4; i++) pinMode(BTN[i], INPUT_PULLUP);
  analogReadResolution(12);
  delay(200);
  cx = analogRead(JOY_X); cy = analogRead(JOY_Y);       // hands off the stick while it boots

  BleGamepadConfiguration cfg;
  cfg.setAutoReport(false);                             // we send exactly one report per loop
  cfg.setControllerType(CONTROLLER_TYPE_GAMEPAD);
  cfg.setButtonCount(5);                                // four buttons + stick click
  cfg.setHatSwitchCount(0);
  cfg.setWhichAxes(true, true, false, false, false, false, false, false);   // X and Y only
  cfg.setAxesMin(0); cfg.setAxesMax(32767);
  pad.begin(&cfg);
}

void loop() {
  if (!pad.isConnected()) { delay(100); return; }       // nothing to do until something pairs

  pad.setX(toAxis(analogRead(JOY_X), cx));
  pad.setY(toAxis(analogRead(JOY_Y), cy));

  for (int i = 0; i < 4; i++) {
    if (digitalRead(BTN[i]) == LOW) pad.press(BUTTON_1 + i); else pad.release(BUTTON_1 + i);
  }
  if (digitalRead(JOY_SW) == LOW) pad.press(BUTTON_5); else pad.release(BUTTON_5);

  pad.sendReport();
  delay(10);                                            // ~100 reports per second
}

What to do: Upload the sketch. Do not touch the stick for the first second because the sketch samples the resting position as the center.

Expected result: The ESP32 advertises itself as "ShillehTek Pad". Nothing visible happens on the board; pairing and testing happen on your phone or PC.

Step 4 - Pair and test

Goal: See your inputs on a screen.

What to do: Android: Settings to Bluetooth to Pair new device, then select "ShillehTek Pad". Open a gamepad tester app and confirm the stick and buttons respond.

What to do: Windows: Settings to Bluetooth to Add device; then run joy.cpl (or use a web gamepad tester in Chrome) to view the axes and buttons. On a Raspberry Pi it shows up in RetroArch input settings like any controller. The ESP32 remembers pairing and reconnects automatically.

Expected result: Axes sit at center when released, reach near full deflection at the edges, and buttons register cleanly.

Step 5 - Tune the feel

Goal: No drift and correct directions.

What to do: If the cursor creeps when you let go, raise DEADZONE to 100 to 150. If up is down, invert Y by using pad.setY(32767 - toAxis(...)).

What to do: If the stick reaches full deflection before the physical stop, the potentiometer may not swing the full 0 to 3.3 V range. Measure your real min and max over Serial and scale to those values instead of assuming 0 to 4095. Lower delay(10) for faster response; 10 ms is already similar to many commercial controllers.

Expected result: A controller that feels stable and predictable instead of drifting like a prototype.

Step 6 - Grow it

Goal: Expand from one stick to a more complete handheld or remote.

What to do: Add a second KY-023 on GPIO36/39 and enable the Z and rZ axes in setWhichAxes() for a twin-stick pad. Enable a hat switch and wire four buttons as a D-pad.

What to do: For battery power, run an 18650 through the TP4056 (cell to TP4056 OUT to ESP32 5V/VIN) and report real battery level with pad.setBatteryLevel(). Or flip the idea: run a BLE central sketch on a second ESP32 in a robot to use this as a custom wireless remote.

Expected result: A flexible BLE controller foundation you can adapt to your project.

Conclusion

This build turns an ESP32 plus a KY-023 joystick and buttons into a BLE HID gamepad that pairs with standard Bluetooth menus and works across Android, Windows, macOS, Linux, and Raspberry Pi. With a simple center calibration and a dead zone, you get stable analog control without drift.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Photo credit: All photos and images in this tutorial are credited to Ernst Sikora on Hackster.io. The original guide by Ernst Sikora served as the reference for this ShillehTek version.

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

All 6 in stock
0 parts selected $0.00