One-off diagnostic scripts from experiments 0xD7-0xDB investigating the I2C BERR deadlock. Documents the systematic elimination of software-only recovery approaches: - i2c_host_test.py: Proved 0xA0 register writes cannot drive I2C bus - i2c_register_test.py: Tested I2C register writability from host - i2c_recovery_boot.py: Attempted I2C state machine recovery via boot - eeprom_flash_a0.py: Host-side EEPROM flash attempt (failed) - boot_ab_test.py / boot_test.py: EEPROM boot reliability testing - a8_autoclear_test.py: BCM4500 command register auto-clear behavior - addr_gateway_test.py: BCM3440 gateway address routing analysis - stock_fw_compare.py / stock_fw_test.py: Stock vs custom fw analysis
260 lines
9.7 KiB
Python
260 lines
9.7 KiB
Python
#!/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()
|