Files
qunexus-firmware/BLE_MIDI_PROTOCOL.md

7.1 KiB
Raw Permalink Blame History

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.