# Bigscreen Beyond Firmware Update Chain

Source repos studied (READ-ONLY):
- `beyond_bootloader` (branch `main` only; HEAD `0004feb`). Repo-relative paths below prefixed `beyond_bootloader/`.
- `quick-beyond-updater` (branch `main`, HEAD `12a6905`; other branches `origin/cm-updater`, `origin/fpga-dfu`). Paths prefixed `quick-beyond-updater/`.

Speculation is explicitly marked **[SPECULATION]**. Everything else is read directly from source.

---

## 1. Target MCU

- **Part: Microchip/Atmel ATSAMG55G19** (ARM Cortex-M4F, "SAM G55" family). Confirmed by `__SAMG55G19__`/`__ATSAMG55G19__` guards (`beyond_bootloader/USB_Bootloader/main.c:176`), startup `startup_samg55g19.c`, linker scripts `samg55g19_*.ld`, and README.
- **Memory** (README + `beyond_bootloader/USB_Bootloader/Device_Startup/samg55g19_flash_bootloader.ld:50-55`):
  - Flash: 512 KB @ `0x00400000`–`0x00480000`. Page size 512 B (`FLASH_PAGE_SIZE 0x200`).
  - RAM: 160 KB @ `0x20000000`–`0x20028000`.
  - Flash sectoring: 4×128 KB sectors; sector 0 split into 8 KB + 8 KB + 112 KB sub-sectors. 16-page (8 KB) minimum page-erase granularity.
- **Toolchain:** Microchip Studio 7.0.2594 (Atmel Studio), GCC ARM, `.atsln`/`.cproj` projects. USB stack = **TinyUSB** (vendored under `tinyusb/`). Language: C.
- On-board USB hub chips driven over I2C (FLEXCOM4) at boot: **Microchip USB3803** (addrs `0x10`, `0x12`, `0x52`). BS1 = 2 hubs, BS2 = 3 hubs — this is how the bootloader auto-detects board version (`main.c:419-484`). Older boards used a USB2517 hub (now `#if 0`-commented out, `main.c:598-674`).

## 2. Flash Memory Map

| Region | Address | Size | Purpose |
|---|---|---|---|
| Bootloader | `0x00400000`–`0x00404000` | 16 KB (two 8 KB sub-sectors) | USB HID bootloader. Linker caps ROM to `0x4000` (`samg55g19_flash_bootloader.ld:39`). |
| App length field | `0x00404400` | 4 B | App image length, LSB-first, at offset `0x400` into app (`__app_size`). |
| Application | `0x00404000`–`0x00480000` | 496 KB | Main firmware. `__app_address = 0x00404000`. |
| App CRC-32 | immediately after app code (`app_addr + app_len`) | 4 B | BZIP2 CRC-32, LSB-first. |
| User Signature | separate 512 B "user signature" flash area (not main array) | 512 B | Written via dedicated EEFC User-Signature commands (`flash_*_signature`, `main.c:855-915`). **[SPECULATION]** holds per-unit calibration/identity. |
| Emulated EEPROM | last 8 KB sub-sectors of app region | — | README warns "Clear app" erases it; it is "simply the last two 8kB sections of Flash". |

App must be linked at offset `0x00404000` (NOT flash base) — README + `read_app_length`/`calc_app_crc` in `main.c`.

## 3. Bootloader Entry / Exit Logic (`beyond_bootloader/USB_Bootloader/main.c`)

- On reset (`main()` lines 119-191): validates app; if valid, jumps straight to it (`_app_exec`, `main.c:680-705` — sets `SCB->VTOR`, loads MSP, branches to reset vector). The bootloader does **not** enumerate on USB unless it stays resident. So a healthy device never shows the bootloader.
- **App validity check** (`check_app`, `main.c:921-972`): (1) initial SP within RAM `0x20000000`–`0x20028000`; (2) reset vector within flash; (3) length sane vs vector-table size and flash end; (4) **stored CRC-32 == hardware-calculated CRC-32** over the whole app. Any failure → bootloader stays in USB HID mode. **This is the core brick-safety net: a bad/incomplete app flash simply traps in the recoverable bootloader.**
- **Forced entry mechanisms** via General Purpose Backup Registers (GPBR, survive reset, cleared on power-loss):
  - Stay in USB bootloader: `GPBR0=0xBB, GPBR1=0x1A` (`BKUP_REG0/1_CODE_FOR_USB_BOOTLOADER`). App sets these then resets to invoke updater (README "Updating Application Firmware").
  - Force SAMG55 **ROM bootloader** (SAM-BA): `GPBR0=0xAA, GPBR1` masked `0x2C00`; requires clearing GPNVM bit 1 (boot-from-ROM) + asserting reset pin 10× in a row (counter in `GPBR1[7:0]`, self-limits at 9 then clears). Comment at `main.h:46-62` calls it the "internal ROM-based I2C bootloader."
  - Power-cycling always clears GPBR → no permanent soft-brick from these flags.
- **Jump from app → bootloader** at runtime: app sends HID feature report `'B'` (0x42); bootloader-targeted code or the app writes GPBR and triggers `RSTC` reset.

## 4. Bootloader USB Protocol (in update mode)

- **USB IDs:** VID **`0x35BD`** (Bigscreen). Bootloader PID **`0x4004`** (`usb_descriptors.c` auto-PID = `0x4000 | HID<<2`). Product string "Bigscreen USB Firmware Upgrade", mfr "Bigscreen".
- Class: **generic HID** (NOT USB-DFU), `TUD_HID_REPORT_DESC_GENERIC_INOUT`, 64-byte IN+OUT reports, report ID 0. (TinyUSB DFU class files are vendored but unused here.)
- Host writes 64-byte OUT reports prefixed with a report-ID byte `0x00`; byte[0] = command. Commands (`main.h:23-34`, handler `tud_hid_set_report_cb` `main.c:260-417`):

| Cmd byte | ASCII | Action | Payload |
|---|---|---|---|
| `0x2A` | `*` | Return SW version string | — (returns e.g. "0.2.2") |
| `0x42` | `B` | Boot into app (reset; runs app if CRC valid) | — |
| `0x22` | `"` | Erase entire app region | — → status |
| `0x65` | `e` | Erase at address: sel 0=8KB,1=16KB,2=sector; addr LE; CRC-8 | status |
| `0x44` | `D` | Cache 8–32 B (mult. of 8) to flash page buf @addr; CRC-8 over whole cmd | status (only on err) |
| `0x50` | `P` | Program cached page (512 B) @addr | status |
| `0x43` | `C` | Read stored app CRC-32 (MSB-first) | 4 B |
| `0x4D` | `M` | Calculate app CRC-32 (MSB-first) | 4 B |
| `0x64`/`0x70`/`0x53` | `d`/`p`/`S` | Write/Program/Read 512 B User-Signature | status / data |
| `0x49` | `I` | Force jump to SAMG55 ROM (SAM-BA) bootloader | — |

- Integrity: each `D`/`e`/`P` command carries a trailing **CRC-8** (poly `0x07`, init 0, no reflection) over the entire command. Address-range checks reject any write/erase outside `0x00404000`–`0x0047FFF8` → **bootloader region is hardware-protected from the normal update path.** Status codes 0x00–0x06 (no error / lock / command / verify / address / size / CRC).
- Host flow (`beyond_bootloader/script/usb_fw_updater.py`, and the production copy `quick-beyond-updater/bs_hmd_tools.py:update_hmd_firmware`): erase (by-blocks, fallback full) → for each 32 B chunk send `D`, then `P` every 512 B → optional `B` to boot. App-mode PID is `0x0101`; updater first opens `0x0101`, sends feature `'B'` to drop to bootloader `0x4004`.

## 5. Bootloader Self-Update — "BL_Overwriter" (the only way to reflash the 16 KB bootloader)

- `beyond_bootloader/BL_Overwriter/` is the **same codebase compiled to run from the app region** (`flash_usb_bootloader.ld` ORIGIN `0x00404000`), PID **`0x5004`** (`USB_PID = 0x5000 | HID<<2`). It exposes `__bl_address = 0x00400000` so, running as the "app," it can erase+program the bootloader sectors that the real bootloader refuses to touch.
- Procedure (`beyond_bootloader/script/bootloader_update.py`): Beyond(`0x0101`) → bootloader(`0x4004`) → load BL_Overwriter as app → boot to overwriter(`0x5004`) → erase+flash NEW bootloader into `0x00400000` (retries ×5) → boot back to bootloader → erase overwriter. **Explicitly flagged dangerous**: argparse help says "If interrupted, device will be non-responsive unless Flashed by JTAG." This is the one genuine brick window.
- Prebuilt images in `beyond_bootloader/build/`: bootloaders `USB_Bootloader_*_ROMBL.hex` (v0.1.2, v0.1.3, v0.2.1 `241031`, v0.2.2 `241113`) and overwriters `BL_Overwriter_v012/v013_*.hex`. "ROMBL" = built to be loadable via the SAMG55 ROM/SAM-BA path. Current bootloader SW string = **"0.2.2"** (`sw_ver.h`).

## 6. quick-beyond-updater — host updater (`quick-beyond-updater/`)

- **Platform: Windows-only.** Python + `imgui_bundle`/`hello_imgui` GUI, `hidapi` (`hidapi.dll` bundled), `pyinstaller` build (`quick_updater.spec`, `build_quick_updater.bat`). Requires admin (UAC self-elevation). Uses Windows registry to find the SteamVR/utility install and `winreg`, plus bundled Windows exes in `tools/` (`dfu-util.exe`, `ListUsbDevs.exe`, `RestartUsbPort.exe`, `DeviceCleanupCmd.exe`, `libusb-1.0.dll`). Internal factory/RMA tool ("update Beyonds in rapidfire, overlapping succession"), v1.9.
- **Device identity model:** a headset = one **Atmel/MCU HID** (VID `0x35BD`, PID `0x0101` app / `0x4004` bootloader) paired with one **Tundra** device (VID **`0x28DE`** Valve, PID **`0x2300`**, product "Controller") by matching USB **port-chain** prefix (`quick_updater.py:437-518`). Tundra = the SteamVR tracking SiP; it is enumerated under Valve's VID, not Bigscreen's.
- It updates **two** components, version-gated:
  1. **MCU app firmware** via the HID bootloader protocol in §4 (`bs_hmd_tools.update_hmd_firmware`).
  2. **Eye-tracking FPGA firmware** via USB-DFU (only when HMD serial starts `BS2E`; see §7).
- Firmware-source resolution (`quick_updater.py:128-181`): (a) `override-*.beyondfw` / `override*.dfu` in working dir; else (b) configured path in `quick_updater_config.yaml`; else (c) the installed **Bigscreen Beyond Driver** under Steam: `.../Bigscreen Beyond Driver/bin/latest.beyondfw` and `/bin/eyetracking_firm.dfu` (`bs_hmd_tools.get_utility_*`). No network/server download — images are local (bundled overrides or the Steam driver install).
- Bundled override images present: `override-0.3.10.beyondfw`, `override-0.3.19.beyondfw` (MCU app), `override-0.4.9.dfu` (FPGA). `cm-updater` branch adds `override_eyetracking_firm_0.5.4.dfu` + a **factory diode-calibration** tool `calibrate_diode.py` (uses `pytrinamic` TMCL stepper rig + `calibot` + lighthouse base stations — calibration, not firmware).
- Live status shown by driving the HMD RGB LED via app feature report `'L'` (`set_light_color`).

### File formats
- **`.beyondfw`** (MCU app container; `load_beyondfw_file` `bs_hmd_tools.py:334-362`, header confirmed by hexdump): `[u8 ver_len][ascii version][u32 start_addr=0x00404000][u32 data_len][raw app binary]` then **SHA-512** (64 B) over all preceding bytes. The raw app binary itself already embeds its length@`0x400` and BZIP2 CRC-32 at the end (consumed by the device). Integrity only — **no cryptographic signature, no encryption.** SHA-512 is checked host-side and uses no secret key.
- **`.dfu`** (FPGA; standard DFU file): trailing 16-byte DFU suffix `...| bcdDevice | idProduct=0x0282 | idVendor=0x35BD | bcdDFU=0x0100 | "UFD" | bLen=16 | CRC32`. Version read from `bcdDevice` (`get_dfu_file_fw_version`). Flashed verbatim by `dfu-util`.

## 7. Eye-tracking FPGA update (USB-DFU)

- The FPGA (eye-tracking camera controller) is reached **through the MCU** via an I2C pass-through HID protocol: feature report `'e'`+`'i'`+len+reg for I2C read (`bs_hmd_tools.read_i2c:645`), and `'e'`+`'B'` to toggle FPGA DFU/run-time mode (`enter_fpga_dfu:864`). FPGA registers: `0xA0` config-ID (`'A'`/0x41 = camera/run-time, `'B'`/0x42 = bootloader/DFU), `0xA1`/`0xA2` = BCD SW version.
- In DFU mode the FPGA enumerates as its own USB device **VID `0x35BD`, PID `0x0282`**, "always at index 2 in the USB port chain" (`enter_fpga_dfu` returns `port_chain[:-2]+['2']`). Flashing = `tools/dfu-util.exe -d ,35BD:0282 -p <chain> -D -` with the `.dfu` streamed on stdin (`bs_usb_tools.dfu_download:173`). FPGA bootloader-mode firmware version constant = `0.1.7` (`quick_updater.py:89`).
- **[SPECULATION]** The DFU target is the eye-tracking module's own controller (Lattice-class FPGA or its config flash). These repos give **no evidence** it is the same silicon as, or a path to, the Synaptics VXR7200 DP-to-MIPI bridge.

## 8. Catalog of firmware-updatable components revealed

| Component | Bus / mode | IDs | Update path | Repo evidence |
|---|---|---|---|---|
| **MCU app** (ATSAMG55G19 main firmware) | USB HID custom bootloader | VID `0x35BD` app `0x0101` / boot `0x4004` | `.beyondfw` via HID `D`/`P` chunked flash | both repos |
| **MCU bootloader** (16 KB) | USB HID via BL_Overwriter app | overwriter PID `0x5004` | `bootloader_update.py` + BL_Overwriter hex (JTAG fallback) | `beyond_bootloader` |
| **Eye-tracking FPGA** (BS2E only) | USB-DFU (entered via MCU I2C passthrough) | VID `0x35BD` PID `0x0282` | `.dfu` via `dfu-util` | `quick-beyond-updater` |
| USB3803 hub config | I2C from MCU at boot | I2C `0x10/0x12/0x52` | runtime register config, not flashed | `beyond_bootloader` |

**Not found in these repos:** any Tundra SiP firmware *update* path (Tundra only appears as a Valve `0x28DE:0x2300` device used for pairing/identity, and `0x28DE:0x2102` in USB-cleanup lists — never flashed here), **VXR7200 flashing**, FPGA *bitstream* loading details, and **linkbox firmware**. The Tundra raw IMU stream (the sauna goal) is not touched by either repo.

## 9. Risks / gotchas / recovery (from code + comments)

- **Safe by design — app flashing:** address-range checks + CRC-32 gate mean an interrupted/garbage app flash just leaves the device sitting in the recoverable USB bootloader (`0x4004`). Re-run the updater.
- **Dangerous — bootloader flashing:** `bootloader_update.py` warns interruption ⇒ "non-responsive unless Flashed by JTAG." Mitigations in code: validates both hex files' CRC and load addresses before starting; erase/flash retried ×5.
- **Deep recovery paths:** (1) SWD/JTAG on the SAMG55; (2) force the on-chip **ROM SAM-BA** monitor (clear GPNVM bit 1 + 10× reset, or HID cmd `'I'`) — note it is described as **I2C**-based, so likely needs hardware access, **[SPECULATION]** not a plain USB recovery. (3) **Power-cycle** clears all GPBR force-flags, breaking any soft loop.
- **Operational:** updater demands the Bigscreen Beyond Utility be closed (HID contention); auto-restarts USB ports / cleans phantom devices when flashes wedge (`bs_usb_tools.restart_port`, `cleanup_beyond_devices`). No anti-rollback anywhere — older versions flash freely (the tool's whole job is forcing specific versions, incl. downgrades via `override-*`).

---

## Relevance to sauna

- **Custom MCU firmware is shippable and low-risk.** The app region (`0x00404000`, 496 KB) is fully owned by us, and the stock bootloader enforces **only** an integrity contract (ARM vector sanity + length@`0x400` + BZIP2 CRC-32). No signature, no encryption, no anti-rollback, no secure boot. To ship custom firmware we just build at the app offset, append length+CRC, and (for the GUI updater) wrap as `.beyondfw` with a SHA-512 trailer — all reproducible from `usb_fw_packager.py` and the formats above.
- **Recovery from a bad flash is essentially free.** A failed app flash can't brick: control returns to the resident USB HID bootloader (PID `0x4004`), so we can iterate firmware safely over USB with no JTAG. Power-cycle clears any stuck state.
- **Bootloader replacement is NOT needed for sauna.** The stock bootloader already exposes everything required to load arbitrary app code. Touching the bootloader (BL_Overwriter) is the only real brick risk and should be avoided unless we must change the 16 KB boot region. Keep a JTAG/SWD rig on hand purely as insurance for bootloader work; not needed for app-level development.
- **The IMU is the open problem, not the update chain.** Raw gyro/accel comes from the Tundra SiP, which presents as a Valve SteamVR "Controller" (`0x28DE:0x2300`) — neither repo flashes it or exposes a raw sensor stream. Custom MCU firmware likely must read the IMU over the MCU↔Tundra link and re-expose it (a new HID report or USB endpoint), so the next investigation target is the MCU↔Tundra interface and the main app firmware (not in these two repos).
- **Eye tracking is irrelevant to a 3DOF/6DOF display** and is a separate DFU-updatable FPGA (`0x35BD:0x0282`); ignore for sauna.
