Commit Graph
4 Commits
Author SHA1 Message Date
nils 3340cafb46 Annotate the USB-MIDI router and the USB-1->CV patch in the disassembly
Add a comment-injection mechanism to roundtrip.py (COMMENTS / PATCH_COMMENTS
address-keyed dicts) so annotations survive regeneration -- they live in the
script, not the generated file.

Annotated in firmware.asm:
  * The router at 0xA57B: register-bank switch, event-buffer save, the
    CV-source-locked early-out, the cable-number switch (cable 0 -> 0x70,
    cable 1 -> 0xc3, cable 2 -> 0x82 = CV ring), the descriptor write via
    0x551e, and the single common routing pass at 0xA5C0.
  * The dispatcher at 0xDF05 / dispatch call at 0xDF28.
  * The usb1_cv_stub at 0x8126 (each instruction) and the retargeted call.

The router's structure clarifies why re-injection is used rather than a
router patch: 0x551e only writes a 3-byte destination descriptor, and the
actual routing pass (0xA5C0+) runs once per invocation, wrapped by a
PSW push/pop -- so two destinations require two full router calls.
2026-08-17 23:09:38 +02:00
nils 5022b59934 USB-1->CV routing patch: mirror cable-0 events into the CV ring
The stock CV_Out_Source only offers Expander / USB-3, so MIDI arriving on
USB port 1 (Control Surface, cable 0) never reaches the CV engine. This
patch retargets the single USB-MIDI dispatch call at 0xDF28 from the
stock cable-number router (0xA57B) to a 23-byte stub written into the 0xFF
padding at 0x8126.

The stub runs the original router unchanged, then inspects the 4-byte
USB-MIDI event buffer at 0x0F9B: if the event came in on cable 0 (top
nibble == 0) it retags it as cable 2 (sets bit 0x20) and calls the router
again, so the event also lands in the USB-3 / CV ring. Events on any other
cable pass through untouched (one router call, as stock).

  0x8126:  23x 0xFF padding  ->  usb1_cv_stub (lcall/mov/movx/.../ret)
  0xDF28:  lcall 0xa57b       ->  lcall L_8126   (-> usb1_cv_stub)

Assembles byte-identical to the patched image (stock + stub + retarget),
matching what patch_c2.py writes to the device over C2. Verified with
`python3 roundtrip.py --patch` (PASS, 3 iterations).

Stock source is regenerable with:  python3 roundtrip.py
2026-08-17 23:00:50 +02:00
nils ae2de3a787 Add recompilable sdas8051 source; round-trips to stock v2.2.1 binary
roundtrip.py converts the radare2 disassembly (qunexus_v2.2.1.asm) into a
single sdas8051 assembler source (firmware.asm) and verifies it re-assembles
byte-identical to the 54,279-byte stock image (base 0x2400).

  31,051 of 31,346 items (99.06%) re-assemble from mnemonics; 295 are
  pinned to .db where r2's display syntax doesn't round-trip through
  sdas8051 (256 ajmp/acall that sdas8051 mis-encodes, 39 data-in-code).
  Converges in 3 iterations.

Also adds patch_usb1_to_cv.asm (the USB-1->CV patch as a standalone
sdas8051 source) and verify_patch_asm.py (proves the assembled patch
matches the bytes written to the device over C2).

Reproduce:  python3 roundtrip.py
2026-08-17 23:00:36 +02:00
nils 27c21396f8 QuNexus v2.2.1 firmware: reverse-engineered disassembly & pseudocode
Decompiled from stock QuNexus_Firmware_v2.2.1.bin (EFM8UB20F64G / 8051,
base 0x2400, 54279 bytes) with radare2 6.2.0.

- qunexus_v2.2.1.asm : full linear disassembly (37287 lines, byte-faithful)
- pseudocode_all.c   : r2 pdc pseudocode for all 1002 functions
- functions.txt     : function index (1002 functions)
- regen.sh + r2script.r2 : one-command reproduction (brew install radare2)

Verified: reconstructing the binary from the asm byte-columns reproduces the
original image byte-for-byte (30560 instructions + 195 data gaps).
2026-08-17 22:22:21 +02:00