Files
qunexus-firmware/BLE_MIDI_PROTOCOL.md

147 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BLE-MIDI add-on: EFM8 ↔ nRF52 custom framed protocol
Plan for adding Bluetooth MIDI to the QuNexus by attaching a BLE-MIDI
module to the EFM8's dormant **UART1** — the same pair wired to the empty
MIDI-expander jack (**P1.2 / P1.3**, pins 44 / 43). UART1 is never enabled
in stock v2.2.1 firmware (no `XBR2` write, no `SBUF1`/`SCON1` use), so we
reclaim it for BLE without disturbing the DIN-MIDI UART0 or the P2/P3/P4
scan bus.
See [[EFM8UB20_PINOUT.md]] for the pin map and the port-config block this
modifies.
## Module selection
**Chosen: Seeed/Raytac MDBT50Q — nRF52840, 1 MB flash / 256 kB RAM, BT 5.4,
~15.5 × 10.5 × 2.05 mm, certified.** SWD is broken out (**pin 51 = SWDIO,
pin 53 = SWDCLK**), it ships **blank**, so we SWD-flash our own BLE-MIDI +
framed-protocol firmware onto it. The nRF52840 has the most BLE-MIDI
reference code of any nRF52 (Nordic nRF5 SDK BLE-MIDI example, Adafruit
Bluefruit MIDI mode) — we lift the BLE-MIDI GATT layer and add our
framed-protocol UART handler on top. Battery-acceptable (system-off ~1.5 µA,
BLE-connected a few mA avg). Wiring uses only 6 of its 61 pads: VDD, GND,
UART TX, UART RX, SWDIO, SWDCLK.
> **Antenna variant — watch the SKU.** The Seeed **MDBT50Q-U1M** is the
> **u.FL** variant: **no onboard antenna**, just a u.FL jack → needs an
> **external 2.4 GHz antenna on a u.FL pigtail** routed to a board edge
> inside the case (keep-out ~3–5 mm at the antenna end). If you'd rather
> avoid the external antenna, get the **chip-antenna variant Raytac
> MDBT50Q-1MEN** (same module, onboard chip antenna, no u.FL). Same SWD
> pins, same firmware. Prefer the chip-antenna SKU unless the u.FL +
> external antenna is deliberately wanted for edge placement.
**Smaller fallback (if case space is tight): Raytac MDBT42Q-512KV2 —
nRF52832, 512 kB / 64 kB, ~6.5 × 7.1 × 1.6 mm, onboard chip antenna.** Same
SWD-exposed/blank profile, lowest power, smallest fit, and it *does* have
the integrated antenna — but less headroom and less ready-made BLE-MIDI
code than the 52840.
### Hard selection rules (do not re-evaluate against these)
- **Must expose SWD and be blank/reflashable.** We load our own BLE-MIDI
firmware; a sealed module is unusable.
- **Reject AT-command / "transparent serial" modules** (e.g. Raytac
MDBT42T-AT / WRL-25466, nRF52805). They advertise the **Nordic UART
Service** (`6E400001-…`), not **BLE-MIDI** (`03B80E5A-…`). iOS/macOS/DAWs
scan for the MIDI UUID and will not present an NUS device as a MIDI
source. Their AT command set cannot change the advertised service. The
MDBT42T-AT also exposes **no SWD** (19 pads, none SWDIO/SWCLK), so it
can't be reflashed either — doubly unsuitable.
- **Reject SPI-only radio modules that need a host to run the BLE stack**
(e.g. Raspberry Pi Radio Module 2 / RMC20452T, Infineon CYW43439). The
BLE stack (BTstack) runs on an RP2040 host over a bespoke gSPI; the
module cannot talk to the EFM8 directly (no SPI on the EFM8, and it
can't run BTstack). That makes a 3-chip build (EFM8 + RP2040 + radio),
and the Wi-Fi combo chip is power-hungry — wrong for battery-later.
(A Pico W is the cheap bench-prototype instantiation of that
architecture, but it is not the final-build path.)
## Transport: frame everything (SLIP-style)
MIDI bytes and control commands share one UART, so **both** are carried in
framed packets. Escaping means any MIDI byte value (0x00–0xFF) is safe in a
payload — no reliance on "undefined MIDI bytes," no collision with real
MIDI status/system bytes.
```
END = 0xC0 ESC = 0xDB (ESC_END = 0xDC, ESC_ESC = 0xDD)
Frame on wire: END CMD LEN payload[0..LEN-1] CRC8 END
- CMD, LEN, payload, CRC8 are byte-stuffed:
0xC0 -> ESC 0xDC , 0xDB -> ESC 0xDD
- LEN = payload byte count (0..255)
- CRC8 = computed over (CMD, LEN, payload) *unstuffed*
- Receiver collects bytes between END markers, un-stuffs, checks CRC, parses.
```
Baud: **31250 8N1** (matches MIDI; frames are short, throughput is ample).
## Command set
```
EFM8 -> nRF52
0x01 MIDI_TX payload = MIDI bytes to send over BLE-MIDI
0x10 ENTER_PAIRING (len 0) start fast advertising / pairing
0x11 DISCONNECT (len 0)
0x12 CLEAR_BONDS (len 0) wipe stored bonds (force re-pair)
0x13 GET_STATUS (len 0)
nRF52 -> EFM8
0x02 MIDI_RX payload = MIDI bytes received over BLE-MIDI (-> inject into router)
0x20 STATUS payload = [state, bonded_peers, rssi]
state: 0=disconnected 1=advertising 2=connected 3=pairing
0x21 EVENT payload = [event_code] (connected / disconnected / bond_added / error)
```
`MIDI_TX`/`MIDI_RX` are just frames whose CMD carries MIDI bytes; the nRF52
routes by CMD — `MIDI_TX` → BLE-MIDI TX, control CMDs → BLE stack actions.
BLE crypto/bonding is handled **entirely by the nRF52**; the EFM8 only
triggers pairing and reads status.
## Data flow on the EFM8 side
- **Outgoing MIDI:** hook the existing MIDI-send path (same place DIN MIDI
out is fed) to also emit a `MIDI_TX` frame on UART1 — a straight mirror,
exactly like the USB-1→CV stub mirrors into the CV ring.
- **Incoming MIDI:** UART1 RX ISR parses frames; on `MIDI_RX`, inject the
bytes into the router/event buffer — same injection mechanism as the
USB-1→CV stub (which injects at `0x8126`).
- **Pairing control:** a key combo (detected in the existing key-scan path)
→ emit `ENTER_PAIRING` / `CLEAR_BONDS` / `DISCONNECT`.
- **Status:** on `STATUS`/`EVENT` frames, set a state byte that the P2 scan
ISR reads to drive the LED matrix (blink = pairing, steady = connected,
off = disconnected).
## EFM8 firmware pieces (asm patches in `firmware.asm`, verified by `roundtrip.py`)
1. **UART1 init:** `XBR2 |= 0x01`, `P1MDOUT 0x3F → 0x37` (RX open-drain),
SCON1, Timer3 baud @31250.
2. **UART1 ISR:** small frame RX state machine (collect → un-stuff → CRC →
dispatch by CMD). A few bytes of state + a small ring buffer.
3. **Frame TX helper** (stuff + CRC + END-wrap) used by the mirror and the
control commands.
4. **MIDI mirror hook + MIDI inject** (reuse the USB-1→CV injection pattern).
5. **Key-combo detect → control frame.**
6. **LED status** state variable read by the P2 scan ISR.
The nRF52 side (BLE-MIDI peripheral + this frame protocol) is a separate
firmware project — spec'd here, not written in this repo.
## Phasing (keeps each step verifiable)
1. **Phase 1 — transport + MIDI:** UART1 + frame RX/TX + `MIDI_TX`/`MIDI_RX`
mirror/inject. BLE-MIDI works end-to-end (auto-pair, Model A). Smallest
useful mod; proves the UART/framing on real hardware.
2. **Phase 2 — control:** add the control commands + key-combo + LED status
(Model B pairing). All on top of the Phase-1 frame layer, no rework.
Both phases are asm patches in `firmware.asm` that round-trip byte-identical
against a known-good image (same workflow as the USB-1→CV patch).
## Safety constraint (hard rule for any firmware mod)
The bootloader region **0x0000–0x23FF must NOT be erased**; only
**0x2400–0xF806** is the app region; **no mass erase should ever be
performed.** Firmware is read-only unless explicitly asked to write. See
`RECOVERY.md` for the C2 flash reader/patcher constraints.