Add experimental I2C debugging and EEPROM analysis tools
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
This commit is contained in:
parent
97c1000d8b
commit
0d6facb321
16 changed files with 3279 additions and 0 deletions
259
tools/i2c_recovery_boot.py
Normal file
259
tools/i2c_recovery_boot.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue