Close

BLE Joystick: yak shaving the smartleds

A project log for Polymorphic Blocks: Python PCB HDL

A Python HDL for PCB design featuring library-based subcircuit design automation and exports ready-to-route KiCad netlists

richard-ducky-linRichard "Ducky" Lin 5 hours ago0 Comments

One of the more complex projects on the mega-panel is the BLE joystick, an battery-powered BLE device with an Xbox joystick and IMU for air mouse capabilities. The idea is a configurable remote controller, such as a presentation remote or generalized gamepad but for general mouse uses.

This device has a nRF52840 main and a CH32V003 coprocessor as the buttons board. The coprocessor allows the inter-board connection to be simple (single 8-pin FPC) by aggregating multiple IOs (including ws2812-style smartleds) over a common I2C bus. The coprocessor is also, kind of surprisingly, cheaper than a fixed-purpose I2C IO expander. Though draws way more sleep current.

The sub-boards and managed connector pairs feature of the HDL allowed the FPCs to be correct-by-construction, and the multi-board assembly worked the first time around. This includes a custom FPC for the XBox joystick itself (so it can sit lower than the PCBs, allowing vertical space for the battery) as well as the FFC to connect the buttons sub-board. Yay!

This board was an iteration from a prior version that used a ESP32-C3 on a single board, but ESP32 on Rust lacks advanced power management which makes it impractical for a battery device. The first revision was also mechanically more simplistic, as one rigid board with the joystick directly mounted, which is poor use of vertical space for a handheld device. Porting the firmware to this version was a bit of work, but because of vendor-neutral Rust embedded HALs, most of the existing code could be directly re-used.

The coprocessor code had to be written from scratch, and while there isn't a standard embedded-hal I2C target API, there is an I2C target HAL for the CH32 series, which works fine.

However, the CH32V003 SPI module, used to generate the smartled signals is ... finicky. The existing smartleds crates make a lot of assumptions, for example that the processor is fast enough to keep the SPI module fed without inter-byte delays, or that there is no glitching before the first byte. Smartleds crates also typically generate the waveforms by using 4 SPI bits per smartled bit, either 1000 or 1110. This technically violates the ws2812 timing spec (not enough low time during a high bit) at some timing configurations. The combination of these cause quite a bit of glitching on each smartleds transaction. So I built a custom smartleds SPI driver with configurable patterns: https://github.com/ducky64/smartleds-spi-lut. This seems to work robustly on the CH32V003 and is as optimized as far as reasonable with standard Rust code.

All the processing does take some time, about 366us from the end of the I2C transaction to when the first smartleds bit gets shifted out. But it works!

Next step is to get the accelerometer working for air mouse functionality. But before that, it needs a USB bootloader, since the case and battery obscure the debugging ports. The yak shaving continues.

Firmware and mechanical (FreeCAD) sources are here: https://github.com/ducky64/blejoystick-rs

Discussions