Properties2
| Type | Practice |
| Note created | Jun 19, 2026 |
OpenPuck officially targets a Pro Micro nRF52840. I ran it on a Seeed Studio XIAO nRF52840 (the plain one, mfr ref 102010448; its bootloader self-identifies as “Sense”). The same nRF52840 chip, but enough is different that a straight build won’t even enumerate over USB. This note records the working procedure and the four fixes it took.
Build with arduino-cli + the Adafruit nRF52 core 1.7.0 — not the Seeed core (Seeed ships an ancient TinyUSB that can’t even compile OpenPuck’s 4-HID requirement).
The four fixes
- App offset + SoftDevice version. The XIAO bootloader ships SoftDevice S140 7.3.0, so the app must link at
0x27000. Thefeather52840target defaults to0x26000(S140 6.1.1) → the app overlaps the SoftDevice and hard-faults. Fix: createnrf52840_s140_v7.ld(a copy ofnrf52840_s140_v6.ldwithFLASH ORIGINmoved0x26000 → 0x27000) in the core’scores/nRF5/linker/, and build against the 7.3.0 headers. - HID interface count. OpenPuck needs 4 HID interfaces: pass
-DCFG_TUD_HID=4. - The killer bug —
InternalFS.begin()hangs. This is what cost the most time. The Adafruit flash backend (flash_nrf5x.c) always programs flash through the SoftDevice SVCs (sd_flash_page_erase/sd_flash_write). OpenPuck runs with the SoftDevice disabled (it drives the radio bare-metal), so on a fresh board the LittleFS format hangs/faults on those SVCs and boot freezes before USB ever comes up. Fix: patch the backend to use direct NVMC erase/write when the SoftDevice is not enabled (safe because the app owns the NVMC then). This is the change that finally made USB enumerate. - Not the clock. A red herring: I suspected the 32.768 kHz crystal. The XIAO nRF52840 does have one, so
USE_LFXOis correct and noUSE_LFRCchange is needed. (A bareBluefruit.begin()does hang on LFXO per a Nordic thread, but OpenPuck never enables the SoftDevice, so it’s irrelevant here.)
Build commands
From the repo root (~/Projects/Personal/openpuck):
arduino-cli compile -b adafruit:nrf52:feather52840 \
--build-property "build.ldscript=nrf52840_s140_v7.ld" \
--build-property "build.sd_version=7.3.0" \
--build-property "build.sd_fwid=0x0123" \
--build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=4" \
--export-binaries OpenPuck
# convert the .hex to a .uf2 (nRF52840 UF2 family id)
python3 <core>/tools/uf2conv/uf2conv.py \
OpenPuck/build/adafruit.nrf52.feather52840/OpenPuck.ino.hex \
-c -f 0xADA52840 -o OpenPuck.uf2Flashing
- Double-click the RST button on the XIAO. The
XIAO-SENSEUSB mass-storage drive mounts (this is the UF2 bootloader). The 1200-baud serial “touch” auto-reset does not work reliably from macOS here, so the physical double-tap is the reliable way in. - Drag the
.uf2onto the drive. It flashes and auto-reboots (a “device not configured” copy error at the end is normal — that’s the auto-eject). - Done: it enumerates as a Valve “Steam Controller Puck” (28DE:1304).
Debugging aids (in the fork)
Two compile flags were added to OpenPuck.ino for bring-up:
-DOPK_LED_BEACON— blinks the red LED (P0.26) at eachsetup()stage (count 1→7; 7 = USB mounted). Invaluable since there’s no serial when puck mode drops CDC.-DOPK_FORCE_DEBUG_CDC— keeps the CDC console in puck mode (boot log over serial).
Leave both off for the clean production build.
Recovery
The UF2 bootloader lives in protected flash and survives everything, so the board is always recoverable: double-click RST → XIAO-SENSE mounts → drop a known-good .uf2 (even CircuitPython) to restore it.