Add tooling + EFM8UB20 QFP48 pinout doc
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.
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the raw firmware image from a KMI QuNexus firmware .syx file.
|
||||
|
||||
Container format (mirrors SysExEncDecode in qt-qunexus/source/midiio/sysexencdecode.cpp):
|
||||
|
||||
F0 00 01 5F 7A 19 [pad 00...] 01 <7-in-8 encoded stream> F7
|
||||
|
||||
* 00 01 5F 7A = manufacturer id bytes, 19 = product, 00 = format
|
||||
* 01 = SX_PACKET_START (sysexencdecode.cpp:33)
|
||||
* the encoded stream packs 7 data bytes as their low 7 bits followed by one
|
||||
byte holding their high bits (bit j = high bit of data byte j) --
|
||||
midi_sx_encode_char(), sysexencdecode.cpp:1830, SX_ENCODE_LEN = 7
|
||||
|
||||
The decoded stream is a packet preamble (00 02 <cat> <type> <crc16>) followed
|
||||
by framed records: 03 <len+1> <binary Intel HEX record>
|
||||
where the record is 3A LL AAAA TT <data...> CC (checksum = two's complement
|
||||
of the sum of LL..data), i.e. Intel HEX in binary rather than ASCII form.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
def sysex_messages(data):
|
||||
msgs, i = [], 0
|
||||
while True:
|
||||
s = data.find(b"\xf0", i)
|
||||
if s < 0:
|
||||
break
|
||||
e = data.find(b"\xf7", s)
|
||||
if e < 0:
|
||||
break
|
||||
msgs.append(data[s:e + 1])
|
||||
i = e + 1
|
||||
return msgs
|
||||
|
||||
|
||||
def decode_7in8(buf):
|
||||
"""Undo midi_sx_encode_char(): 7 low-7-bit bytes, then a high-bits byte."""
|
||||
out = bytearray()
|
||||
for i in range(0, len(buf) - 7, 8):
|
||||
hi = buf[i + 7]
|
||||
for j in range(7):
|
||||
out.append(buf[i + j] | (0x80 if (hi >> j) & 1 else 0))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_message(msg):
|
||||
"""Strip the sysex header up to SX_PACKET_START and 7-in-8 decode the body."""
|
||||
body = msg[1:-1] # drop F0 / F7
|
||||
i = 6 # manufacturer id (4) + product + format
|
||||
while i < len(body) and body[i] == 0x00:
|
||||
i += 1 # padding before the packet start
|
||||
if i >= len(body) or body[i] != 0x01: # SX_PACKET_START
|
||||
return b""
|
||||
return decode_7in8(body[i + 1:])
|
||||
|
||||
|
||||
def parse_hex_records(stream):
|
||||
"""Scan the decoded stream for checksum-valid binary Intel HEX records."""
|
||||
records, i, skipped = [], 0, 0
|
||||
while i < len(stream):
|
||||
if stream[i] != 0x3A:
|
||||
i += 1
|
||||
skipped += 1
|
||||
continue
|
||||
if i + 5 > len(stream):
|
||||
break
|
||||
ln = stream[i + 1]
|
||||
end = i + 5 + ln # 3A LL AA AA TT data...
|
||||
if end >= len(stream):
|
||||
break
|
||||
rec = stream[i + 1:end] # LL AAAA TT data (checksummed span)
|
||||
if (sum(rec) + stream[end]) & 0xFF != 0:
|
||||
i += 1 # not a real record, keep scanning
|
||||
skipped += 1
|
||||
continue
|
||||
records.append((stream[i + 4], # type
|
||||
(stream[i + 2] << 8) | stream[i + 3], # address
|
||||
bytes(rec[4:]))) # data
|
||||
i = end + 1
|
||||
return records, skipped
|
||||
|
||||
|
||||
def build_image(records):
|
||||
"""Apply Intel HEX records (types 00/01/02/04) to a sparse address space."""
|
||||
mem, base, eof = {}, 0, False
|
||||
for rtype, addr, data in records:
|
||||
if rtype == 0x00:
|
||||
for k, b in enumerate(data):
|
||||
mem[base + addr + k] = b
|
||||
elif rtype == 0x01:
|
||||
eof = True
|
||||
elif rtype == 0x02 and len(data) == 2:
|
||||
base = ((data[0] << 8) | data[1]) << 4
|
||||
elif rtype == 0x04 and len(data) == 2:
|
||||
base = ((data[0] << 8) | data[1]) << 16
|
||||
return mem, eof
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("syx")
|
||||
ap.add_argument("-o", "--out", help="write flat binary image here")
|
||||
ap.add_argument("--hex", help="also write a standard ASCII .hex file here")
|
||||
ap.add_argument("--fill", default="0xFF", help="gap fill byte (default 0xFF)")
|
||||
args = ap.parse_args()
|
||||
|
||||
data = open(args.syx, "rb").read()
|
||||
msgs = sysex_messages(data)
|
||||
stream = b"".join(decode_message(m) for m in msgs)
|
||||
records, skipped = parse_hex_records(stream)
|
||||
mem, eof = build_image(records)
|
||||
|
||||
print(f"file : {args.syx}")
|
||||
print(f"sysex messages : {len(msgs)}")
|
||||
print(f"decoded stream : {len(stream)} bytes ({skipped} non-record bytes skipped)")
|
||||
print(f"hex records : {len(records)} (EOF record seen: {eof})")
|
||||
if not mem:
|
||||
print("no data records found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
lo, hi = min(mem), max(mem)
|
||||
types = sorted({t for t, _, _ in records})
|
||||
print(f"record types : {[hex(t) for t in types]}")
|
||||
print(f"address range : 0x{lo:08X} - 0x{hi:08X} ({hi - lo + 1} bytes span)")
|
||||
print(f"bytes covered : {len(mem)} (gaps: {hi - lo + 1 - len(mem)})")
|
||||
|
||||
fill = int(args.fill, 0)
|
||||
img = bytes(mem.get(a, fill) for a in range(lo, hi + 1))
|
||||
if args.out:
|
||||
open(args.out, "wb").write(img)
|
||||
print(f"wrote : {args.out} ({len(img)} bytes, base 0x{lo:08X})")
|
||||
if args.hex:
|
||||
lines, base = [], None
|
||||
for a in range(lo, hi + 1, 16):
|
||||
chunk = bytes(mem.get(a + k, fill) for k in range(min(16, hi + 1 - a)))
|
||||
upper = a >> 16
|
||||
if upper != base:
|
||||
base = upper
|
||||
rec = bytes([2, 0, 0, 4, upper >> 8, upper & 0xFF])
|
||||
lines.append(":" + (rec + bytes([(-sum(rec)) & 0xFF])).hex().upper())
|
||||
rec = bytes([len(chunk), (a >> 8) & 0xFF, a & 0xFF, 0]) + chunk
|
||||
lines.append(":" + (rec + bytes([(-sum(rec)) & 0xFF])).hex().upper())
|
||||
lines.append(":00000001FF")
|
||||
open(args.hex, "w").write("\n".join(lines) + "\n")
|
||||
print(f"wrote : {args.hex}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user