Extract real FX2 firmware from I2C EEPROM

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)
This commit is contained in:
Ryan Malloy 2026-02-11 05:08:20 -07:00
parent 757da08987
commit ba37105e2a
5 changed files with 352 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

251
tools/eeprom_dump.py Normal file
View File

@ -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()

101
tools/eeprom_probe.py Normal file
View File

@ -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