Close

Catching up to current state

A project log for ESP32 Bluetooth Gamepad

A bluetooth Gamepad with an analog joystick

tyrigityrigi 02/01/2021 at 02:050 Comments

I'm starting this log about 80% of the way through the project, so I'll attempt to re-create the timeline faithfully.

First off, I started by building Billiam's Sherbet Keypad (https://www.billiam.org/2019/05/29/sherbet-an-ergonomic-keypad). I won't go into much in the way of details, since he's done a great write-up of his own. I liked it, and it worked well, but I wanted a wireless version. So I decided to add an ESP32. I chose this chip since A) I knew it had bluetooth, and B) I plan on using this chip for other projects in the future. Plus it's really cheap and capable. So I started with a dead-bug style breakout, wired up the keypad to some GPIO, and got cracking. 

Problem #1
The keypad uses the Teensy USB HID arduino libraries to enumerate as a keyboard and generic joystick. The problem is, that I'm working in PlatformIO with Visual Studio Code, and I'm using a different chip with a different architecture. The biggest issue is that, apart from the Teensyduino libraries, it appears as though no-one has ever made a library that enables a device to enumerate as both a keyboard and joystick. Understandable, since the two don't normally play together super great, but with the addition of Steam's controller stuff, a good number of games can seamlessly switch between the two inputs. If you don't mind some icons flickering, it works really well.  The lack of library meant I'd have to make my own. Fortunately, there are already a number of libraries for the ESP32 that enable it to enumerate as a keyboard and as a gamepad over bluetooth. Just not at the same time. I'm using elements from these two libraries:

https://github.com/T-vK/ESP32-BLE-Keyboard

https://github.com/lemmingDev/ESP32-BLE-Gamepad

Smashing these two libraries into one isn't as easy as copy-paste though. Fortunately, at least for HID, bluetooth just acts like a wireless bridge. Both ends of the wireless bridge are actually using USB HID, so I can use the USB documents for information on the enumeration process. There's a pretty dense and unhelpful description of the tables in this document:

https://usb.org/sites/default/files/hut1_21.pdf

For a bit more comprehensible explanation, I found these pages:

https://who-t.blogspot.com/2018/12/understanding-hid-report-descriptors.html

https://stackoverflow.com/questions/21606991/custom-hid-device-hid-report-descriptor

https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/

I was then able to dig into the Teensyduino libraries, and slowly start to make sense of what I was seeing by comparing with the two ESP32 libraries I linked above. Over time, I managed to get what I thought was a functionally correct USB HID descriptor put together, but I just couldn't seem to make it work. Then I found a site that parses a USB HID descriptor from a list of hex numbers:

https://eleccelerator.com/usbdescreqparser/

It's an old page and looks like it's been abandoned since 2016, but it still works just fine. I was able to use this to figure out a couple of issues by comparing the interpreted output against what I was expecting, and I actually got something functional!

Problem #2

The keypad was now enumerating as a gamepad, and successfully emitting keypresses, button presses, and joystick position values. But, when I went back into Steam and tried to use it with a game, I ran into an issue. Steam was showing two XBox compatible controllers. I thought I might have accidentally left something plugged into my computer, but no. And when I launched a game and tried using the gamepad, I got erratic outputs. Nothing was working correctly. Somehow, there was a second channel enumerated and interfering with the keypress and joystick commands. Eventually, after several hours of google-fu, I found a hint: XBox One controllers used to exhibit the same issue, but Steam had since introduced a patch. I thought on this for a while, and eventually came up with an idea. One of the differences between the 360 and One eras of controllers was an integrated speaker. I tried commenting out the section of the USB HID descriptor I had that was enabling media keys, and presto: no more second controller. So my weird hybrid library doesn't support media keys. Maybe I'll figure out a solution someday, but I don't really want or need media keys on my gamepad, so I'll leave it for now.

Problem #3

I'd received my first revision of the PCB I'd designed. I soldered everything together and plugged it into my computer, and it started writing out boot information as it booted up over serial! So I started an upload of my code, and.... nothing. No response. I was getting good serial output from the chip, but the chip just wouldn't respond to anything. After sleeping on the issue, I remembered a note I'd seen about input tolerances on the chip: max input voltage is VCC+0.3V. Since the VCC max is 3.3V, that meant that the maximum input voltage is 3.6V. I had one of the ADC pins connected to the positive terminal of the Li-Ion battery, which was fully charged. A fully charged Li-Ion battery is closer to about 4.2V. Sure enough, I'd blasted a pin with too much voltage. I swapped out the ESP, and everything was good to go! I successfully uploaded the code and got the keyboard keys working. I'd forgotten the Joystick connections, so that would have to wait for the next revision.

Problem #4

It was around this point that I received my second revision of my custom PCB. I'd forgotten the joystick breakout on the first run, so I hadn't had a chance to test it with anything but the dead-bug breakout. I got everything wired up, and the joystick was way out of calibration. rather than being centered on the X-Y axes, it was somewhere around the middle of the bottom left quadrant. Apparently my lead wires to the dead-bug were long enough to affect the voltage measurement from the potentiometers. I didn't want to have to open up VS Code and manually re-calibrate this thing every time something changed once it was finished, so I took the time to figure out an auto-calibration. The stick appears to stay somewhere in the middle of a range, but that range can change. So, I just keep a record of three variables per axis. Max, Min, and current reading. If the current reading exceeds either end of the spectrum, move it accordingly. Messy and really depends on the voltage swing across the axis being linear, but so far I haven't had any issues with it, and it feels good to my super-casual usage.

Feature addition #1

I want to know how much battery my keypad has left, but I don't want to break out the multimeter to do it. But I'd messed up the ADC connection previously and damaged an ESP in the process. So I modified the PCB with a small voltage divider to reduce the maximum battery voltage (about 4.2V) down to something safe to plug into the chip. This voltage divider would constantly be draining the battery, but if I chose high enough values, the current draw should be relatively insignificant. The ADC is high-impedance, so a very low current through the divider shouldn't be a problem for measurement. I chose the values of 120K and 330K ohms, which brings the current through the divider when the battery is at full charge down to about 9 micro amps. It'll eventually drain the battery, but I plan on using this thing fairly often, so it shouldn't have an opportunity to run totally dead. Combine that with the fact that I'll be using a standard 18650 battery (which will be really easy to replace), and I'm not really concerned with the passive drain. If it becomes an issue, I might add a MOSFET triggered by the regulated voltage rail to cut off the divider when the power switch is off.

Feature addition #2

Now that I've got battery measurement figured out, I can see about figuring out battery life. I fully charged my batteries, turned the pad on, and left it alone (I hadn't implemented any power-saving stuff, so it was running full-bore, doing 3-4 bluetooth updates every millisecond). After about 7 hours, it reported 25% battery remaining. This isn't super accurate, since my code assumes a linear voltage degradation (it's more like cubic, but linear enough through most of the range to be acceptable). This was considerably better than I'd expected. I'd measured the current draw to be somewhere around 150-180mA, and with my crappy used laptop battery pack 18650's, I'd calculated something like a 6 hour battery life, full charge to full dead. But I wanted to see if I could make it better. So I started by reducing the number of updates over bluetooth to a single update per scan cycle. Next, I increased the delay between scans to the maximum tolerable. I used the super-scientific method of setting the delay really high and tapping a key 3 times as quickly as I could. If there were three characters in notepad, the delay was fast enough. If there was less than 3 characters, it had missed some of the keypresses. I settled on a 20ms delay between cycles. I couldn't get the keypad to miss any keypresses at this rate, but I could still get missed presses at 25ms. This was good, but it was still sending an update every 20ms, whether there was any changes or not. This isn't great, because every update means the chip goes into transmit mode, which uses more power. The less time spent transmitting, the longer the battery will last.

Next step was to enable the keypad to only send updates once the keypad's state had actually changed. Keypresses are easy, they're discrete events that are easy to detect. Updating the battery level and joystick position are a bit more complicated. For the battery, I implemented averaging over 128 samples, and then the keypad only updates the value over bluetooth if it changes by more than 5%. The joystick was the complicated one. If you've ever used an ADC on a microcontroller, you know that the value is never stable. And I can't just average the value, or the joystick will be super slow and sluggish to respond to movement. So, in order to filter out the fluctuations, I decided to use a threshold of sorts. I compare the current ADC value to the previous ADC value, and if the value has changed by more than a certain percentage, then I update the position. I started with a 1% change, but that still let through some of the jitter, so I ended up bumping it up to a 3% change in value. Now the keypad only sends an update packet when something actually changes, which brings the average current consumption during standby down to around 80-90mA.

Next up: Implement a deep-sleep mode after a set period of inactivity that the keypad can be woken from by pressing any key!

Discussions