Add EEPROM boot firmware (exp 0xDB) and supporting tools

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).
This commit is contained in:
Ryan Malloy 2026-02-20 10:56:21 -07:00
parent bbdcb243dc
commit 3d2cd477b2
8 changed files with 5621 additions and 4440 deletions

144
docs/EEPROM-RECOVERY.md Normal file
View File

@ -0,0 +1,144 @@
# SkyWalker-1 EEPROM Recovery Guide
The device is soft-bricked: the FX2 boot ROM hangs trying to load
corrupted firmware from EEPROM, preventing USB enumeration.
## Symptoms
- Hub shows `0101 power connect []` (D+ pull-up active, no enumeration)
- dmesg: `device descriptor read/8, error -110` (timeout)
- Does not enumerate as bare FX2 (04B4:8613) either
- NanoVNA on same hub works fine (hub hardware is OK)
## Root Cause
The EEPROM (24C128 at I2C 0x51) likely has corrupted boot data. The
FX2LP boot ROM reads the EEPROM at power-up and hangs if the C2 image
has invalid load record lengths or addresses. The boot ROM occupies
the 8051 core, preventing USB control transfer processing.
## Recovery Options (pick one)
### Option A: SOIC Clip + External Programmer (Recommended)
Blank the first byte of the EEPROM so the boot ROM falls back to
bare FX2 enumeration. Then reload via USB.
**Hardware needed:**
- SOIC-8 test clip (Pomona 5250 or similar, ~$5)
- CH341A USB programmer (~$3) or Bus Pirate or any I2C-capable tool
- OR: Raspberry Pi / Arduino with I2C
**Steps:**
1. Power OFF the SkyWalker-1 (unplug USB)
2. Locate the 24C128 EEPROM on the PCB (SOIC-8 package near the FX2)
3. Clip the SOIC clip onto the EEPROM
4. Connect to your I2C programmer (SDA, SCL, VCC, GND)
5. Read and save the EEPROM contents (16KB backup!)
6. Write 0xFF to address 0x0000 (corrupts the C2 magic byte)
7. Remove clip, plug in SkyWalker-1
8. Device should enumerate as bare FX2 (04B4:8613)
9. Load custom firmware via `fw_load.py`
10. Use the custom firmware to write good C2 image back to EEPROM
**With CH341A:**
```bash
# Read backup
flashrom -p ch341a_spi -c "AT24C128" -r eeprom_backup.bin
# Or use i2c-tools if CH341A is in I2C mode:
# i2cdetect -l (find the CH341A bus)
# i2cdump -y <bus> 0x51 b > dump.txt
```
**With Raspberry Pi (I2C):**
```bash
# Enable I2C: raspi-config -> Interfaces -> I2C
# Connect EEPROM: SDA->GPIO2, SCL->GPIO3, VCC->3.3V, GND->GND
i2cdetect -y 1 # Should show 0x51
# Read first byte
i2cget -y 1 0x51 0x00
# Write 0xFF to byte 0 (corrupts C2 header)
i2cset -y 1 0x51 0x00 0xFF
```
### Option B: Hold SDA HIGH During Boot
Prevent the EEPROM from responding by holding SDA HIGH, forcing
the boot ROM to see "no EEPROM" and enumerate as bare FX2.
**Steps:**
1. Locate the SDA test point or EEPROM pin 5 (SDA)
2. Connect a 1kΩ pull-up to 3.3V on SDA
3. Power on the SkyWalker-1
4. If it enumerates as bare FX2 (04B4:8613), load firmware:
```bash
python3 tools/fw_load.py load firmware/build/skywalker1.ihx
```
5. Remove the pull-up
6. Use the loaded firmware to reprogram the EEPROM
**Note:** This only works if the SDA pull-up is strong enough to
override the EEPROM's SDA output. May need to experiment with
pull-up values (470Ω to 4.7kΩ).
### Option C: Desolder EEPROM Pin
Most reliable but requires soldering skill.
1. Lift EEPROM pin 5 (SDA) from the PCB pad
2. Power on → enumerates as bare FX2
3. Load firmware via USB
4. Resolder pin 5
5. Use firmware to reprogram EEPROM with good C2 image
### Option D: Wait + Watch (Long Shot)
If the boot ROM eventually times out on the I2C read, the device
will briefly enumerate as bare FX2. This might take several minutes.
```bash
# Watch for bare FX2 enumeration
sudo dmesg -w | grep -E "04b4|8613|New USB"
# In another terminal, keep power cycling every 5 minutes
while true; do
sudo uhubctl -l 1-5.4.4 -p 3 -a off
sleep 5
sudo uhubctl -l 1-5.4.4 -p 3 -a on
sleep 300 # wait 5 minutes
done
```
If it appears even briefly:
```bash
python3 tools/fw_load.py load firmware/build/skywalker1.ihx --force
```
## After Recovery
Once the device enumerates (as bare FX2 or with loaded firmware):
1. **Load custom firmware to RAM:**
```bash
python3 tools/fw_load.py load firmware/build/skywalker1.ihx
```
2. **Reprogram EEPROM with good C2 image:**
```bash
# The custom firmware needs EEPROM write support first
# (vendor command to relay I2C writes to EEPROM)
python3 tools/eeprom_write.py flash firmware/build/skywalker1_eeprom.bin
```
3. **Or restore stock firmware:**
If you have a backup of the original EEPROM contents, flash that
instead of the custom firmware.
## Prevention
- Never send `BOOT_8PSK (0x89)` with mode 0x84 ("firmware load")
unless you know what data the firmware expects
- Always backup EEPROM before experiments that touch vendor commands
- The stock firmware's I2C proxy (0x83/0x84) may have side effects
on the EEPROM that aren't documented

View File

@ -74,7 +74,8 @@ DVB-S2 is not supported. See [Section 14](#14-dvb-s2-incompatibility).
+--[ I2C EEPROM 0x51 ]
|
USB 2.0 HS | I2C Bus (400 kHz)
Host PC <----> [ CY7C68013A FX2LP ] <-----> [ BCM4500 Demod 0x08 ]
Host PC <----> [ CY7C68013A FX2LP ] <--I2C--> [ BCM3440 Tuner 0x10 ] <--gateway--> [ BCM4500 Demod ]
<--I2C--> [ BCM4500 Direct 0x08 (status only) ]
| 8051 @ 48 MHz | |
| GPIF Engine |<-----------+ 8-bit parallel TS
| EP2 Bulk IN |
@ -364,24 +365,39 @@ Writing 0x01 to CPUCS halts the CPU. New code is written to RAM. Writing 0x00 re
## 6. BCM4500 Demodulator Interface
### 6.1 I2C Addressing
### 6.1 I2C Addressing — BCM3440 Tuner Gateway
> **CRITICAL (2025-02-19):** The BCM4500's registers are accessed THROUGH the
> BCM3440 tuner's I2C gateway at address 0x10, NOT directly at 0x08.
> Stock firmware v2.06 disassembly of FUN_CODE_0DDD, FUN_CODE_10F2, and all
> internal register access functions confirms device address 0x10 is used
> for every register read/write.
| Parameter | Value |
|-----------|-------|
| 7-bit I2C address | 0x08 |
| 8-bit write address | 0x10 |
| 8-bit read address | 0x11 |
| BCM3440 tuner gateway (7-bit) | **0x10** — all BCM4500 register access |
| BCM3440 wire write / read | 0x20 / 0x21 |
| BCM4500 direct (7-bit) | 0x08 — status byte only, no register addressing |
| BCM4500 wire write / read | 0x10 / 0x11 |
| Bus speed | 400 kHz |
| FX2 I2C controller SFRs | I2CS, I2DAT, I2CTL |
| Alternate probe addresses (v2.13) | 0x3F, 0x7F |
The custom firmware and kernel driver use the 7-bit address 0x08. The stock firmware writes `addr << 1` = 0x10 for write and `(addr << 1) | 1` = 0x11 for read, which is the standard I2C convention for 7-bit address 0x08.
The BCM3440 tuner acts as an I2C bridge/gateway: register accesses in the 0xA0+ range sent to the tuner's address (0x10) are transparently forwarded to the BCM4500 demodulator. The BCM4500's own I2C address (0x08) only exposes a single status byte via simple reads — it does NOT support register-addressed reads at that address.
The v2.13 firmware probes addresses 0x7F and 0x3F at startup (INT0 handler) to detect which demodulator variant is present. These may be alternative I2C address configurations or addresses for different demodulator sub-systems.
**Stock firmware evidence (functions at wire address 0x20/0x21):**
- `FUN_CODE_0DDD` (init blocks): writes A6/A7/A8 via `LCALL 0x1A81` with R7=0x10
- `FUN_CODE_10F2` (PLL/firmware download): writes A9/AA/AB via `LCALL 0x1A81` with R7=0x10
- `FUN_CODE_15E9` (config mode): writes A0 via device 0x10
- `FUN_CODE_1556` (generic read): combined read `[S][0x20][reg][Sr][0x21][data][P]`
**Stock I2C_READ (0x84) vendor command for BCM4500 (address 0x08):** Simple read only — `[S][0x11][data][P]` — no register address sent. Returns whatever the BCM4500's I2C slave has ready (global status byte). Register address from wIndex is completely ignored for device 0x08 (confirmed by disassembly of `FUN_CODE_2036`).
The v2.13 firmware probes addresses 0x7F and 0x3F at startup (INT0 handler) to detect which demodulator variant is present.
### 6.2 Direct Registers
Accessed via standard I2C write/read to the BCM4500's device address:
Accessed via I2C write/read through the BCM3440 gateway (address 0x10):
| Register | Function |
|----------|----------|

View File

@ -9,4 +9,8 @@ CODE_SIZE=--code-size 0x3c00
include $(FX2LIBDIR)lib/fx2.mk
load: $(BUILDDIR)/$(BASENAME).bix
../tools/fw_load.py $(BUILDDIR)/$(BASENAME).bix
../tools/fw_load.py load $(BUILDDIR)/$(BASENAME).ihx
eeprom: $(BUILDDIR)/$(BASENAME).ihx
python3 ../tools/eeprom_write.py convert $(BUILDDIR)/$(BASENAME).ihx \
-o $(BUILDDIR)/$(BASENAME)_eeprom.bin

File diff suppressed because it is too large Load Diff

View File

@ -1,251 +1,157 @@
#!/usr/bin/env python3
"""
Genpix SkyWalker-1 EEPROM firmware dump tool.
Genpix SkyWalker-1 EEPROM exploration tool.
Reads the Cypress FX2 boot EEPROM via the I2C_READ vendor command.
Protocol: I2C_READ (0x84), wValue=0x51, wIndex=offset, length=chunk_size
Reads the calibration EEPROM (AT24Cxxx at I2C addr 0x51) via the custom
firmware's EEPROM_READ (0xC0) vendor command. This uses 16-bit addressing
directly, bypassing the stock firmware's single-byte I2C_READ protocol.
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)
Primary purpose: Find where PLL configuration data is stored so the
bcm4500_load_pll_config() function reads from the correct address.
"""
import usb.core, usb.util, sys, struct
import sys
sys.path.insert(0, 'tools')
from skywalker_lib import SkyWalker1
VENDOR_ID = 0x09C0
PRODUCT_ID = 0x0203
I2C_READ = 0x84
EEPROM_SLAVE = 0x51
CMD_EEPROM_READ = 0xC0
sw = SkyWalker1()
sw.open()
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 eeprom_read(addr, length):
"""Read bytes from EEPROM at 16-bit address.
wValue = address, wIndex = length."""
return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length)
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 hex_dump(addr, data):
"""Print hex dump with ASCII sidebar."""
for i in range(0, len(data), 16):
chunk = data[i:i + 16]
hex_str = ' '.join(f'{b:02X}' for b in chunk)
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
print(f' {addr + i:04X}: {hex_str:<48s} |{ascii_str}|')
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('=== EEPROM Exploration ===')
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
# Step 1: Determine EEPROM size by aliasing detection
print('--- Size Detection ---')
data_0000 = eeprom_read(0x0000, 16)
data_4000 = eeprom_read(0x4000, 16)
data_8000 = eeprom_read(0x8000, 16)
print(f' 0x0000: {data_0000.hex(" ")}')
print(f' 0x4000: {data_4000.hex(" ")}')
print(f' 0x8000: {data_8000.hex(" ")}')
if data_0000 == data_4000:
print(' Result: 0x4000 ALIASES to 0x0000 → AT24C128 (16KB)')
eeprom_size = 16384
elif data_0000 == data_8000:
print(' Result: 0x8000 aliases to 0x0000 → AT24C256 (32KB)')
eeprom_size = 32768
else:
consecutive_ff = 0
print(' Result: All different → AT24C512+ (64KB+)')
eeprom_size = 65536
print()
if offset % 1024 == 0:
print(f"\r 0x{offset:04X} / 0x{args.max_size:04X} ", end="", flush=True)
# Step 2: Dump first 512 bytes (FX2 boot firmware header + data)
print('--- EEPROM 0x0000-0x01FF (C2 boot header region) ---')
for addr in range(0x0000, 0x0200, 64):
data = eeprom_read(addr, 64)
hex_dump(addr, data)
print()
print(f"\r Read {len(eeprom)} bytes total ")
# Step 3: Scan for PLL-like 20-byte blocks
# Format: [count(1-16), A9_val, AA_val, unused_byte, AB_data[count], padding...]
# Sentinel: count=0
print('--- Scanning for PLL config blocks ---')
print(' Format: [count, A9, AA, unused, AB_data[count]]')
print(' Sentinel: count=0')
print()
# Save raw EEPROM
with open(args.output, 'wb') as f:
f.write(eeprom)
print(f" Saved raw EEPROM to: {args.output}")
# Scan the entire EEPROM in 20-byte strides
pll_candidates = []
for addr in range(0, min(eeprom_size, 0x4000), 20):
data = eeprom_read(addr, 20)
count = data[0]
# Look for potential sentinel (count=0) preceded by valid blocks
if count == 0 and addr > 0:
# Check if previous 20 bytes looked like PLL data
prev = eeprom_read(addr - 20, 20)
if 1 <= prev[0] <= 16:
pll_candidates.append({
'sentinel_addr': addr,
'last_block_addr': addr - 20,
'last_count': prev[0],
'last_a9': prev[1],
'last_aa': prev[2],
})
# 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}")
if pll_candidates:
print(' Found sentinel(s):')
for c in pll_candidates:
print(f' Sentinel at 0x{c["sentinel_addr"]:04X}')
print(f' Last block at 0x{c["last_block_addr"]:04X}: '
f'count={c["last_count"]} A9=0x{c["last_a9"]:02X} AA=0x{c["last_aa"]:02X}')
# Walk backwards to find start of PLL data
start = c['last_block_addr']
while start >= 20:
prev = eeprom_read(start - 20, 20)
if 1 <= prev[0] <= 16:
start -= 20
else:
print(f" [{i}] INVALID (raw_len=0x{rec['raw_len']:04X}) "
f"at EEPROM offset 0x{rec['offset']:04X}")
break
print(f' PLL data likely starts at: 0x{start:04X}')
# Dump the PLL blocks
print(f' PLL block dump:')
for baddr in range(start, c['sentinel_addr'] + 20, 20):
block = eeprom_read(baddr, 20)
cnt = block[0]
if cnt == 0:
print(f' 0x{baddr:04X}: [sentinel count=0]')
break
ab = block[4:4 + cnt]
print(f' 0x{baddr:04X}: count={cnt} A9=0x{block[1]:02X} '
f'AA=0x{block[2]:02X} unused=0x{block[3]:02X} '
f'AB=[{ab.hex(" ")}]')
else:
print(' No PLL sentinel found in first 16KB!')
print(' Dumping any 20-byte-aligned blocks with count 1-16:')
for addr in range(0, min(eeprom_size, 0x1000), 20):
data = eeprom_read(addr, 20)
count = data[0]
if 1 <= count <= 16:
ab = data[4:4 + count]
print(f' 0x{addr:04X}: count={count} A9=0x{data[1]:02X} '
f'AA=0x{data[2]:02X} unused=0x{data[3]:02X} '
f'AB=[{ab.hex(" ")}]')
print()
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)")
# Step 4: Dump around the 16KB boundary (where our code expects PLL data)
if eeprom_size > 16384:
print('--- EEPROM 0x3FE0-0x4060 (16KB boundary) ---')
for addr in range(0x3FE0, 0x4060, 64):
data = eeprom_read(addr, 64)
hex_dump(addr, data)
print()
# 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
# Step 5: Check for 0xFF regions (empty/erased)
print('--- Empty region scan ---')
last_was_ff = False
for addr in range(0, min(eeprom_size, 0x4000), 64):
data = eeprom_read(addr, 64)
is_ff = all(b == 0xFF for b in data)
if is_ff and not last_was_ff:
print(f' 0xFF starts at 0x{addr:04X}')
last_was_ff = True
elif not is_ff and last_was_ff:
print(f' Data resumes at 0x{addr:04X}')
last_was_ff = False
if last_was_ff:
print(f' 0xFF continues to end of scanned region')
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()
sw.close()
print()
print('=== Done ===')

View File

@ -33,6 +33,132 @@ 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:
@ -353,6 +479,72 @@ def cmd_verify(args):
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):
@ -548,6 +740,21 @@ def main():
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')
@ -561,12 +768,19 @@ def main():
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)

View File

@ -316,6 +316,312 @@ def write_segments(dev, segments, verbose=False):
return total
# -- I2C controller cleanup --
# FX2LP I2C controller XDATA registers (accessible via vendor request 0xA0)
I2CS_ADDR = 0xE678 # I2C Control/Status
I2DAT_ADDR = 0xE679 # I2C Data
I2CTL_ADDR = 0xE67A # I2C Control
# I2CS bit definitions
I2CS_START = 0x80
I2CS_STOP = 0x40
I2CS_LASTRD = 0x20
I2CS_ID1 = 0x10
I2CS_ID0 = 0x08
I2CS_BERR = 0x04
I2CS_ACK = 0x02
I2CS_DONE = 0x01
def i2cs_decode(val):
"""Decode I2CS register value into human-readable string."""
flags = []
for bit, name in [(7, 'START'), (6, 'STOP'), (5, 'LASTRD'),
(4, 'ID1'), (3, 'ID0'), (2, 'BERR'),
(1, 'ACK'), (0, 'DONE')]:
if val & (1 << bit):
flags.append(name)
id_val = (val >> 3) & 0x03
state = {0: 'idle', 1: 'data', 2: 'addr-wait', 3: 'busy'}[id_val]
return f"0x{val:02X} ({' | '.join(flags) if flags else 'idle'}) [state={state}]"
def build_i2c_cleanup_stub():
"""Build a tiny 8051 stub that terminates any stuck I2C transaction.
USB vendor request 0xA0 can READ I2C registers but WRITES are ignored
by the I2C controller during CPU halt (the peripheral only recognizes
8051-initiated XDATA writes). So we need the 8051 itself to do the
cleanup.
The stub:
1. Reads I2CS
2. If BERR: clears it
3. If mid-transaction (ID bits): reads I2DAT, sends STOP
4. If residual flags: sends STOP
5. Loops up to 10 times
6. Stores diagnostics at XDATA 0x3C00-0x3C07
7. Enters infinite loop (host halts CPU after reading diagnostics)
Diagnostics layout:
0x3C00: 0xAA = stub started
0x3C01: I2CS at entry (before any cleanup)
0x3C02: I2CS after cleanup (should be 0x00)
0x3C03: iteration count (how many loops needed)
0x3C04: I2DAT value read during cleanup
0x3C05: 0xDD = stub completed successfully
"""
# 8051 machine code — hand-assembled for clarity
code = []
# 0x0000: LJMP 0x0010 (skip interrupt vector area)
code += [0x02, 0x00, 0x10]
# Pad to 0x0010
while len(code) < 0x10:
code += [0x00]
# ========== 0x0010: Main code ==========
# --- Marker: stub started (0xAA → 0x3C00) ---
code += [0x74, 0xAA] # MOV A, #0xAA
code += [0x90, 0x3C, 0x00] # MOV DPTR, #0x3C00
code += [0xF0] # MOVX @DPTR, A
# --- Read initial I2CS → 0x3C01 ---
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 (I2CS)
code += [0xE0] # MOVX A, @DPTR
code += [0x90, 0x3C, 0x01] # MOV DPTR, #0x3C01
code += [0xF0] # MOVX @DPTR, A
# --- Init loop counter R0=0 ---
code += [0x78, 0x00] # MOV R0, #0
# ========== CLEANUP LOOP ==========
loop_top = len(code)
# Increment loop counter
code += [0x08] # INC R0
# Read I2CS
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 (I2CS)
code += [0xE0] # MOVX A, @DPTR (A = I2CS)
# If I2CS == 0x00 (idle), jump to done
code += [0x60] # JZ done (offset filled later)
jz_done_pc = len(code)
code += [0x00] # placeholder
# Check BERR (bit 2): if set, clear it
code += [0x30, 0xE2] # JNB ACC.2, skip_berr
jnb_berr_pc = len(code)
code += [0x00] # placeholder
# Clear BERR: write 0x04 to I2CS
code += [0x74, 0x04] # MOV A, #0x04
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678
code += [0xF0] # MOVX @DPTR, A
# Small delay
code += [0x79, 100] # MOV R1, #100
code += [0xD9, 0xFE] # DJNZ R1, $-2
# Jump to loop check
code += [0x80] # SJMP loop_check
sjmp_check1_pc = len(code)
code += [0x00] # placeholder
skip_berr = len(code)
# Check ID bits (bits 3,4): if either set, flush I2DAT + STOP
code += [0x54, 0x18] # ANL A, #0x18 (mask ID bits)
code += [0x60] # JZ skip_id
jz_skip_id_pc = len(code)
code += [0x00] # placeholder
# Read I2DAT (flush pending data)
code += [0x90, 0xE6, 0x79] # MOV DPTR, #0xE679 (I2DAT)
code += [0xE0] # MOVX A, @DPTR
code += [0x90, 0x3C, 0x04] # MOV DPTR, #0x3C04
code += [0xF0] # MOVX @DPTR, A (save I2DAT)
# Small delay
code += [0x79, 100] # MOV R1, #100
code += [0xD9, 0xFE] # DJNZ R1, $-2
# Send STOP: write 0x40 to I2CS
code += [0x74, 0x40] # MOV A, #0x40
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678
code += [0xF0] # MOVX @DPTR, A
# Longer delay for STOP to complete
code += [0x7A, 10] # MOV R2, #10
code += [0x79, 250] # MOV R1, #250
code += [0xD9, 0xFE] # DJNZ R1, $-2
code += [0xDA, 0xFC] # DJNZ R2, $-4
# Jump to loop check
code += [0x80] # SJMP loop_check
sjmp_check2_pc = len(code)
code += [0x00] # placeholder
skip_id = len(code)
# Residual flags — send STOP anyway
code += [0x74, 0x40] # MOV A, #0x40
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678
code += [0xF0] # MOVX @DPTR, A
code += [0x7A, 10] # MOV R2, #10
code += [0x79, 250] # MOV R1, #250
code += [0xD9, 0xFE] # DJNZ R1, $-2
code += [0xDA, 0xFC] # DJNZ R2, $-4
loop_check = len(code)
# Loop up to 10 times
code += [0xB8, 10] # CJNE R0, #10, loop_top
cjne_pc = len(code)
code += [(loop_top - (cjne_pc + 1)) & 0xFF]
# ========== DONE ==========
done = len(code)
# Store final I2CS → 0x3C02
code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678
code += [0xE0] # MOVX A, @DPTR
code += [0x90, 0x3C, 0x02] # MOV DPTR, #0x3C02
code += [0xF0] # MOVX @DPTR, A
# Store iteration count → 0x3C03
code += [0xE8] # MOV A, R0
code += [0x90, 0x3C, 0x03] # MOV DPTR, #0x3C03
code += [0xF0] # MOVX @DPTR, A
# Marker: stub done (0xDD → 0x3C05)
code += [0x74, 0xDD] # MOV A, #0xDD
code += [0x90, 0x3C, 0x05] # MOV DPTR, #0x3C05
code += [0xF0] # MOVX @DPTR, A
# Infinite loop
code += [0x80, 0xFE] # SJMP $ (loop forever)
# ========== Patch jump offsets ==========
# All relative jump offsets: target - (offset_byte_position + 1)
# because 8051 PC points to the NEXT instruction when the branch executes.
code[jz_done_pc] = (done - (jz_done_pc + 1)) & 0xFF
code[jnb_berr_pc] = (skip_berr - (jnb_berr_pc + 1)) & 0xFF
code[sjmp_check1_pc] = (loop_check - (sjmp_check1_pc + 1)) & 0xFF
code[jz_skip_id_pc] = (skip_id - (jz_skip_id_pc + 1)) & 0xFF
code[sjmp_check2_pc] = (loop_check - (sjmp_check2_pc + 1)) & 0xFF
return bytes(code)
def i2c_cleanup(dev):
"""Attempt host-side I2C controller recovery after CPU halt.
After halting the stock firmware mid-I2C-transaction, I2CS reads 0x1A
(mid-transaction, no BERR). BERR (0xF6) only appears on CPU restart.
Strategy: write STOP to I2CS from the host via 0xA0 vendor request
BEFORE restarting the CPU. If the I2C controller accepts host writes,
the pending transaction ends cleanly and our firmware gets a working
I2C controller.
Even if the controller doesn't process STOP while halted, latching the
bit in the register may cause the hardware to execute it on CPU restart,
preventing BERR from being set.
"""
print(f"\n I2C controller recovery (host-side):")
# 1. Read initial state
i2cs = fx2_ram_read(dev, I2CS_ADDR, 1)
if not i2cs:
print(f" I2CS read failed — skipping recovery")
return
i2cs_val = i2cs[0]
print(f" I2CS initial: {i2cs_decode(i2cs_val)}")
if i2cs_val == 0x00:
print(f" I2C controller idle — no recovery needed")
return
# Also read I2CTL for diagnostics
i2ctl = fx2_ram_read(dev, I2CTL_ADDR, 1)
if i2ctl:
speed = "400kHz" if i2ctl[0] & 0x01 else "100kHz"
print(f" I2CTL: 0x{i2ctl[0]:02X} ({speed})")
berr_set = bool(i2cs_val & I2CS_BERR)
if berr_set:
print(f" BERR already set at halt time — unusual")
# Try clearing BERR (write bit 2)
print(f" Writing 0x04 to I2CS (BERR clear)...")
fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_BERR]))
time.sleep(0.010)
i2cs2 = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs2:
print(f" I2CS after: {i2cs_decode(i2cs2[0])}")
return
# 2. Mid-transaction — attempt recovery
id_bits = (i2cs_val >> 3) & 0x03
print(f" Transaction state: ID={id_bits} ({'idle' if id_bits == 0 else 'active'})")
# Strategy A: Read I2DAT to flush pending byte, then STOP
print(f" [A] Flushing I2DAT + STOP...")
i2dat = fx2_ram_read(dev, I2DAT_ADDR, 1)
if i2dat:
print(f" I2DAT read: 0x{i2dat[0]:02X}")
time.sleep(0.005)
fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_STOP]))
time.sleep(0.010)
i2cs_a = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs_a:
print(f" I2CS after: {i2cs_decode(i2cs_a[0])}")
if i2cs_a[0] == 0x00 or (i2cs_a[0] & I2CS_DONE):
print(f" ✓ Recovery may have worked!")
return
# Strategy B: LASTRD + STOP (end read transaction cleanly)
print(f" [B] LASTRD + STOP...")
fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_LASTRD | I2CS_STOP]))
time.sleep(0.010)
i2cs_b = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs_b:
print(f" I2CS after: {i2cs_decode(i2cs_b[0])}")
if i2cs_b[0] == 0x00 or (i2cs_b[0] & I2CS_DONE):
print(f" ✓ Recovery may have worked!")
return
# Strategy C: Just STOP again (in case controller needed time)
print(f" [C] Retry STOP...")
fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_STOP]))
time.sleep(0.020)
i2cs_c = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs_c:
print(f" I2CS after: {i2cs_decode(i2cs_c[0])}")
# If BERR appeared during recovery attempts, try to clear it
if i2cs_c and (i2cs_c[0] & I2CS_BERR):
print(f" [D] BERR appeared — attempting clear...")
fx2_ram_write(dev, I2CS_ADDR, bytes([I2CS_BERR]))
time.sleep(0.010)
i2cs_d = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs_d:
print(f" I2CS after: {i2cs_decode(i2cs_d[0])}")
# Final state
time.sleep(0.010)
i2cs_final = fx2_ram_read(dev, I2CS_ADDR, 1)
if i2cs_final:
print(f" I2CS final: {i2cs_decode(i2cs_final[0])}")
if i2cs_final[0] == 0x00:
print(f" ✓ I2C controller recovered!")
elif not (i2cs_final[0] & I2CS_BERR):
print(f" ~ I2C controller not idle but no BERR — STOP may be latched")
else:
print(f" ✗ BERR persists — host-side recovery did not work")
# -- Subcommand handlers --
def cmd_load(args):
@ -372,9 +678,56 @@ def cmd_load(args):
try:
# Step 1: Halt CPU
if not args.no_reset:
if args.settle_delay > 0:
print(f"\n Settle delay: waiting {args.settle_delay}s for stock "
f"firmware I2C to finish...")
time.sleep(args.settle_delay)
print(f" Settle complete.")
# Pre-halt I2C flush: send vendor requests to the stock firmware
# that trigger I2C operations. After the firmware completes the
# I2C transaction (including STOP), the controller should be idle.
# We then immediately halt before any new I2C operation starts.
#
# Try multiple approaches — the stock firmware may support
# different subsets of the gp8psk vendor request protocol.
print("\n Pre-halt I2C flush...")
i2c_flushed = False
# Approach 1: GET_SIGNAL_LOCK (0x90) — reads BCM4500 via I2C
try:
lock_data = dev.ctrl_transfer(
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
0x90, 0, 0, 1, 2000)
print(f" 0x90 GET_SIGNAL_LOCK: 0x{lock_data[0]:02X} (I2C to BCM4500)")
i2c_flushed = True
except usb.core.USBError:
print(f" 0x90 not supported")
# Approach 2: I2C_READ (0x84) with shifted address
if not i2c_flushed:
try:
eeprom = dev.ctrl_transfer(
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
0x84, 0xA2, 0, 1, 2000) # addr<<1 per gp8psk
print(f" 0x84 I2C_READ: 0x{eeprom[0]:02X} (EEPROM)")
i2c_flushed = True
except usb.core.USBError:
print(f" 0x84 not supported")
# Approach 3: GET_8PSK_CONFIG (0x80) — may trigger I2C indirectly
if not i2c_flushed:
try:
cfg = dev.ctrl_transfer(
usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN,
0x80, 0, 0, 1, 2000)
print(f" 0x80 GET_8PSK_CONFIG: 0x{cfg[0]:02X}")
i2c_flushed = True
except usb.core.USBError:
print(f" 0x80 not supported")
if not i2c_flushed:
print(f" No stock vendor requests succeeded — halt may catch mid-I2C")
print("\n[1/3] Halting CPU (CPUCS = 0x01)...")
cpu_halt(dev)
time.sleep(0.05)
time.sleep(0.01) # minimal delay — I2C should be idle from flush
# Verify halt
readback = fx2_ram_read(dev, CPUCS_ADDR, 1)
@ -387,7 +740,15 @@ def cmd_load(args):
else:
print("\n[1/3] Skipping CPU reset (--no-reset)")
# Step 2: Load segments
# Step 1.5: I2C controller cleanup (two-stage boot)
# The stock firmware's I2C polling is almost certainly interrupted
# by our CPUCS halt. The I2C controller runs independently — it
# enters a stuck state that causes BERR (I2CS=0xF6) on CPU restart.
# Must run BEFORE firmware load since the cleanup stub uses 0x0000.
if not args.no_reset:
i2c_cleanup(dev)
# Step 2: Load segments (CPU is halted from cleanup or step 1)
step = "2/3" if not args.no_reset else "2/2"
print(f"\n[{step}] Loading {len(segments)} segment(s) into RAM...")
written = write_segments(dev, segments, verbose=args.verbose)
@ -563,6 +924,9 @@ Power-cycle the device to restore the factory-programmed firmware.
help="Show detailed transfer progress")
p_load.add_argument('--force', action='store_true',
help="Allow loading to unknown VID/PID devices")
p_load.add_argument('--settle-delay', type=float, default=0, metavar='SECONDS',
help="Wait N seconds before halting CPU (lets stock firmware "
"finish I2C init — may avoid I2C BERR on restart)")
# reset
p_reset = sub.add_parser('reset',

View File

@ -377,8 +377,12 @@ class SkyWalker1:
# -- Power and boot --
def boot(self, on: bool = True) -> int:
"""Power on/off the 8PSK demodulator."""
data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=1)
"""Power on/off the 8PSK demodulator.
Custom firmware returns 3 bytes: [config_status, boot_stage, debug].
Stock firmware returns 1 byte. Request 3 to handle both.
"""
data = self._vendor_in(CMD_BOOT_8PSK, value=int(on), length=3)
return data[0]
def start_intersil(self, on: bool = True) -> int: