A dual-mode (USB/BLE) game controller firmware for Seeed Studio XIAO nRF52840, inspired by FreeJoy.
- 4 analog axes (12-bit resolution)
- 16 digital buttons with debouncing
- Dual-mode operation: USB HID or BLE HID with manual switching
- Advanced calibration: Per-axis min/max, center, deadzone, and curves
- Web-based configurator: Zero-install configuration via WebBluetooth
- Persistent configuration: Stored in Flash memory
- Low latency: <1ms in USB mode, ~7.5-15ms in BLE mode
- Battery support: LiPo with USB charging
- Board: Seeed Studio XIAO nRF52840
- Analog inputs: 4x potentiometers (10kΩ recommended) on A0-A3
- Digital inputs: 16x buttons with pull-up resistors (or use internal pull-ups)
- Optional: LiPo battery (3.7V, 100-500mAh) for wireless operation
- Optional: Mode selection button on D0
- A0 (P0.02) - Axis 1 (X)
- A1 (P0.03) - Axis 2 (Y)
- A2 (P0.28) - Axis 3 (Z)
- A3 (P0.29) - Axis 4 (Rz) `¶++
- D1 (P0.04) - Button 1
- D2 (P0.05) - Button 2
- D3 (P0.06) - Button 3
- D4 (P0.07) - Button 4
- D5 (P0.08) - Button 5
- D6 (P0.09) - Button 6
- D7 (P0.10) - Button 7
- D8 (P0.11) - Button 8
- D9 (P0.12) - Button 9
- D10 (P0.13) - Button 10
- MOSI (P0.26) - Button 11
- MISO (P0.27) - Button 12
- SCK (P0.30) - Button 13
- TX (P0.31) - Button 14
- RX (P0.00) - Button 15
- SCL (P0.01) - Button 16
- D0 (P1.11) - Mode selection (hold on boot: USB if HIGH, BLE if LOW)
- LED_RED - Status indicator
- LED_BLUE - Connection indicator
cd firmware/seedjoy
~/bin/arduino-cli compile --fqbn Seeeduino:nrf52:xiaonRF52840 --output-dir build .
~/bin/arduino-cli upload -p /dev/cu.usbmodem* --fqbn Seeeduino:nrf52:xiaonRF52840 --input-dir build .Or double-click reset button and drag build/seedjoy.ino.zip to the XIAO drive.
- Install Arduino IDE 2.0+
- Add board manager URL in Preferences:
- Seeeduino:
https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
- Seeeduino:
- Install "Seeed nRF52 Boards" from Board Manager
- Open
firmware/seedjoy/seedjoy.ino - Important: Select board "Seeed XIAO nRF52840" (NOT "Sense")
- If compilation fails, clear cache and restart IDE:
rm -rf ~/Library/Caches/arduino/*
- Upload to board
- Download latest
seedjoy-firmware.uf2from Releases - Double-click RESET button on XIAO to enter bootloader mode
- Drag
seedjoy-firmware.uf2to the XIAO drive that appears
- USB Mode: Hold mode button (D0) HIGH during boot, or keep USB connected
- BLE Mode: Hold mode button (D0) LOW during boot, or disconnect USB
- Mode is saved and persists across reboots
- Plug in USB-C cable
- Device appears as "SeedJoy Controller" in system joystick settings
- Test in Windows Game Controllers or Linux
jstest
- Open Bluetooth settings
- Pair with "SeedJoy-XXXX" (XXXX = last 4 chars of MAC)
- Device appears as HID gamepad
- Note: BLE HID may require pairing on first use
Open the web configurator and connect to your device:
- Open configurator:
file:///path/to/configurator/index.html(or serve locally with HTTPS) - Click "Connect Device" to pair via WebBluetooth
- The device automatically enters Configuration Mode for 60 seconds:
- HID input is disabled to prevent unwanted keystrokes/mouse movements
- IMPORTANT: All axes and buttons are disabled by default for safety
- This prevents floating pins from generating random input
- Hold the MODE button to stay in configuration mode
- Send
Hvia serial to enable HID immediately
- Read Configuration: Click "Read from Device" to download current settings
- Enable Your Inputs:
- Enable only the axes/buttons you have physically connected
- Axes on unconnected pins will read electrical noise and cause chaos
- Example: If you only have a potentiometer on A0, enable only Axis 0
- Calibrate and Configure:
- Calibrate enabled axes with live preview
- Remap buttons as needed
- Adjust deadzones and curves
- Write Configuration: Click "Write to Device" to save settings to Flash
- Configuration is automatically saved and persists across reboots
SAFETY WARNING:
⚠️ Only enable axes/buttons that have physical hardware connected- Floating (unconnected) pins read random electrical noise
- This causes random keyboard/mouse input that can crash your system
- When in doubt, leave inputs disabled until you connect hardware
Features:
- ✅ Read/write configuration via BLE
- ✅ Real-time axis monitoring with live graphs
- ✅ Real-time button state monitoring
- ✅ Remote calibration (min/center/max)
- ✅ Battery level monitoring
- ✅ Automatic configuration mode (prevents HID interference)
- ✅ Safe defaults: all inputs disabled until explicitly enabled
- ✅ Export/import configuration profiles (localStorage)
The web-based configurator uses WebBluetooth API (requires Chrome/Edge/Opera).
Features:
- Pin assignment (visual pinout diagram)
- Axis calibration wizard with live preview
- Deadzone and curve configuration
- Button mapping and testing
- Firmware update (UF2 upload)
- Export/import configuration profiles
Offline Use:
Download the configurator from Releases and open index.html in your browser.
Configuration is stored as JSON in Flash at address 0x7C000. You can also configure via serial commands (see docs/protocol.md).
cd firmware/seedjoy
# Arduino CLI (recommended)
~/bin/arduino-cli compile --fqbn Seeeduino:nrf52:xiaonRF52840 --output-dir build .
# Build outputs in build/ directory:
# - seedjoy.ino.hex (for programmer)
# - seedjoy.ino.zip (for UF2 bootloader)
# - seedjoy.ino.elf (debug symbols)Upload:
# Method 1: Arduino CLI
~/bin/arduino-cli upload -p /dev/cu.usbmodem* --fqbn Seeeduino:nrf52:xiaonRF52840 --input-dir build .
# Method 2: UF2 Bootloader (drag & drop)
# 1. Double-click reset button on XIAO
# 2. Drag build/seedjoy.ino.zip to XIAO drivecd configurator
# Serve with Python
python3 -m http.server 8000
# Or Node.js
npx http-server -p 8000
# Open https://localhost:8000 (requires HTTPS for WebBluetooth)
# For HTTPS, use ngrok or mkcert- Serial Monitor: 115200 baud, prints boot info, mode, config changes
- BLE Sniffer: Use nRF Sniffer for Bluetooth LE to debug BLE packets
- USB HID: Use Wireshark with USBPcap to capture HID reports
firmware/
├── seedjoy/
│ ├── seedjoy.ino # Main sketch (setup, loop, mode manager)
│ ├── config.h # DeviceConfig structure
│ ├── axes.h/cpp # Analog axis processing
│ ├── buttons.h/cpp # Button debouncing
│ ├── usb_hid.h/cpp # USB HID implementation
│ ├── ble_hid.h/cpp # BLE HID implementation
│ ├── storage.h/cpp # Flash config storage
│ └── protocol.h/cpp # Configuration protocol
configurator/
├── index.html # Main UI
├── app.js # Application logic
├── ble.js # WebBluetooth communication
├── calibration.js # Axis calibration wizard
├── styles.css # Styling
└── assets/ # SVG pinout, icons
docs/
├── hardware.md # Wiring diagrams
├── protocol.md # Config protocol spec
└── api.md # Web configurator API
- 4 axes, 16 buttons
- 74HC165 shift register support (up to 144 total buttons)
- USB/BLE dual mode
- Basic web configurator UI
- WebBluetooth connection
- Battery monitoring via BLE
- BLE configuration service (custom GATT)
- Axis calibration over BLE
- Flash storage read/write over BLE
- Configuration mode to prevent HID interference
- Real-time button monitoring in configurator
- Improved JSON parsing with ArduinoJson library
- Configuration import/export from configurator UI
- Firmware update via WebBluetooth (DFU)
- 8 axes (via external ADC: ADS1115)
- Rotary encoders (2-4)
- LED support (status, backlighting)
- Shift layers (2x button count)
- Sensor support (TLE5011, AS5600)
- Button matrices (64+ buttons)
- Advanced power management
- OTA firmware updates via BLE DFU
##Random keyboard/mouse input when connecting via Bluetooth**
- This happens when axes/buttons are enabled but not physically connected
- Floating pins read electrical noise and generate random HID input
- Solution: In configurator, disable all unused axes and buttons
- Only enable inputs that have actual hardware connected
- Write the config to device to save the safe configuration
** Troubleshooting
Device not recognized in USB mode
- Check USB cable (must support data, not charge-only)
- Try different USB port
- Reinstall drivers (Windows: device manager > update driver)
Cannot pair in BLE mode
- Clear Bluetooth pairing list
- Reset device (double-tap RESET button)
- Some OS require manual HID driver installation for BLE gamepads
Axes drifting or jittering
- Increase deadzone in configurator
- Use higher quality potentiometers
- Add capacitors (0.1µF) across potentiometer outputs
- Calibrate axes in configurator
Low battery life in BLE mode
- Reduce connection interval (latency trade-off)
- Enable sleep mode in advanced settings
- Use larger battery (500mAh recommended)
Configuration not saving
- Check that device is connected via BLE
- Verify config service is available (check serial output)
- Ensure latest firmware is uploaded
- Try "Read from Device" first to verify connection
MIT License - see LICENSE file
Inspired by FreeJoy by Alexandr Yaroshenko
Contributions welcome! Please open issues for bugs or feature requests.
- Documentation: See
docs/folder - Issues: GitHub Issues
- Community: Discord (link TBD)