# Building and Flashing the Beyond MCU Firmware (headless)

The official build is **Microchip Studio** (open `Beyond_Synaptics.atsln`). But
the firmware also builds **fully headless** via the CMake/Ninja setup on the
`build-cmake` branch + an `arm-none-eabi-gcc` toolchain — no Studio needed. This
is the path used to build, package, and flash the M5 doze firmware (0.4.x).
Everything below was verified on Windows (HFENDULEAM), 2026-06.

## Toolchain (one-time install)

```
winget install --id Arm.ArmGnuToolchain   # arm-none-eabi-gcc 12.2 (~224 MB)
winget install --id Ninja-build.Ninja
python -m pip install crc                  # for the packager scripts
```

arm-gcc lands at (note the spaces — quote it):
`C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.2 mpacbti-rel1\bin`
winget does **not** add either to PATH; prepend both for the build shell:

```bash
export PATH="/c/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/12.2 mpacbti-rel1/bin:$LOCALAPPDATA/Microsoft/WinGet/Links:$PATH"
```

## Build

The CMake files (`CMakeLists.txt`, `CMakePresets.json`, `arm-gcc-toolchain.cmake`,
`utils.cmake`) come from the `build-cmake` branch. **Build Release**, not Debug:

```bash
cmake --preset arm-gcc-toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build out/build/arm-gcc-toolchain.cmake
# -> out/build/arm-gcc-toolchain.cmake/beyond_synaptics.{elf,bin,hex}
```

Output app is ~75 KB; the app region is ~500 KB, so size is a non-issue.

### Gotchas (all hit + fixed once; here so you don't rediscover them)

- **Build Release (`-O3`), not the preset's default Debug (`-Og`).** Studio ships
  `-O3`. Under Debug, the non-`static` `inline` helpers in `Devices/tundra_uart.h`
  (`circular_buffer_get/put`) are not inlined and have no external definition →
  link error `undefined reference to circular_buffer_get`. `-O3` inlines them away.
- **Project include dirs must precede the ASF/CMSIS dirs.** The samg55 CMSIS tree
  ships its own `component/adc.h` + `instance/adc.h`; if those `-I` dirs come first,
  a bare `#include "adc.h"` resolves to the CMSIS one and shadows
  `src/Drivers/adc.h` → a cascade of "unknown type `USB_Flip_State_t`" /
  "implicit declaration of `init_adc`/`get_cc1_val`". Fix in `CMakeLists.txt`:
  list `src/Devices/ src/Drivers/ src/config/ src/crash_handler/` right after `src/`.
- **The `build-cmake` source list drifts from `main`.** Reconcile against the
  authoritative `.cproj` `<Compile Include=...>` list. Known deltas fixed:
  `adc_driver.c` → `adc.c`, add `Devices/tundra_uart.c`.
- FPU + libm are available (`-mfpu=fpv4-sp-d16`, links `m`), so `powf` etc. work
  in tasks (the doze gamma sweep uses it). `printf` is `iprintf` — no float
  *formatting*, but float *math* is fine.

## Package

Two-step, mirroring the `.cproj` post-build event. `{VERSION}` = MAJOR+MINOR+PATCH
from `sw_ver.h` (e.g. 0.4.1 -> "041"). Run from `scripts/`, use **absolute** output
paths (the packager's naive `.split('.')` mangles leading `../` paths):

```bash
# 1. raw .hex -> AppImage (embeds app length @0x400 + CRC32 the bootloader
#    verifies on boot; emits both .hex and .bin)
py usb_fw_packager_cli.py -o "<abs>/dist/Beyond_AppImage_v041.hex" "<abs>/out/build/arm-gcc-toolchain.cmake/beyond_synaptics.hex"

# 2. AppImage -> .beyondfw (version header + start addr + binary + SHA512;
#    the format the Beyond Utility / quick-beyond-updater flashes). Accepts .hex.
py utility_fw_packager_cli.py -v 0.4.1 "<abs>/dist/Beyond_AppImage_v041.hex" "<abs>/dist/latest.beyondfw"
```

`.beyondfw` layout: `[vlen:1][version ascii][start_addr:4 LE][binlen:4 LE][binary][sha512:64]`.
Sanity-check against `../quick-beyond-updater/override-*.beyondfw` (same start
`0x404000`, same structure).

## Flash (headless)

Use the canonical `quick-beyond-updater/bs_hmd_tools.py` API. HID IDs: app =
`35BD:0101`, bootloader = `35BD:4004`. Flashing the **AppImage** leaves the
bootloader untouched → always recoverable (a bad app just fails its boot CRC and
stays in the bootloader, re-flashable).

```python
import sys, os; sys.path.insert(0, r"...\quick-beyond-updater"); os.chdir(r"...\quick-beyond-updater")
import hid, bs_hmd_tools as t
payload = t.load_beyondfw_file(r"...\dist\latest.beyondfw")     # verifies SHA512
bl = t.enter_hmd_bootloader(device_path=<app_path>)             # app -> bootloader, returns boot path
t.update_hmd_firmware(firmware_payload=payload, bootloader_path=bl, reset_when_done=True)
# verify: t.get_hmd_fw_version(device_path=<app_path_after_reboot>) -> "0.4.1"
```

### **Known flash quirk — always retry**

The first programming attempt **reliably fails mid-write** around address
`0x00404Axx` with `code 46` (a transient HID write glitch). A second attempt —
targeting the **bootloader directly** (the device is already in bootloader after
the failed attempt) — succeeds every time. Always wrap the flash in a 2-4×
retry loop that re-targets `35BD:4004`. See `dist/reflash_doze.py` for a working
retry+verify driver. (Root cause unknown; worth a real look someday, not blocking.)
