#!/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()