Previous RAM dumps via 0xA0 vendor request turned out to be live FIFO data, not firmware - the Genpix FX2 firmware overrides the standard 0xA0 handler. Discovered that I2C_READ (0x84) with wValue=0x51 and wIndex=offset reads the boot EEPROM directly. EEPROM contents (Cypress C2 format): - VID:PID 09C0:0203, config 0x40 (400kHz I2C) - 9,472 bytes of 8051 firmware in 10 load records - Code range 0x0000-0x24FF, entry at LJMP 0x188D - Ghidra auto-analysis finds 61 functions Tools: eeprom_dump.py (full dump), eeprom_probe.py (I2C protocol discovery)
252 lines
8.7 KiB
Python
252 lines
8.7 KiB
Python
#!/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()
|