Firmware: Rewrite skywalker1.c for EEPROM boot experiment — tests whether I2C hardware controller works after FX2 boot ROM completes EEPROM load (bypassing the CPUCS restart that triggers BERR). Tools: - fw_load.py: Add I2C cleanup stub, pre-halt register flush, improved error handling and segment loading - eeprom_write.py: Add IHX→C2 EEPROM image converter (16KB format with length-prefixed segments, checksum) - eeprom_dump.py: Refactor for cleaner output, better hex display - skywalker_lib.py: Minor I2C register constant updates Docs: - EEPROM-RECOVERY.md: Four recovery options for soft-bricked device (SOIC clip, SDA pull-up, desolder, wait-for-timeout) - Master reference: Updated with EEPROM boot findings Status: EEPROM flash blocked — stock firmware I2C proxy returns pipe errors, host-side 0xA0 writes proven unable to drive peripheral bus. Device boot ROM intermittently hangs on EEPROM I2C read (~3-6% success).
790 lines
26 KiB
Python
Executable File
790 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Genpix SkyWalker-1 EEPROM firmware flash tool.
|
|
|
|
Writes C2-format firmware images to the Cypress FX2 boot EEPROM via
|
|
the I2C_WRITE vendor command.
|
|
|
|
Protocol:
|
|
I2C_WRITE (0x83): wValue=0x51, wIndex=offset, data=bytes
|
|
I2C_READ (0x84): wValue=0x51, wIndex=offset, length=chunk_size
|
|
|
|
The EEPROM uses 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)
|
|
|
|
WARNING: Flashing incorrect firmware can brick the device. The FX2
|
|
boots from this EEPROM on power-up -- a corrupted image means the
|
|
device will not enumerate on USB until the EEPROM is reprogrammed
|
|
with an external programmer or the FX2 boot ROM's A0 vendor request.
|
|
"""
|
|
import usb.core, usb.util, sys, struct, time, os
|
|
|
|
VENDOR_ID = 0x09C0
|
|
PRODUCT_ID = 0x0203
|
|
I2C_WRITE = 0x83
|
|
I2C_READ = 0x84
|
|
EEPROM_SLAVE = 0x51
|
|
|
|
# EEPROM page write parameters
|
|
PAGE_SIZE = 16 # Conservative page size for 24Cxx EEPROMs
|
|
WRITE_CYCLE_MS = 10 # Max internal write cycle time per page
|
|
MAX_EEPROM_SIZE = 16384 # 128Kbit / 16KB max addressable via wIndex
|
|
|
|
|
|
# -- Intel HEX parser (shared with fw_load.py) --
|
|
|
|
def parse_ihx(data):
|
|
"""Parse an Intel HEX file. Returns list of (address, bytes) segments."""
|
|
segments = []
|
|
base_addr = 0
|
|
|
|
for raw_line in data.splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
if isinstance(line, bytes):
|
|
line = line.decode('ascii', errors='replace')
|
|
if not line.startswith(':'):
|
|
continue
|
|
|
|
hex_str = line[1:]
|
|
if len(hex_str) < 10:
|
|
continue
|
|
try:
|
|
raw = bytes.fromhex(hex_str)
|
|
except ValueError:
|
|
continue
|
|
|
|
byte_count = raw[0]
|
|
addr = (raw[1] << 8) | raw[2]
|
|
rec_type = raw[3]
|
|
rec_data = raw[4:4 + byte_count]
|
|
|
|
if rec_type == 0x00:
|
|
full_addr = base_addr + addr
|
|
segments.append((full_addr, bytes(rec_data)))
|
|
elif rec_type == 0x01:
|
|
break
|
|
elif rec_type == 0x02:
|
|
base_addr = ((rec_data[0] << 8) | rec_data[1]) << 4
|
|
elif rec_type == 0x04:
|
|
base_addr = ((rec_data[0] << 8) | rec_data[1]) << 16
|
|
|
|
return segments
|
|
|
|
|
|
def coalesce_segments(segments):
|
|
"""Merge adjacent/overlapping segments into contiguous blocks."""
|
|
if not segments:
|
|
return []
|
|
|
|
sorted_segs = sorted(segments, key=lambda s: s[0])
|
|
merged = []
|
|
cur_addr, cur_data = sorted_segs[0]
|
|
cur_data = bytearray(cur_data)
|
|
|
|
for addr, data in sorted_segs[1:]:
|
|
cur_end = cur_addr + len(cur_data)
|
|
if addr <= cur_end:
|
|
overlap = cur_end - addr
|
|
if overlap >= 0:
|
|
cur_data.extend(data[overlap:] if overlap < len(data) else b'')
|
|
else:
|
|
merged.append((cur_addr, bytes(cur_data)))
|
|
cur_addr = addr
|
|
cur_data = bytearray(data)
|
|
|
|
merged.append((cur_addr, bytes(cur_data)))
|
|
return merged
|
|
|
|
|
|
def create_c2_image(segments, vid=0x09C0, pid=0x0203, did=0x0000, config=0x40):
|
|
"""Create a Cypress C2 EEPROM boot image from code segments.
|
|
|
|
C2 format:
|
|
Header (8 bytes): 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 E6 00 00 (write CPUCS=0x00 to release CPU)
|
|
|
|
CONFIG byte:
|
|
bit 6: 1 = 400kHz I2C (used during EEPROM load)
|
|
bit 2: 1 = disconnect (don't drive I2C after load)
|
|
"""
|
|
image = bytearray()
|
|
|
|
# Header
|
|
image.append(0xC2)
|
|
image.append(vid & 0xFF)
|
|
image.append((vid >> 8) & 0xFF)
|
|
image.append(pid & 0xFF)
|
|
image.append((pid >> 8) & 0xFF)
|
|
image.append(did & 0xFF)
|
|
image.append((did >> 8) & 0xFF)
|
|
image.append(config & 0xFF)
|
|
|
|
# Data records — filter out SFR region (0xE000+)
|
|
skipped = 0
|
|
for addr, data in segments:
|
|
if addr >= 0xE000:
|
|
skipped += len(data)
|
|
continue
|
|
# Truncate if segment extends into SFR region
|
|
end = addr + len(data)
|
|
if end > 0xE000:
|
|
data = data[:0xE000 - addr]
|
|
skipped += end - 0xE000
|
|
|
|
length = len(data)
|
|
if length == 0:
|
|
continue
|
|
|
|
# Split large segments (boot ROM may have record size limits)
|
|
chunk_max = 1023 # conservative limit
|
|
offset = 0
|
|
while offset < length:
|
|
chunk_len = min(chunk_max, length - offset)
|
|
chunk_addr = addr + offset
|
|
image.append((chunk_len >> 8) & 0xFF)
|
|
image.append(chunk_len & 0xFF)
|
|
image.append((chunk_addr >> 8) & 0xFF)
|
|
image.append(chunk_addr & 0xFF)
|
|
image.extend(data[offset:offset + chunk_len])
|
|
offset += chunk_len
|
|
|
|
# End marker: write 0x00 to CPUCS (0xE600)
|
|
image.extend([0x80, 0x01, 0xE6, 0x00, 0x00])
|
|
|
|
return bytes(image), skipped
|
|
|
|
|
|
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."""
|
|
return dev.ctrl_transfer(
|
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
|
|
I2C_READ, EEPROM_SLAVE, offset, length, 2000)
|
|
|
|
|
|
def eeprom_write(dev, offset, data):
|
|
"""Write data to EEPROM at given offset. Caller handles page alignment."""
|
|
return dev.ctrl_transfer(
|
|
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT,
|
|
I2C_WRITE, EEPROM_SLAVE, offset, data, 2000)
|
|
|
|
|
|
def eeprom_read_all(dev, size, label="Reading"):
|
|
"""Read entire EEPROM contents up to size bytes."""
|
|
chunk_size = 64
|
|
data = bytearray()
|
|
for offset in range(0, size, chunk_size):
|
|
remaining = min(chunk_size, size - offset)
|
|
chunk = eeprom_read(dev, offset, remaining)
|
|
if chunk is None:
|
|
print(f"\n Read failed at offset 0x{offset:04X}")
|
|
return None
|
|
data.extend(bytes(chunk))
|
|
if offset % 1024 == 0:
|
|
pct = offset * 100 // size
|
|
print(f"\r {label}: 0x{offset:04X} / 0x{size:04X} [{pct:3d}%]",
|
|
end="", flush=True)
|
|
print(f"\r {label}: 0x{size:04X} / 0x{size:04X} [100%] ")
|
|
return data
|
|
|
|
|
|
def parse_c2_header(data):
|
|
"""Parse Cypress C2 boot EEPROM header. Returns dict or None."""
|
|
if len(data) < 8:
|
|
return None
|
|
if data[0] != 0xC2:
|
|
return None
|
|
|
|
vid = data[2] << 8 | data[1]
|
|
pid = data[4] << 8 | data[3]
|
|
did = data[6] << 8 | data[5]
|
|
config = data[7]
|
|
|
|
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:
|
|
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 print_c2_header(header, prefix=" "):
|
|
"""Display parsed C2 header fields."""
|
|
print(f"{prefix}Format: C2 (Large EEPROM, code loads to internal RAM)")
|
|
print(f"{prefix}VID: 0x{header['vid']:04X}"
|
|
f" {'(Genpix)' if header['vid'] == 0x09C0 else ''}")
|
|
print(f"{prefix}PID: 0x{header['pid']:04X}"
|
|
f" {'(SkyWalker-1)' if header['pid'] == 0x0203 else ''}")
|
|
print(f"{prefix}DID: 0x{header['did']:04X}")
|
|
print(f"{prefix}Config: 0x{header['config']:02X}", end="")
|
|
|
|
config_flags = []
|
|
if header["config"] & 0x40:
|
|
config_flags.append("400kHz I2C")
|
|
if header["config"] & 0x04:
|
|
config_flags.append("disconnect")
|
|
if config_flags:
|
|
print(f" ({', '.join(config_flags)})")
|
|
else:
|
|
print()
|
|
|
|
|
|
def print_c2_records(records, prefix=" "):
|
|
"""Display parsed C2 load records."""
|
|
total_code = 0
|
|
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"{prefix}[{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":
|
|
print(f"{prefix}[{i}] END MARKER -> entry point: "
|
|
f"0x{rec['entry_point']:04X}")
|
|
else:
|
|
print(f"{prefix}[{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) "
|
|
f"at EEPROM offset 0x{rec['offset']:04X}")
|
|
|
|
data_recs = [r for r in records if r["type"] == "data"]
|
|
print(f"\n{prefix}Total firmware: {total_code} bytes in "
|
|
f"{len(data_recs)} segments")
|
|
end_recs = [r for r in records if r["type"] == "end"]
|
|
if end_recs:
|
|
print(f"{prefix}Entry point: 0x{end_recs[0]['entry_point']:04X} "
|
|
f"(LJMP target after boot)")
|
|
|
|
|
|
def validate_c2_image(data, label="image"):
|
|
"""Validate a C2 firmware image. Returns (header, records) or exits."""
|
|
if len(data) < 12:
|
|
print(f" {label}: too small ({len(data)} bytes, need at least 12)")
|
|
return None, None
|
|
|
|
if data[0] != 0xC2:
|
|
print(f" {label}: not a C2 image (first byte: 0x{data[0]:02X}, "
|
|
f"expected 0xC2)")
|
|
return None, None
|
|
|
|
header = parse_c2_header(data)
|
|
if header is None:
|
|
print(f" {label}: failed to parse C2 header")
|
|
return None, None
|
|
|
|
records = parse_records(data)
|
|
if not records:
|
|
print(f" {label}: no load records found")
|
|
return None, None
|
|
|
|
end_recs = [r for r in records if r["type"] == "end"]
|
|
invalid_recs = [r for r in records if r["type"] == "invalid"]
|
|
if not end_recs:
|
|
print(f" {label}: WARNING -- no end marker found")
|
|
if invalid_recs:
|
|
print(f" {label}: WARNING -- {len(invalid_recs)} invalid record(s)")
|
|
|
|
return header, records
|
|
|
|
|
|
def cmd_info(args):
|
|
"""Parse and display C2 header info from a .bin file."""
|
|
if not os.path.exists(args.file):
|
|
print(f"File not found: {args.file}")
|
|
sys.exit(1)
|
|
|
|
with open(args.file, 'rb') as f:
|
|
data = f.read()
|
|
|
|
print(f"C2 Image: {args.file}")
|
|
print(f"File size: {len(data)} bytes")
|
|
print("=" * 40)
|
|
|
|
header, records = validate_c2_image(data, args.file)
|
|
if header is None:
|
|
sys.exit(1)
|
|
|
|
print("\nHeader:")
|
|
print_c2_header(header)
|
|
|
|
print("\nLoad Records:")
|
|
print_c2_records(records)
|
|
|
|
# Compute EEPROM usage (header + record headers + data + end marker)
|
|
if records:
|
|
last = records[-1]
|
|
if last["type"] == "end":
|
|
eeprom_end = last["offset"] + 4
|
|
elif last["type"] == "data":
|
|
eeprom_end = last["offset"] + 4 + last["length"]
|
|
else:
|
|
eeprom_end = last["offset"]
|
|
print(f"\n EEPROM footprint: {eeprom_end} bytes "
|
|
f"(0x{eeprom_end:04X})")
|
|
|
|
|
|
def cmd_backup(args):
|
|
"""Dump current EEPROM contents to a file."""
|
|
print("Genpix SkyWalker-1 EEPROM Backup")
|
|
print("=" * 40)
|
|
|
|
dev = find_device()
|
|
print(f"Found device: Bus {dev.bus} Addr {dev.address}")
|
|
intf = detach_driver(dev)
|
|
|
|
try:
|
|
size = args.max_size
|
|
print(f"\nReading EEPROM ({size} bytes)...")
|
|
data = eeprom_read_all(dev, size)
|
|
if data is None:
|
|
print("Backup failed: read error")
|
|
sys.exit(1)
|
|
|
|
with open(args.output, 'wb') as f:
|
|
f.write(data)
|
|
print(f" Saved to: {args.output}")
|
|
|
|
# Show header info
|
|
header = parse_c2_header(data)
|
|
if header:
|
|
print("\nHeader:")
|
|
print_c2_header(header)
|
|
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")
|
|
|
|
|
|
def cmd_verify(args):
|
|
"""Compare a .bin file against current EEPROM contents."""
|
|
if not os.path.exists(args.file):
|
|
print(f"File not found: {args.file}")
|
|
sys.exit(1)
|
|
|
|
with open(args.file, 'rb') as f:
|
|
image = f.read()
|
|
|
|
print("Genpix SkyWalker-1 EEPROM Verify")
|
|
print("=" * 40)
|
|
|
|
# Validate the image first
|
|
header, records = validate_c2_image(image, args.file)
|
|
if header is None:
|
|
sys.exit(1)
|
|
|
|
print(f"\nImage: {args.file} ({len(image)} bytes)")
|
|
print_c2_header(header)
|
|
|
|
dev = find_device()
|
|
print(f"\nFound device: Bus {dev.bus} Addr {dev.address}")
|
|
intf = detach_driver(dev)
|
|
|
|
try:
|
|
print(f"\nReading EEPROM ({len(image)} bytes)...")
|
|
eeprom = eeprom_read_all(dev, len(image), label="Verify")
|
|
if eeprom is None:
|
|
print("Verify failed: read error")
|
|
sys.exit(1)
|
|
|
|
# Compare byte-by-byte
|
|
mismatches = []
|
|
for i in range(len(image)):
|
|
if i < len(eeprom) and image[i] != eeprom[i]:
|
|
mismatches.append(i)
|
|
|
|
if not mismatches:
|
|
print(f"\n MATCH -- EEPROM contents match {args.file}")
|
|
else:
|
|
print(f"\n MISMATCH -- {len(mismatches)} byte(s) differ:")
|
|
for off in mismatches[:32]:
|
|
exp = image[off]
|
|
got = eeprom[off] if off < len(eeprom) else 0xFF
|
|
print(f" 0x{off:04X}: expected 0x{exp:02X}, "
|
|
f"got 0x{got:02X}")
|
|
if len(mismatches) > 32:
|
|
print(f" ... and {len(mismatches) - 32} more")
|
|
sys.exit(1)
|
|
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")
|
|
|
|
|
|
def cmd_convert(args):
|
|
"""Convert Intel HEX (.ihx) to Cypress C2 EEPROM format (.bin)."""
|
|
if not os.path.exists(args.file):
|
|
print(f"File not found: {args.file}")
|
|
sys.exit(1)
|
|
|
|
with open(args.file, 'rb') as f:
|
|
raw = f.read()
|
|
|
|
print("Genpix SkyWalker-1 IHX → C2 Converter")
|
|
print("=" * 40)
|
|
|
|
# Parse IHX
|
|
segments = parse_ihx(raw)
|
|
if not segments:
|
|
print(" No code segments found in IHX file")
|
|
sys.exit(1)
|
|
segments = coalesce_segments(segments)
|
|
|
|
total_code = sum(len(d) for _, d in segments)
|
|
min_addr = min(a for a, _ in segments)
|
|
max_addr = max(a + len(d) - 1 for a, d in segments)
|
|
print(f"\nInput: {args.file}")
|
|
print(f" Segments: {len(segments)}")
|
|
print(f" Code size: {total_code} bytes")
|
|
print(f" Address: 0x{min_addr:04X} - 0x{max_addr:04X}")
|
|
|
|
# Create C2 image
|
|
vid = int(args.vid, 0) if args.vid else VENDOR_ID
|
|
pid = int(args.pid, 0) if args.pid else PRODUCT_ID
|
|
did = int(args.did, 0) if args.did else 0x0000
|
|
config = int(args.config, 0) if args.config else 0x40
|
|
|
|
image, skipped = create_c2_image(segments, vid, pid, did, config)
|
|
if skipped:
|
|
print(f" Skipped: {skipped} bytes (SFR region 0xE000+)")
|
|
|
|
# Validate the image we just created
|
|
header, records = validate_c2_image(image, "generated")
|
|
if header is None:
|
|
print(" INTERNAL ERROR: generated image failed validation")
|
|
sys.exit(1)
|
|
|
|
print(f"\nOutput: {args.output}")
|
|
print(f" Image size: {len(image)} bytes")
|
|
print(f" EEPROM use: {len(image) * 100 / MAX_EEPROM_SIZE:.1f}% "
|
|
f"of {MAX_EEPROM_SIZE} bytes")
|
|
|
|
print("\nC2 Header:")
|
|
print_c2_header(header)
|
|
|
|
print("\nLoad Records:")
|
|
print_c2_records(records)
|
|
|
|
if len(image) > MAX_EEPROM_SIZE:
|
|
print(f"\n WARNING: image ({len(image)} bytes) exceeds EEPROM "
|
|
f"capacity ({MAX_EEPROM_SIZE} bytes)")
|
|
sys.exit(1)
|
|
|
|
with open(args.output, 'wb') as f:
|
|
f.write(image)
|
|
|
|
print(f"\n Written: {args.output} ({len(image)} bytes)")
|
|
print(f" Flash with: python {sys.argv[0]} flash {args.output}")
|
|
|
|
|
|
def cmd_flash(args):
|
|
"""Write a C2-format .bin file to the EEPROM."""
|
|
if not os.path.exists(args.file):
|
|
print(f"File not found: {args.file}")
|
|
sys.exit(1)
|
|
|
|
with open(args.file, 'rb') as f:
|
|
image = f.read()
|
|
|
|
print("Genpix SkyWalker-1 EEPROM Flash")
|
|
print("=" * 40)
|
|
print()
|
|
print(" *** FIRMWARE FLASH -- READ CAREFULLY ***")
|
|
print(" Writing bad firmware will brick the device.")
|
|
print(" The SkyWalker-1 boots from this EEPROM on power-up.")
|
|
print(" A corrupted image = no USB enumeration.")
|
|
print()
|
|
|
|
# Validate input image
|
|
img_header, img_records = validate_c2_image(image, args.file)
|
|
if img_header is None:
|
|
sys.exit(1)
|
|
|
|
print(f"Image: {args.file} ({len(image)} bytes)")
|
|
print_c2_header(img_header)
|
|
|
|
# Size sanity check
|
|
if len(image) > MAX_EEPROM_SIZE:
|
|
print(f"\n Image too large: {len(image)} bytes "
|
|
f"(max {MAX_EEPROM_SIZE})")
|
|
sys.exit(1)
|
|
|
|
if len(image) < 12:
|
|
print(f"\n Image too small: {len(image)} bytes")
|
|
sys.exit(1)
|
|
|
|
# Connect to device
|
|
dev = find_device()
|
|
print(f"\nFound device: Bus {dev.bus} Addr {dev.address}")
|
|
intf = detach_driver(dev)
|
|
|
|
try:
|
|
# Check VID/PID against the connected device
|
|
if not args.force:
|
|
if img_header["vid"] != VENDOR_ID:
|
|
print(f"\n VID mismatch: image has 0x{img_header['vid']:04X},"
|
|
f" device is 0x{VENDOR_ID:04X}")
|
|
print(" Use --force to override")
|
|
sys.exit(1)
|
|
if img_header["pid"] != PRODUCT_ID:
|
|
print(f"\n PID mismatch: image has 0x{img_header['pid']:04X},"
|
|
f" device is 0x{PRODUCT_ID:04X}")
|
|
print(" Use --force to override")
|
|
sys.exit(1)
|
|
elif img_header["vid"] != VENDOR_ID or img_header["pid"] != PRODUCT_ID:
|
|
print(f"\n WARNING: VID/PID mismatch (--force active)")
|
|
print(f" Image: VID=0x{img_header['vid']:04X} "
|
|
f"PID=0x{img_header['pid']:04X}")
|
|
print(f" Device: VID=0x{VENDOR_ID:04X} PID=0x{PRODUCT_ID:04X}")
|
|
|
|
# Backup current EEPROM
|
|
if not args.no_backup:
|
|
ts = time.strftime("%Y%m%d_%H%M%S")
|
|
backup_file = f"eeprom_backup_{ts}.bin"
|
|
print(f"\nBacking up current EEPROM to {backup_file}...")
|
|
backup = eeprom_read_all(dev, MAX_EEPROM_SIZE, label="Backup")
|
|
if backup is None:
|
|
print(" Backup failed: read error. Aborting.")
|
|
sys.exit(1)
|
|
with open(backup_file, 'wb') as f:
|
|
f.write(backup)
|
|
print(f" Backup saved: {backup_file} ({len(backup)} bytes)")
|
|
|
|
# Show what's currently on the EEPROM
|
|
cur_header = parse_c2_header(backup)
|
|
if cur_header:
|
|
print("\n Current EEPROM:")
|
|
print_c2_header(cur_header, prefix=" ")
|
|
else:
|
|
print("\n Skipping backup (--no-backup)")
|
|
|
|
# Dry-run stops here
|
|
if args.dry_run:
|
|
print("\n DRY RUN -- would write {0} bytes in {1} pages".format(
|
|
len(image), (len(image) + PAGE_SIZE - 1) // PAGE_SIZE))
|
|
print(" No changes made.")
|
|
return
|
|
|
|
# Final confirmation
|
|
print(f"\nAbout to write {len(image)} bytes to EEPROM...")
|
|
print(" Press Ctrl+C within 3 seconds to abort.")
|
|
try:
|
|
for i in range(3, 0, -1):
|
|
print(f"\r Writing in {i}... ", end="", flush=True)
|
|
time.sleep(1)
|
|
print("\r Writing now... ")
|
|
except KeyboardInterrupt:
|
|
print("\n Aborted.")
|
|
return
|
|
|
|
# Write in page-sized chunks
|
|
total_pages = (len(image) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
write_errors = 0
|
|
|
|
for page_num in range(total_pages):
|
|
offset = page_num * PAGE_SIZE
|
|
end = min(offset + PAGE_SIZE, len(image))
|
|
chunk = image[offset:end]
|
|
|
|
pct = (page_num + 1) * 100 // total_pages
|
|
print(f"\r Write: 0x{offset:04X} / 0x{len(image):04X} "
|
|
f"[{pct:3d}%]", end="", flush=True)
|
|
|
|
try:
|
|
written = eeprom_write(dev, offset, chunk)
|
|
if written != len(chunk):
|
|
print(f"\n Short write at 0x{offset:04X}: "
|
|
f"sent {len(chunk)}, wrote {written}")
|
|
write_errors += 1
|
|
except usb.core.USBError as e:
|
|
print(f"\n Write error at 0x{offset:04X}: {e}")
|
|
write_errors += 1
|
|
|
|
# Wait for EEPROM internal write cycle
|
|
time.sleep(WRITE_CYCLE_MS / 1000.0)
|
|
|
|
print(f"\r Write: 0x{len(image):04X} / 0x{len(image):04X} "
|
|
f"[100%] ")
|
|
|
|
if write_errors:
|
|
print(f"\n WARNING: {write_errors} write error(s) occurred")
|
|
|
|
# Verify by reading back
|
|
print(f"\nVerifying ({len(image)} bytes)...")
|
|
verify = eeprom_read_all(dev, len(image), label="Verify")
|
|
if verify is None:
|
|
print(" Verify failed: read error")
|
|
print(" *** EEPROM STATE UNKNOWN -- check before power cycling ***")
|
|
sys.exit(1)
|
|
|
|
mismatches = []
|
|
for i in range(len(image)):
|
|
if i < len(verify) and image[i] != verify[i]:
|
|
mismatches.append(i)
|
|
|
|
if not mismatches:
|
|
print(f"\n VERIFIED -- all {len(image)} bytes match")
|
|
print(" Flash complete. Power cycle the device to boot new firmware.")
|
|
else:
|
|
print(f"\n VERIFY FAILED -- {len(mismatches)} byte(s) differ:")
|
|
for off in mismatches[:16]:
|
|
exp = image[off]
|
|
got = verify[off] if off < len(verify) else 0xFF
|
|
print(f" 0x{off:04X}: wrote 0x{exp:02X}, "
|
|
f"read 0x{got:02X}")
|
|
if len(mismatches) > 16:
|
|
print(f" ... and {len(mismatches) - 16} more")
|
|
print("\n *** EEPROM CONTENTS DO NOT MATCH IMAGE ***")
|
|
print(" Do NOT power cycle until this is resolved.")
|
|
sys.exit(1)
|
|
|
|
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")
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(
|
|
description="SkyWalker-1 EEPROM firmware flash tool")
|
|
sub = parser.add_subparsers(dest='command', required=True)
|
|
|
|
# info
|
|
p_info = sub.add_parser('info',
|
|
help='Parse and display C2 header from a .bin file')
|
|
p_info.add_argument('file', help='C2 firmware image (.bin)')
|
|
|
|
# backup
|
|
p_backup = sub.add_parser('backup',
|
|
help='Dump current EEPROM to a file')
|
|
p_backup.add_argument('-o', '--output', default='skywalker1_eeprom.bin',
|
|
help='Output file (default: skywalker1_eeprom.bin)')
|
|
p_backup.add_argument('--max-size', type=int, default=MAX_EEPROM_SIZE,
|
|
help=f'Bytes to read (default: {MAX_EEPROM_SIZE})')
|
|
|
|
# verify
|
|
p_verify = sub.add_parser('verify',
|
|
help='Compare .bin file against EEPROM')
|
|
p_verify.add_argument('file', help='C2 firmware image (.bin)')
|
|
|
|
# convert
|
|
p_convert = sub.add_parser('convert',
|
|
help='Convert Intel HEX (.ihx) to C2 EEPROM format')
|
|
p_convert.add_argument('file', help='Input firmware file (.ihx or .hex)')
|
|
p_convert.add_argument('-o', '--output', default=None,
|
|
help='Output C2 image (.bin). Default: <basename>_eeprom.bin')
|
|
p_convert.add_argument('--vid', default=None,
|
|
help=f'USB VID (default: 0x{VENDOR_ID:04X})')
|
|
p_convert.add_argument('--pid', default=None,
|
|
help=f'USB PID (default: 0x{PRODUCT_ID:04X})')
|
|
p_convert.add_argument('--did', default=None,
|
|
help='USB DID (default: 0x0000)')
|
|
p_convert.add_argument('--config', default=None,
|
|
help='C2 CONFIG byte (default: 0x40 = 400kHz I2C)')
|
|
|
|
# flash
|
|
p_flash = sub.add_parser('flash',
|
|
help='Write C2 firmware image to EEPROM')
|
|
p_flash.add_argument('file', help='C2 firmware image (.bin)')
|
|
p_flash.add_argument('--dry-run', action='store_true',
|
|
help='Show what would happen without writing')
|
|
p_flash.add_argument('--no-backup', action='store_true',
|
|
help='Skip pre-flash EEPROM backup')
|
|
p_flash.add_argument('--force', action='store_true',
|
|
help='Override VID/PID mismatch check')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Default output filename for convert
|
|
if args.command == 'convert' and args.output is None:
|
|
base = os.path.splitext(args.file)[0]
|
|
args.output = base + '_eeprom.bin'
|
|
|
|
if args.command == 'info':
|
|
cmd_info(args)
|
|
elif args.command == 'backup':
|
|
cmd_backup(args)
|
|
elif args.command == 'verify':
|
|
cmd_verify(args)
|
|
elif args.command == 'convert':
|
|
cmd_convert(args)
|
|
elif args.command == 'flash':
|
|
cmd_flash(args)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|