Files
qunexus-firmware/patch_usb1_to_cv.asm
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

47 lines
2.4 KiB
NASM

; ---------------------------------------------------------------------------
; USB-1 -> CV routing patch for QuNexus firmware v2.2.1
;
; Goal: MIDI arriving on USB port 1 (Control Surface, cable 0) should also
; reach the CV engine. The stock firmware's CV_Out_Source only offers
; Expander / USB-3, so cable-0 events never enter the CV ring.
;
; Mechanism: retarget the single USB-MIDI dispatch call at 0xDF28 from the
; stock router (0xA57B) to a small stub (0x8126). The stub first 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).
;
; Abort-safety (matches patch_c2.py): the stub page (0x8000) is programmed
; BEFORE the retarget page (0xDE00), so a mid-failure never leaves a call to
; unprogrammed flash.
;
; This file assembles with sdas8051 (SDCC): sdas8051 -l patch_usb1_to_cv.asm
; The resulting bytes must match exactly what patch_c2.py writes to the
; device over C2 (see verify_patch_asm.py).
; ---------------------------------------------------------------------------
.area CODE (ABS)
; --- Stub routine (programmed into 0xFF padding at 0x8126, page 0x8000) ---
.org 0x8126
usb1_cv_stub:
lcall 0xa57b ; 12 a5 7b original cable-number router
mov dptr, #0x0f9b ; 90 0f 9b USB-MIDI 4-byte event buffer
movx a, @dptr ; e0 load event[0] (cable nibble in top 4 bits)
anl a, #0xf0 ; 54 f0 isolate cable number
jnz stub_done ; 70 0b not cable 0 -> nothing to mirror
movx a, @dptr ; e0 reload event[0]
orl a, #0x20 ; 44 20 retag cable 0 as cable 2
movx @dptr, a ; f0 store back
mov r6, #0x0f ; 7e 0f DPTR high = 0x0F (buffer page)
mov r7, #0x9b ; 7f 9b DPTR low = 0x9B (buffer addr)
lcall 0xa57b ; 12 a5 7b route again -> USB-3 / CV ring
stub_done:
ret ; 22
; --- Retarget: the single dispatch call site (page 0xDE00) ---------------
; Stock at 0xDF28 is `lcall 0xa57b` (12 a5 7b). Patched to call the stub.
.org 0xdf28
lcall 0x8126 ; 12 81 26 -> usb1_cv_stub