Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/MrJefter/sdvx-controller/llms.txt

Use this file to discover all available pages before exploring further.

The SDVX controller firmware is a bare-metal STM32 HAL project developed in STM32CubeIDE, targeting the STM32F401 microcontroller on a WeAct BlackPill v3.0 board. It exposes the controller to the host PC as a USB HID joystick device, handling button debouncing via hardware interrupts, quadrature encoder decoding via dedicated timer peripherals, and a three-layer LED compositing system driven entirely by DMA — all within a tight 1 ms main loop.

Main Loop

The firmware’s while(1) loop executes five functions in strict order every millisecond. Each call is responsible for a distinct subsystem: updating LEDs, reading physical inputs, packaging the USB report, watching for a DFU trigger, and transmitting the report.
while (1)
{
    visHandle();         // LED update — checks DMA, gates to ~50 FPS, kicks ws2812b_handle()
    Read_Encoders();     // Read TIM3/TIM4 counters, compute signed deltas, apply EMA smoothing
    Process_Buttons();   // Pack button_stable_state[] bitmask into joystickReport.buttons
    Check_DFU_Entry();   // If START held >= 5000 ms, call GoToBootloader()
    Send_HID_Report();   // USBD_CUSTOM_HID_SendReport() when USB is in CONFIGURED state
    HAL_Delay(MAIN_LOOP_DELAY_MS); // 1 ms delay — sets the base polling cadence
}
CallPurpose
visHandle()Guards on ws2812b.transferComplete, delegates to visHandle2() at 50 FPS, then fires a new DMA transfer
Read_Encoders()Snapshots TIM3 (VOL-L) and TIM4 (VOL-R) counters, derives signed 16-bit deltas, feeds EMA, scales to uint16_t axis values
Process_Buttons()Iterates button_stable_state[] and packs one bit per button into joystickReport.buttons
Check_DFU_Entry()Compares HAL_GetTick() against dfu_button_press_time; if ≥ 5000 ms, calls GoToBootloader()
Send_HID_Report()Calls USBD_CUSTOM_HID_SendReport() only when hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED
HAL_Delay(1)1 ms sleep — determines the effective ~1 kHz polling rate

USB HID Reporting

The firmware registers as a Custom HID device using the STM32 USB Device Library. The HID report descriptor (in usbd_custom_hid_if.c) defines a joystick application collection containing:
  • 7 buttons (1 bit each) mapped to BT-A, BT-B, BT-C, BT-D, FX-L, FX-R, and START
  • 1 padding bit to align to a byte boundary
  • 2 axes (X and Y, 16 bits each, range 0–65535) for the two rotary encoders (VOL-L and VOL-R)
The total report is 5 bytes (JOYSTICK_REPORT_SIZE = 5): 1 byte of button flags + 4 bytes of axis data. The report is transmitted every loop iteration when the USB device is in the configured state.

Button Debouncing

All seven buttons are wired with pull-up resistors and trigger EXTI interrupts on both rising and falling edges (GPIO_MODE_IT_RISING_FALLING). The debouncing logic lives entirely in HAL_GPIO_EXTI_Callback():
  1. On each edge, the callback checks whether HAL_GetTick() >= button_lockout_timer[i].
  2. If the lockout has expired, it reads the current raw GPIO state and updates button_stable_state[i].
  3. It then sets a new lockout: button_lockout_timer[i] = current_tick + DEBOUNCE_LOCKOUT_MS (3 ms).
  4. Any further edges within those 3 ms are silently ignored.
This interrupt-driven lockout approach avoids polling entirely for button state changes while still cleanly rejecting contact bounce.

Encoder Reading

The two LPD3806 rotary encoders (VOL-L and VOL-R) are decoded entirely in hardware using the STM32’s timer encoder interface:
  • TIM3 decodes VOL-L (pins PA6/PA7, KNOBLA/KNOBLB)
  • TIM4 decodes VOL-R (pins PB6/PB7, KNOBRA/KNOBRB)
Both timers use TIM_ENCODERMODE_TI12 (both edges of both channels counted), with an auto-reload period of 0xFFFF (65535) to allow full wrap-around. Each call to Read_Encoders() snapshots the current 16-bit counter and computes the delta via a signed int16_t cast, which correctly handles wrap-around in both directions. An Exponential Moving Average (EMA) is then applied to smooth axis output:
smoothed = (0.15 × raw_accumulated) + (0.85 × smoothed_previous)
The smoothed value is divided by 10.0 and clamped to uint16_t before being placed into the HID report. The raw per-frame deltas are also published in g_encoder_delta_x / g_encoder_delta_y for the LED subsystem to consume.

LED Effect System

The WS2812B LED strip (12 LEDs, connected to PB0) is driven via DMA/PWM using Martin Hubacek’s ws2812b library. The effect pipeline runs at a gated ~50 FPS (20 ms interval) and composites three independent layers:
  1. Fog effect — a sum-of-sines noise field modulated over time produces a slowly drifting purple ambient glow (FOG_BASE_R/G/B = 150, 0, 255). High-noise regions receive a white brightening mix.
  2. Pulse effect — when any button is pressed, the LEDs mapped to that button flash white for 250 ms with a linear fade-out.
  3. Encoder glow — rotation on VOL-L illuminates its ring LEDs in cyan; VOL-R illuminates its ring in magenta. Activity decays at 0.955× per frame for a smooth trail effect.
The final GRB byte triplet for each LED is written into frameBuffer[3 × 12] by visScript(). visHandle() gates execution on ws2812b.transferComplete and fires ws2812b_handle() to start the next DMA transfer when a new frame is ready.

DFU Bootloader Entry

The firmware includes a software DFU entry mechanism to re-flash without physical access to the BOOT0 button. Holding the START button for 5 seconds triggers GoToBootloader():
static void GoToBootloader(void) {
    HAL_GPIO_WritePin(BOOT0_PORT, BOOT0_PIN, GPIO_PIN_SET); // Drive PA15 HIGH
    HAL_Delay(250); // Allow pin state to stabilise
    NVIC_SystemReset(); // Trigger system reset → MCU boots into DFU
}
The function drives PA15 high before resetting. This works only if a 150 nF ceramic capacitor has been added between BOOT0 and PA15 on the BlackPill board, which holds the BOOT0 line high during the reset pulse long enough for the bootloader ROM to see it.

Building

Import the HID_SDVX_NEW4 project into STM32CubeIDE and compile the firmware binary.

Flashing

Flash the firmware via USB DFU mode with dfu-util or via SWD with an ST-Link adapter.

Configuration

Tune debounce timing, encoder smoothing, LED colors, and DFU hold time.

Pinout

GPIO pin assignments for buttons, encoders, LEDs, and the BOOT0 control line.

Build docs developers (and LLMs) love