147 lines
7.1 KiB
Markdown
147 lines
7.1 KiB
Markdown
# 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. |