From 3340cafb46f54e8317f1213bc407c1e7a0bb0278 Mon Sep 17 00:00:00 2001 From: nils Date: Mon, 17 Aug 2026 23:09:38 +0200 Subject: [PATCH] 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. --- firmware.asm | 25 +++++++++++++++++++++++++ roundtrip.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/firmware.asm b/firmware.asm index 6f5787c..d220c11 100644 --- a/firmware.asm +++ b/firmware.asm @@ -14034,18 +14034,30 @@ movx @r0, a jc L_817D rr a xch a, r5 +; usb1_cv_stub: mirror cable-0 (USB-1) events into the CV ring (dest 0x82); first run the original router unchanged L_8126: lcall 0xa57b +; USB-MIDI 4-byte event buffer at 0x0F9B mov dptr, #0x0f9b +; load event[0] (cable nibble in top 4 bits) movx a, @dptr +; isolate cable number (top nibble of event[0]) anl a, #0xf0 +; not cable 0 -> done (no mirror needed) jnz L_813C +; reload event[0] movx a, @dptr +; retag cable 0 as cable 2 (set bit 0x20) orl a, #0x20 +; store back movx @dptr, a +; DPTR high = 0x0F (buffer page) mov r6, #0x0f +; DPTR low = 0x9B (buffer addr) mov r7, #0x9b +; route again -> USB-3 / CV ring (dest 0x82) lcall 0xa57b +; done L_813C: ret mov r7, a @@ -19577,17 +19589,22 @@ mov dptr, #0x0e1c mov a, #0x04 movx @dptr, a ret +; USB-MIDI router (fcn.0000a57b): route the 4-byte event at @r6:r7 to a per-cable destination ring (called from 0xdf28) push 0xd0 +; switch to register bank 1 (PSW.RS0 = 1) mov 0xd0, #0x08 +; save event-buffer pointer r6:r7 -> 0x0d3b/0x0d3c mov dptr, #0x0d3b mov a, r6 movx @dptr, a inc dptr mov a, r7 movx @dptr, a +; if CV-source-locked flag (bit 0x22.2) set, early-out (r7 = 1) jnb 0x12, L_A590 mov r7, #0x01 ljmp 0xa660 +; read event[0]; extract cable number (high nibble) -> r7 L_A590: mov dptr, #0x0d3b lcall 0xcb9c @@ -19595,10 +19612,12 @@ swap a anl a, #0x0f mov r7, a jnz L_A5A4 +; cable 0 (USB-1 / Control Surface) -> dest 0x70 mov r3, #0x01 mov r2, #0x03 mov r1, #0x70 sjmp L_A5BA +; cable 1 -> dest 0xc3 L_A5A4: mov a, r7 cjne a, #0x01, L_A5B0 @@ -19606,15 +19625,18 @@ mov r3, #0x01 mov r2, #0x01 mov r1, #0xc3 sjmp L_A5BA +; cable 2 (USB-3) -> dest 0x82 (CV ring) L_A5B0: mov a, r7 cjne a, #0x02, L_A5C0 mov r3, #0x01 mov r2, #0x03 mov r1, #0x82 +; write destination descriptor (r3,r2,r1) -> 0x0d3d/e/f (fcn.0000551e) L_A5BA: mov dptr, #0x0d3d lcall 0x551e +; common routing pass: copy MIDI bytes from the event buffer to the selected destination ring (runs ONCE per router invocation) L_A5C0: lcall 0xcba6 lcall 0x52ca @@ -19706,6 +19728,7 @@ xch a, r7 mov 0xf0, r7 lcall 0x532d mov r7, #0x01 +; restore PSW; return (r7 = status) pop 0xd0 ret push 0xd0 @@ -28348,6 +28371,7 @@ movx @dptr, a mov r6, 0x04 mov r7, 0x05 ret +; USB-MIDI event dispatcher (fcn.0000df05) push 0xd0 mov 0xd0, #0x08 mov dptr, #0x0f9b @@ -28369,6 +28393,7 @@ sjmp L_DF34 L_DF24: mov r6, #0x0f mov r7, #0x9b +; dispatch call -> usb1_cv_stub (was: router 0xa57b) lcall L_8126 mov a, r7 jz L_DF32 diff --git a/roundtrip.py b/roundtrip.py index 86ff4c4..231c0ea 100644 --- a/roundtrip.py +++ b/roundtrip.py @@ -51,6 +51,44 @@ ABS_OPS = {"ljmp", "lcall", "ajmp", "acall"} LINE_RE = re.compile( r'^\s*[\\|/]?\s*0x([0-9a-fA-F]{8})\s+([0-9a-fA-F]+)\s+(\S.*)$') +# Annotated addresses. Emitted as `;` comment lines before the instruction, +# so the disassembly reads as a narrative. These survive regeneration (they +# live in this script, not in the generated file). Patch-mode comments +# (PATCH_COMMENTS) override COMMENTS at the same address. +COMMENTS = { + 0xa57b: "USB-MIDI router (fcn.0000a57b): route the 4-byte event at @r6:r7 to a per-cable destination ring (called from 0xdf28)", + 0xa57d: "switch to register bank 1 (PSW.RS0 = 1)", + 0xa580: "save event-buffer pointer r6:r7 -> 0x0d3b/0x0d3c", + 0xa588: "if CV-source-locked flag (bit 0x22.2) set, early-out (r7 = 1)", + 0xa590: "read event[0]; extract cable number (high nibble) -> r7", + 0xa59c: "cable 0 (USB-1 / Control Surface) -> dest 0x70", + 0xa5a4: "cable 1 -> dest 0xc3", + 0xa5b0: "cable 2 (USB-3) -> dest 0x82 (CV ring)", + 0xa5ba: "write destination descriptor (r3,r2,r1) -> 0x0d3d/e/f (fcn.0000551e)", + 0xa5c0: "common routing pass: copy MIDI bytes from the event buffer to the selected destination ring (runs ONCE per router invocation)", + 0xa660: "restore PSW; return (r7 = status)", + 0xdf05: "USB-MIDI event dispatcher (fcn.0000df05)", + 0xdf28: "dispatch call -> router 0xa57b", +} + +# Patch-mode-only annotations (the stub + retargeted call site). +# Each comment describes the instruction AT that address. +PATCH_COMMENTS = { + 0x8126: "usb1_cv_stub: mirror cable-0 (USB-1) events into the CV ring (dest 0x82); first run the original router unchanged", + 0x8129: "USB-MIDI 4-byte event buffer at 0x0F9B", + 0x812c: "load event[0] (cable nibble in top 4 bits)", + 0x812d: "isolate cable number (top nibble of event[0])", + 0x812f: "not cable 0 -> done (no mirror needed)", + 0x8131: "reload event[0]", + 0x8132: "retag cable 0 as cable 2 (set bit 0x20)", + 0x8134: "store back", + 0x8135: "DPTR high = 0x0F (buffer page)", + 0x8137: "DPTR low = 0x9B (buffer addr)", + 0x8139: "route again -> USB-3 / CV ring (dest 0x82)", + 0x813c: "done", + 0xdf28: "dispatch call -> usb1_cv_stub (was: router 0xa57b)", +} + def signed(b): return b - 0x100 if b >= 0x80 else b @@ -181,8 +219,13 @@ def emit_source(items, labels, pinned, patch=False): lines.append(".area CODE (ABS)") lines.append(".org 0x%04x" % BASE) item_line = {} + cmt = dict(COMMENTS) + if patch: + cmt.update(PATCH_COMMENTS) for idx, it in enumerate(items): a = it["addr"] + if a in cmt: + lines.append("; " + cmt[a]) if a in labels: lines.append("L_%04X:" % a) if it["is_gap"] or it["force_db"] or a in pinned: