Make the repo self-contained (no dependency on the parent firmware-tools/ checkout): copy in the patch/disasm/syx tools and the c2probe RP2040 C2 flash-reader/patcher firmware. - patch_usb1_to_cv.py : byte-level USB-1->CV patch (imported by roundtrip.py) - d8051.py : standalone 8051 disassembler - syx_extract.py : SysEx extractor - c2probe/ : RP2040 C2 flash reader + host scripts (c2probe.c, cdc/dump_flash/patch_c2/reflash_page/verify.py, CMake build) - RECOVERY.md : C2 flash recovery procedure - EFM8UB20_PINOUT.md : reverse-engineered QFP48 pinout + firmware pin usage - roundtrip.py : import local patch_usb1_to_cv (parent as fallback) - .gitignore : exclude c2probe/.venv, c2probe/build Verified: stock + patched round-trips still re-assemble byte-identical.
7.2 KiB
EFM8UB20F64G (QFP48) — reverse-engineered pinout & firmware pin usage
QuNexus main MCU = EFM8UB20F64G-B-QFP48 (48-pin TQFP). App firmware v2.2.1.
There is no QFN48 variant of the EFM8UB20F64G — the datasheet lists only QFP48, QFP32, and QFN32. The 48-pin part is therefore QFP48, and the QFP48 ADC channel map is the one that matches the firmware.
All findings below are derived from the disassembly (qunexus_v2.2.1.asm) and
verified against the SiLabs header (SFR addresses) and the EFM8UB2 reference
manual (ADC channel→pin map). The pin configuration and which pins are
actively toggled are directly proven from the code; the identity of the
external device on the P2/P3/P4 bus is inferred from the bus structure.
Port configuration
The entire crossbar/port setup is one block at 0xde6f, written once
(firmware-wide — XBR / MDOUT / MDIN / SKIP are never written elsewhere):
| Register | Value | Meaning |
|---|---|---|
| XBR0 | 0x01 | UART0 only — no SPI, no SMBus, no comparators routed |
| XBR1 | 0x43 | crossbar enabled (XBARE = 1) |
| XBR2 | 0x00 | default, never written — UART1/SMB1 off |
| P0SKIP | 0xCF | skip P0.0-3,6,7 → leave P0.4/P0.5 for UART0 |
| P1SKIP | 0x03 | skip P1.0/P1.1 |
| P2SKIP | 0x00 | default, never written |
| P3SKIP | 0x00 | default (P3SKIP = 0xDF, never written) |
| P0MDIN | 0x30 | P0.4/P0.5 digital; P0.0-3,6,7 analog |
| P1MDIN | 0x3F | P1.0-5 digital; P1.6/P1.7 analog |
| P2/P3/P4MDIN | 0xFF | all digital |
| P0MDOUT | 0x10 | P0.4 push-pull (TX); rest open-drain |
| P1MDOUT | 0x3F | P1.0-5 push-pull; P1.6/7 open-drain |
| P2MDOUT | 0xFF | all push-pull |
| P3MDOUT | 0xF0 | P3.4-7 push-pull; P3.0-3 open-drain |
| P4MDOUT | 0xFF | all push-pull |
The P4 port latch is SFR 0xC7 — not 0xC0 (0xC0 is SMB0CN0, the SMBus control register). Easy to misidentify.
Peripherals actually used
- UART0 → P0.4 (TX) / P0.5 (RX) = the 5-pin DIN MIDI port. (USB-MIDI is a separate class engine over D+/D−, not this UART.)
- ADC0:
AMX0P = 0x11→ P0.3 (ADC0P.17 on QFP48),AMX0N = 0x1f(GND, single-ended),ADC0CN0 = 0x02(enabled). AMX0P is written exactly once, so the firmware reads a single fixed ADC channel. ADC0L/H (0xBD/0xBE) are read at0x8a37,0xa838,0xcadd. → An external analog mux (steered by the digital scan bus) feeds many sensors into P0.3. - No SPI0, no SMB0, no hardware EMIF — none of their config registers are ever written. The parallel bus below is bit-banged.
Runtime GPIO — a bit-banged parallel bus to an external engine
The EFM8 is not driving the LEDs/touch directly; it talks to an external chip (CPLD / expander / LED+touch controller) over a hand-strobed parallel bus, and advances a select matrix on a timer tick:
- P2.0–P2.7 (push-pull): written from a Timer ISR —
mov P2,aat0xc537setb TR2, endsreti. Therlc a/djnz/cpl aloop builds a walking one-hot pattern → scan / select bus.
- P3.0–P3.3 (open-drain): toggled with
orl/anl P3,#0x0f(0xc942,0xc997,0xca89,0xcad1,0xcad8,0xd774) → control strobes. - P4.0–P4.7 (push-pull, SFR 0xC7):
mov P4,a(0xc950,0xc999,0xd778), always paired with a P3 strobe → 8-bit data bus. - P1.5 (push-pull): toggled (
clr P1.5@0x860e,mov@0xe38f) → GPIO output, function unknown. P1.0/P1.1 are skipped = reserved GPIO.
Full QFP48 pin table (firmware function)
| Pin | Port | Firmware function | Used? |
|---|---|---|---|
| 1 | P0.5 | UART0 RX — MIDI In | ✓ |
| 2 | P0.4 | UART0 TX — MIDI Out | ✓ |
| 3 | P0.3 | ADC input (single channel, ext. mux output) | ✓ |
| 4 | P0.2 | analog, no ADC/CMP fn | ○ unused |
| 5 | P0.1 | analog, no ADC/CMP fn | ○ unused |
| 6 | P0.0 | analog, no ADC/CMP fn | ○ unused |
| 7 | GND | ground | — |
| 8 | D+ | USB (USB-MIDI class) | ✓ |
| 9 | D− | USB | ✓ |
| 10 | VDD | supply / reg output | — |
| 11 | VREGIN | 5V reg input | — |
| 12 | VBUS | USB VBUS sense | ✓ |
| 13 | RST/C2CK | reset / C2 flash clock | ✓ |
| 14 | C2D | C2 flash data | ✓ |
| 15 | P4.7 | data bus D7 | ✓ |
| 16 | P4.6 | data bus D6 | ✓ |
| 17 | P4.5 | data bus D5 | ✓ |
| 18 | P4.4 | data bus D4 | ✓ |
| 19 | P4.3 | data bus D3 | ✓ |
| 20 | P4.2 | data bus D2 | ✓ |
| 21 | P4.1 | data bus D1 | ✓ |
| 22 | P4.0 | data bus D0 | ✓ |
| 23 | P3.7 | push-pull out, never written | ○ static |
| 24 | P3.6 | push-pull out, referenced once | ○ ~unused |
| 25 | P3.5 | push-pull out, never written | ○ static |
| 26 | P3.4 | push-pull out, never written | ○ static |
| 27 | P3.3 | open-drain strobe | ✓ |
| 28 | P3.2 | open-drain strobe | ✓ |
| 29 | P3.1 | open-drain strobe | ✓ |
| 30 | P3.0 | open-drain strobe | ✓ |
| 31 | P2.7 | scan/select (timer ISR) | ✓ |
| 32 | P2.6 | scan/select | ✓ |
| 33 | P2.5 | scan/select | ✓ |
| 34 | P2.4 | scan/select | ✓ |
| 35 | P2.3 | scan/select | ✓ |
| 36 | P2.2 | scan/select | ✓ |
| 37 | P2.1 | scan/select | ✓ |
| 38 | P2.0 | scan/select | ✓ |
| 39 | P1.7 | analog (EMIF /WR not used) | ○ unused |
| 40 | P1.6 | analog (EMIF /RD not used) | ○ unused |
| 41 | P1.5 | GPIO output (toggled) | ✓ |
| 42 | P1.4 | CNVSTR/GPIO (ADC is SW-triggered) | ○ ~unused |
| 43 | P1.3 | push-pull GPIO | ○ ~unused |
| 44 | P1.2 | push-pull GPIO | ○ ~unused |
| 45 | P1.1 | skipped GPIO | ○ ~unused |
| 46 | P1.0 | skipped GPIO | ○ ~unused |
| 47 | P0.7 | XTAL2 — internal oscillator | ○ unused |
| 48 | P0.6 | XTAL1 — internal oscillator | ○ unused |
Legend: ✓ actively driven/read by firmware · ○ configured but no active drive found (likely unused/static) · — power/USB/debug (hardware).
Architecture summary
The EFM8UB20 is essentially a USB-MIDI class engine + DIN-MIDI UART + bus master, not the thing doing the LED/touch work:
- USB (pins 8/9/12) = USB-MIDI class traffic.
- UART0 (pins 1/2) = 5-pin DIN MIDI in/out.
- ADC (pin 3, P0.3) = one analog channel reading an external analog mux.
- Bit-banged parallel bus to an external LED/touch engine: P4 = 8-bit data (pins 15–22), P3.0–3 = strobes (pins 27–30), P2 = scan/select (pins 31–38, advanced by a timer ISR).
- C2 (pins 13/14) = how we flash/read it.
Used pins: 1, 2, 3 (MIDI + ADC), 8, 9, 11, 12 (USB), 13, 14 (C2 debug), 15–22 (P4 data), 27–30 (P3 strobes), 31–38 (P2 scan), 41 (P1.5 GPIO). Everything else is configured but shows no active drive.
Caveat
The "external LED/touch engine" on the P2/P3/P4 bus is an inference from the bus structure and the walking-one scan pattern — the firmware's driving of the bus is directly visible, but what sits on the other end can only be confirmed from board photos or a schematic.
Sources
- EFM8UB2 Reference Manual, Table 12.1 (AMX0P ADC channel→pin map): https://www.silabs.com/documents/public/reference-manuals/efm8ub2-rm.pdf
- EFM8UB20F64G-B-QFP48 datasheet, Table 6.1 (QFP48 pin definitions): https://resources.ampheo.com/static/datasheets/silicon-labs/efm8ub20f64g-b-qfp48.pdf
- si_efm8ub2_defs.h (SFR address verification): https://www.keil.com/dd/docs/c51/silabs/efm8ub2/inc/si_efm8ub2_defs.h