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.

When the SDVX Controller is connected over USB, the host operating system enumerates it as a standard HID Joystick device (Generic Desktop page, Usage 0x04). No driver installation is required on Windows, macOS, or Linux. The firmware sends a compact 5-byte report at the end of every 1 ms main-loop iteration, provided the USB device is in the USBD_STATE_CONFIGURED state.

Report structure

The joystick report is defined in main.c as:
typedef struct {
    uint8_t  buttons;   // 7 button bits + 1 padding bit
    uint16_t axis_x;    // VOL-L encoder value (little-endian)
    uint16_t axis_y;    // VOL-R encoder value (little-endian)
} USBD_JoystickReport_TypeDef;
Total size: 5 bytes (JOYSTICK_REPORT_SIZE = 5).

Byte-level breakdown

Byte(s)BitsFieldTypeRangeDescription
00Button 1 — BT-Abit0–11 = pressed
01Button 2 — BT-Bbit0–11 = pressed
02Button 3 — BT-Cbit0–11 = pressed
03Button 4 — BT-Dbit0–11 = pressed
04Button 5 — FX-Lbit0–11 = pressed
05Button 6 — FX-Rbit0–11 = pressed
06Button 7 — STARTbit0–11 = pressed
07Paddingconst0Always 0 (required by HID spec)
1–2Axis X — VOL-Luint16_t0–65535Smoothed VOL-L (left encoder) value
3–4Axis Y — VOL-Ruint16_t0–65535Smoothed VOL-R (right encoder) value
Both axis_x and axis_y are transmitted little-endian (LSB first), which is the natural byte order for uint16_t on the Cortex-M4.

Button mapping

The firmware builds the buttons byte by iterating over the buttons[] array and shifting each stable button state into its corresponding bit position:
const Button_TypeDef buttons[7] = {
    {GPIOA, BTN1_Pin},  // index 0 → BT-A  → bit 0
    {GPIOA, BTN2_Pin},  // index 1 → BT-B  → bit 1
    {GPIOA, BTN3_Pin},  // index 2 → BT-C  → bit 2
    {GPIOA, BTN4_Pin},  // index 3 → BT-D  → bit 3
    {GPIOA, FXL_Pin},   // index 4 → FX-L  → bit 4
    {GPIOA, FXR_Pin},   // index 5 → FX-R  → bit 5
    {GPIOA, START_Pin}, // index 6 → START → bit 6
};
Because buttons are active-low (GPIO pulled to GND when pressed), the EXTI callback inverts the raw GPIO level before storing it in button_stable_state[]. A 3 ms debounce lockout prevents spurious transitions.

Axis values and smoothing

The axis values are cumulative (not delta) and are derived from the 16-bit hardware timer counters (TIM3 for VOL-L, TIM4 for VOL-R):
  1. The per-frame delta is computed as a signed 16-bit difference from the previous counter value, correctly handling wrap-around at the 0 / 65535 boundary.
  2. The delta is accumulated into a raw 32-bit integer (current_axis_x_raw, current_axis_y_raw).
  3. An Exponential Moving Average (EMA) with α = 0.15 is applied to smooth out jitter:
smoothed_axis_x = (0.15f * current_axis_x_raw) + (0.85f * smoothed_axis_x);
  1. The smoothed value is divided by 10.0, rounded, and cast to uint16_t for the report.
Because the axis value accumulates continuously, it will wrap around at the uint16_t boundaries (0 → 65535 and 65535 → 0) as the encoder spins. This is expected behaviour — rhythm game clients treat the axis as a relative spinner and compute their own delta from successive report values.
To verify axis and button output on your PC, use jstest-gtk (Linux) or open “Set up USB Game Controllers” from the Windows Start menu and select the SDVX Controller device. You should see button indicators light up on press and both axis sliders move as you turn the knobs.

Raw HID report descriptor

The complete HID report descriptor, as it appears in usbd_custom_hid_if.c, is shown below. It declares 7 one-bit buttons (bits 0–6), one padding bit (bit 7), and two 16-bit absolute axes (X and Y) with a logical range of 0–65535.
static uint8_t CUSTOM_HID_ReportDesc_FS[] =
{
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x04,        // Usage (Joystick)
  0xA1, 0x01,        // Collection (Application)

  // Buttons — 7 × 1 bit
  0x05, 0x09,        //   Usage Page (Button)
  0x19, 0x01,        //   Usage Minimum (Button 1)
  0x29, 0x07,        //   Usage Maximum (Button 7)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x07,        //   Report Count (7)
  0x81, 0x02,        //   Input (Data, Var, Abs)

  // Padding — 1 bit
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x01,        //   Report Count (1)
  0x81, 0x03,        //   Input (Const, Var, Abs)

  // Axes — 2 × 16 bit
  0x05, 0x01,        //   Usage Page (Generic Desktop Ctrls)
  0x09, 0x30,        //   Usage (X)
  0x09, 0x31,        //   Usage (Y)
  0x15, 0x00,        //   Logical Minimum (0)
  0x26, 0xFF, 0xFF,  //   Logical Maximum (65535)
  0x75, 0x10,        //   Report Size (16)
  0x95, 0x02,        //   Report Count (2)
  0x81, 0x02,        //   Input (Data, Var, Abs)

  0xC0               // End Collection
};

Build docs developers (and LLMs) love