#!/usr/bin/env python3 """Two-stage boot with I2C slave recovery. The FX2LP I2C controller enters a stuck state (I2CS=0xF6) when the CPU restarts after being halted mid-I2C-transaction. The stock firmware was interrupted by fw_load.py's CPUCS halt while an I2C transfer was in progress. The slave (BCM4500/BCM3440) is still driving SDA LOW, waiting for clock pulses to finish its byte. When CPUCS goes back to 0: 1. I2C pull-ups reconnect 2. Controller detects SDA LOW (slave still holding) 3. Controller enters permanent BERR state (0xF6) 4. All subsequent I2C operations fail Fix: two-stage boot process: Stage 1: Upload a tiny slave recovery stub, run it briefly to release the stuck slave (32 SCL pulses @ ~10us + STOP), then halt again. Stage 2: Upload the real firmware. The I2C bus is now clean, so when CPUCS goes to 0, the controller initializes correctly. """ import sys import time import usb.core import usb.util I2CS_ADDR = 0xE678 I2CTL_ADDR = 0xE67A CPUCS_ADDR = 0xE600 def fx2_read(dev, addr, n=1): return dev.ctrl_transfer(0xC0, 0xA0, addr, 0, n, timeout=1000) def fx2_write(dev, addr, data): dev.ctrl_transfer(0x40, 0xA0, addr, 0, data, timeout=1000) def i2cs_str(v): flags = [] for bit, name in [(7,'START'),(6,'STOP'),(5,'LASTRD'), (4,'ID1'),(3,'ID0'),(2,'BERR'),(1,'ACK'),(0,'DONE')]: if v & (1 << bit): flags.append(name) return f"0x{v:02X} ({' | '.join(flags) if flags else 'idle'})" def build_recovery_stub(): """Build an 8051 stub that holds BCM4500 in reset to release the I2C bus. The I2C controller (I2CS=0xF6 BERR) has exclusive control of PA0 (SDA) and PA1 (SCL). GPIO operations on those pins are OVERRIDDEN by the I2C engine — bit-bang SCL clocking has NO EFFECT on the actual bus. Strategy: assert BCM4500 RESET (PA5 LOW) and KEEP it asserted. The BCM4500's I2C interface goes high-impedance, releasing SDA. Then when the host halts and loads real firmware, CPUCS restart will find SDA=HIGH and the I2C controller will initialize cleanly (no BERR). The real firmware handles BCM_RESET de-assertion during its normal init. CRITICAL: Do NOT modify OEA bits 0-1 — the I2C controller owns those. Diagnostics in XDATA: 0x3C00: 0xAA = stub started 0x3C01: initial OEA 0x3C02: initial IOA 0x3C03: I2CS at boot (via XDATA 0xE678) 0x3C04: IOA after asserting BCM4500 reset (~50ms hold) 0x3C05: 0xDD = stub complete, looping with BCM4500 held in reset """ code = [] # 0x0000: LJMP main_start (to 0x0010) code += [0x02, 0x00, 0x10] # LJMP 0x0010 # 0x0003-0x000F: padding 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 # --- Capture initial OEA → 0x3C01 --- code += [0xE5, 0xB2] # MOV A, OEA code += [0x90, 0x3C, 0x01] # MOV DPTR, #0x3C01 code += [0xF0] # MOVX @DPTR, A # --- Capture initial IOA → 0x3C02 --- code += [0xE5, 0x80] # MOV A, IOA code += [0x90, 0x3C, 0x02] # MOV DPTR, #0x3C02 code += [0xF0] # MOVX @DPTR, A # --- Capture initial I2CS → 0x3C03 --- code += [0x90, 0xE6, 0x78] # MOV DPTR, #0xE678 code += [0xE0] # MOVX A, @DPTR code += [0x90, 0x3C, 0x03] # MOV DPTR, #0x3C03 code += [0xF0] # MOVX @DPTR, A # --- Assert BCM4500 RESET: PA5 LOW, make output --- # PA5 = IOA bit 5, bit address 0x85 (IOA base 0x80 + 5) # ORL OEA with only bit 5 — do NOT touch bits 0-1 (I2C engine) code += [0xC2, 0x85] # CLR PA5 (assert reset) code += [0x43, 0xB2, 0x20] # ORL OEA, #0x20 (PA5 = output) # Hold reset for ~50ms # R2=250, R1=240: 250*240*3 = 180,000 cycles = ~3.75ms # R0=15: 15 * 3.75ms = ~56ms code += [0x78, 15] # MOV R0, #15 reset_outer = len(code) code += [0x7A, 250] # MOV R2, #250 reset_mid = len(code) code += [0x79, 240] # MOV R1, #240 code += [0xD9, 0xFE] # DJNZ R1, $-2 djnz2_pc = len(code) + 2 code += [0xDA, (reset_mid - djnz2_pc) & 0xFF] # DJNZ R2, reset_mid djnz0_pc = len(code) + 2 code += [0xD8, (reset_outer - djnz0_pc) & 0xFF] # DJNZ R0, reset_outer # --- Capture IOA during reset → 0x3C04 --- code += [0xE5, 0x80] # MOV A, IOA code += [0x90, 0x3C, 0x04] # MOV DPTR, #0x3C04 code += [0xF0] # MOVX @DPTR, A # --- Marker: stub complete (0xDD → 0x3C05) --- # BCM4500 stays in reset! Do NOT de-assert. code += [0x74, 0xDD] # MOV A, #0xDD code += [0x90, 0x3C, 0x05] # MOV DPTR, #0x3C05 code += [0xF0] # MOVX @DPTR, A # Infinite loop — BCM4500 held in reset code += [0x80, 0xFE] # SJMP $ (loop forever) return bytes(code) def main(): dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) if not dev: print("SkyWalker-1 not found") sys.exit(1) print("Two-Stage Boot: I2C Slave Recovery (v2 — with markers)") print("=" * 55) # Stage 0: Initial state print(f"\n[0] Device found: Bus {dev.bus} Addr {dev.address}") # Stage 1: Halt CPU print("\n[1] Halting CPU...") fx2_write(dev, CPUCS_ADDR, bytes([0x01])) time.sleep(0.05) i2cs = fx2_read(dev, I2CS_ADDR, 1)[0] print(f" I2CS during halt = {i2cs_str(i2cs)}") # Clear XDATA diagnostic area first (so stale data doesn't confuse us) print(" Clearing XDATA 0x3C00-0x3C0F...") fx2_write(dev, 0x3C00, bytes([0x00] * 16)) # Stage 2: Upload stub print("\n[2] Uploading slave recovery stub...") stub = build_recovery_stub() print(f" Stub size: {len(stub)} bytes") fx2_write(dev, 0x0000, stub) # Verify upload by reading back first 16 bytes readback = fx2_read(dev, 0x0000, 16) match = all(readback[i] == stub[i] for i in range(16)) print(f" Upload verify (first 16): {'MATCH' if match else 'MISMATCH'}") if not match: print(f" Expected: {' '.join(f'{b:02X}' for b in stub[:16])}") print(f" Got: {' '.join(f'{b:02X}' for b in readback)}") # Stage 3: Run stub (~50ms reset hold + ~100ms settle + margin) print("\n[3] Running stub (500ms)...") fx2_write(dev, CPUCS_ADDR, bytes([0x00])) time.sleep(0.5) # Stage 4: Halt and read diagnostics print("\n[4] Halting CPU after recovery...") fx2_write(dev, CPUCS_ADDR, bytes([0x01])) time.sleep(0.05) # Read all diagnostic bytes diag = fx2_read(dev, 0x3C00, 8) i2cs_halt = fx2_read(dev, I2CS_ADDR, 1)[0] print(f"\n Raw XDATA 0x3C00-0x3C07:") print(f" {' '.join(f'{b:02X}' for b in diag)}") # Decode diagnostics marker_start = diag[0] # 0x3C00: expect 0xAA oea_init = diag[1] # 0x3C01: initial OEA ioa_init = diag[2] # 0x3C02: initial IOA i2cs_init = diag[3] # 0x3C03: initial I2CS ioa_during_rst = diag[4] # 0x3C04: IOA during BCM4500 reset marker_done = diag[5] # 0x3C05: expect 0xDD def bus_str(ioa): sda = 'H' if ioa & 1 else 'L' scl = 'H' if ioa & 2 else 'L' rst = 'H' if ioa & 0x20 else 'L' return f"SDA={sda} SCL={scl} RST={rst}" print(f"\n Markers:") print(f" Start (0xAA?): 0x{marker_start:02X} {'✓' if marker_start == 0xAA else '✗ STUB DID NOT EXECUTE'}") print(f" Done (0xDD?): 0x{marker_done:02X} {'✓' if marker_done == 0xDD else '✗'}") if marker_start != 0xAA: print(f"\n FATAL: Stub never started!") else: print(f"\n Initial state:") print(f" OEA = 0x{oea_init:02X}") print(f" IOA = 0x{ioa_init:02X} {bus_str(ioa_init)}") print(f" I2CS = {i2cs_str(i2cs_init)}") print(f"\n BCM4500 held in RESET (~50ms):") print(f" IOA = 0x{ioa_during_rst:02X} {bus_str(ioa_during_rst)}") sda_rst = 'H' if ioa_during_rst & 1 else 'L' if sda_rst == 'H': print(f" ✓ SDA HIGH with BCM4500 in reset") else: print(f" ✗ SDA still LOW — not the BCM4500 holding it") print(f"\n I2CS during halt: {i2cs_str(i2cs_halt)}") print(f" BCM4500 is held in RESET (PA5 driven LOW)") # Stage 5: Now load real firmware with BCM4500 still in reset # The real firmware's init sequence will de-assert BCM_RESET. # Since SDA is HIGH (slave in reset), the I2C controller should # initialize cleanly when CPUCS restarts for the real firmware. print(f"\n[5] BCM4500 held in reset. Loading real firmware...") print(f" The CPUCS restart for firmware load will re-init I2C controller.") print(f" With BCM4500 in reset (SDA=HIGH), I2CS should come up clean.") import subprocess import os fw_path = os.path.join(os.path.dirname(__file__), '..', 'firmware', 'build', 'skywalker1.ihx') fw_path = os.path.abspath(fw_path) fw_load = os.path.join(os.path.dirname(__file__), 'fw_load.py') if os.path.exists(fw_path): print(f"\n Running: python3 {fw_load} load {fw_path} --wait 5") result = subprocess.run( ['python3', fw_load, 'load', fw_path, '--wait', '5'], capture_output=True, text=True, timeout=30 ) print(result.stdout) if result.stderr: print(f" stderr: {result.stderr}") else: print(f"\n Firmware not found: {fw_path}") print(f" Build with: cd firmware && make") print(f" Then run: python3 tools/fw_load.py load {fw_path} --wait 5") if __name__ == '__main__': main()