fightstick

Firmware

The fightstick firmware is a bare-metal C application targeting the STM32F042K4 microcontroller on the stm32f0-usbd-devboard. It implements a USB HID gamepad with a 2-axis digital joystick and 12 buttons. The firmware uses no HAL or RTOS -- it directly accesses peripheral registers via CMSIS headers and relies on the usbd-fs-stm32 library for USB device functionality.

Building from source

Prerequisites

  • CMake 3.25 or later
  • ARM GNU Toolchain (arm-none-eabi)
  • Ninja (recommended) or Make

The build system automatically fetches cmake-cmsis-stm32 and usbd-fs-stm32 via CMake's FetchContent mechanism. No manual dependency installation is needed beyond the toolchain.

Configure and build

cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
cmake --build build

Output artifacts

File Description
fightstick.elf ELF binary
fightstick.bin Raw binary
fightstick.hex Intel HEX
fightstick.dfu DFU with suffix (for dfu-util)
fightstick.map Linker map

Memory layout

Region Start address Size
Flash 0x08000000 16 KB
RAM 0x20000000 6 KB

Flashing

See the Hardware Build Manual for general ST-Link instructions. The cmake target for ST-Link flashing using st-flash binary from the open-source stlink tools:

cmake --build build --target fightstick-stlink-write

Using USB DFU

The STM32F042K4 has a built-in USB DFU bootloader in system memory. There are two ways to enter DFU mode:

  • Empty MCU -- a new, unprogrammed STM32F042K4 boots directly into its system memory DFU bootloader, so it is ready to flash immediately
  • Button combination -- hold BTN09 (pin 18, B0) and BTN10 (pin 19, B1) simultaneously while powering on or resetting the device

When the firmware detects both BTN09 and BTN10 held at startup, it writes a magic value (0xdeadbeef) to an RTC backup register and triggers a software reset. On the subsequent boot, the firmware reads this value and jumps to the system bootloader at 0x1FFFC400.

See the Hardware Build Manual for general DFU flashing instructions using dfu-util.

Architecture overview

Clock configuration

The firmware configures the system clock to use the HSI48 internal oscillator directly at 48 MHz with no PLL. Both AHB and APB buses run at 48 MHz (prescaler /1). One flash wait state is enabled as required for 48 MHz operation.

Peripheral map

Peripheral Function
GPIOA PA0--PA9 Button inputs (BTN01--BTN08, BTN11, BTN12) with internal pull-ups
GPIOA PA15 Status LED output (active high)
GPIOB PB0--PB1 Button inputs (BTN09, BTN10) with internal pull-ups
GPIOB PB3--PB6 Joystick direction inputs (LEFT, RIGHT, DOWN, UP) with internal pull-ups
TIM17 HID idle rate timer (one-pulse mode)
USB Full-speed USB device peripheral
RTC BKP0R Bootloader entry flag register

Main loop

The main() function initializes GPIO ports with pull-ups, checks for the DFU button combination, then configures the clock, idle timer, and USB peripheral. The main loop calls usbd_task() continuously to process USB events.

On each USB IN token for endpoint 1, the usbd_in_cb() callback reads all 16 GPIO inputs and constructs the HID report. The report is only sent when the input state changes or when the HID idle timer expires, reducing unnecessary USB traffic.

Main source files

File Purpose
main.c GPIO initialization, clock setup, USB callbacks, input polling, and main loop
descriptors.c USB device, configuration, interface, HID, and endpoint descriptors; HID report descriptor; string descriptors
bootloader.c DFU bootloader entry logic using RTC backup register and system memory jump
idle.c HID idle rate implementation using TIM17 in one-pulse mode

USB HID protocol

Device identity

Field Value
USB version 2.0 Full-Speed
Device class HID (interface-level)
VID 0x1d50
PID 0x619a
Manufacturer rgm.io
Product fightstick
Serial number STM32 internal unique ID
Max power 40 mA (bus-powered)
HID version 1.11

Endpoints

Direction Type Max packet size Interval
EP1 IN Interrupt 64 bytes 1 ms

HID report descriptor

The device uses the Generic Desktop / Gamepad usage page with a single input report.

Report ID Kind Size Description
1 Input 2 bytes (+ 1 byte report ID) Joystick axes and buttons

Report layout (3 bytes total):

Byte Bits Field Values
0 7:0 Report ID Always 0x01
1 1:0 X axis -1 (left), 0 (center), 1 (right)
1 3:2 Y axis -1 (up), 0 (center), 1 (down)
1 4 Button 1 0 = released, 1 = pressed
1 5 Button 2 0 = released, 1 = pressed
1 6 Button 3 0 = released, 1 = pressed
1 7 Button 4 0 = released, 1 = pressed
2 0 Button 5 0 = released, 1 = pressed
2 1 Button 6 0 = released, 1 = pressed
2 2 Button 7 0 = released, 1 = pressed
2 3 Button 8 0 = released, 1 = pressed
2 4 Button 9 0 = released, 1 = pressed
2 5 Button 10 0 = released, 1 = pressed
2 6 Button 11 0 = released, 1 = pressed
2 7 Button 12 0 = released, 1 = pressed

Idle rate

The firmware implements HID Set_Idle and Get_Idle requests. The default idle rate is 500 ms (idle value 125). When the idle rate is non-zero, the firmware sends the current report periodically even if no input state has changed. When set to 0, reports are only sent on input changes.