diff --git a/firmware-dump/skywalker1_eeprom.bin b/firmware-dump/skywalker1_eeprom.bin new file mode 100644 index 0000000..91bc611 Binary files /dev/null and b/firmware-dump/skywalker1_eeprom.bin differ diff --git a/firmware-dump/skywalker1_eeprom_flat.bin b/firmware-dump/skywalker1_eeprom_flat.bin new file mode 100644 index 0000000..fdc8247 Binary files /dev/null and b/firmware-dump/skywalker1_eeprom_flat.bin differ diff --git a/firmware-dump/skywalker1_eeprom_full64k.bin b/firmware-dump/skywalker1_eeprom_full64k.bin new file mode 100644 index 0000000..ee374ae Binary files /dev/null and b/firmware-dump/skywalker1_eeprom_full64k.bin differ diff --git a/tools/eeprom_dump.py b/tools/eeprom_dump.py new file mode 100644 index 0000000..169caef --- /dev/null +++ b/tools/eeprom_dump.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +Genpix SkyWalker-1 EEPROM firmware dump tool. + +Reads the Cypress FX2 boot EEPROM via the I2C_READ vendor command. +Protocol: I2C_READ (0x84), wValue=0x51, wIndex=offset, length=chunk_size + +The EEPROM contains firmware in Cypress C2 IIC boot format: + - Header: C2 VID_L VID_H PID_L PID_H DID_L DID_H CONFIG + - Records: LEN_H LEN_L ADDR_H ADDR_L DATA[LEN] + - End: 80 01 ENTRY_H ENTRY_L (reset vector) +""" +import usb.core, usb.util, sys, struct + +VENDOR_ID = 0x09C0 +PRODUCT_ID = 0x0203 +I2C_READ = 0x84 +EEPROM_SLAVE = 0x51 + + +def find_device(): + dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) + if dev is None: + print("SkyWalker-1 not found") + sys.exit(1) + return dev + + +def detach_driver(dev): + intf_num = None + for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + try: + dev.detach_kernel_driver(intf.bInterfaceNumber) + intf_num = intf.bInterfaceNumber + except usb.core.USBError as e: + print(f"Cannot detach driver: {e}") + print("Try: sudo modprobe -r dvb_usb_gp8psk") + sys.exit(1) + try: + dev.set_configuration() + except: + pass + return intf_num + + +def eeprom_read(dev, offset, length=64): + """Read from EEPROM at given offset.""" + # wIndex holds the EEPROM byte offset (16-bit, so max 64KB) + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + I2C_READ, EEPROM_SLAVE, offset, length, 2000) + + +def parse_c2_header(data): + """Parse Cypress C2 boot EEPROM header.""" + if data[0] != 0xC2: + print(f" Not a C2 EEPROM (first byte: 0x{data[0]:02X})") + return None + + vid = data[2] << 8 | data[1] + pid = data[4] << 8 | data[3] + did = data[6] << 8 | data[5] + config = data[7] + + print(f" Format: C2 (Large EEPROM, code loads to internal RAM)") + print(f" VID: 0x{vid:04X} {'(Genpix)' if vid == 0x09C0 else ''}") + print(f" PID: 0x{pid:04X} {'(SkyWalker-1)' if pid == 0x0203 else ''}") + print(f" DID: 0x{did:04X}") + print(f" Config: 0x{config:02X}", end="") + + config_flags = [] + if config & 0x40: + config_flags.append("400kHz I2C") + if config & 0x04: + config_flags.append("disconnect") + if config_flags: + print(f" ({', '.join(config_flags)})") + else: + print() + + return {"vid": vid, "pid": pid, "did": did, "config": config} + + +def parse_records(data, offset=8): + """Parse C2 load records from EEPROM data.""" + records = [] + while offset < len(data) - 4: + rec_len = (data[offset] << 8) | data[offset + 1] + rec_addr = (data[offset + 2] << 8) | data[offset + 3] + + if rec_len == 0x8001: + # End marker - rec_addr is the entry point (reset vector) + records.append({ + "type": "end", + "entry_point": rec_addr, + "offset": offset + }) + break + elif rec_len == 0 or rec_len > 0x4000: + records.append({ + "type": "invalid", + "raw_len": rec_len, + "offset": offset + }) + break + + rec_data = data[offset + 4:offset + 4 + rec_len] + records.append({ + "type": "data", + "length": rec_len, + "load_addr": rec_addr, + "data": bytes(rec_data), + "offset": offset + }) + offset += 4 + rec_len + + return records + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Dump SkyWalker-1 EEPROM firmware") + parser.add_argument('-o', '--output', default='skywalker1_eeprom.bin', + help='Output file for raw EEPROM dump') + parser.add_argument('--extract', action='store_true', + help='Also extract firmware as flat binary') + parser.add_argument('--max-size', type=int, default=16384, + help='Maximum EEPROM size to read (default: 16384)') + args = parser.parse_args() + + print("Genpix SkyWalker-1 EEPROM Dump") + print("=" * 40) + + dev = find_device() + print(f"Found device: Bus {dev.bus} Addr {dev.address}") + intf = detach_driver(dev) + + try: + # Read EEPROM + chunk_size = 64 # Max reliable USB control transfer + eeprom = bytearray() + consecutive_ff = 0 + + print(f"\nReading EEPROM (max {args.max_size} bytes)...") + + for offset in range(0, args.max_size, chunk_size): + # wIndex only goes up to 0xFFFF, which covers 64KB EEPROMs + data = eeprom_read(dev, offset, chunk_size) + + if data is None: + print(f"\n Read failed at offset 0x{offset:04X}") + break + + chunk = bytes(data) + eeprom.extend(chunk) + + # Check for end of data + if all(b == 0xFF for b in chunk): + consecutive_ff += 1 + if consecutive_ff >= 4: + print(f"\r End of data at 0x{len(eeprom):04X} (0xFF padding) ") + break + else: + consecutive_ff = 0 + + if offset % 1024 == 0: + print(f"\r 0x{offset:04X} / 0x{args.max_size:04X} ", end="", flush=True) + + print(f"\r Read {len(eeprom)} bytes total ") + + # Save raw EEPROM + with open(args.output, 'wb') as f: + f.write(eeprom) + print(f" Saved raw EEPROM to: {args.output}") + + # Parse header + print(f"\n{'=' * 40}") + print("EEPROM Header:") + header = parse_c2_header(eeprom) + + if header: + # Parse load records + print(f"\nLoad Records:") + records = parse_records(eeprom) + total_code = 0 + entry_point = None + + for i, rec in enumerate(records): + if rec["type"] == "data": + end_addr = rec["load_addr"] + rec["length"] - 1 + preview = rec["data"][:8].hex(' ') + print(f" [{i}] {rec['length']:5d} bytes -> " + f"0x{rec['load_addr']:04X}-0x{end_addr:04X} " + f"[{preview}...]") + total_code += rec["length"] + elif rec["type"] == "end": + entry_point = rec["entry_point"] + print(f" [{i}] END MARKER -> entry point: 0x{entry_point:04X}") + else: + print(f" [{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) " + f"at EEPROM offset 0x{rec['offset']:04X}") + + print(f"\n Total firmware: {total_code} bytes in " + f"{sum(1 for r in records if r['type'] == 'data')} records") + if entry_point: + print(f" Entry point: 0x{entry_point:04X} (LJMP target after boot)") + + # Extract flat binary + if args.extract and records: + # Build memory image + mem = bytearray(0x10000) # 64KB address space + for b in range(len(mem)): + mem[b] = 0xFF + + for rec in records: + if rec["type"] == "data": + addr = rec["load_addr"] + mem[addr:addr + rec["length"]] = rec["data"] + + # Find actual used range + min_addr = min(r["load_addr"] for r in records if r["type"] == "data") + max_addr = max(r["load_addr"] + r["length"] + for r in records if r["type"] == "data") + + flat_file = args.output.replace('.bin', '_flat.bin') + with open(flat_file, 'wb') as f: + f.write(mem[min_addr:max_addr]) + print(f"\n Flat binary: {flat_file}") + print(f" Address range: 0x{min_addr:04X}-0x{max_addr:04X} " + f"({max_addr - min_addr} bytes)") + + # Also save full 64KB image for Ghidra + full_file = args.output.replace('.bin', '_full64k.bin') + with open(full_file, 'wb') as f: + f.write(mem) + print(f" Full 64K image: {full_file} (for Ghidra, load at 0x0000)") + + finally: + if intf is not None: + try: + usb.util.release_interface(dev, intf) + dev.attach_kernel_driver(intf) + print("\nRe-attached kernel driver") + except: + print("\nNote: run 'sudo modprobe dvb_usb_gp8psk' to reload") + + +if __name__ == '__main__': + main() diff --git a/tools/eeprom_probe.py b/tools/eeprom_probe.py new file mode 100644 index 0000000..5d7bcd2 --- /dev/null +++ b/tools/eeprom_probe.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Probe I2C EEPROM address setting on Genpix SkyWalker-1.""" +import usb.core, usb.util, time + +dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) +for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + dev.detach_kernel_driver(intf.bInterfaceNumber) + break +try: + dev.set_configuration() +except: + pass + +I2C_READ = 0x84 +I2C_WRITE = 0x83 + +def vin(req, value=0, index=0, length=64): + try: + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + req, value, index, length, 2000) + except: + return None + +def vout(req, value=0, index=0, data=b''): + try: + return dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + req, value, index, data, 2000) + except Exception as e: + print(f" OUT error: {e}") + return None + +# Known first 8 bytes at offset 0: c2 c0 09 03 02 00 00 40 +# Known bytes at offset 8 (from our read): 03 ff 00 00 02 18 8d 30 +REF_0 = "c2 c0 09 03 02 00 00 40" +REF_8 = "03 ff 00 00 02 18 8d 30" + +def show(label, data): + if data is not None: + h = bytes(data[:8]).hex(' ') + match = "" + if h == REF_0: match = " <-- OFFSET 0" + elif h == REF_8: match = " <-- OFFSET 8" + print(f" {label}: {h}{match}") + else: + print(f" {label}: FAILED") + +print("=== Approach 1: I2C_WRITE data=[addr_h, addr_l], then I2C_READ ===") +vout(I2C_WRITE, 0x51, 0, bytes([0x00, 0x08])) +show("After set 0x0008", vin(I2C_READ, 0x51, 0, 8)) +vout(I2C_WRITE, 0x51, 0, bytes([0x00, 0x00])) +show("After set 0x0000", vin(I2C_READ, 0x51, 0, 8)) + +print("\n=== Approach 2: wValue=addr, wIndex=slave ===") +vout(I2C_WRITE, 0x0008, 0x51) +show("After set 0x0008", vin(I2C_READ, 0x51, 0, 8)) +vout(I2C_WRITE, 0x0000, 0x51) +show("After set 0x0000", vin(I2C_READ, 0x51, 0, 8)) + +print("\n=== Approach 3: wValue=slave, wIndex=addr ===") +vout(I2C_WRITE, 0x51, 0x0008) +show("After set 0x0008", vin(I2C_READ, 0x51, 0, 8)) +vout(I2C_WRITE, 0x51, 0x0000) +show("After set 0x0000", vin(I2C_READ, 0x51, 0, 8)) + +print("\n=== Approach 4: I2C_READ with wIndex=offset ===") +show("wIndex=0x0000", vin(I2C_READ, 0x51, 0x0000, 8)) +show("wIndex=0x0008", vin(I2C_READ, 0x51, 0x0008, 8)) +show("wIndex=0x0040", vin(I2C_READ, 0x51, 0x0040, 8)) + +print("\n=== Approach 5: I2C_READ with (slave<<8|offset) in wValue ===") +show("wValue=0x5100", vin(I2C_READ, 0x5100, 0, 8)) +show("wValue=0x5108", vin(I2C_READ, 0x5108, 0, 8)) + +print("\n=== Approach 6: I2C_READ with wValue=offset (no slave) ===") +show("wValue=0x0000", vin(I2C_READ, 0x0000, 0, 8)) +show("wValue=0x0008", vin(I2C_READ, 0x0008, 0, 8)) +show("wValue=0x0040", vin(I2C_READ, 0x0040, 0, 8)) + +print("\n=== Approach 7: Larger reads to check page boundaries ===") +data = vin(I2C_READ, 0x51, 0, 64) +if data: + show("First 8 of 64", data[:8]) + show("Bytes 8-15", data[8:16]) + show("Bytes 56-63", data[56:64]) + # Check if bytes 8-15 match REF_8 + if bytes(data[8:16]).hex(' ') == REF_8: + print(" ** Bytes 8-15 match expected offset 8 data!") + print(" ** The 64-byte read IS returning sequential EEPROM data!") + +# Reattach +for cfg in dev: + for intf in cfg: + try: + usb.util.release_interface(dev, intf.bInterfaceNumber) + dev.attach_kernel_driver(intf.bInterfaceNumber) + except: + pass