7.1 KiB
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_TXframe 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 at0x8126). - Pairing control: a key combo (detected in the existing key-scan path)
→ emit
ENTER_PAIRING/CLEAR_BONDS/DISCONNECT. - Status: on
STATUS/EVENTframes, 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)
- UART1 init:
XBR2 |= 0x01,P1MDOUT 0x3F → 0x37(RX open-drain), SCON1, Timer3 baud @31250. - UART1 ISR: small frame RX state machine (collect → un-stuff → CRC → dispatch by CMD). A few bytes of state + a small ring buffer.
- Frame TX helper (stuff + CRC + END-wrap) used by the mirror and the control commands.
- MIDI mirror hook + MIDI inject (reuse the USB-1→CV injection pattern).
- Key-combo detect → control frame.
- 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)
- Phase 1 — transport + MIDI: UART1 + frame RX/TX +
MIDI_TX/MIDI_RXmirror/inject. BLE-MIDI works end-to-end (auto-pair, Model A). Smallest useful mod; proves the UART/framing on real hardware. - 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.