Skip to content
 
 

Repository files navigation

FreeJoyX

Firmware build Wire-format header sync

FreeJoyX project: Firmware · Configurator

STILL IN INITIAL DEVELOPMENT STAGES

Repo is up for feedback and suggestions, I'll update this when it's ready to try.

FreeJoyX is a fork of FreeJoy — a widely configurable USB HID game-device firmware. It allows you to build your own HOTAS, pedals, steering wheel, sim-racing button box, etc., or customize a purchased one. FreeJoyX extends upstream FreeJoy with a second hardware-quadrature encoder, a reworked software-encoder path (explicit pin pairing, selectable 1×/2×/4× decode, and per-detent queue mode — see the Encoders wiki page), boolean-logic virtual buttons, tap and double-tap gesture button types, and a port to the STM32F411 BlackPill alongside the original BluePill.

Supported boards

Target MCU Driver layer USB stack
f103 (BluePill) STM32F103C8T6 StdPeriph USB-FS-Device
f411 (WeAct BlackPill V3.x) STM32F411CEU6 STM32 LL (+ HAL flash driver) ST USB Device Library

Both targets share the same dev_config_t wire format; the configurator dispatches per-board pin tables based on a board_id byte added in firmware v1.7.7 and rejects cross-board configuration writes (with the configurator's cross-board converter to bridge the gap). Wire format generation is currently FIRMWARE_VERSION 0x0070 (dedicated shift buttons — shift modifiers are now full button objects in their own shift_buttons[] array, so a shift can be a Toggle latching shift-lock, a Logic combination, or a Sequential-toggle that cycles shift layers with one button, and shift triggers are never HID-reported and don't consume a joystick button slot; logic debounce is now a single global logic_debounce_ms shared by every logic button and shift); the prior 0x0050 (decoupled queue-mode encoder gap, alongside a hardened 1x/2x/4x software quadrature decode), 0x0040 (explicit slow-encoder pin pairing), 0x0030 (MCP23017/MCP23S17 GPIO expanders), 0x0020, 0x0010 generations and the upstream 0x17XX lineage are still readable and forward-migratable by the configurator.

On F411, I2C2_SDA defaults to PB9 (AF9) so it sits disjoint from SPI1 (PB3/4/5, AF5) — a single build can run I2C and SPI sensors together. PB3 stays a legal alternative SDA routing (mutex with SPI1_SCK); the firmware picks whichever slot the configurator writes the role to. The pre-PR-#52 wire layout that placed SDA on slot 22 (PB2 on F411, no I2C cap) is not supported — flashing this firmware with such a config silently fails to bring up I2C; update the configurator and re-save the config to wire SDA on PB9. F103 SDA stays on PB11 / AF4 — unchanged.

Hardware verification status

Both targets build clean in CI, but on-hardware testing is ongoing and differs per board. As of v0.1.10:

Board Verified on real hardware Not yet hardware-verified
F103 BluePill Running on the maintainer's test rig: shift registers, direct axes (analog inputs read by the MCU's own ADC), and logic buttons all confirmed working. Flashing over the HID bootloader is routine. — (the day-to-day reference build)
F411 BlackPill Enumeration, configurator connect, and flashing (HID upgrade and USB DFU install) work end-to-end. Buttons confirmed reporting correctly in Windows game-controller testers — this is the fix in v0.1.10 (see Release notes). Buttons only so far. Analog axes, sensors (SPI/I2C), encoders, LEDs and shift registers are not yet exercised on physical F411 hardware.

Subsystems marked "not yet verified" on F411 are expected to work — the wire format and most application code are shared with the tested F103 path — but they haven't been run on a physical F411 yet, so treat them as unproven until they are.

Getting started

See the upstream FreeJoy wiki for the original feature set and flashing/configuration walkthrough. The features added by FreeJoyX are documented in the FreeJoyX wiki — including Encoders, Button Types, and the Timer Model — with deeper design notes in the plan files alongside this repo (F103_FASTENC_PLAN.md, F103_LOGIC_PLAN.md, F411_PORT_PLAN.md, F103_GESTURE_PLAN.md).

Features

FreeJoyX supports the following external periphery:

  • 8 analog inputs (12 bit output resolution)
  • axis-to-buttons function (up to 12 buttons per axis)
  • buttons/encoders to axis function
  • axis auto-detect (v0.1.3+) — reports a raw value per AXIS_ANALOG pin so the configurator can identify a rotated pot before it's mapped to an axis (see Axes)
  • 128 digital inputs (buttons, toggle switches, hat povs, encoders, logic-driven virtual buttons, tap, double-tap)
  • 8 shift modifiers (bumped from 5 in v1.7.8)
  • 4 hat povs
  • 2 hardware-quadrature (fast) encoders — Enc 1 on TIM1 (PA8/PA9), Enc 2 on TIM4 (PB6/PB7), opt-in
  • 14 software incremental encoders with explicit pin pairing — each encoder names its own Pin A / Pin B (from any two pins tagged Encoder), with a Swap button that exchanges the two pins to reverse direction, instead of the old positional zip of Encoder A/Encoder B button slots (wire gen 0x0040; upgraded configs keep their encoders via automatic pair synthesis). Each decodes with a Ben Buxton full/half-step state machine at selectable 1x / 2x / 4x resolution, plus an optional queue mode that emits one clean button pulse per detent — ON time set by Encoder press, OFF gap by the new decoupled Encoder gap (wire gen 0x0050) — so a fast spin's steps aren't dropped
  • shift registers 74HC165 and CD4021 — each register's data/latch/clk pin is selectable per register (defaults to the positional/pin-order mapping), so a multi-chain build can pick which physical pin drives which register
  • GPIO button expanders — MCP23017 (I2C, addresses 0x20–0x27) and MCP23S17 (SPI); up to 8 chips total in any mix on the shared bus, 16 buttons each. Several MCP23S17 chips can share one CS pin, distinguished by their A2:A0 hardware address strap (0–7)
  • digital sensors TLE5010/5011, TLE5012B, AS5048A, AS5600, MLX90393 (SPI interface only)
  • external ADCs ADS1115 and MCP3201/02/04/08
  • 4 PWM channels for lighting
  • 24 LEDs (single or matrix) bindable to button states or controlled by host software
  • 50 addressable LEDs WS2812B or PL9823 with effects and SimHub control
  • device name and other USB settings

Axes

FreeJoyX supports up to 8 analog inputs at pins A0–A7 and digital sensors as axis sources. Every axis has its own settings, including:

  • Source/destination (X, Y, Z, Rx, Ry, Rz, Slider1, Slider2)
  • Output enabling/disabling
  • Resolution
  • Calibration (manual or auto)
  • Smoothing (7 levels of filtering)
  • Inversion
  • Deadband (dynamic or center)
  • Axis offset option (magnet offset)
  • Curve shaping
  • Functions for combined axes
  • Buttons from axes
  • Axes from buttons/encoders

Since v0.1.3 the params report carries detect_axis_raw[] — a raw value per AXIS_ANALOG pin (PA0–PA7), sampled whether or not an axis sources the pin — so the configurator's rotate-to-detect can identify an analog pot even before it's mapped to a logical axis (the analog equivalent of the physical button bitmap). Params-report-only change: dev_config_t is unchanged, so no factory reset. External SPI/I2C sensors are not covered (their addressing is axis-bound). See AXIS_DETECT_PLAN.md.

Buttons

Up to 128 digital inputs can be wired as single inputs (tied to VCC or GND), button matrices, shift register inputs, GPIO-expander inputs (MCP23017 I2C / MCP23S17 SPI), or axis-to-buttons inputs. Each slot can be configured as:

  • Regular push button
  • Inverted push button
  • Toggle switch ON/OFF, ON, or OFF
  • POV hat button
  • Incremental encoder input
  • Radio buttons
  • Sequential buttons
  • 8 shifts
  • Logic — boolean function of two physical buttons (AND, OR, NAND, NOR, XOR, XNOR); ON-ON-ON 3-position switches with 2 GPIOs and binary-encoded rotary switches are first-class use cases. (NOT and A AND NOT B exist in the wire-format enum for back-compat with shipped configs but are no longer offered in the configurator picker — NOT A duplicates NORMAL + invert, and the inhibit-gate pattern can be built from AND with a NORMAL+invert slot for B.)
  • Tap — release-within-cutoff virtual button: fires briefly when the physical is pressed and released within the global cutoff window (default 200 ms). Holding past the cutoff aborts without firing, letting any sister NORMAL slot take the hold.
  • Double tap — hold-while-second-tap-held virtual button within a global window (default 200 ms). The output mirrors the physical while the second tap is held.

Tap and double-tap can coexist with NORMAL on the same physical input; gesture wins, so NORMAL is suppressed if the gesture fires within the window. Mixing them with TOGGLE/RADIO/SEQUENTIAL/POV/ENCODER/LOGIC on the same physical input is blocked by the configurator's per-physical coexistence filter.

Building

The firmware lives under armgcc/ and uses a make-based build with arm-none-eabi-gcc. On Windows the build must be invoked from MSYS2's MinGW64 shell so the GNU assembler can write its temporary files correctly.

# Build F103 BluePill firmware
make TARGET=f103

# Build F411 BlackPill firmware
make TARGET=f411

# Produce versioned artefacts in <configurator>/firmware/
make TARGET=f103 install-firmware
make TARGET=f411 install-firmware

# Cross-target release: builds both, copies into the configurator's firmware folder
make release RELEASE_VERSION=v0.1.1

Output binaries are named freejoyx-<board>-<app|boot>-<version>.bin so the configurator's flasher picks the correct image per connected board.

Config write rejection codes

When the configurator writes a config to the device, the firmware validates the incoming dev_config_t after the last packet arrives. On rejection it returns a single byte in REPORT_ID_CONFIG_OUT:

Code Reason User action
0xFE Wire-format generation mismatch (firmware_version & 0xFFF0 doesn't match the running firmware) Reflash to a firmware version on the same wire-format generation as the config, or load a config saved at the running firmware's generation
0xFD board_id mismatch (config was saved for a different board) Load a config saved for the connected board, or use the configurator's cross-board converter on the load path

Older firmware that predates the split (#27) sends 0xFE for both cases. Newer configurators handle both byte codes; older configurators silently ignore 0xFD. See application/Src/usb_app.c "Last packet received. Check version + board_id" for the firmware-side logic.

Release notes

Full history on the Releases page. The current wire-format generation is FIRMWARE_VERSION 0x0070 (dedicated shift buttons). Upgrading a board from an older generation factory-resets it on first boot; the configurator reads the old config first and migrates it forward, so re-writing it after the flash restores your mapping.

Unreleased

  • ADC acquisition rework (F103 + F411). The ADC now free-runs into a circular DMA ring instead of busy-waiting in the tick ISR, and the F103 ADC clock is brought into spec (RCC_PCLK2_Div6 → 12 MHz; was the reset default 36 MHz, 2.6× over the 14 MHz max — F411 keeps its in-spec /4 → 24 MHz). Readout is a spike-rejecting trimmed mean that preserves the smoothing the axis filter/deadband were tuned against. Fixes the F411 USB-report jitter caused by the blocking conversion and stops encoder-2/sensors being clock-gated every ADC window. No wire-format change.
  • GPIO button expanders — wire gen 0x0030. MCP23017 (I2C) and MCP23S17 (SPI) GPIO button expanders, up to 8 chips in any mix on the shared bus, 16 buttons each. Crosses the &0xFFF0 mask, so upgrading from 0x0020 factory-resets the board (the configurator migrates the old config forward).

v0.2.0

  • F411 clock self-sufficiency — the app configures its own PLL / flash-latency / ART and no longer depends on the bootloader's clock setup, so an SWD cold-boot comes up at the right speed. Plus USB config-queue / SET_REPORT hardening and mute-in-place disabled buttons (a disabled button keeps its HID number and reports 0 rather than renumbering the others). Debounce default lowered 50 → 20 ms. No wire-format change (FIRMWARE_VERSION stays 0x0020).

v0.1.12

  • F411: fixed a clock-base miscalculation that ran every timer 2× slow — the source of the reported input lag. The F411 LL setup divided the already-doubled 96 MHz APB1 timer clock where it should divide by 200000 (F103's StdPeriph divides the un-doubled PCLK1 and relies on the APB1 ×2). Three sites: board_tick.c (TIM2 ran at 1 kHz not 2 kHz → GetMillis() half-rate → button debounce, exchange period, gesture windows all 2× their set durations — e.g. a 50 ms debounce acted like 100 ms); board_pwm.c (LED PWM 500 Hz instead of 1 kHz); board_i2c.c (Fast-Mode SCL ran at 266 kHz instead of 400 kHz — Standard-Mode duty formula used in Fast Mode). F103 unaffected. No wire-format change; FIRMWARE_VERSION stays 0x0020, so upgrading does not factory-reset the board. (FreeJoyX#65)

v0.1.11

  • F411: the board re-enumerates itself after a flash — no manual power-cycle. The F411 bootloader now does an NVIC_SystemReset() once the app is written instead of jumping straight into it. On OTG-FS the old bare jump didn't reliably drop D+, so Windows never saw a disconnect→reconnect and the board sat dark until you replugged; a full reset cycles the OTG core and the D+ line, so the host re-enumerates on its own (~2–3 s, which is what the configurator's post-flash watcher already waits for). On the reset the bootloader finds the DFU magic cleared and the app valid and jumps to it. F103 (whose USB_Shutdown drops D+ properly) keeps its bare jump. Bootloader-only change; no wire-format / FIRMWARE_VERSION impact — takes effect once the new bootloader is installed via USB DFU.

v0.1.10

  • F411: joystick reports no longer freeze in Windows game-controller testers — the joystick HID report now builds into its own TX buffer instead of sharing one with the params report. On F411 the OTG-FS FIFO load is deferred to the TXFE IRQ that runs after the tick, so the shared buffer was being overwritten with params bytes before the joystick load ran — putting REPORT_ID_PARAM data on the joystick pipe, which Windows discarded (axes/buttons showed frozen in joy.cpl/VPC while a configurator session was open). F103 was immune (synchronous PMA write). Also folds in the earlier F411 bring-up fixes on this branch (config VID/PID applied to the device descriptor, Delay_us core-clock scaling, bootloader duplicate-start guard).
  • Version-syncFREEJOYX_VERSION 0.1.9 → 0.1.10 in lockstep with the configurator's v0.1.10. No wire-format change; FIRMWARE_VERSION stays 0x0020, so this does not factory-reset the board.

v0.1.5

  • Version-sync release — no functional firmware change. FREEJOYX_VERSION bumped 0.1.4 → 0.1.5 in lockstep with the configurator's v0.1.5 (USB-DFU install diagnostics + bundled flasher/firmware). FIRMWARE_VERSION stays 0x0020, so this does not factory-reset the board.

v0.1.4

  • F411: jumper-free reboot into system DFU (#55) — the configurator can drop the board into ROM USB DFU without a BOOT0 jumper.
  • F411: I2C2_SDA routed to PB9 (AF9) so SPI and I2C coexist on one build (the old PB3-shared mutex is gone).
  • Config-read: atomic snapshot + per-read checksum for more robust reads.
  • Image footer embeds the firmware semver so the configurator reads board + version straight from the .bin.
  • Fix: dynamic deadband on inverted axes (#47).
  • Folds in the never-tagged v0.1.3: axis auto-detect reports a raw value per AXIS_ANALOG pin so the configurator can find a pot before it's mapped.

Continuous integration

Three GitHub Actions workflows run on every push:

  • firmware.yml — matrix-builds both f103 and f411 (app + boot) on Ubuntu with gcc-arm-none-eabi, uploads versioned .bin artifacts.
  • header-sync.yml — clones anpeaco/FreeJoyXConfiguratorQt as a sibling and diffs common_types.h + common_defines.h after stripping comments and whitespace. Catches wire-format drift between firmware and configurator. Firmware-only sections are wrapped in /* SYNC_SKIP_BEGIN ... SYNC_SKIP_END */ markers so the check ignores them. A mirror workflow runs in the configurator repo on its own pushes; both sides catch drift independently.
  • release.yml — on v* tag push, runs make release RELEASE_VERSION=<tag> and publishes the four binaries (F103 app + boot, F411 app + boot) to a GitHub Release with auto-generated notes.

Tagging the same vX.Y.Z here and on the configurator repo in lockstep produces a matched release pair: four firmware .bins here, plus the configurator's FreeJoyXConfiguratorQt-linux-<tag>.tar.gz and FreeJoyXConfiguratorQt-windows-<tag>.zip (self-contained windeployqt bundle) on that side. The configurator's release workflow additionally supports workflow_dispatch for retro-adding a missing platform asset to an existing release.

To cut a release locally:

git tag v0.1.4
git push origin v0.1.4
# Release workflow builds + publishes the binaries automatically.

Contributing

See CONTRIBUTING.md for build, commit, and PR guidance. See STYLE.md for the inherited code style and the wire-format lockstep rule.

FreeJoy Configurator utility

FreeJoyX is paired with FreeJoyXConfiguratorQt — the matching desktop configurator (forked from FreeJoyConfiguratorQt). The two repos must stay in lockstep: application/Inc/common_types.h and application/Inc/common_defines.h are kept in manual sync with the configurator's copies on every wire-format change.

About

FreeJoy fork - STM32F103/F411 USB HID game device firmware. Pairs with anpeaco/FreeJoyXConfiguratorQt.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages