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:
parent
bbdcb243dc
commit
3d2cd477b2
144
docs/EEPROM-RECOVERY.md
Normal file
144
docs/EEPROM-RECOVERY.md
Normal 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
|
||||
@ -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 |
|
||||
|----------|----------|
|
||||
|
||||
@ -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
@ -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)
|
||||
print('=== EEPROM Exploration ===')
|
||||
print()
|
||||
|
||||
# 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:
|
||||
print(' Result: All different → AT24C512+ (64KB+)')
|
||||
eeprom_size = 65536
|
||||
print()
|
||||
|
||||
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
|
||||
# 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()
|
||||
|
||||
vid = data[2] << 8 | data[1]
|
||||
pid = data[4] << 8 | data[3]
|
||||
did = data[6] << 8 | data[5]
|
||||
config = data[7]
|
||||
# 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()
|
||||
|
||||
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="")
|
||||
# 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],
|
||||
})
|
||||
|
||||
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)})")
|
||||
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:
|
||||
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()
|
||||
|
||||
# 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()
|
||||
|
||||
return {"vid": vid, "pid": pid, "did": did, "config": config}
|
||||
# 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')
|
||||
|
||||
|
||||
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()
|
||||
sw.close()
|
||||
print()
|
||||
print('=== Done ===')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
368
tools/fw_load.py
368
tools/fw_load.py
@ -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',
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user