
Bluetooth Arcade Joystick
A RetroPie is a lot of fun, but quite a few of the games one might want to play really need a classic arcade joystick. And that joystick needs to be usable from the comfort of the recliner, which is definitely not in USB cable range of the TV. The solution is obvious: build a bluetooth joystick controller.
To build this, we’ll need the software to drive it, and the hardware to run it on. The basic design is to use an ESP32 dev board, which exposes enough GPIO pins to support a 4-way joystick and 8 buttons, and a li-po battery to keep the thing running.
The software first requires an understanding of the communication mechanism to be used - bluetooth low energy, which not only has the possibility of consuming lower power than bluetooth classic (AFAIK on an ESP32 it currently does not) but also has very low latency (and on an ESP32, this is definitely true). This is essential for having a good gaming experience.
Bluetooth Low Energy
BLE operates in a client-server model, where the peripheral provide services to a central client device. The ESP-IDF framework provides BLE support via Bluedroid, but documentation is a little sparse. Even more annoying, the Bluetooth SIG reworked their website and broke huge numbers of deep links at some point in the relatively recent past, so many articles include broken links. This one will not link directly to the BT SIG for this reason.
Creating a Bluetooth Low Energy joystick means understanding an entire stack of acronyms. The device appears as a Human Interface Device (HID), which is the same meaning as for USB devices. This is layered into the Generic Attribute Profile (GATT) of BLE. GATT operates alongside the Generic Access Profile (GAP), which manages connections and advertisements.
A GATT application profile is a sequence of services, where each service is a sequence of characteristics. There are predefined services available, including the HID service (16-bit UUID 0x1812), the battery service (0x180f), and the device information service (0x180a). A HID device should provide all three of these services.
The Battery Service has a single characteristic for reporting the current battery level. An XML description of the service can be found via Google; it’s published by the BT SIG at unstable URLs.
The key documents to obtain from the BT SIG are the HID over GATT profile (HOGP), the core Bluetooth specification 4.2 or later, and the supplement to the core specification (the CSS document), version 8 or later. The HOGP spec needs the most attention, while the other two mostly have tables and definitions to refer to.
BLE on ESP32 with Bluedroid
The library is driven by callbacks with event codes. Setup of a BLE server happens in stages as various events are triggered, which is sort of documented in the walkthroughs. The primary sequence is:
Set up the Bluetooth stack.
This process involves enabling NVRAM, initialising and enabling the BT controller, initialising and enabling the Bluedroid application stack, registering callbacks for GAP and GATTS events, and finally registering an application. This application registration triggers our first event,
ESP_GATTS_REG_EVT
.Construct advertising data and begin advertising.
The application registration event triggers a callback in the GATTS portion of the stack. At this point, the GAP parameters for the device should be configured: device name, appearance, advertising and scan response data. Each of advertising and scan response data is capped at 31 bytes, so using the additional 31 bytes of a scan response may be necessary for larger advertisements. An event is triggered when each has completed successfully, the
ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT
and theESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT
events. When both events have fired, advertising may be started, which will in turn trigger anESP_GAP_BLE_ADV_START_COMPLETE_EVT
event.The Bluedroid stack will take care of packing advertising data into advertising and scan response data packets. The order in which it writes data is flags, appearance, device name, manufacturer data, TX power, 16-bit UUIDs, 32-bit UUIDs, 128-bit UUIDs, service solicitation UUIDs, and finally service data. Most of these items are not particularly important for a joystick device: appearance, device name, and 16-bit UUIDs are the most valuable. The TX power is flagged “TODO” in the version of the library I have, so there’s no point worrying about that one. The stack will also take care of translating 128-bit UUIDs to the right format based on their value.
After setting up and starting advertisements, the device will be visible to BT hosts, but it will not respond to connection requests yet.
Define services and characteristics.
An application in BLE consists of a sequence of services, each of which is a sequence of characteristics. Each characteristic is a declaration, a value, and a possibly empty set of descriptors. All of these things are defined using GATT attributes. For example, a partial HID service declaration would consist of these ordered attributes:
- A Service Declaration attribute for the HID Service UUID
- A Characteristic Declaration attribute for a read/notify value
- A HID Report attribute containing the value itself
- A Client Characteristic Configuration (CCC) Descriptor attribute
- A Report Reference Descriptor attribute
This partial declaration only has a single characteristic; a full HID service must have a Report Map, at least one Report, and a HID Information characteristic, and may have further Reports, a Control Point, and for keyboards and mice, a Protocol Mode value and Boot Report(s), used for a host that’s booting up and can’t use the full HID spec.
Bluedroid allows these to be created one by one, or defined in a table and created in one shot. The latter is much simpler: create the service in one call, and in the ensuing callback start the service. The stack can also take care of responding to read and write requests for you.
For the HID profile, the CCC allows the client to inform the server whether it wants notifications enabled or not, and the Report Reference indicates the type(s) (Input, Output, or Feature) and identifiers of the Report.
Alongside the HID Service itself, a HID Device must also implement the Battery Service and Device Information Service.
Security, security, security, security.
The HID profile requires a secured connection. This involves bonding - a long-term exchange of keys that the server and client will use for future connections. As the gamepad has no output controls and only limited input controls, it’s easiest to use a no-PIN bonding mechanism. We’re not too worried about the security of a game controller, so there’s little reason to require anything beyond a confirmation box on the host device.
Bluedroid in ESP-IDF requires that at least two security parameters are set:
ESP_BLE_SM_AUTHEN_REQ_MODE
, set toESP_LE_AUTH_REQ_SC_BOND
to require bonding,HID profile conformance details.
The HID profile has some additional requirements and recommendations to follow regarding advertising, connecting to non-bonded and bonded hosts, and reconnections. I glossed over most of this, which likely will cause problems down the line.
With all of this sorted out, the joystick pairs and bonds.
The remaining code is trivial: interrupts on any of the input pins (rising or falling edges) cause all pins to be read and a BLE notification message to be sent. The delay on this appears to be singificantly smaller than the delay caused by my aging reflexes - the ESP32 is a reasonably fast chip, there’s next to nothing executing on it, and the BLE transport sends tiny packets very quickly.
Construction
I’ve provided supplier links to most parts, but everything’s pretty generic here, so find your own options as you please. Some parts I had hanging around, so not everything has a link, but nothing is hard to find. Construction is easy, most of the work in this project was in the software.
Bill of Materials
- A zero-delay USB joystick kit
- An acrylic case - I chose a clear case to expose the guts with pride
- An ESP32 dev board (DOIT devkit v1, 36-pin variant; most boards will work)
- A li-po battery module for an 18650 battery
- A toggle switch for power control
- A set of XHP connectors in 2, 3, and 4 pin configurations
- A perfboard large enough to hold the connectors and dev board
- A 40-pin female header strip
The joystick kit includes a 4-way joystick, 8x 30mm buttons, 2x 22mm buttons, cables with XHP plugs, and a zero-delay USB encoder board that we will not use. The case only has 6x 30mm holes and 2x22mm holes, so two buttons are also unused. The particular combination I wound up with didn’t quite work perfectly - the buttons are push-through and should snap into place, but the lid of the case is a little too thick for them to snap in. Screw type buttons would be a better choice with this case.
The 40-pin header can be cut in half (pull out a sacrificial pin and cut in the hole it left, don’t try and cut between two pins) and trimmed to get two 18-pin headers to hold the ESP32 module; this means any mistakes don’t cost me a whole module, and a subsequent improved version is possible as well.
Onto the perfboard I soldered the headers, 8 2-pin connectors for the buttons, and a 5-pin male header for the joystick. I didn’t have a 5-pin XHP socket in my kit, alas. When laying this part out, pay attention to where the board will sit in the case and how far the cables can reach - I soldered in one header for the joystick before verifying that the cable could reach that location, and needed to add a second header on the other end of the board.
Another pair of headers for a power cable, with the high side line passing through the toggle switch (drill a small hole in the rear of the case to mount the switch), jumper wires as appropriate, and everything’s ready to go.
Closing remarks
This was a fun project - it didn’t stress my mediocre hardware abilities, I learned a lot about bluetooth, I applied material on FreeRTOS I’d learned recently while completing a Master’s degree by coursework, and the thing works !
There’s a few things that aren’t quite right on the build. The screws that hold the case together come loose very easily as the case rattles around during play, and the joystick’s knob also loosens over time - a bit plumber’s tape or a spring washer will likely fix these problems. I have to open up the case to recharge the battery, for which I have a Micro-USB panel mount part on order - wiring the power lines to the charger and the data lines to the ESP32 should do the trick, though this means I’ll want to run power in via the USB port at all times instead of trying to switch from 3V in to 5V via the on-board regulator.
The main problem with the whole setup though is that the particular battery board I’m using doesn’t have any charge measurement capabilities. This means I run blind on how long I can expect the battery to last. It’s got a decent operational lifetime, I haven’t run it down enough to see if the board safely shuts off when the battery gets low but I have played for 4+hrs in a sitting without it flaking out - so it’s good enough for my purposes. A further revision would definitely change the battery board out for something that can measure charge too.
Extra buttons or an 8-way joystick could be nice, but would require a shift register. If I go down this route I’ll probably design a PCB, and maybe see about bringing the battery charger on-board. At this point, it’d also be nice to implement a soft-touch power switch and allow the ESP32 to shut itself down if the device is left idle.