Close

BlueRetro Software Design

A project log for BlueRetro

Multiplayer Bluetooth controllers adapter for retro video game consoles

jacques-gagnonJacques Gagnon 08/23/2020 at 16:090 Comments

Cores, Tasks & ISR

Selecting the ESP32 right away steered my software design in the direction of using one core for Bluetooth and the other for the wired interface.

The ESP-IDF expose the Bluetooth controller via a virtual HCI interface. The esp_vhci_host_send_packet function is used to queue a packet to be transmitted and the esp_vhci_host_register_callback function can be used to register a callback function to be called when a packet is received.

When doing a controller adapter, the most important thing to keep in mind is to keep latency at a minimum. From my experience playing around with the ESP-IDF example, I found out that the context switch between tasks or ISR are quite long. To minimize latency from context switch I decided to simply completely avoid them to begging with. Usually you are taught to do as little as possible in callbacks and interrupt handlers but in this case I think it makes a lot of sense to keep everything within either the Bluetooth controller task receive callback and the wired interface interrupt handler.

I implemented the first version of BlueRetro this way.

First version original design

I had quite a few problems due to this design in the first version. The Bluetooth controller needs to be ready before you queue another packet which is not instantaneous. Quite often in the Bluetooth host stack the need to queue multiple packet at once arise. So I had to wait within the RX callback context to Q any additional packets which was far from ideal. Also calling the packet TX function within the RX callback context is probably not a good thing to do. I had quite often crash within the BT task at that time and I thought I might be getting some kind of deadlock due to the way I was handling TX packets.

In the next iteration of my Bluetooth host stack I modified the design to include a TX queue that will be serviced by its own FreeRTOS task. This  made the stack a lot simpler and resolved my stability issues. The host stack is still run within the RX callback context but any packets I need to TX are inserted in a ring buffer. The TX task is blocking on that queue and will wake up anytime a packet need to be transmitted.

Current software design

Adapting various input type to various output type

The adapter function is called if any Bluetooth HID data is received. This function first convert the input data to a generic input data structure. This input structure is then used to apply any mapping or scaling function and save in another output structure of the same format. Base on the system configured or detected, the output structure is taken and converted in a format used by the target system wired driver on the shared memory.

No locking is required for that shared memory for a few reasons. Only the Bluetooth controller task writes to it (via the RX callback) and wired interrupt only read. Interrupt on wired side may happen while we are updating the data in share memory but it's not an issue. Button data is kept in a bit field variable from 8 up to 32 bits. Bits are updated one by one due to the need to map them. So you could have bits from the last Bluetooth report mixed with bits from the current Bluetooth report but it is not a problem since what is important is to provide the latest information available for each button at the time the wired interrupt trigger. 

For axes most retro system uses only 8 bits value and we are assured any byte copy is made in an atomic operation by the CPU. So it's not possible to have mangled byte in shared memory while we update it, it's either the old or new value, nothing in between is possible. If the need for 32 bits axis value arises, it's just a matter of keeping the data align on 32 bit boundary and making sure the axis values are accessed via uint32_t value to keep the operation atomic.

The wired drivers implement each console protocol. Console request for status will trigger an interrupt that will take care of sending the data in shared memory in the proper low level format.

Plan B

The ESP32 is a bit on the slow side with its GPIO transition at around 1-2 us and that might turn out to be an issue with some system. Also some console like the Atari 5200, Jaguar and the NeoGeo just require too much IO than the ESP32 got. For those situations I plan to have some cable adapter including a CPLD or an FPGA. This would require some software design change to implement. Rather than writing the Bluetooth output data in shared memory the data would be instead sent to a buffer within the CPLD/FPGA via SPI.

Possible new design

Since the 2nd CPU core would be now free I might change the design to queue the HID data packet and run the adapter function on the second core. But doing so might add too much latency and I might end up to simply running the Bluetooth TX task on it.

Discussions