Wireless Gamepad with ESP32 and BLE
2022-05-20 | By M5Stack
License: General Public License Arduino
* Thanks for the source code and project information provided by @Ernst Sikora
Software apps and online services
My motivation for this project was to explore Bluetooth Low Energy in more depth and to build a device with peripherals and battery supply. Developing the software for a gamepad appeared to be somewhat challenging for me. In fact, it turned out to involve a good portion of investigation into topics such as:
- Human interface device (HID) protocol
- HID over GATT profile
- Button debouncing
- Battery capacity metering
- RTOS tasks
- Decoding of backtraces in a crash dump
In the course of the project, I also contributed a little bit to the Arduino-ESP32 framework by submitting an enhancement of the BLE library.
Key Features of the Gamepad
- Controls: One joystick and two buttons
- Connectivity: Human Interface Device (HID) via Bluetooth Low Energy (BLE)
- Display: Shows BLE connection state and input control state
- Power: Battery level is estimated and reported to the host device
Development Environment
As for my previous projects, I used Visual Studio Code with the PlatformIO IDE for this project. In the platformio.ini file of the project, a single library dependency to the M5StickC library is included. Furthermore, I used the build_flags option to enable debug-level log messages, and the monitor_filters option to enable file logging and exception stack trace decoding:
[env:M5StickC_Debug]
platform = espressif32
board = m5stick-c
framework = arduino
lib_deps =
M5StickC
upload_speed = 1500000
monitor_speed = 115200
build_type = debug
build_flags = -D CORE_DEBUG_LEVEL=4 ; 'Debug'
monitor_filters = log2file, esp32_exception_decoder, default
Source Code
The commented source code is available in the GitHub repository of this project.
Gamepad Controls
The reading of the control elements of the gamepad is done within the M5StickC_GamepadIO class.
The joystick unit has got an integrated ATmega328P microcontroller which provides the axis positions and push state of the joystick via an I2C interface. The M5StickC accesses this data using the I2C library:
if ( Wire.requestFrom(kI2CjoystickUnitAddr, kI2CjoystickUnitNumBytes) ) {
joyRawX_ = Wire.read();
joyRawY_ = Wire.read();
joyPressed_ = Wire.read();
}
The button states of the "dual button" unit are read using two digital ports of the M5StickC. The naive solution to just read the button states when they are needed leads to major drawbacks. Very short button presses may remain undetected. In addition, so-called "bouncing" may lead to falsely detecting press and release events that did not occur.
Therefore, the reading of the button states takes place within a dedicated task at a high rate (every 5 ms). Press and release events are detected by applying a debounce algorithm to the raw values obtained.
See [1] for an in-depth treatment of the bounce issue and debounce algorithms.
Gamepad Connectivity: Bluetooth Low Energy (BLE)
The ultimate goal of this project is to provide the data obtained from the control elements of the gamepad to some host device such as a PC, TV, or smartphone. Basically, this can be accomplished using, for example, USB (cabled), Classic Bluetooth, or Bluetooth Low Energy communication. Recent game controllers tend to use Bluetooth Low Energy (BLE), and so do I in this project.
How is it possible to achieve interoperability between all kinds of human interface devices (HID) such as a gamepad with different kinds of host devices such as a Smart TV using BLE communication? The answer is provided by the Bluetooth SIG in the "HID over GATT profile" specification (see [2]).
Luckily, the ESP32 BLE library provides the BLEHIDDevice class which implements the HID over GATT profile. Furthermore, there are some examples around demonstrating how to implement a gamepad using the BLE library: see [3] and [4].
In my project, the BLE communication tasks are implemented mainly in the GamepadBLE class. The following overview is meant to allow for a better understanding:
M5StickC_GamepadApp::setupBLE()
- Initialize the BLE device and set the device name
- Create a BLE GATT server instance
GamepadBLE::start()
- Create a BLEHIDDevice instance encompassing all required GATT services and characteristics of an HID device.
- Provide values for the "Manufacturer Name String" characteristic (UUID 0x2A29) and the "PnP ID" characteristic (UUID 0x2A50) of the "Device Information" service (UUID 0x180A)
- Set the values of the "HID Information" characteristic (UUID 0x2A4A) and the "Report Map" characteristic (UUID 0x2A4B) of the "Human Interface Device" service (UUID 0x1812)
- Create the characteristic for reporting the gamepad state (UUID 0x2A4D) and enable server-initiated notifications for it
- Enable server-initiated notifications for the "battery level" characteristic
- Start all services
- Setup the BLE advertisement data and the scan response data
- Start the BLE advertising and wait for a host to connect
When a host device is connected, the gamepad device sends its data periodically using the following operations:
GamepadBLE::updateInputReport() : Updates the HID report data and sends it to the host device as a BLE notification.
GamepadBLE::updateBatteryLevel() : Updates the battery level and sends it out as a BLE notification.
Some Remarks on HID Reports and HID Report Maps
The gamepad provides a so-called "HID report" to the host device containing the position values for each axis of the joystick as well as the state of each button.
The host device receives the HID report as a plain sequence of bytes. In order to decode this sequence of bytes correctly, the host device requires some information about its structure and the types and value ranges of all items within this structure. The gamepad provides this kind of information to the host device using the so-called "HID report map". Helpful information on HID report maps can be found in [5] and [6].
For the implementation of the gamepad, it is important that the length and structure of the HID report are exactly as defined by the HID report map also taking into account padding bits and byte alignment. In this project, I have implemented the HID report as follows:
#pragma pack(push, 1)
typedef struct
{
uint8_t btn01 : 1; // Button 1 Primary/trigger, Value = 0 to 1
uint8_t btn02 : 1; // Button 2 Secondary, Value = 0 to 1
uint8_t btn03 : 1; // Button 3 Tertiary, Value = 0 to 1
[...]
uint8_t : 2; // Pad
int16_t stickLX; // Left stick X value -32768..32767
int16_t stickLY; // Left stick Y value -32768..32767
[...]
} tGamepadReportStructGeneric2;
#pragma pack(pop)
For transferring the data to the host device, the struct is serialized into a plain sequence of bytes:
pInputCharacteristicId1_->setValue( (uint8_t*) &gamepadData_, sizeof(gamepadData_));
pInputCharacteristicId1_->notify();
Battery Level Estimation
For estimating the battery level, the AXP192 power management unit, a component of the M5StickC device, is used. I have implemented the battery level estimation in the M5StickC_PowerManagement class.
The AXP192 provides information e.g. about battery voltage, whether an external power source is available, and the charge current. When a power source is present, the AXP192 charges its LiPo battery up to a voltage of about 4.2 V, and then gradually reduces the charge current. At 3.2 V the AXP192 switches off the device in order to avoid the battery being damaged.
For determining the battery capacity, the AXP192 has got a so-called coulomb counter. The AXP192 increases this counter during charging and decreases it during discharging.
The M5StickC_PowerManagement::computeBatteryCapacity() operation evaluates the charge state and sets the coulomb counter to zero, when the battery is (almost) fully discharged. During charging, the operation learns the maximum value of the coulomb counter and stores it in non-volatile memory using M5StickC_PowerManagement::writeFloatToAxpStorage(). At startup, the stored capacity value is retrieved from non-volatile memory using M5StickC_PowerManagement::retrieveCoulombCounterMaxValue().
Information on the AXP192 in English is somewhat difficult to find. I used [7] as the main source along with the source code of the M5StickC library.
References
[1] Online article "Embed With Elliot: Debounce Your Noisy Buttons, Part II", Elliot Williams, 2015. URL: https://hackaday.com/2015/12/10/embed-with-elliot-debounce-your-noisy-buttons-part-ii
[2] Specification "HID over GATT profile", Bluetooth SIG, 2011. URL: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=245141
[3] Github project "ESP32-BLE-Gamepad", lemmingDev. URL: https://github.com/lemmingDev/ESP32-BLE-Gamepad
[4] Github project "ESP32-HID-Gamepad", Nathan Diven. URL: https://github.com/NateXVI/ESP32-HID-Gamepad
[5] Online article "Human Interface Devices (HID) Information", USB Implementers Forum. URL: https://www.usb.org/hid
[6] Online article "Tutorial about USB HID Report Descriptors", Frank Zhao. URL: https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors
[7] Wiki "AXP192 Information", RolandoMagico. URL: https://github.com/RolandoMagico/TTGO-T-Beam/wiki/AXP192-Information
Schematics
Wiring of the Gamepad
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum