From 0d6facb321d5f6e21b5ecda02951ebc9889a4360 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 20 Feb 2026 10:57:10 -0700 Subject: [PATCH] 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 --- tools/a8_autoclear_test.py | 95 +++++ tools/addr_gateway_test.py | 128 ++++++ tools/boot_ab_test.py | 194 +++++++++ tools/boot_deep_verify.py | 154 +++++++ tools/boot_reg_probe.py | 189 ++++++++ tools/boot_test.py | 177 ++++++++ tools/eeprom_deep_scan.py | 112 +++++ tools/eeprom_flash_a0.py | 736 ++++++++++++++++++++++++++++++++ tools/eeprom_pll_find.py | 108 +++++ tools/eeprom_sentinel_scan.py | 77 ++++ tools/i2c_host_test.py | 196 +++++++++ tools/i2c_recovery_boot.py | 259 +++++++++++ tools/i2c_register_test.py | 126 ++++++ tools/indirect_loopback_test.py | 176 ++++++++ tools/stock_fw_compare.py | 210 +++++++++ tools/stock_fw_test.py | 342 +++++++++++++++ 16 files changed, 3279 insertions(+) create mode 100644 tools/a8_autoclear_test.py create mode 100644 tools/addr_gateway_test.py create mode 100644 tools/boot_ab_test.py create mode 100644 tools/boot_deep_verify.py create mode 100644 tools/boot_reg_probe.py create mode 100644 tools/boot_test.py create mode 100644 tools/eeprom_deep_scan.py create mode 100644 tools/eeprom_flash_a0.py create mode 100644 tools/eeprom_pll_find.py create mode 100644 tools/eeprom_sentinel_scan.py create mode 100644 tools/i2c_host_test.py create mode 100644 tools/i2c_recovery_boot.py create mode 100644 tools/i2c_register_test.py create mode 100644 tools/indirect_loopback_test.py create mode 100644 tools/stock_fw_compare.py create mode 100644 tools/stock_fw_test.py diff --git a/tools/a8_autoclear_test.py b/tools/a8_autoclear_test.py new file mode 100644 index 0000000..0ed0ad0 --- /dev/null +++ b/tools/a8_autoclear_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Test whether BCM4500 register A8 auto-clears after write commands. + +If A8 auto-clears to 0x00 (or 0x02) after writing 0x03, then +bcm_poll_ready() would always return TRUE, masking the fact that +the DSP isn't processing commands. + +Uses enhanced 0xB6 diagnostic: + wValue = page, wIndex = A8 command (0 = default READ) + Returns 8 bytes: + [0] write_A6_ok + [1] A6 readback + [2] write_A8_ok + [3] A8 IMMEDIATE readback (no delay) + [4] A8 after 2ms delay + [5] A7 data result + [6] A6 final state + [7] echo: command sent +""" +import sys +import time +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_I2C_DIAG = 0xB6 +CMD_I2C_RAW_READ = 0xB5 +BCM4500_ADDR = 0x08 + +sw = SkyWalker1() +sw.open() +print('=== A8 Auto-Clear Test ===') +print(f'Firmware: {sw.get_fw_version()}') + +# Boot with full sequence +print('\n--- Booting BCM4500 (full boot) ---') +result = sw._vendor_in(0x89, value=1, index=0, length=3) +cfg, stage = result[0], result[1] +print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}') + +# Read A8 default state before any commands +print('\n--- A8 default state (after boot, before diagnostic) ---') +a8_default = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xA8, length=1) +print(f' A8 default: 0x{a8_default[0]:02X} (bit0={a8_default[0] & 0x01})') + +# Test 1: READ command (0x01) — this was the previous behavior +print('\n=== Test 1: A8 with READ command (0x01) ===') +diag1 = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8) +print(f' A6 write ok: {diag1[0]}') +print(f' A6 readback: 0x{diag1[1]:02X}') +print(f' A8 write ok: {diag1[2]}') +print(f' A8 IMMEDIATE: 0x{diag1[3]:02X} (bit0={diag1[3] & 0x01})') +print(f' A8 after 2ms: 0x{diag1[4]:02X} (bit0={diag1[4] & 0x01})') +print(f' A7 data: 0x{diag1[5]:02X}') +print(f' A6 final: 0x{diag1[6]:02X}') +print(f' Command sent: 0x{diag1[7]:02X}') + +# Reset A8 back to default by reading +time.sleep(0.01) + +# Test 2: WRITE command (0x03) — the one used by init blocks +print('\n=== Test 2: A8 with WRITE command (0x03) ===') +diag2 = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x03, length=8) +print(f' A6 write ok: {diag2[0]}') +print(f' A6 readback: 0x{diag2[1]:02X}') +print(f' A8 write ok: {diag2[2]}') +print(f' A8 IMMEDIATE: 0x{diag2[3]:02X} (bit0={diag2[3] & 0x01})') +print(f' A8 after 2ms: 0x{diag2[4]:02X} (bit0={diag2[4] & 0x01})') +print(f' A7 data: 0x{diag2[5]:02X}') +print(f' A6 final: 0x{diag2[6]:02X}') +print(f' Command sent: 0x{diag2[7]:02X}') + +# Test 3: Try other A8 values +print('\n=== Test 3: Various A8 values ===') +for cmd in [0x00, 0x02, 0x04, 0xFF]: + time.sleep(0.01) + diag = sw._vendor_in(CMD_I2C_DIAG, value=0x00, index=cmd, length=8) + print(f' CMD=0x{cmd:02X}: A8_imm=0x{diag[3]:02X} A8_2ms=0x{diag[4]:02X} ' + f'A7=0x{diag[5]:02X}') + +# Analysis +print('\n=== Analysis ===') +if diag2[3] != 0x03 or diag2[4] != 0x03: + print(f' !! A8 AUTO-CLEARS after WRITE command!') + print(f' Wrote 0x03, immediate={diag2[3]:02X}, after_2ms={diag2[4]:02X}') + if (diag2[3] & 0x01) == 0 or (diag2[4] & 0x01) == 0: + print(f' bcm_poll_ready() sees bit0=0 → always returns TRUE') + print(f' Init blocks appear to succeed but DSP never processes them!') +else: + print(f' A8 retains WRITE command (0x03). No auto-clear.') + if (diag1[3] == 0x01) and (diag2[3] == 0x03): + print(f' Both READ and WRITE commands stick. DSP is genuinely not processing.') + +sw.close() +print('\n=== Done ===') diff --git a/tools/addr_gateway_test.py b/tools/addr_gateway_test.py new file mode 100644 index 0000000..7867167 --- /dev/null +++ b/tools/addr_gateway_test.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""BCM4500 I2C address gateway verification test. + +Compares register reads at address 0x08 (BCM4500 direct) vs 0x10 +(BCM3440 tuner gateway) to confirm the stock firmware disassembly +finding: all BCM4500 register access goes through the tuner at 0x10. + +HYPOTHESIS: + - Reads at 0x08 return the same status byte for all registers + - Reads at 0x10 return different, register-specific values + - Writes at 0x10 actually reach BCM4500 registers + - The indirect register protocol (A6/A7/A8) works at 0x10 + +Run with custom firmware v3.02+ (needs 0xB5 raw I2C read). +""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_I2C_RAW_READ = 0xB5 + +ADDR_DIRECT = 0x08 # BCM4500 direct +ADDR_GATEWAY = 0x10 # BCM3440 tuner gateway + +REGS = [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB] + +sw = SkyWalker1() +sw.open() +print('=== BCM4500 I2C Address Gateway Test ===') +print(f'Firmware: {sw.get_fw_version()}') + +# Boot first +print('\n--- Booting BCM4500 ---') +result = sw._vendor_in(0x89, value=1, index=0, length=3) +cfg, stage = result[0], result[1] +print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}') + +# Test 1: Read registers via direct address (0x08) +print(f'\n=== Test 1: Registers via direct address 0x{ADDR_DIRECT:02X} (BCM4500) ===') +direct_vals = {} +for reg in REGS: + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_DIRECT, index=reg, length=1) + direct_vals[reg] = data[0] + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + except Exception as e: + direct_vals[reg] = None + print(f' 0x{reg:02X}: FAILED ({e})') + +# Test 2: Read registers via gateway address (0x10) +print(f'\n=== Test 2: Registers via gateway address 0x{ADDR_GATEWAY:02X} (BCM3440) ===') +gw_vals = {} +for reg in REGS: + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_GATEWAY, index=reg, length=1) + gw_vals[reg] = data[0] + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + except Exception as e: + gw_vals[reg] = None + print(f' 0x{reg:02X}: FAILED ({e})') + +# Test 3: Read tuner-specific registers (low range) at 0x10 +print(f'\n=== Test 3: BCM3440 tuner registers (0x00-0x0F) at 0x{ADDR_GATEWAY:02X} ===') +for reg in range(0x00, 0x10): + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=ADDR_GATEWAY, index=reg, length=1) + print(f' 0x{reg:02X}: 0x{data[0]:02X}', end='') + except Exception: + print(f' 0x{reg:02X}: FAIL', end='') + if (reg + 1) % 8 == 0: + print() + +# Test 4: Write to A6 via gateway, then read back +print(f'\n=== Test 4: Write A6=0x42 via gateway, read back ===') +try: + # Write using vendor OUT (need to construct this) + # Use bcm_direct_write equivalent through 0xB2 (RAW_DEMOD_WRITE) + # Actually, we need a raw I2C write command... + # Let's use the B6 diagnostic which writes A6 and reads back + diag = sw._vendor_in(0xB6, value=0x42, index=0x01, length=8) + print(f' B6 diag (via current FW addr):') + print(f' A6 write ok: {diag[0]}') + print(f' A6 readback: 0x{diag[1]:02X}') + print(f' A8 write ok: {diag[2]}') + print(f' A8 immediate: 0x{diag[3]:02X}') + print(f' A8 after 2ms: 0x{diag[4]:02X}') + print(f' A7 data: 0x{diag[5]:02X}') + print(f' A6 final: 0x{diag[6]:02X}') + print(f' Cmd sent: 0x{diag[7]:02X}') +except Exception as e: + print(f' FAILED: {e}') + +# Analysis +print('\n' + '=' * 60) +print('ANALYSIS') +print('=' * 60) + +direct_unique = set(v for v in direct_vals.values() if v is not None) +gw_unique = set(v for v in gw_vals.values() if v is not None) + +print(f'\nDirect (0x{ADDR_DIRECT:02X}): {len(direct_unique)} unique values: ' + f'{", ".join(f"0x{v:02X}" for v in sorted(direct_unique))}') +print(f'Gateway (0x{ADDR_GATEWAY:02X}): {len(gw_unique)} unique values: ' + f'{", ".join(f"0x{v:02X}" for v in sorted(gw_unique))}') + +if len(direct_unique) <= 2 and len(gw_unique) > 2: + print('\n >> HYPOTHESIS CONFIRMED!') + print(' >> Direct 0x08 returns same status for all regs') + print(' >> Gateway 0x10 returns register-specific values') + print(' >> FIX: BCM4500_ADDR should be 0x10 (tuner gateway)') +elif len(gw_unique) <= 2: + print('\n >> Gateway also returns uniform values -- may need different timing') + print(' >> or the tuner gateway requires initialization first') +else: + print('\n >> Unexpected results -- check values above') + +# Show side-by-side comparison +print('\n Reg Direct(0x08) Gateway(0x10) Different?') +for reg in REGS: + d = direct_vals.get(reg) + g = gw_vals.get(reg) + d_str = f'0x{d:02X}' if d is not None else 'FAIL' + g_str = f'0x{g:02X}' if g is not None else 'FAIL' + diff = ' <--' if d != g else '' + print(f' 0x{reg:02X} {d_str:>12} {g_str:>13} {diff}') + +sw.close() +print('\n=== Done ===') diff --git a/tools/boot_ab_test.py b/tools/boot_ab_test.py new file mode 100644 index 0000000..8e9ddc2 --- /dev/null +++ b/tools/boot_ab_test.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""A/B test: compare BCM4500 register state with and without firmware download. + +Uses wIndex boot flags on BOOT_8PSK (0x89): + bit 0 (0x01): skip EEPROM firmware download + bit 1 (0x02): add 200ms DSP startup delay after download + bit 2 (0x04): skip register init blocks + +Test matrix: + A) wIndex=0x01 — skip FW download, init blocks only + B) wIndex=0x00 — full boot (FW download + init blocks) + C) wIndex=0x02 — full boot + 200ms DSP delay + D) wIndex=0x04 — FW download only, skip init blocks +""" +import sys +import time +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_RAW_DEMOD_READ = 0xB1 +CMD_I2C_RAW_READ = 0xB5 +CMD_GET_PLL_DIAG = 0xBF +BCM4500_ADDR = 0x08 + +# Key direct registers +KEY_REGS = [ + (0xA0, 'CFG_MODE'), + (0xA2, 'STATUS'), + (0xA4, 'LOCK'), + (0xA6, 'PAGE'), + (0xA7, 'DATA'), + (0xA8, 'CMD'), + (0xA9, 'PLL_A9'), + (0xAA, 'PLL_AA'), + (0xAB, 'PLL_AB'), +] + + +def read_key_regs(sw, label): + """Read key BCM4500 registers via raw I2C (ground truth).""" + print(f' --- {label} ---') + results = {} + for reg, name in KEY_REGS: + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + val = data[0] + results[reg] = val + marker = '' + if val == 0x00: + marker = ' (zero)' + elif val == 0x02: + marker = ' << non-zero!' + elif val != 0x00: + marker = ' << non-zero!' + print(f' 0x{reg:02X} ({name:8s}): 0x{val:02X}{marker}') + except Exception as e: + results[reg] = None + print(f' 0x{reg:02X} ({name:8s}): FAILED ({e})') + nonzero = sum(1 for v in results.values() if v and v != 0) + print(f' Non-zero registers: {nonzero}/{len(results)}') + return results + + +def boot_with_flags(sw, wval=1, flags=0, label=''): + """Send BOOT_8PSK with wValue and wIndex (boot flags).""" + print(f'\n{"="*60}') + print(f'BOOT: wValue={wval}, wIndex=0x{flags:02X} — {label}') + print(f'{"="*60}') + + # Send boot command: wValue=boot mode, wIndex=flags + try: + result = sw._vendor_in(0x89, value=wval, index=flags, length=3) + cfg = result[0] + stage = result[1] + bits = [] + if cfg & 0x01: bits.append('Started') + if cfg & 0x02: bits.append('FW_Loaded') + if cfg & 0x04: bits.append('Intersil') + if cfg & 0x08: bits.append('DVBmode') + print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})') + print(f' Boot stage: 0x{stage:02X}' + + (' (COMPLETE)' if stage == 0xFF else f' (stopped at {stage})')) + except Exception as e: + print(f' Boot command failed: {e}') + return None + + # PLL diagnostics + try: + pd = sw._vendor_in(CMD_GET_PLL_DIAG, value=0, index=0, length=10) + print(f' PLL diag: eeprom={"Y" if pd[0] else "N"} ' + f'blocks={pd[2]} ' + f'exit={"OK" if pd[6]==1 else "FAIL" if pd[6]==0 else "skip"} ' + f'result={"OK" if pd[7]==1 else "FAIL"}') + except Exception: + pass + + return read_key_regs(sw, 'Registers immediately after boot') + + +def main(): + sw = SkyWalker1() + sw.open() + print('=== BCM4500 Boot A/B Test ===') + print(f'Firmware: {sw.get_fw_version()}') + + # Pre-boot register state (BCM4500 may still be in stock-firmware state) + print('\n' + '='*60) + print('PRE-BOOT: Register state before any boot command') + print('='*60) + pre = read_key_regs(sw, 'Before boot') + + # Shutdown first to establish baseline + print('\n--- Shutting down BCM4500 ---') + sw._vendor_in(0x89, value=0, index=0, length=3) + time.sleep(0.5) + + # ===== TEST A: Init blocks only (no firmware download) ===== + regs_a = boot_with_flags(sw, wval=1, flags=0x01, + label='Init blocks ONLY (skip FW download)') + + # Read again after 200ms + time.sleep(0.2) + regs_a2 = read_key_regs(sw, 'After 200ms settle (test A)') + + # Shutdown and wait + print('\n--- Shutting down BCM4500 ---') + sw._vendor_in(0x89, value=0, index=0, length=3) + time.sleep(0.5) + + # ===== TEST B: Full boot (firmware download + init blocks) ===== + regs_b = boot_with_flags(sw, wval=1, flags=0x00, + label='Full boot (FW download + init blocks)') + + # Read again after 200ms + time.sleep(0.2) + regs_b2 = read_key_regs(sw, 'After 200ms settle (test B)') + + # Shutdown and wait + print('\n--- Shutting down BCM4500 ---') + sw._vendor_in(0x89, value=0, index=0, length=3) + time.sleep(0.5) + + # ===== TEST C: Full boot + 200ms DSP startup delay ===== + regs_c = boot_with_flags(sw, wval=1, flags=0x02, + label='Full boot + 200ms DSP delay') + + regs_c2 = read_key_regs(sw, 'Registers (test C)') + + # Shutdown and wait + print('\n--- Shutting down BCM4500 ---') + sw._vendor_in(0x89, value=0, index=0, length=3) + time.sleep(0.5) + + # ===== TEST D: Firmware download only (no init blocks) ===== + regs_d = boot_with_flags(sw, wval=1, flags=0x04, + label='FW download ONLY (skip init blocks)') + + time.sleep(0.2) + regs_d2 = read_key_regs(sw, 'After 200ms settle (test D)') + + # ===== SUMMARY ===== + print('\n' + '='*60) + print('SUMMARY: Register 0xA2 (STATUS) across tests') + print('='*60) + + def val_str(regs, reg=0xA2): + if regs is None: + return 'FAIL' + v = regs.get(reg) + if v is None: + return 'FAIL' + return f'0x{v:02X}' + + print(f' Pre-boot (stock FW state): {val_str(pre)}') + print(f' A) Init only, immediate: {val_str(regs_a)}') + print(f' A) Init only, +200ms: {val_str(regs_a2)}') + print(f' B) Full boot, immediate: {val_str(regs_b)}') + print(f' B) Full boot, +200ms: {val_str(regs_b2)}') + print(f' C) Full boot + DSP delay: {val_str(regs_c)}') + print(f' D) FW only, +200ms: {val_str(regs_d2)}') + + print() + print('Expected: A should show 0x02 (previous working state)') + print(' B should show 0x00 (current broken state)') + print(' C tests if DSP just needs more startup time') + print(' D isolates firmware download effect') + + sw.close() + print('\n=== Done ===') + + +if __name__ == '__main__': + main() diff --git a/tools/boot_deep_verify.py b/tools/boot_deep_verify.py new file mode 100644 index 0000000..9f470b1 --- /dev/null +++ b/tools/boot_deep_verify.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Deep verification: full register dump + indirect register test. + +Checks whether: +1. Direct registers (0xA0-0xBF) are truly all 0x02 or vary +2. Indirect registers respond (proves DSP core is running) +3. Signal monitoring works after boot +4. Boot with vs without FW download produces different indirect reg values +""" +import sys +import time +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_RAW_DEMOD_READ = 0xB1 +CMD_I2C_RAW_READ = 0xB5 +BCM4500_ADDR = 0x08 + + +def full_direct_dump(sw, label): + """Read all direct registers 0xA0-0xBF via raw I2C.""" + print(f'\n --- {label}: Direct Register Dump 0xA0-0xBF ---') + values = {} + for reg in range(0xA0, 0xC0): + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + values[reg] = data[0] + except Exception: + values[reg] = None + + # Print in 2 rows of 16 + for base in [0xA0, 0xB0]: + row = [] + for reg in range(base, base + 16): + v = values.get(reg) + row.append(f'{v:02X}' if v is not None else '??') + print(f' {base:02X}: {" ".join(row)}') + + unique = set(v for v in values.values() if v is not None) + print(f' Unique values: {sorted(f"0x{v:02X}" for v in unique)}') + return values + + +def indirect_reg_test(sw, label): + """Read indirect registers to test DSP core responsiveness. + Uses 0xB1 with wIndex=0 (indirect mode): wValue=page.""" + print(f'\n --- {label}: Indirect Register Test ---') + # Key DSP pages: 0x00 (config), 0x06 (acq), 0x07 (AGC), + # 0x0A (Viterbi), 0x0F (transport) + pages = [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F, 0x10, 0x20] + results = {} + for page in pages: + try: + data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page, + index=0, length=1) + val = data[0] + results[page] = val + marker = ' << DSP alive!' if val != 0 else '' + print(f' Page 0x{page:02X}: 0x{val:02X}{marker}') + except Exception as e: + results[page] = None + print(f' Page 0x{page:02X}: FAILED ({e})') + + nonzero = sum(1 for v in results.values() if v and v != 0) + print(f' Non-zero pages: {nonzero}/{len(pages)}') + return results + + +def boot_and_test(sw, flags, label): + """Boot with given flags and run full diagnostics.""" + print(f'\n{"="*60}') + print(f'TEST: {label} (wIndex=0x{flags:02X})') + print(f'{"="*60}') + + # Shutdown + wait + sw._vendor_in(0x89, value=0, index=0, length=3) + time.sleep(0.5) + + # Boot + result = sw._vendor_in(0x89, value=1, index=flags, length=3) + cfg, stage = result[0], result[1] + bits = [] + if cfg & 0x01: bits.append('Started') + if cfg & 0x02: bits.append('FW_Loaded') + print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})') + print(f' Stage: 0x{stage:02X}' + + (' (COMPLETE)' if stage == 0xFF else f' (at {stage})')) + + # Full register dumps + direct = full_direct_dump(sw, label) + indirect = indirect_reg_test(sw, label) + + # Wait and re-test indirect (DSP may need startup time) + time.sleep(0.5) + indirect2 = indirect_reg_test(sw, f'{label} +500ms') + + # Signal monitor + print(f'\n --- Signal Monitor ---') + try: + sig = sw.signal_monitor() + print(f' {sig}') + except Exception as e: + print(f' Failed: {e}') + + return direct, indirect, indirect2 + + +def main(): + sw = SkyWalker1() + sw.open() + print('=== BCM4500 Deep Boot Verification ===') + print(f'Firmware: {sw.get_fw_version()}') + + # Test 1: Init blocks only (no FW download) + d1, i1, i1b = boot_and_test(sw, 0x01, + 'Init blocks only (skip FW download)') + + # Test 2: Full boot (FW download + init blocks) + d2, i2, i2b = boot_and_test(sw, 0x00, + 'Full boot (FW download + init blocks)') + + # Compare indirect registers between the two modes + print(f'\n{"="*60}') + print('COMPARISON: Indirect Registers (init-only vs full boot)') + print('='*60) + pages = [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F, 0x10, 0x20] + for page in pages: + v1 = i1b.get(page) + v2 = i2b.get(page) + v1s = f'0x{v1:02X}' if v1 is not None else '??' + v2s = f'0x{v2:02X}' if v2 is not None else '??' + diff = ' << DIFFERENT!' if v1 != v2 else '' + print(f' Page 0x{page:02X}: init-only={v1s} full={v2s}{diff}') + + # Compare direct registers + print(f'\nDirect register comparison (0xA0-0xBF):') + diffs = [] + for reg in range(0xA0, 0xC0): + v1 = d1.get(reg) + v2 = d2.get(reg) + if v1 != v2: + diffs.append(f' 0x{reg:02X}: init-only=0x{v1:02X} full=0x{v2:02X}') + if diffs: + print('\n'.join(diffs)) + else: + print(' No differences (all registers identical)') + + sw.close() + print('\n=== Done ===') + + +if __name__ == '__main__': + main() diff --git a/tools/boot_reg_probe.py b/tools/boot_reg_probe.py new file mode 100644 index 0000000..9dfdaf8 --- /dev/null +++ b/tools/boot_reg_probe.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Probe BCM4500 register behavior after boot. + +1. Multi-byte reads via 0xB5 (do adjacent registers differ?) +2. Step-by-step indirect read via 0xB6 diagnostic +3. Write a register and read back (is the BCM alive or echoing?) +4. Try tuning to see if the signal path works +""" +import sys +import time +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_I2C_RAW_READ = 0xB5 +CMD_I2C_DIAG = 0xB6 +CMD_RAW_DEMOD_READ = 0xB1 +CMD_RAW_DEMOD_WRITE = 0xB2 +BCM4500_ADDR = 0x08 + +sw = SkyWalker1() +sw.open() +print('=== BCM4500 Register Probe ===') +print(f'Firmware: {sw.get_fw_version()}') + +# Boot with full sequence +print('\n--- Booting BCM4500 (full boot) ---') +result = sw._vendor_in(0x89, value=1, index=0, length=3) +cfg, stage = result[0], result[1] +print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}') + +# ============================================================ +# TEST 1: Multi-byte read — read 16 bytes starting at 0xA0 +# ============================================================ +print('\n=== Test 1: Multi-byte read (16 bytes from 0xA0) ===') +print('If BCM4500 truly maps different registers, bytes should differ.') +try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xA0, length=16) + hex_str = ' '.join(f'{b:02X}' for b in data) + print(f' 0xA0-0xAF: {hex_str}') + unique = set(data) + print(f' Unique values: {sorted(f"0x{v:02X}" for v in unique)}') + if len(unique) == 1: + print(f' ALL SAME VALUE: 0x{data[0]:02X} — BCM4500 may be returning') + print(f' a fixed status byte, not true register contents.') +except Exception as e: + print(f' Failed: {e}') + +# Read second block 0xB0-0xBF +try: + data2 = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xB0, length=16) + hex_str = ' '.join(f'{b:02X}' for b in data2) + print(f' 0xB0-0xBF: {hex_str}') +except Exception as e: + print(f' Failed: {e}') + +# ============================================================ +# TEST 2: Write-then-read — does the BCM4500 retain writes? +# ============================================================ +print('\n=== Test 2: Write-then-read (register 0xA6 PAGE) ===') +print('Write 0x42 to 0xA6, read back. If we get 0x42, register works.') +print('If we get 0x02, the chip may be ignoring writes.') + +# Read current value +try: + before = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xA6, length=1) + print(f' Before write: 0xA6 = 0x{before[0]:02X}') +except Exception as e: + print(f' Read failed: {e}') + +# Write 0x42 to 0xA6 via 0xB1 direct write +try: + # 0xB2: RAW_DEMOD_WRITE — wValue=register, wIndex=data + sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0xA6, index=0x42, length=0) +except Exception: + # 0xB2 might not return data + pass + +time.sleep(0.01) + +# Read back +try: + after = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xA6, length=1) + print(f' After write 0x42: 0xA6 = 0x{after[0]:02X}') + if after[0] == 0x42: + print(f' >> REGISTER WORKS — BCM4500 is alive and accepting writes!') + elif after[0] == 0x02: + print(f' >> Still 0x02 — chip may be ignoring writes or returning status') + else: + print(f' >> Got 0x{after[0]:02X} — unexpected') +except Exception as e: + print(f' Read failed: {e}') + +# Restore 0xA6 to 0x00 +try: + sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0xA6, index=0x00, length=0) +except Exception: + pass + +# ============================================================ +# TEST 3: Step-by-step indirect read via 0xB6 +# ============================================================ +print('\n=== Test 3: Step-by-step indirect read (0xB6) ===') +print('Reading indirect register page 0x06 (acquisition config)') +try: + diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0, length=8) + labels = [ + 'Write 0xA6 ok', # [0] + 'Readback 0xA6', # [1] + 'Write 0xA8 ok', # [2] + 'Readback 0xA8', # [3] + 'Readback 0xA7', # [4] — the indirect register data + 'Direct read 0xA6', # [5] + 'Direct read 0xA7', # [6] + 'Direct read 0xA8', # [7] + ] + for i, label in enumerate(labels): + ok_marker = '' + if i in [0, 2]: + ok_marker = ' (success)' if diag[i] == 0x01 else ' (FAILED!)' if diag[i] == 0x00 else '' + print(f' [{i}] {label:20s}: 0x{diag[i]:02X}{ok_marker}') + + print() + if diag[0] == 0x01 and diag[2] == 0x01: + print(' Writes to A6/A8 succeeded.') + if diag[1] == 0x06: + print(f' A6 readback = 0x06 — page register WORKS.') + else: + print(f' A6 readback = 0x{diag[1]:02X} — expected 0x06!') + if diag[4] != 0x00: + print(f' A7 (indirect data) = 0x{diag[4]:02X} — DSP responding!') + else: + print(f' A7 (indirect data) = 0x00 — DSP may not be running.') + else: + print(' Write step FAILED — I2C issue.') +except Exception as e: + print(f' Diagnostic failed: {e}') + +# Try a few more pages +for page in [0x00, 0x07, 0x0F]: + try: + diag = sw._vendor_in(CMD_I2C_DIAG, value=page, index=0, length=8) + a6_ok = 'ok' if diag[0] == 1 else 'FAIL' + a8_ok = 'ok' if diag[2] == 1 else 'FAIL' + print(f' Page 0x{page:02X}: A6={a6_ok} A6_rb=0x{diag[1]:02X} ' + f'A8={a8_ok} A8_rb=0x{diag[3]:02X} ' + f'A7_data=0x{diag[4]:02X} ' + f'direct=[A6=0x{diag[5]:02X} A7=0x{diag[6]:02X} A8=0x{diag[7]:02X}]') + except Exception as e: + print(f' Page 0x{page:02X}: {e}') + +# ============================================================ +# TEST 4: Read registers 0x00-0x9F (below the A0 range) +# ============================================================ +print('\n=== Test 4: Registers OUTSIDE 0xA0-0xBF range ===') +print('If BCM4500 returns 0x02 for everything, it might do so for ALL addresses.') +for reg in [0x00, 0x10, 0x50, 0x80, 0x90, 0x9F, 0xC0, 0xD0, 0xFF]: + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' Reg 0x{reg:02X}: 0x{data[0]:02X}') + except Exception as e: + print(f' Reg 0x{reg:02X}: FAILED ({e})') + +# ============================================================ +# TEST 5: Quick tune test (no dish needed — just check AGC) +# ============================================================ +print('\n=== Test 5: Tune to 1200 MHz / 27500 ksps (AGC check) ===') +try: + # Enable Intersil (LNB controller) + sw._vendor_in(0x8A, value=1, index=0, length=1) + time.sleep(0.1) + print(' Intersil enabled') + + # Tune: 1200 MHz, 27500 ksps, QPSK + result = sw.tune_monitor(freq_mhz=1200, sr_ksps=27500, mod_index=0, dwell_ms=200) + print(f' Tune result: {result}') + if result.get('agc1', 0) > 0 or result.get('agc2', 0) > 0: + print(' >> AGC non-zero — tuner + demod signal path is alive!') + else: + print(' >> AGC=0 — signal path not working (or tuner not configured)') +except Exception as e: + print(f' Tune failed: {e}') + +sw.close() +print('\n=== Done ===') diff --git a/tools/boot_test.py b/tools/boot_test.py new file mode 100644 index 0000000..3375b84 --- /dev/null +++ b/tools/boot_test.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Quick boot test for PLL config firmware.""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_RAW_DEMOD_READ = 0xB1 +CMD_I2C_SCAN = 0xB4 +CMD_GET_PLL_DIAG = 0xBF + +sw = SkyWalker1() +sw.open() +print('=== SkyWalker-1 Custom Firmware Boot Test ===') +print() + +# Check firmware version +ver = sw.get_fw_version() +print(f'Firmware version: {ver}') + +# Check config status +cfg = sw.get_config() +print(f'Config status: 0x{cfg:02X}') +bits = [] +if cfg & 0x01: bits.append('Started') +if cfg & 0x02: bits.append('FW_Loaded') +if cfg & 0x04: bits.append('Intersil') +if cfg & 0x08: bits.append('DVBmode') +if cfg & 0x10: bits.append('22kHz') +if cfg & 0x20: bits.append('18V') +if cfg & 0x40: bits.append('DCtuned') +if cfg & 0x80: bits.append('Armed') +print(f' Flags: {" | ".join(bits) if bits else "(none)"}') +print() + +# Check last error before boot +err = sw.get_last_error() +print(f'Last error (pre-boot): 0x{err:02X}') +print() + +# Boot the BCM4500 +print('--- Booting BCM4500 ---') +boot_result = sw.boot() +print(f'Boot result: {boot_result}') +cfg = sw.get_config() +print(f'Config after boot: 0x{cfg:02X}') +bits = [] +if cfg & 0x01: bits.append('Started') +if cfg & 0x02: bits.append('FW_Loaded') +if cfg & 0x04: bits.append('Intersil') +if cfg & 0x08: bits.append('DVBmode') +if cfg & 0x10: bits.append('22kHz') +if cfg & 0x20: bits.append('18V') +if cfg & 0x40: bits.append('DCtuned') +if cfg & 0x80: bits.append('Armed') +print(f' Flags: {" | ".join(bits) if bits else "(none)"}') + +err = sw.get_last_error() +ERR_NAMES = { + 0x00: 'ERR_OK', + 0x01: 'ERR_I2C_TIMEOUT', + 0x02: 'ERR_I2C_NAK', + 0x03: 'ERR_I2C_ARB_LOST', + 0x04: 'ERR_BCM_NOT_READY (PLL config failed)', + 0x05: 'ERR_BCM_TIMEOUT', +} +print(f'Last error after boot: 0x{err:02X} = {ERR_NAMES.get(err, "unknown")}') +print() + +# PLL config diagnostics +print('--- PLL Config Diagnostics ---') +try: + pd = sw._vendor_in(CMD_GET_PLL_DIAG, value=0, index=0, length=10) + print(f' EEPROM present: {"YES" if pd[0] else "NO"}') + print(f' First block count: 0x{pd[1]:02X}' + (' (sentinel=0, no PLL data!)' if pd[1] == 0 else + (' (not reached)' if pd[1] == 0xFF else f' ({pd[1]} AB bytes)'))) + print(f' Blocks written: {pd[2]}') + print(f' Last A9 value: 0x{pd[3]:02X}' + (' (none)' if pd[3] == 0xFF else '')) + print(f' Last AA value: 0x{pd[4]:02X}' + (' (none)' if pd[4] == 0xFF else '')) + print(f' Last AB count: 0x{pd[5]:02X}' + (' (none)' if pd[5] == 0xFF else '')) + print(f' Config mode exit: {"OK" if pd[6] == 1 else ("FAIL" if pd[6] == 0 else "not reached")}') + print(f' Overall PLL result: {"SUCCESS" if pd[7] == 1 else "FAILED"}') + print(f' Boot stage: 0x{pd[8]:02X}' + (' (all complete)' if pd[8] == 0xFF else f' (stopped at stage {pd[8]})')) + print(f' Last error: 0x{pd[9]:02X}') +except Exception as e: + print(f' PLL diag failed: {e}') +print() + +# I2C bus scan +print('--- I2C Bus Scan ---') +try: + bitmap = sw._vendor_in(CMD_I2C_SCAN, value=0, index=0, length=16) + found = [] + for byte_idx in range(16): + for bit_idx in range(8): + if bitmap[byte_idx] & (1 << bit_idx): + addr = byte_idx * 8 + bit_idx + found.append(addr) + labels = {0x08: 'BCM4500', 0x10: 'BCM3440', 0x51: 'EEPROM'} + for addr in found: + label = labels.get(addr, '') + print(f' 0x{addr:02X} {label}') + if not found: + print(' (no devices found!)') +except Exception as e: + print(f' Scan failed: {e}') +print() + +# Try reading signal +print('--- Signal Check ---') +try: + sig = sw.signal_monitor() + print(f'Signal monitor: {sig}') +except Exception as e: + print(f'Signal monitor failed: {e}') + +# Read BCM4500 direct registers via 0xB1 vendor command +# wValue=register address, wIndex=1 for direct read mode +print() +print('--- BCM4500 Direct Register Reads ---') +key_regs = [ + (0xA0, 'CFG_MODE'), + (0xA2, 'STATUS'), + (0xA4, 'LOCK'), + (0xA9, 'PLL_A9'), + (0xAA, 'PLL_AA'), + (0xAB, 'PLL_AB'), +] +for reg, label in key_regs: + try: + data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=reg, index=1, length=1) + val = data[0] + print(f' 0x{reg:02X} ({label:8s}): 0x{val:02X}') + except Exception as e: + print(f' 0x{reg:02X} ({label:8s}): FAILED ({e})') + +# Indirect register reads via 0xB1 with wIndex=0 (indirect mode) +# wValue=page, wIndex=0. Only meaningful if the DSP core is running. +print() +print('--- BCM4500 Indirect Register Reads (DSP core test) ---') +for page in [0x06, 0x07, 0x0F]: + try: + data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page, index=0, length=1) + val = data[0] + alive = ' << DSP responding' if val != 0 else '' + print(f' Page 0x{page:02X}: 0x{val:02X}{alive}') + except Exception as e: + print(f' Page 0x{page:02X}: FAILED ({e})') + +# Cross-check: read key registers via 0xB5 (raw I2C, writes into EP0BUF directly) +# This bypasses the 0xB1 handler's val variable entirely +CMD_I2C_RAW_READ = 0xB5 +BCM4500_ADDR = 0x08 +print() +print('--- Cross-check via 0xB5 Raw I2C Read (BCM4500 @ 0x08) ---') +for reg, label in key_regs: + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, index=reg, length=1) + val = data[0] + tag = ' ** I2C FAIL (0xFF) **' if val == 0xFF else '' + print(f' 0x{reg:02X} ({label:8s}): 0x{val:02X}{tag}') + except Exception as e: + print(f' 0x{reg:02X} ({label:8s}): FAILED ({e})') + +# Full register dump 0xA0-0xBF via 0xB5 raw I2C (ground truth) +print() +print('--- Full Direct Register Dump 0xA0-0xBF (via 0xB5 raw I2C) ---') +for reg in range(0xA0, 0xC0): + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, index=reg, length=1) + val = data[0] + print(f' 0x{reg:02X}: 0x{val:02X}') + except Exception as e: + print(f' 0x{reg:02X}: FAIL') + +sw.close() +print() +print('=== Test Complete ===') diff --git a/tools/eeprom_deep_scan.py b/tools/eeprom_deep_scan.py new file mode 100644 index 0000000..2688615 --- /dev/null +++ b/tools/eeprom_deep_scan.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Deep EEPROM scan — find the real PLL data location.""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_EEPROM_READ = 0xC0 + +sw = SkyWalker1() +sw.open() + + +def ee(addr, length): + return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length) + + +def hex_line(addr, data): + hex_str = ' '.join(f'{b:02X}' for b in data) + return f'{addr:04X}: {hex_str}' + + +# Dump 0x3F00-0x4100 (area around the boundary — zero gap between FX2 and BCM firmware?) +print('=== Region around FX2 firmware end (0x2530) ===') +for addr in range(0x2530, 0x2600, 16): + d = ee(addr, 16) + print(f' {hex_line(addr, d)}') + +print() +print('=== Region 0x3F00-0x4080 (end of first 16KB, start of second) ===') +for addr in range(0x3F00, 0x4080, 16): + d = ee(addr, 16) + print(f' {hex_line(addr, d)}') + +# Check the END of the AT24C256 (0x7F00-0x7FFF) +print() +print('=== End of EEPROM 0x7F00-0x7FFF ===') +for addr in range(0x7F00, 0x8000, 16): + d = ee(addr, 16) + print(f' {hex_line(addr, d)}') + +# Parse the second image at 0x4000 to find its end +print() +print('=== Parsing 0x4000 as C2-like records ===') +offset = 0x4000 +# First check if it's a C2 header +header = ee(0x4000, 4) +print(f' Header bytes: {header.hex(" ")}') +# Could be: [len_hi, len_lo, addr_hi, addr_lo] +rec_len = (header[0] << 8) | header[1] +rec_addr = (header[2] << 8) | header[3] +print(f' As record: len={rec_len} (0x{rec_len:04X}), addr=0x{rec_addr:04X}') +print() + +# Try parsing as load records (same format as C2 but without the 8-byte header) +offset = 0x4000 +print(' Attempting record parse:') +total_size = 0 +while offset < 0x7000: + hdr = ee(offset, 4) + rlen = (hdr[0] << 8) | hdr[1] + raddr = (hdr[2] << 8) | hdr[3] + + if rlen == 0x8001: + print(f' [{total_size}] END MARKER at 0x{offset:04X} → entry=0x{raddr:04X}') + offset += 4 + break + elif rlen == 0 or rlen > 0x4000: + print(f' [{total_size}] STOP at 0x{offset:04X}: len=0x{rlen:04X} addr=0x{raddr:04X}') + # Dump surrounding bytes + d = ee(offset, 32) + print(f' Bytes: {d.hex(" ")}') + offset += 4 + break + + end = raddr + rlen - 1 + print(f' [{total_size}] {rlen:5d} bytes at EEPROM 0x{offset:04X} → 0x{raddr:04X}-0x{end:04X}') + offset += 4 + rlen + total_size += rlen + if total_size > 30000: + print(' (aborting, too much data)') + break + +print(f' Second image ends at: 0x{offset:04X}') +print() + +# Check immediately after second image for PLL data +print(f'=== After second image: 0x{offset:04X}-0x{offset+256:04X} ===') +for addr in range(offset, offset + 256, 16): + d = ee(addr, 16) + print(f' {hex_line(addr, d)}') + +# Also check for PLL blocks at this offset +print() +print(f'=== PLL block scan starting at 0x{offset:04X} ===') +for addr in range(offset, offset + 400, 20): + block = ee(addr, 20) + count = block[0] + if count == 0: + print(f' 0x{addr:04X}: [sentinel count=0]') + break + elif 1 <= count <= 16: + ab = block[4:4 + count] + print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} ' + f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]') + else: + print(f' 0x{addr:04X}: NOT PLL (first byte=0x{count:02X})') + # But continue scanning + pass + +sw.close() +print() +print('=== Done ===') diff --git a/tools/eeprom_flash_a0.py b/tools/eeprom_flash_a0.py new file mode 100644 index 0000000..07e51e5 --- /dev/null +++ b/tools/eeprom_flash_a0.py @@ -0,0 +1,736 @@ +#!/usr/bin/env python3 +""" +EEPROM flash via host-side I2C orchestration (boot ROM 0xA0). + +Writes C2 firmware images to the SkyWalker-1 EEPROM by driving the +FX2LP's I2C controller from the host while the CPU is halted. + +Background: + - Stock firmware I2C proxy (0x83/0x84) returns pipe errors + - Custom firmware via RAM: I2CS BERR (0xF6) on CPUCS restart + - Pre-halt I2C flush (0x90) + halt → I2CS = 0x01 (clean/idle) + - Boot ROM 0xA0 can read/write I2C registers at 0xE678-0xE67A + - We drive the I2C controller from the host, one register write at + a time, to program the EEPROM + +Strategy: + 1. Pre-halt flush: GET_SIGNAL_LOCK (0x90) finishes stock firmware I2C + 2. Halt CPU: CPUCS = 0x01 (I2C controller stays idle) + 3. Write I2C registers via 0xA0 to orchestrate EEPROM write + 4. Power-cycle to boot from new EEPROM image +""" +import usb.core, usb.util, sys, time, os, subprocess, argparse + +# USB IDs +SKYWALKER_VID = 0x09C0 +SKYWALKER_PID = 0x0203 + +# FX2LP register addresses (XDATA space) +CPUCS_ADDR = 0xE600 +I2CS_ADDR = 0xE678 +I2DAT_ADDR = 0xE679 +I2CTL_ADDR = 0xE67A + +# I2CS bit masks +bmSTART = 0x80 +bmSTOP = 0x40 +bmLASTRD = 0x20 +bmID1 = 0x10 +bmID0 = 0x08 +bmBERR = 0x04 +bmACK = 0x02 +bmDONE = 0x01 + +# EEPROM parameters (24C128) +EEPROM_I2C_ADDR = 0x51 # 7-bit address +EEPROM_PAGE_SIZE = 64 # bytes per page write +EEPROM_SIZE = 16384 # 16KB total +EEPROM_WRITE_MS = 5 # max internal write cycle time + +# Boot ROM vendor request +A0_REQUEST = 0xA0 + + +def i2cs_str(val): + """Human-readable I2CS register decode.""" + flags = [] + if val & bmSTART: flags.append('START') + if val & bmSTOP: flags.append('STOP') + if val & bmLASTRD: flags.append('LASTRD') + if val & bmBERR: flags.append('BERR') + if val & bmACK: flags.append('ACK') + if val & bmDONE: flags.append('DONE') + 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 'clear'}] state={state}" + + +def find_device(): + """Find SkyWalker-1 on USB.""" + dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) + if dev is None: + print("ERROR: SkyWalker-1 not found on USB") + sys.exit(1) + print(f" Device: Bus {dev.bus} Addr {dev.address}") + return dev + + +def detach_driver(dev): + """Detach kernel driver if attached.""" + for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + try: + dev.detach_kernel_driver(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 + + +# ── FX2 register access via 0xA0 ───────────────────────────────── + +def a0_read(dev, addr, length=1): + """Read from FX2 XDATA address space via boot ROM 0xA0.""" + return bytes(dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + A0_REQUEST, addr, 0, length, 2000)) + + +def a0_write(dev, addr, data): + """Write to FX2 XDATA address space via boot ROM 0xA0.""" + if isinstance(data, int): + data = bytes([data]) + dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + A0_REQUEST, addr, 0, data, 2000) + + +# ── Host-side I2C primitives ───────────────────────────────────── + +def i2c_read_status(dev): + """Read I2CS register.""" + return a0_read(dev, I2CS_ADDR, 1)[0] + + +def i2c_wait_done(dev, timeout_ms=50): + """Poll I2CS for DONE bit. Returns (success, i2cs_value).""" + deadline = time.time() + timeout_ms / 1000.0 + while time.time() < deadline: + val = i2c_read_status(dev) + if val & bmDONE: + return True, val + if val & bmBERR: + return False, val + time.sleep(0.001) # 1ms between polls + return False, val + + +def i2c_start(dev): + """Assert I2C START condition.""" + a0_write(dev, I2CS_ADDR, bmSTART) + + +def i2c_stop(dev): + """Assert I2C STOP condition and wait for completion.""" + a0_write(dev, I2CS_ADDR, bmSTOP) + deadline = time.time() + 0.050 + while time.time() < deadline: + val = i2c_read_status(dev) + if not (val & bmSTOP): + return True, val + time.sleep(0.001) + return False, val + + +def i2c_write_byte(dev, byte_val): + """Write a byte to I2DAT, wait for DONE. Returns (ack, i2cs).""" + a0_write(dev, I2DAT_ADDR, byte_val) + ok, status = i2c_wait_done(dev) + if not ok: + return False, status + return bool(status & bmACK), status + + +def i2c_read_byte(dev, last=False): + """Read a byte from I2DAT. Set last=True for NACK (last byte).""" + if last: + a0_write(dev, I2CS_ADDR, bmLASTRD) + # Reading I2DAT triggers the next SCL clock cycle + val = a0_read(dev, I2DAT_ADDR, 1)[0] + ok, status = i2c_wait_done(dev) + return val, ok, status + + +# ── Higher-level I2C operations ────────────────────────────────── + +def i2c_probe(dev, addr_7bit): + """Probe an I2C device. Returns True if it ACKs its address.""" + i2c_start(dev) + ack, status = i2c_write_byte(dev, addr_7bit << 1) + i2c_stop(dev) + return ack + + +def eeprom_write_page(dev, mem_addr, data): + """Write up to EEPROM_PAGE_SIZE bytes to EEPROM at mem_addr. + + EEPROM protocol: START + slave_W + addr_H + addr_L + data... + STOP + Then wait for internal write cycle (ACK polling or fixed delay). + """ + if len(data) > EEPROM_PAGE_SIZE: + raise ValueError(f"Page write max {EEPROM_PAGE_SIZE} bytes, got {len(data)}") + + addr_h = (mem_addr >> 8) & 0xFF + addr_l = mem_addr & 0xFF + + # START + i2c_start(dev) + + # Slave address (write) + ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1) + if not ack: + i2c_stop(dev) + return False, "no ACK on slave address" + + # Memory address high byte + ack, status = i2c_write_byte(dev, addr_h) + if not ack: + i2c_stop(dev) + return False, "no ACK on addr_H" + + # Memory address low byte + ack, status = i2c_write_byte(dev, addr_l) + if not ack: + i2c_stop(dev) + return False, "no ACK on addr_L" + + # Data bytes + for i, byte_val in enumerate(data): + ack, status = i2c_write_byte(dev, byte_val) + if not ack: + i2c_stop(dev) + return False, f"no ACK on data byte {i} (0x{byte_val:02X})" + + # STOP (initiates EEPROM internal write cycle) + i2c_stop(dev) + + # Wait for write cycle via ACK polling + # The EEPROM NACKs its address during the write cycle, then ACKs + # when the cycle completes. Timeout after 20ms. + deadline = time.time() + 0.020 + while time.time() < deadline: + time.sleep(0.001) + i2c_start(dev) + ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1) + if ack: + i2c_stop(dev) + return True, "ok" + i2c_stop(dev) + + return False, "write cycle timeout (no ACK after 20ms)" + + +def eeprom_read_bytes(dev, mem_addr, length): + """Read bytes from EEPROM. + + Protocol: START + slave_W + addr_H + addr_L + + rSTART + slave_R + data[0] ... data[n-1] + NACK + STOP + """ + addr_h = (mem_addr >> 8) & 0xFF + addr_l = mem_addr & 0xFF + + # Write phase: set address pointer + i2c_start(dev) + + ack, status = i2c_write_byte(dev, EEPROM_I2C_ADDR << 1) + if not ack: + i2c_stop(dev) + return None, "no ACK on slave address (write phase)" + + ack, status = i2c_write_byte(dev, addr_h) + if not ack: + i2c_stop(dev) + return None, "no ACK on addr_H" + + ack, status = i2c_write_byte(dev, addr_l) + if not ack: + i2c_stop(dev) + return None, "no ACK on addr_L" + + # Read phase: repeated START + slave_R + i2c_start(dev) + + ack, status = i2c_write_byte(dev, (EEPROM_I2C_ADDR << 1) | 1) + if not ack: + i2c_stop(dev) + return None, "no ACK on slave address (read phase)" + + # Read data bytes + result = bytearray() + for i in range(length): + is_last = (i == length - 1) + val, ok, status = i2c_read_byte(dev, last=is_last) + if not ok: + i2c_stop(dev) + return bytes(result), f"read failed at byte {i}" + result.append(val) + + # Dummy read to complete the last byte cycle + _ = a0_read(dev, I2DAT_ADDR, 1) + + # STOP + i2c_stop(dev) + + return bytes(result), "ok" + + +# ── Device preparation ─────────────────────────────────────────── + +def prepare_device(dev, verbose=False): + """Pre-halt flush, halt CPU, verify I2C controller is clean. + + Returns True if I2C controller is ready for host-side operations. + """ + print("\n Phase 1: Prepare I2C controller") + print(" " + "─" * 40) + + # Step 1: Pre-halt I2C flush + print(" Sending GET_SIGNAL_LOCK (0x90) to flush I2C...") + try: + lock = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0x90, 0, 0, 1, 2000) + print(f" Response: 0x{lock[0]:02X}") + except usb.core.USBError as e: + print(f" 0x90 failed: {e}") + # Try alternative flush commands + try: + cfg = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + 0x80, 0, 0, 1, 2000) + print(f" Fallback 0x80 response: 0x{cfg[0]:02X}") + except usb.core.USBError: + print(" No flush commands worked — proceeding anyway") + + # Step 2: Halt CPU + print(" Halting CPU (CPUCS = 0x01)...") + a0_write(dev, CPUCS_ADDR, 0x01) + time.sleep(0.010) + + cpucs = a0_read(dev, CPUCS_ADDR, 1)[0] + if not (cpucs & 0x01): + print(f" CPUCS readback: 0x{cpucs:02X} — halt may have failed") + return False + print(f" CPU halted (CPUCS = 0x{cpucs:02X})") + + # Step 3: Read I2C controller state + i2cs = i2c_read_status(dev) + i2ctl = a0_read(dev, I2CTL_ADDR, 1)[0] + print(f" I2CS: {i2cs_str(i2cs)}") + print(f" I2CTL: 0x{i2ctl:02X} ({'400kHz' if i2ctl & 1 else '100kHz'})") + + if i2cs & bmBERR: + print(" BERR is set — I2C controller is stuck") + print(" This usually means the CPU was restarted after halt") + return False + + # Step 4: Set I2C speed to 400kHz + if not (i2ctl & 0x01): + print(" Setting I2CTL = 0x01 (400kHz)...") + a0_write(dev, I2CTL_ADDR, 0x01) + i2ctl2 = a0_read(dev, I2CTL_ADDR, 1)[0] + print(f" I2CTL readback: 0x{i2ctl2:02X}") + + # Step 5: Quick I2C bus probe + print("\n Phase 2: I2C bus probe") + print(" " + "─" * 40) + + addrs_to_probe = [ + (0x51, "EEPROM (24C128)"), + (0x50, "EEPROM alt addr"), + (0x08, "BCM4500 demod"), + (0x10, "BCM3440 tuner"), + ] + + found_eeprom = False + for addr, name in addrs_to_probe: + ack = i2c_probe(dev, addr) + status_after = i2c_read_status(dev) + result = "ACK" if ack else "NACK" + marker = " <--" if ack and addr in (0x50, 0x51) else "" + print(f" 0x{addr:02X} {name:20s}: {result:4s} " + f"(I2CS={i2cs_str(status_after)}){marker}") + if ack and addr in (0x50, 0x51): + found_eeprom = True + + # Check for BERR after each probe + if status_after & bmBERR: + print(f" BERR after probe — host-side I2C may not work") + return False + + if verbose: + # Read a few bytes from EEPROM to verify reads work + if found_eeprom: + print("\n Phase 2b: EEPROM read test") + print(" " + "─" * 40) + data, msg = eeprom_read_bytes(dev, 0x0000, 8) + if data: + print(f" EEPROM[0x0000..0x0007]: {data.hex(' ')}") + if data[0] == 0xC2: + print(f" First byte is 0xC2 — valid C2 boot header!") + else: + print(f" First byte is 0x{data[0]:02X} — not a C2 header") + else: + print(f" Read failed: {msg}") + return False + + if not found_eeprom: + print(" No EEPROM found at 0x50 or 0x51") + return False + + print(f"\n I2C controller ready for EEPROM operations") + return True + + +# ── Subcommands ────────────────────────────────────────────────── + +def cmd_probe(args): + """Test host-side I2C by probing the bus.""" + print("SkyWalker-1 Host-Side I2C Probe (0xA0)") + print("=" * 45) + + dev = find_device() + detach_driver(dev) + + ok = prepare_device(dev, verbose=True) + if not ok: + print("\n FAIL: Host-side I2C does not work") + print(" The 0xA0 vendor request may not trigger I2C hardware,") + print(" or the I2C bus is in a bad state.") + sys.exit(1) + else: + print("\n SUCCESS: Host-side I2C is working!") + print(" EEPROM detected and readable via boot ROM 0xA0") + + +def cmd_read(args): + """Read EEPROM contents via host-side I2C.""" + size = args.size + print("SkyWalker-1 EEPROM Read (host-side I2C)") + print("=" * 45) + + dev = find_device() + detach_driver(dev) + + ok = prepare_device(dev, verbose=False) + if not ok: + print("\n FAIL: Cannot prepare I2C") + sys.exit(1) + + print(f"\n Reading {size} bytes from EEPROM...") + chunk_size = 32 # read in small chunks (each byte = 2+ USB transfers) + data = bytearray() + errors = 0 + + for offset in range(0, size, chunk_size): + remaining = min(chunk_size, size - offset) + chunk, msg = eeprom_read_bytes(dev, offset, remaining) + if chunk is None: + print(f"\n Read failed at 0x{offset:04X}: {msg}") + errors += 1 + data.extend(b'\xff' * remaining) + else: + data.extend(chunk) + + if offset % 256 == 0 or offset + remaining >= size: + pct = (offset + remaining) * 100 // size + print(f"\r Read: 0x{offset + remaining:04X}/{size:04X} [{pct:3d}%]", + end="", flush=True) + + print() + + if errors: + print(f"\n {errors} read error(s)") + + # Hex dump + if args.hex_dump: + print(f"\n EEPROM contents (first {min(256, len(data))} bytes):") + for i in range(0, min(256, len(data)), 16): + row = data[i:i + 16] + hex_part = ' '.join(f'{b:02X}' for b in row) + ascii_part = ''.join( + chr(b) if 0x20 <= b < 0x7F else '.' for b in row) + print(f" {i:04X}: {hex_part:<48s} {ascii_part}") + + # Check C2 header + if len(data) >= 8 and data[0] == 0xC2: + vid = data[2] << 8 | data[1] + pid = data[4] << 8 | data[3] + print(f"\n C2 header: VID=0x{vid:04X} PID=0x{pid:04X} " + f"CONFIG=0x{data[7]:02X}") + + if args.output: + with open(args.output, 'wb') as f: + f.write(data) + print(f"\n Saved to: {args.output}") + + +def cmd_write(args): + """Write a C2 firmware image to EEPROM via host-side I2C.""" + image_path = args.file + if not os.path.exists(image_path): + print(f"File not found: {image_path}") + sys.exit(1) + + with open(image_path, 'rb') as f: + image = f.read() + + print("SkyWalker-1 EEPROM Flash (host-side I2C)") + print("=" * 45) + + # Validate image + if len(image) < 8 or image[0] != 0xC2: + print(f" Not a C2 image (first byte: 0x{image[0]:02X})") + sys.exit(1) + if len(image) > EEPROM_SIZE: + print(f" Image too large: {len(image)} > {EEPROM_SIZE}") + sys.exit(1) + + vid = image[2] << 8 | image[1] + pid = image[4] << 8 | image[3] + config = image[7] + print(f" Image: {image_path}") + print(f" Size: {len(image)} bytes ({len(image)*100//EEPROM_SIZE}% of EEPROM)") + print(f" VID: 0x{vid:04X} PID: 0x{pid:04X} CONFIG: 0x{config:02X}") + + dev = find_device() + detach_driver(dev) + + ok = prepare_device(dev, verbose=True) + if not ok: + print("\n FAIL: Cannot prepare I2C — aborting") + sys.exit(1) + + # Backup current EEPROM first (just the header to be safe) + if not args.no_backup: + print(f"\n Backing up EEPROM header...") + hdr, msg = eeprom_read_bytes(dev, 0x0000, 8) + if hdr: + print(f" Current header: {hdr.hex(' ')}") + if hdr[0] == 0xC2: + cur_vid = hdr[2] << 8 | hdr[1] + cur_pid = hdr[4] << 8 | hdr[3] + print(f" Current: VID=0x{cur_vid:04X} PID=0x{cur_pid:04X}") + else: + print(f" Header read failed: {msg}") + if not args.force: + print(" Use --force to proceed without backup verification") + sys.exit(1) + + # Dry run? + if args.dry_run: + pages = (len(image) + EEPROM_PAGE_SIZE - 1) // EEPROM_PAGE_SIZE + print(f"\n DRY RUN: would write {len(image)} bytes in {pages} pages") + return + + # Write confirmation + print(f"\n *** WRITING {len(image)} BYTES TO EEPROM ***") + print(f" This replaces the boot firmware. A bad write = bricked device.") + print(f" Press Ctrl+C within 3 seconds to abort.") + try: + for i in range(3, 0, -1): + print(f"\r Starting in {i}... ", end="", flush=True) + time.sleep(1) + print("\r Writing now... ") + except KeyboardInterrupt: + print("\n Aborted.") + return + + # Write image in page-sized chunks + total_pages = (len(image) + EEPROM_PAGE_SIZE - 1) // EEPROM_PAGE_SIZE + write_errors = 0 + start_time = time.time() + + for page_num in range(total_pages): + offset = page_num * EEPROM_PAGE_SIZE + end = min(offset + EEPROM_PAGE_SIZE, len(image)) + chunk = image[offset:end] + + pct = (page_num + 1) * 100 // total_pages + elapsed = time.time() - start_time + rate = (offset + len(chunk)) / elapsed if elapsed > 0 else 0 + eta = (len(image) - offset - len(chunk)) / rate if rate > 0 else 0 + print(f"\r Write: 0x{offset:04X}/0x{len(image):04X} " + f"[{pct:3d}%] {rate:.0f} B/s ETA {eta:.0f}s ", + end="", flush=True) + + ok, msg = eeprom_write_page(dev, offset, chunk) + if not ok: + print(f"\n Write error at 0x{offset:04X}: {msg}") + write_errors += 1 + if write_errors >= 3: + print("\n Too many errors — aborting") + print(" *** EEPROM STATE UNKNOWN ***") + sys.exit(1) + + elapsed = time.time() - start_time + print(f"\r Write: 0x{len(image):04X}/0x{len(image):04X} " + f"[100%] done in {elapsed:.1f}s ") + + if write_errors: + print(f"\n WARNING: {write_errors} write error(s)") + + # Verify + print(f"\n Verifying ({len(image)} bytes)...") + verify_chunk_size = 32 + mismatches = 0 + first_mismatch = None + + for offset in range(0, len(image), verify_chunk_size): + remaining = min(verify_chunk_size, len(image) - offset) + chunk, msg = eeprom_read_bytes(dev, offset, remaining) + if chunk is None: + print(f"\n Verify read failed at 0x{offset:04X}: {msg}") + mismatches += remaining + continue + + for i, (expected, got) in enumerate(zip(image[offset:offset+remaining], chunk)): + if expected != got: + if first_mismatch is None: + first_mismatch = offset + i + mismatches += 1 + if mismatches <= 8: + print(f"\n Mismatch at 0x{offset+i:04X}: " + f"wrote 0x{expected:02X} read 0x{got:02X}") + + if offset % 256 == 0 or offset + remaining >= len(image): + pct = (offset + remaining) * 100 // len(image) + print(f"\r Verify: 0x{offset+remaining:04X}/0x{len(image):04X} " + f"[{pct:3d}%]", end="", flush=True) + + print() + + if mismatches == 0: + print(f"\n VERIFIED: all {len(image)} bytes match") + print(f" Flash complete in {elapsed:.1f}s") + print(f"\n Power-cycle the device to boot the new firmware.") + if args.power_cycle: + do_power_cycle() + else: + print(f"\n VERIFY FAILED: {mismatches} byte(s) differ " + f"(first at 0x{first_mismatch:04X})") + if mismatches > 8: + print(f" ({mismatches - 8} additional mismatches not shown)") + print(f"\n *** DO NOT POWER CYCLE ***") + print(f" Re-run this tool to retry, or use an external programmer.") + sys.exit(1) + + +def do_power_cycle(): + """Power-cycle SkyWalker-1 via uhubctl.""" + print(f"\n Power-cycling via uhubctl...") + try: + # Off + r = subprocess.run( + ['sudo', 'uhubctl', '-l', '1-5.4.4', '-p', '3', '-a', 'off'], + capture_output=True, text=True, timeout=10) + if r.returncode != 0: + print(f" uhubctl off failed: {r.stderr.strip()}") + print(f" Manually unplug and replug the device.") + return + print(f" Port 3 powered off") + time.sleep(2) + + # On + r = subprocess.run( + ['sudo', 'uhubctl', '-l', '1-5.4.4', '-p', '3', '-a', 'on'], + capture_output=True, text=True, timeout=10) + if r.returncode != 0: + print(f" uhubctl on failed: {r.stderr.strip()}") + return + print(f" Port 3 powered on") + print(f" Waiting for device to enumerate...") + + time.sleep(3) + dev = usb.core.find(idVendor=SKYWALKER_VID, idProduct=SKYWALKER_PID) + if dev: + print(f" Device found: Bus {dev.bus} Addr {dev.address}") + print(f" New firmware is running!") + else: + print(f" Device not found yet — may need a few more seconds") + + except FileNotFoundError: + print(f" uhubctl not found — manually power-cycle the device") + except subprocess.TimeoutExpired: + print(f" uhubctl timed out") + + +def cmd_power_cycle(args): + """Power-cycle the SkyWalker-1 via uhubctl.""" + print("SkyWalker-1 Power Cycle") + print("=" * 45) + do_power_cycle() + + +def main(): + parser = argparse.ArgumentParser( + description="SkyWalker-1 EEPROM flash via host-side I2C (boot ROM 0xA0)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +This tool drives the FX2LP's I2C controller from the host while the +CPU is halted. It works around the stock firmware's broken I2C proxy +and the BERR bug triggered by CPUCS restart. + +examples: + %(prog)s probe # test if host-side I2C works + %(prog)s read -o backup.bin # backup current EEPROM + %(prog)s write firmware_eeprom.bin # flash new C2 image + %(prog)s write firmware_eeprom.bin -P # flash + auto power-cycle + %(prog)s power-cycle # just power-cycle the device +""") + sub = parser.add_subparsers(dest='command', required=True) + + # probe + sub.add_parser('probe', help='Test host-side I2C bus access') + + # read + p_read = sub.add_parser('read', help='Read EEPROM contents') + p_read.add_argument('-o', '--output', help='Save to file') + p_read.add_argument('--size', type=int, default=EEPROM_SIZE, + help=f'Bytes to read (default: {EEPROM_SIZE})') + p_read.add_argument('--hex', dest='hex_dump', action='store_true', + help='Show hex dump') + + # write + p_write = sub.add_parser('write', help='Flash C2 image to EEPROM') + p_write.add_argument('file', help='C2 firmware image (.bin)') + p_write.add_argument('--dry-run', action='store_true', + help='Show what would happen without writing') + p_write.add_argument('--no-backup', action='store_true', + help='Skip header backup check') + p_write.add_argument('--force', action='store_true', + help='Continue despite warnings') + p_write.add_argument('-P', '--power-cycle', action='store_true', + help='Auto power-cycle after successful flash') + + # power-cycle + sub.add_parser('power-cycle', help='Power-cycle via uhubctl') + + args = parser.parse_args() + + dispatch = { + 'probe': cmd_probe, + 'read': cmd_read, + 'write': cmd_write, + 'power-cycle': cmd_power_cycle, + } + dispatch[args.command](args) + + +if __name__ == '__main__': + main() diff --git a/tools/eeprom_pll_find.py b/tools/eeprom_pll_find.py new file mode 100644 index 0000000..41086dc --- /dev/null +++ b/tools/eeprom_pll_find.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Find the exact EEPROM address where PLL config starts. + +The stock firmware's FUN_CODE_10F2 presumably finds the PLL data by +computing an offset from the C2 boot firmware structure. This script +parses the C2 records to find where the firmware ends, then checks +if PLL data immediately follows. +""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_EEPROM_READ = 0xC0 + +sw = SkyWalker1() +sw.open() + + +def eeprom_read(addr, length): + return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length) + + +print('=== C2 Firmware Structure Analysis ===') +print() + +# Read C2 header (8 bytes) +header = eeprom_read(0x0000, 8) +print(f'Header: {header.hex(" ")}') +assert header[0] == 0xC2, 'Not a C2 EEPROM!' +vid = header[2] << 8 | header[1] +pid = header[4] << 8 | header[3] +print(f' VID: 0x{vid:04X} PID: 0x{pid:04X}') +print() + +# Parse C2 load records +offset = 8 +records = [] +print('C2 Load Records:') +while offset < 0x8000: + hdr = eeprom_read(offset, 4) + rec_len = (hdr[0] << 8) | hdr[1] + rec_addr = (hdr[2] << 8) | hdr[3] + + if rec_len == 0x8001: + print(f' [{len(records)}] END MARKER at EEPROM 0x{offset:04X} → entry=0x{rec_addr:04X}') + records.append({'type': 'end', 'offset': offset, 'entry': rec_addr}) + offset += 4 + break + elif rec_len == 0 or rec_len > 0x4000: + print(f' [{len(records)}] INVALID at EEPROM 0x{offset:04X}: len=0x{rec_len:04X}') + records.append({'type': 'invalid', 'offset': offset}) + offset += 4 + break + + end_addr = rec_addr + rec_len - 1 + print(f' [{len(records)}] {rec_len:5d} bytes at EEPROM 0x{offset:04X} → RAM 0x{rec_addr:04X}-0x{end_addr:04X}') + records.append({'type': 'data', 'offset': offset, 'len': rec_len, 'addr': rec_addr}) + offset += 4 + rec_len + +print(f'\nFirmware ends at EEPROM offset: 0x{offset:04X}') +print() + +# Check what's right after the firmware +print(f'--- Data immediately after firmware (0x{offset:04X}) ---') +for addr in range(offset, offset + 128, 16): + data = eeprom_read(addr, 16) + hex_str = ' '.join(f'{b:02X}' for b in data) + print(f' {addr:04X}: {hex_str}') + +print() + +# Now check: does PLL data start right after the C2 firmware? +print(f'--- PLL block check starting at 0x{offset:04X} ---') +for addr in range(offset, offset + 200, 20): + block = eeprom_read(addr, 20) + count = block[0] + if count == 0: + print(f' 0x{addr:04X}: [sentinel count=0]') + break + elif 1 <= count <= 16: + ab = block[4:4 + count] + print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} ' + f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]') + else: + print(f' 0x{addr:04X}: count=0x{count:02X} (invalid, not PLL data)') + break + +print() + +# Also check the known PLL location from the scan +print('--- Confirmed PLL data at 0x125C (from EEPROM scan) ---') +for addr in range(0x125C, 0x12C0, 20): + block = eeprom_read(addr, 20) + count = block[0] + if count == 0: + print(f' 0x{addr:04X}: [sentinel count=0]') + break + elif 1 <= count <= 16: + ab = block[4:4 + count] + print(f' 0x{addr:04X}: count={count} A9=0x{block[1]:02X} AA=0x{block[2]:02X} ' + f'unused=0x{block[3]:02X} AB=[{ab.hex(" ")}]') + else: + print(f' 0x{addr:04X}: NOT PLL (count=0x{count:02X})') + break + +sw.close() +print() +print('=== Done ===') diff --git a/tools/eeprom_sentinel_scan.py b/tools/eeprom_sentinel_scan.py new file mode 100644 index 0000000..ec7ff0f --- /dev/null +++ b/tools/eeprom_sentinel_scan.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Find the count=0 sentinel in the BCM4500 firmware data at 0x4000+.""" +import sys +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_EEPROM_READ = 0xC0 +sw = SkyWalker1() +sw.open() + + +def ee(addr, length): + return sw._vendor_in(CMD_EEPROM_READ, value=addr, index=length, length=length) + + +print('=== Scanning for count=0 sentinel from 0x4000 ===') +print() + +blocks = 0 +total_bytes = 0 +addr = 0x4000 + +while addr < 0x8000: + data = ee(addr, 20) + count = data[0] + + if count == 0: + print(f' SENTINEL FOUND at 0x{addr:04X} after {blocks} blocks ({total_bytes} payload bytes)') + break + + if count > 16: + print(f' INVALID count=0x{count:02X} at 0x{addr:04X} after {blocks} blocks') + # Show context + for ctx_addr in range(max(0x4000, addr - 60), addr + 60, 20): + d = ee(ctx_addr, 20) + marker = ' ← INVALID' if ctx_addr == addr else '' + print(f' 0x{ctx_addr:04X}: count={d[0]:3d} A9=0x{d[1]:02X} AA=0x{d[2]:02X}{marker}') + break + + # Valid block + a9 = data[1] + aa = data[2] + ab_bytes = count + total_bytes += ab_bytes + + if blocks < 5 or blocks % 100 == 0: + ab = data[4:4 + min(count, 8)] + print(f' Block {blocks:4d} @ 0x{addr:04X}: count={count:2d} A9=0x{a9:02X} AA=0x{aa:02X} ' + f'AB=[{ab.hex(" ")}{"..." if count > 8 else ""}]') + + blocks += 1 + addr += 20 + +else: + print(f' NO SENTINEL found before 0x8000 ({blocks} blocks scanned)') + +print() +print(f'Summary: {blocks} blocks, {total_bytes} payload bytes') +print(f'Address range: 0x4000 - 0x{addr:04X}') + +# Show the sentinel and what follows +if addr < 0x8000: + print() + print(f'--- Data around sentinel at 0x{addr:04X} ---') + for a in range(max(0x4000, addr - 40), addr + 60, 20): + d = ee(a, 20) + cnt = d[0] + if cnt == 0: + print(f' 0x{a:04X}: [SENTINEL count=0] rest: {d[1:].hex(" ")}') + elif 1 <= cnt <= 16: + print(f' 0x{a:04X}: count={cnt} A9=0x{d[1]:02X} AA=0x{d[2]:02X}') + else: + print(f' 0x{a:04X}: [non-PLL: 0x{cnt:02X}] {d[:16].hex(" ")}') + +sw.close() +print() +print('=== Done ===') diff --git a/tools/i2c_host_test.py b/tools/i2c_host_test.py new file mode 100644 index 0000000..f13a587 --- /dev/null +++ b/tools/i2c_host_test.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Test I2C communication from host side while FX2LP CPU is halted. + +When the CPU is halted (CPUCS=1), I2CS=0x0A (clean state). When the CPU +runs (CPUCS=0), I2CS=0xF6 (stuck). This script tests whether the I2C +controller is functional during CPU halt by attempting to read the boot +EEPROM header (which MUST contain 0xC0 or 0xC2). + +Uses USB vendor command 0xA0 to read/write XDATA registers directly. +""" + +import sys +import time +import usb.core +import usb.util + +# XDATA register addresses +I2CS_ADDR = 0xE678 +I2DAT_ADDR = 0xE679 +I2CTL_ADDR = 0xE67A +CPUCS_ADDR = 0xE600 + +# I2CS bit masks +bmSTART = 0x80 +bmSTOP = 0x40 +bmLASTRD = 0x20 +bmBERR = 0x04 +bmACK = 0x02 +bmDONE = 0x01 + +EEPROM_ADDR_W = 0xA2 # 0x51 << 1 | 0 (write) +EEPROM_ADDR_R = 0xA3 # 0x51 << 1 | 1 (read) + +def fx2_read(dev, addr, length=1): + """Read XDATA register(s) via USB vendor command 0xA0.""" + return dev.ctrl_transfer(0xC0, 0xA0, addr, 0, length, timeout=1000) + +def fx2_write(dev, addr, data): + """Write XDATA register(s) via USB vendor command 0xA0.""" + dev.ctrl_transfer(0x40, 0xA0, addr, 0, data, timeout=1000) + +def i2cs_str(val): + """Decode I2CS register value.""" + flags = [] + if val & 0x80: flags.append('START') + if val & 0x40: flags.append('STOP') + if val & 0x20: flags.append('LASTRD') + if val & 0x04: flags.append('BERR') + if val & 0x02: flags.append('ACK') + if val & 0x01: flags.append('DONE') + return f"0x{val:02X} ({' | '.join(flags) if flags else 'idle'})" + +def wait_done(dev, timeout_ms=100): + """Poll I2CS for DONE bit.""" + deadline = time.monotonic() + timeout_ms / 1000.0 + while time.monotonic() < deadline: + i2cs = fx2_read(dev, I2CS_ADDR, 1)[0] + if i2cs & bmDONE: + return True, i2cs + time.sleep(0.001) + return False, i2cs + +def main(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("SkyWalker-1 not found") + sys.exit(1) + + print("SkyWalker-1 I2C Host-Side Test") + print("=" * 50) + + # Step 1: Halt CPU + print("\n[1] Halting CPU (CPUCS=1)...") + fx2_write(dev, CPUCS_ADDR, bytes([0x01])) + time.sleep(0.05) + + # Step 2: Read I2C state + i2cs = fx2_read(dev, I2CS_ADDR, 1)[0] + i2ctl = fx2_read(dev, I2CTL_ADDR, 1)[0] + print(f" I2CS = {i2cs_str(i2cs)}") + print(f" I2CTL = 0x{i2ctl:02X}") + + if i2cs == 0xF6: + print(" WARNING: I2CS stuck at 0xF6 even during CPU halt!") + print(" I2C test will likely fail.") + + # Step 3: Try to write I2CTL and read back + print("\n[2] Testing register writability...") + fx2_write(dev, I2CTL_ADDR, bytes([0x01])) # 400kHz + time.sleep(0.01) + i2ctl_rb = fx2_read(dev, I2CTL_ADDR, 1)[0] + print(f" Wrote I2CTL=0x01, read back 0x{i2ctl_rb:02X}", end="") + if i2ctl_rb == 0x01: + print(" (write works!)") + else: + print(f" (write IGNORED — register is read-only during halt)") + + # Step 4: Try hardware I2C — read EEPROM header byte at address 0x0000 + print("\n[3] Attempting EEPROM read via hardware I2C...") + print(" Target: EEPROM 0x51, address 0x0000 (boot header)") + + # Issue START + print("\n [START] Writing I2CS = 0x80...") + fx2_write(dev, I2CS_ADDR, bytes([bmSTART])) + done, i2cs = wait_done(dev, 200) + print(f" I2CS = {i2cs_str(i2cs)}, DONE={'YES' if done else 'NO'}") + + if not done: + print(" START didn't complete. Trying alternative: write I2DAT first...") + # Some controllers need I2DAT written before START can proceed + # Write EEPROM address (0xA2 = 0x51 write) + fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_W])) + time.sleep(0.01) + i2cs = fx2_read(dev, I2CS_ADDR, 1)[0] + print(f" After I2DAT=0xA2: I2CS = {i2cs_str(i2cs)}") + + if not done: + # Try the Cypress-documented sequence: START is issued by + # writing the slave address to I2DAT AFTER writing START to I2CS + print("\n Trying standard Cypress I2C sequence...") + # Re-issue START + fx2_write(dev, I2CS_ADDR, bytes([bmSTART])) + time.sleep(0.001) + # Write slave address — this should clock the address byte + fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_W])) + done, i2cs = wait_done(dev, 200) + print(f" After START + I2DAT=0xA2: I2CS = {i2cs_str(i2cs)}, DONE={'YES' if done else 'NO'}") + + if done: + ack = 'ACK' if (i2cs & bmACK) else 'NAK' + print(f" Address phase: {ack}") + + if done: + # Write EEPROM address bytes (16-bit address: 0x0000) + print("\n Writing address 0x0000...") + fx2_write(dev, I2DAT_ADDR, bytes([0x00])) # addr high + done, i2cs = wait_done(dev, 200) + ack = 'ACK' if (i2cs & bmACK) else 'NAK' + print(f" Addr high: {ack}, DONE={'YES' if done else 'NO'}") + + if done: + fx2_write(dev, I2DAT_ADDR, bytes([0x00])) # addr low + done, i2cs = wait_done(dev, 200) + ack = 'ACK' if (i2cs & bmACK) else 'NAK' + print(f" Addr low: {ack}, DONE={'YES' if done else 'NO'}") + + if done: + # Re-START for read phase + print("\n Re-START for read phase...") + fx2_write(dev, I2CS_ADDR, bytes([bmSTART])) + time.sleep(0.001) + fx2_write(dev, I2DAT_ADDR, bytes([EEPROM_ADDR_R])) + done, i2cs = wait_done(dev, 200) + ack = 'ACK' if (i2cs & bmACK) else 'NAK' + print(f" Read addr: {ack}, DONE={'YES' if done else 'NO'}") + + if done: + # Read first byte (and only byte — set LASTRD + STOP) + print("\n Reading first byte (LASTRD + STOP)...") + fx2_write(dev, I2CS_ADDR, bytes([bmLASTRD])) + time.sleep(0.001) + # Dummy read to trigger byte transfer + dummy = fx2_read(dev, I2DAT_ADDR, 1)[0] + done, i2cs = wait_done(dev, 200) + if done: + data = fx2_read(dev, I2DAT_ADDR, 1)[0] + fx2_write(dev, I2CS_ADDR, bytes([bmSTOP])) + print(f" Boot header byte: 0x{data:02X}", end="") + if data == 0xC0: + print(" (C0 = no renumerate)") + elif data == 0xC2: + print(" (C2 = renumerate)") + else: + print(f" (unexpected!)") + print("\n *** EEPROM READ SUCCESSFUL! ***") + else: + print(f" Read DONE timeout. I2CS = {i2cs_str(i2cs)}") + + # Final state + i2cs_final = fx2_read(dev, I2CS_ADDR, 1)[0] + print(f"\n[4] Final I2CS = {i2cs_str(i2cs_final)}") + + # Try STOP to clean up + fx2_write(dev, I2CS_ADDR, bytes([bmSTOP])) + time.sleep(0.01) + i2cs_stop = fx2_read(dev, I2CS_ADDR, 1)[0] + print(f" After STOP: I2CS = {i2cs_str(i2cs_stop)}") + + # Release CPU + print("\n[5] Releasing CPU (CPUCS=0)...") + fx2_write(dev, CPUCS_ADDR, bytes([0x00])) + time.sleep(0.5) + print(" CPU released. Device will re-enumerate.") + +if __name__ == '__main__': + main() diff --git a/tools/i2c_recovery_boot.py b/tools/i2c_recovery_boot.py new file mode 100644 index 0000000..a5d62b5 --- /dev/null +++ b/tools/i2c_recovery_boot.py @@ -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() diff --git a/tools/i2c_register_test.py b/tools/i2c_register_test.py new file mode 100644 index 0000000..c651862 --- /dev/null +++ b/tools/i2c_register_test.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Quick diagnostic: test if 0xA0 writes to I2C registers trigger hardware.""" +import usb.core, usb.util, time + +VID, PID = 0x09C0, 0x0203 +A0 = 0xA0 +I2CS = 0xE678; I2DAT = 0xE679; I2CTL = 0xE67A; CPUCS = 0xE600 + +dev = usb.core.find(idVendor=VID, idProduct=PID) +for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + dev.detach_kernel_driver(intf.bInterfaceNumber) +try: + dev.set_configuration() +except: + pass + +def a0r(addr, n=1): + return bytes(dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, A0, addr, 0, n, 2000)) + +def a0w(addr, val): + dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, A0, addr, 0, bytes([val]), 2000) + +# Pre-halt flush +try: + lock = dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, 0x90, 0, 0, 1, 2000) + print(f"Pre-halt flush (0x90): 0x{lock[0]:02X}") +except Exception as e: + print(f"Flush failed: {e}") + +# Halt +a0w(CPUCS, 0x01) +time.sleep(0.01) +print(f"CPUCS after halt: 0x{a0r(CPUCS)[0]:02X}") + +print(f"\n=== Register State After Halt ===") +i2cs_orig = a0r(I2CS)[0] +i2ctl_orig = a0r(I2CTL)[0] +print(f"I2CS: 0x{i2cs_orig:02X}") +print(f"I2CTL: 0x{i2ctl_orig:02X}") + +# Test 1: I2CTL writability +print(f"\n=== Test 1: I2CTL writability ===") +print(f"I2CTL before: 0x{a0r(I2CTL)[0]:02X}") +a0w(I2CTL, 0x00) +time.sleep(0.001) +v = a0r(I2CTL)[0] +print(f"I2CTL after 0x00 write: 0x{v:02X} {'CHANGED' if v == 0x00 else 'unchanged'}") +a0w(I2CTL, 0x01) +time.sleep(0.001) +v = a0r(I2CTL)[0] +print(f"I2CTL after 0x01 write: 0x{v:02X}") + +# Test 2: Write START to I2CS, read back +print(f"\n=== Test 2: I2CS START ===") +print(f"I2CS before START: 0x{a0r(I2CS)[0]:02X}") +a0w(I2CS, 0x80) # bmSTART +for i in range(5): + val = a0r(I2CS)[0] + print(f" I2CS read {i}: 0x{val:02X}") + time.sleep(0.002) + +# Test 3: Write to I2DAT +print(f"\n=== Test 3: I2DAT write (EEPROM addr) ===") +print(f"I2CS before I2DAT write: 0x{a0r(I2CS)[0]:02X}") +a0w(I2DAT, 0xA2) # EEPROM 0x51<<1 +for i in range(5): + val = a0r(I2CS)[0] + print(f" I2CS read {i}: 0x{val:02X}") + time.sleep(0.002) + +# Test 4: Read I2DAT +print(f"\n=== Test 4: I2DAT read ===") +dat = a0r(I2DAT)[0] +print(f"I2DAT read: 0x{dat:02X}") +print(f"I2CS after I2DAT read: 0x{a0r(I2CS)[0]:02X}") + +# Test 5: Write STOP +print(f"\n=== Test 5: I2CS STOP ===") +a0w(I2CS, 0x40) # bmSTOP +for i in range(3): + val = a0r(I2CS)[0] + print(f" I2CS read {i}: 0x{val:02X}") + time.sleep(0.002) + +# Test 6: Write arbitrary values to I2CS +print(f"\n=== Test 6: I2CS raw write/read ===") +for test_val in [0x00, 0xFF, 0x04, 0x80]: + a0w(I2CS, test_val) + time.sleep(0.001) + rb = a0r(I2CS)[0] + changed = "CHANGED" if rb != i2cs_orig else "unchanged" + print(f" Wrote 0x{test_val:02X}, read 0x{rb:02X} {changed}") + +# Test 7: XDATA RAM write/read (control test) +print(f"\n=== Test 7: XDATA RAM control test ===") +TEST_ADDR = 0x3C00 +orig = a0r(TEST_ADDR)[0] +a0w(TEST_ADDR, 0xBE) +time.sleep(0.001) +readback = a0r(TEST_ADDR)[0] +ok = "OK" if readback == 0xBE else "FAIL" +print(f"RAM[0x3C00]: wrote 0xBE, read 0x{readback:02X} {ok}") + +# Test 8: Try writing 0x04 to I2CS (BERR clear bit) +print(f"\n=== Test 8: BERR clear bit ===") +a0w(I2CS, 0x04) +time.sleep(0.001) +v = a0r(I2CS)[0] +print(f"After writing 0x04: I2CS=0x{v:02X}") + +# Summary +print(f"\n=== Summary ===") +final_i2cs = a0r(I2CS)[0] +print(f"Final I2CS: 0x{final_i2cs:02X} (started at 0x{i2cs_orig:02X})") +if final_i2cs == i2cs_orig: + print("I2CS NEVER CHANGED -- host-side I2C register writes ignored by hardware") + print("\nImplication: 0xA0 writes reach XDATA address space but the I2C") + print("controller only responds to 8051 MOVX instructions, not USB engine writes.") + print("The boot ROM's 0xA0 handler uses a different bus path for register access.") +else: + print("I2CS DID CHANGE -- host-side I2C register writes may work!") diff --git a/tools/indirect_loopback_test.py b/tools/indirect_loopback_test.py new file mode 100644 index 0000000..dbfe964 --- /dev/null +++ b/tools/indirect_loopback_test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""BCM4500 indirect register loopback test. + +Write a known value via indirect write, then read it back. +Tests multiple approaches: +1. Multi-byte A6+A7+A8 in one transaction (0xB2 uses bcm_indirect_write) +2. Separate writes via 0xB6 diagnostic +3. Read via 0xB6 with various delays +4. Read using 0xB1 (bcm_indirect_read wrapper) + +If we can write-then-read a value back, the DSP command processor works. +If not, we need to look at the I2C transaction structure more carefully. +""" +import sys +import time +sys.path.insert(0, 'tools') +from skywalker_lib import SkyWalker1 + +CMD_RAW_DEMOD_READ = 0xB1 +CMD_RAW_DEMOD_WRITE = 0xB2 +CMD_I2C_RAW_READ = 0xB5 +CMD_I2C_DIAG = 0xB6 +BCM4500_ADDR = 0x08 + +sw = SkyWalker1() +sw.open() +print('=== BCM4500 Indirect Register Loopback Test ===') +print(f'Firmware: {sw.get_fw_version()}') + +# Boot with full sequence +print('\n--- Booting BCM4500 (full boot) ---') +result = sw._vendor_in(0x89, value=1, index=0, length=3) +cfg, stage = result[0], result[1] +print(f' Config: 0x{cfg:02X}, Stage: 0x{stage:02X}') +time.sleep(0.1) + +# ============================================================ +# Test 1: Read default indirect register values +# ============================================================ +print('\n=== Test 1: Default indirect register values (0xB1) ===') +for page in [0x00, 0x01, 0x06, 0x07, 0x0A, 0x0F]: + try: + data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=page, index=0, length=1) + print(f' Page 0x{page:02X}: 0x{data[0]:02X}') + except Exception as e: + print(f' Page 0x{page:02X}: FAILED ({e})') + +# ============================================================ +# Test 2: Write via 0xB2 (multi-byte A6+A7+A8), then read via 0xB1 +# ============================================================ +print('\n=== Test 2: Write 0x42 to page 0x00, read back (0xB2 write, 0xB1 read) ===') +try: + # 0xB2: bcm_indirect_write(page=0x00, val=0x42) + # Writes A6=0x00, A7=0x42, A8=0x03 in one 3-byte I2C transaction + sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0x00, index=0x42, length=0) +except Exception: + pass # May not return data + +time.sleep(0.05) + +# Read back via 0xB1 (bcm_indirect_read with 1ms delay) +try: + data = sw._vendor_in(CMD_RAW_DEMOD_READ, value=0x00, index=0, length=1) + print(f' Read back page 0x00: 0x{data[0]:02X}') + if data[0] == 0x42: + print(' >> LOOPBACK SUCCESS! DSP is processing commands!') + elif data[0] == 0x00: + print(' >> Got 0x00 — either DSP not running or write/read protocol broken') + else: + print(f' >> Unexpected: 0x{data[0]:02X}') +except Exception as e: + print(f' Read failed: {e}') + +# ============================================================ +# Test 3: Direct register reads before/after indirect write +# ============================================================ +print('\n=== Test 3: Direct register state before/after indirect commands ===') +print(' Before indirect write:') +for reg in [0xA6, 0xA7, 0xA8]: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + +# Write via 0xB2 +try: + sw._vendor_in(CMD_RAW_DEMOD_WRITE, value=0x06, index=0xAB, length=0) +except Exception: + pass + +time.sleep(0.01) +print(' After indirect write (page=0x06, data=0xAB):') +for reg in [0xA6, 0xA7, 0xA8]: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + +# ============================================================ +# Test 4: Manual step-by-step with individual I2C writes +# ============================================================ +print('\n=== Test 4: Manual indirect read using individual raw I2C writes ===') +print(' Writing A6=0x06 via direct I2C write...') + +# We don't have a raw I2C write command, but we can use the 0xB6 diagnostic +# which does individual writes and reads for us. + +# Test 4a: 0xB6 with READ command (wIndex=0x01) +print('\n 4a: 0xB6 diagnostic — READ page 0x06:') +diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8) +print(f' A6 write ok: {diag[0]}') +print(f' A6 readback: 0x{diag[1]:02X}') +print(f' A8 write ok: {diag[2]}') +print(f' A8 immediate: 0x{diag[3]:02X}') +print(f' A8 after 2ms: 0x{diag[4]:02X}') +print(f' A7 data: 0x{diag[5]:02X}') +print(f' A6 final: 0x{diag[6]:02X}') + +# ============================================================ +# Test 5: Try reading A7 with longer delays +# ============================================================ +print('\n=== Test 5: Indirect read with longer delays ===') +print(' Maybe the DSP needs more time to process the command...') + +# Use 0xB6 to write A6=0x06 and A8=0x01 +sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8) + +for delay_ms in [10, 50, 100, 500]: + time.sleep(delay_ms / 1000.0) + # Read A7 via raw I2C + try: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=0xA7, length=1) + print(f' After {delay_ms:4d}ms: A7=0x{data[0]:02X}') + except Exception as e: + print(f' After {delay_ms:4d}ms: FAILED ({e})') + +# ============================================================ +# Test 6: Direct register dump after all tests +# ============================================================ +print('\n=== Test 6: Register state after all tests ===') +print(' A0-AF:') +for reg in range(0xA0, 0xB0): + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' 0x{reg:02X}=0x{data[0]:02X}', end='') + if (reg % 8) == 7: + print() +print() + +# ============================================================ +# Test 7: Power cycle BCM4500 and check pre-command register state +# ============================================================ +print('\n=== Test 7: Reboot and check BEFORE any indirect commands ===') +sw._vendor_in(0x89, value=0, index=0, length=3) # shutdown +time.sleep(0.5) +sw._vendor_in(0x89, value=1, index=0, length=3) # full boot +time.sleep(0.1) +print(' Fresh boot — direct reg reads (no indirect commands issued):') +for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + +# Now issue ONE indirect read command and check if registers change +print('\n After ONE indirect read (page 0x06):') +diag = sw._vendor_in(CMD_I2C_DIAG, value=0x06, index=0x01, length=8) +print(f' A7 data: 0x{diag[5]:02X} (this is the indirect read result)') + +# Check if direct registers changed +print(' Direct register check after indirect command:') +for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]: + data = sw._vendor_in(CMD_I2C_RAW_READ, value=BCM4500_ADDR, + index=reg, length=1) + print(f' 0x{reg:02X}: 0x{data[0]:02X}') + +sw.close() +print('\n=== Done ===') diff --git a/tools/stock_fw_compare.py b/tools/stock_fw_compare.py new file mode 100644 index 0000000..0d13f6c --- /dev/null +++ b/tools/stock_fw_compare.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Compare BCM4500 register behavior: stock EEPROM firmware vs custom firmware. + +USAGE: +1. Power cycle the SkyWalker-1 (unplug/replug USB) +2. Run this script IMMEDIATELY (before loading custom FW) +3. The script tests registers under stock firmware first, + then loads custom firmware and tests again. + +If stock firmware shows different register behavior, the issue is +in our custom firmware's boot sequence. +""" +import sys +import time +import usb.core +sys.path.insert(0, 'tools') + +VID = 0x09C0 +PID = 0x0203 +BCM4500_ADDR = 0x08 + +# ============================================================ +# Raw USB helpers (work with any firmware) +# ============================================================ +def vendor_in(dev, cmd, value=0, index=0, length=1): + """Send a vendor IN request and return the response bytes.""" + return dev.ctrl_transfer( + 0xC0, # bmRequestType: vendor, device-to-host + cmd, # bRequest + value, # wValue + index, # wIndex + length # wLength + ) + +def vendor_out(dev, cmd, value=0, index=0, data=None): + """Send a vendor OUT request.""" + dev.ctrl_transfer( + 0x40, # bmRequestType: vendor, host-to-device + cmd, # bRequest + value, # wValue + index, # wIndex + data if data else b'' + ) + +def read_bcm_reg(dev, reg): + """Read one BCM4500 register via stock-compatible I2C read (0xB5). + This might not exist on stock firmware, so we use the + stock READ_8PSK_REG (0x81) as a fallback.""" + try: + data = vendor_in(dev, 0xB5, value=BCM4500_ADDR, index=reg, length=1) + return data[0] + except Exception: + return None + +def read_bcm_reg_stock(dev, reg): + """Read BCM4500 register via stock READ_8PSK_REG (0x81). + wValue = register address, returns 1 byte.""" + try: + data = vendor_in(dev, 0x81, value=reg, index=0, length=1) + return data[0] + except Exception: + return None + +def read_all_key_regs(dev, method='0xB5'): + """Read key BCM4500 registers.""" + regs = [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB] + results = {} + for reg in regs: + if method == '0xB5': + val = read_bcm_reg(dev, reg) + else: + val = read_bcm_reg_stock(dev, reg) + results[reg] = val + v_str = f'0x{val:02X}' if val is not None else 'FAIL' + print(f' 0x{reg:02X}: {v_str}') + return results + +# ============================================================ +# MAIN +# ============================================================ +print('=== Stock vs Custom Firmware BCM4500 Comparison ===') +print() + +# Find the device +dev = usb.core.find(idVendor=VID, idProduct=PID) +if dev is None: + print('ERROR: SkyWalker-1 not found!') + print('Make sure to power cycle the device first (stock firmware must be running)') + sys.exit(1) + +print(f'Found SkyWalker-1: Bus {dev.bus} Addr {dev.address}') +print(f' VID=0x{dev.idVendor:04X} PID=0x{dev.idProduct:04X}') + +# Try to get firmware version (our custom command) +try: + fw = vendor_in(dev, 0x80, value=0, index=0, length=3) + print(f' Firmware response (0x80): {list(fw)}') +except Exception as e: + print(f' Firmware version (0x80): {e}') + +# ============================================================ +# Phase 1: Test under stock firmware (before boot command) +# ============================================================ +print('\n' + '='*60) +print('PHASE 1: Stock firmware — BEFORE boot command') +print('='*60) + +print('\n Registers via 0x81 (READ_8PSK_REG):') +regs_stock_pre_81 = {} +for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]: + val = read_bcm_reg_stock(dev, reg) + regs_stock_pre_81[reg] = val + v_str = f'0x{val:02X}' if val is not None else 'FAIL' + print(f' 0x{reg:02X}: {v_str}') + +print('\n Registers via 0xB5 (I2C_RAW_READ) — may fail on stock FW:') +regs_stock_pre_b5 = {} +for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8]: + val = read_bcm_reg(dev, reg) + regs_stock_pre_b5[reg] = val + v_str = f'0x{val:02X}' if val is not None else 'N/A (stock FW lacks 0xB5)' + print(f' 0x{reg:02X}: {v_str}') + +# ============================================================ +# Phase 2: Boot BCM4500 under stock firmware +# ============================================================ +print('\n' + '='*60) +print('PHASE 2: Stock firmware — boot BCM4500 (0x89)') +print('='*60) + +try: + result = vendor_in(dev, 0x89, value=1, index=0, length=3) + print(f' Boot result: [{result[0]:02X}, {result[1]:02X}, {result[2]:02X}]') + cfg = result[0] + bits = [] + if cfg & 0x01: bits.append('Started') + if cfg & 0x02: bits.append('FW_Loaded') + if cfg & 0x04: bits.append('Intersil') + if cfg & 0x08: bits.append('DVBmode') + print(f' Config: 0x{cfg:02X} ({" | ".join(bits) if bits else "none"})') + print(f' Stage: 0x{result[1]:02X}') +except Exception as e: + print(f' Boot failed: {e}') + +time.sleep(0.5) + +print('\n Registers via 0x81 after boot:') +regs_stock_post_81 = {} +for reg in [0xA0, 0xA2, 0xA4, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB]: + val = read_bcm_reg_stock(dev, reg) + regs_stock_post_81[reg] = val + v_str = f'0x{val:02X}' if val is not None else 'FAIL' + marker = '' + if val is not None and reg in regs_stock_pre_81: + if regs_stock_pre_81.get(reg) != val: + marker = f' (was 0x{regs_stock_pre_81[reg]:02X})' if regs_stock_pre_81[reg] is not None else '' + print(f' 0x{reg:02X}: {v_str}{marker}') + +# Try indirect read via stock firmware's 0x81 with indirect addressing +# (0x81 might support indirect reads differently) +print('\n Testing indirect register reads via 0x81:') +for page in [0x00, 0x06, 0x07, 0x0F]: + # Stock firmware might use a different convention for indirect reads + # Try reading page register values + val = read_bcm_reg_stock(dev, page) + v_str = f'0x{val:02X}' if val is not None else 'FAIL' + print(f' Page 0x{page:02X} via 0x81: {v_str}') + +# Try signal monitor +print('\n Signal status:') +try: + sig = vendor_in(dev, 0x82, value=0, index=0, length=4) + print(f' GET_8PSK_SIGNAL (0x82): {list(sig)}') +except Exception as e: + print(f' 0x82: {e}') + +try: + lock = vendor_in(dev, 0x83, value=0, index=0, length=1) + print(f' GET_8PSK_LOCK (0x83): 0x{lock[0]:02X}') +except Exception as e: + print(f' 0x83: {e}') + +# ============================================================ +# Summary +# ============================================================ +print('\n' + '='*60) +print('SUMMARY') +print('='*60) + +def compare_regs(label, regs): + vals = set(v for v in regs.values() if v is not None) + if len(vals) == 1: + print(f' {label}: ALL = 0x{list(vals)[0]:02X}') + elif len(vals) == 0: + print(f' {label}: ALL FAILED') + else: + print(f' {label}: Mixed: {", ".join(f"0x{v:02X}" for v in sorted(vals))}') + +compare_regs('Stock FW before boot (0x81)', regs_stock_pre_81) +compare_regs('Stock FW after boot (0x81)', regs_stock_post_81) + +print() +print('If stock FW shows DIFFERENT register values (not all 0x02),') +print('then the BCM4500 is truly functional under stock FW and our') +print('custom boot sequence is missing something.') +print() +print('If stock FW also shows all 0x02, then the register behavior') +print('is normal and the 0x02 IS the legitimate power-on value.') + +print('\n=== Done ===') diff --git a/tools/stock_fw_test.py b/tools/stock_fw_test.py new file mode 100644 index 0000000..bdfba5d --- /dev/null +++ b/tools/stock_fw_test.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +""" +Stock firmware BCM4500 boot diagnostic. + +Runs the stock v2.06 firmware boot sequence (same as kernel gp8psk driver), +then performs comprehensive register dumps to capture the BCM4500 state — +including the critical A9/AA/AB PLL registers written by FUN_CODE_10F2 +(which our custom firmware currently skips). + +Also scans the I2C bus to find device 0x51 (calibration EEPROM) that the +stock firmware reads PLL configuration data from. + +Usage: + python tools/stock_fw_test.py [--dump-all] [--i2c-scan] + +Requirements: + - SkyWalker-1 connected via USB (stock firmware loaded) + - pyusb installed +""" + +import sys +import struct + +try: + import usb.core + import usb.util +except ImportError: + print("pyusb required: pip install pyusb") + sys.exit(1) + + +# USB IDs +VID = 0x09C0 +PID = 0x0203 + +# Stock firmware vendor commands (same as kernel driver) +GET_8PSK_CONFIG = 0x80 +I2C_WRITE = 0x83 +I2C_READ = 0x84 +ARM_TRANSFER = 0x85 +TUNE_8PSK = 0x86 +GET_SIGNAL_STRENGTH = 0x87 +BOOT_8PSK = 0x89 +START_INTERSIL = 0x8A +SET_LNB_VOLTAGE = 0x8B +GET_SIGNAL_LOCK = 0x90 +GET_FW_VERS = 0x92 + +# Config status bits +BM_STARTED = 0x01 +BM_FW_LOADED = 0x02 +BM_INTERSIL = 0x04 + +# I2C addresses (7-bit) +BCM4500_ADDR = 0x08 +BCM3440_ADDR = 0x10 +EEPROM_ADDR = 0x51 # Calibration EEPROM found in FUN_CODE_10F2 disassembly + + +def vendor_in(dev, cmd, value=0, index=0, length=1): + """Send a vendor IN control transfer (device -> host).""" + return bytes(dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, + cmd, value, index, length, 2000)) + + +def vendor_out(dev, cmd, value=0, index=0, data=None): + """Send a vendor OUT control transfer (host -> device).""" + dev.ctrl_transfer( + usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_OUT, + cmd, value, index, data or b'', 2000) + + +def decode_config(cfg): + """Decode config status byte to human-readable string.""" + bits = [] + names = [ + (0x01, "Started"), (0x02, "FW_Loaded"), (0x04, "Intersil"), + (0x08, "DVBmode"), (0x10, "22kHz"), (0x20, "18V"), + (0x40, "DCtuned"), (0x80, "Armed"), + ] + for mask, name in names: + if cfg & mask: + bits.append(name) + return " | ".join(bits) if bits else "(none)" + + +def i2c_read(dev, addr, reg, length=1): + """Read from I2C device via stock firmware 0x84 command.""" + return vendor_in(dev, I2C_READ, value=addr, index=reg, length=length) + + +def i2c_scan(dev, start=0x03, end=0x77): + """Scan I2C bus for responding devices using stock firmware I2C_READ.""" + found = [] + for addr in range(start, end + 1): + try: + val = i2c_read(dev, addr, 0x00, length=1) + found.append((addr, val[0])) + except usb.core.USBError: + pass + return found + + +def dump_bcm4500_direct_regs(dev): + """Read all BCM4500 direct registers 0xA0-0xBF after boot. + + These are the PLL/config registers. FUN_CODE_10F2 writes to A0, A9, AA, AB + during boot from calibration EEPROM data. Our custom firmware skips this. + """ + regs = {} + for reg in range(0xA0, 0xC0): + try: + val = i2c_read(dev, BCM4500_ADDR, reg, length=1) + regs[reg] = val[0] + except usb.core.USBError: + regs[reg] = None + return regs + + +def main(): + dump_all = "--dump-all" in sys.argv + do_scan = "--i2c-scan" in sys.argv or dump_all + + print("=" * 60) + print("Stock Firmware BCM4500 Boot Diagnostic") + print("=" * 60) + + # --- Step 0: Find device --- + dev = usb.core.find(idVendor=VID, idProduct=PID) + if dev is None: + print("\nERROR: SkyWalker-1 not found on USB") + sys.exit(1) + + print(f"\nFound SkyWalker-1: Bus {dev.bus} Addr {dev.address}") + product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "?" + serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "?" + print(f" Product: {product}") + print(f" Serial: {serial}") + + # Detach kernel driver if attached + for cfg in dev: + for intf in cfg: + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + dev.detach_kernel_driver(intf.bInterfaceNumber) + try: + dev.set_configuration() + except Exception: + pass + + # --- Step 1: Pre-boot state --- + print("\n--- Step 1: Pre-boot state ---") + cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1) + print(f" Config: 0x{cfg[0]:02X} = {decode_config(cfg[0])}") + + try: + ver = vendor_in(dev, GET_FW_VERS, length=6) + print(f" FW Version: {ver[2]}.{ver[0]:02d}.{ver[1]}") + except Exception as e: + print(f" FW Version: read failed ({e})") + + # --- Step 2: Boot BCM4500 (kernel driver sequence) --- + print("\n--- Step 2: BOOT_8PSK (0x89, wValue=1) ---") + try: + result = vendor_in(dev, BOOT_8PSK, value=1, length=1) + print(f" Boot response: 0x{result[0]:02X} = {decode_config(result[0])}") + except usb.core.USBError as e: + print(f" Boot FAILED: {e}") + try: + result = vendor_in(dev, BOOT_8PSK, value=1, length=3) + print(f" Boot response (3 bytes): {result.hex(' ')}") + except usb.core.USBError as e2: + print(f" Boot also failed with 3 bytes: {e2}") + sys.exit(1) + + cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1) + started = bool(cfg[0] & BM_STARTED) + fw_loaded = bool(cfg[0] & BM_FW_LOADED) + print(f" Config after boot: 0x{cfg[0]:02X} = {decode_config(cfg[0])}") + print(f" Started: {started}, FW Loaded: {fw_loaded}") + + # --- Step 3: Enable LNB power supply (Intersil) --- + print("\n--- Step 3: START_INTERSIL (0x8A, wValue=1) ---") + try: + result = vendor_in(dev, START_INTERSIL, value=1, length=1) + print(f" Intersil response: 0x{result[0]:02X} = {decode_config(result[0])}") + except usb.core.USBError as e: + print(f" Intersil FAILED: {e}") + + # --- Step 4: Cancel pending MPEG transfers --- + print("\n--- Step 4: ARM_TRANSFER (0x85, wValue=0) ---") + try: + vendor_out(dev, ARM_TRANSFER, value=0) + print(" OK") + except usb.core.USBError as e: + print(f" Failed: {e}") + + cfg = vendor_in(dev, GET_8PSK_CONFIG, length=1) + print(f" Final config: 0x{cfg[0]:02X} = {decode_config(cfg[0])}") + + # --- Step 5: Signal strength (indirect register reads) --- + print("\n--- Step 5: Signal reads ---") + sig_all_zero = True + try: + sig = vendor_in(dev, GET_SIGNAL_STRENGTH, length=6) + snr_raw = struct.unpack_from(' 0x7E else (reg & 0xFE) + markers = [] + if val == echo_expected: + markers.append("ECHO") + if reg in (0xA9, 0xAA, 0xAB): + markers.append("★ PLL") + if reg == 0xA0: + markers.append("CONFIG_MODE") + tag = f" ({', '.join(markers)})" if markers else "" + print(f" 0x{reg:02X}: 0x{val:02X}{tag}") + + # Highlight the critical PLL values + a9 = regs.get(0xA9) + aa = regs.get(0xAA) + ab = regs.get(0xAB) + a0 = regs.get(0xA0) + print(f"\n *** Critical PLL registers ***") + print(f" A0 (config mode): 0x{a0:02X}" if a0 is not None else " A0: FAILED") + print(f" A9 (PLL div?): 0x{a9:02X}" if a9 is not None else " A9: FAILED") + print(f" AA (PLL div?): 0x{aa:02X}" if aa is not None else " AA: FAILED") + print(f" AB (PLL cfg?): 0x{ab:02X}" if ab is not None else " AB: FAILED") + + # Try multi-byte read of AB (FUN_CODE_10F2 writes variable-length data here) + print("\n AB multi-byte read (up to 8 bytes):") + try: + ab_multi = i2c_read(dev, BCM4500_ADDR, 0xAB, length=8) + print(f" {ab_multi.hex(' ')}") + except usb.core.USBError as e: + print(f" FAILED: {e}") + + # --- Step 7: Other known registers --- + print("\n--- Step 7: Other BCM4500 registers ---") + other_regs = [(0xF0, "F0"), (0xF8, "F8"), (0xF9, "F9")] + for reg, name in other_regs: + try: + val = i2c_read(dev, BCM4500_ADDR, reg, length=1) + echo = min(reg & 0xFE, 0x7E) + marker = " (ECHO)" if val[0] == echo else "" + print(f" Reg 0x{name}: 0x{val[0]:02X}{marker}") + except usb.core.USBError as e: + print(f" Reg 0x{name}: FAILED ({e})") + + # --- Step 8: BCM3440 tuner control read --- + print("\n--- Step 8: BCM3440 tuner control read (@ 0x10) ---") + tuner_ok = False + try: + val = i2c_read(dev, BCM3440_ADDR, 0x00, length=4) + tuner_ok = not all(b == 0 for b in val) + print(f" Tuner regs 0x00-0x03: {val.hex(' ')} {'(OK)' if tuner_ok else '(all zero!)'}") + except usb.core.USBError as e: + print(f" Tuner read failed: {e}") + + # --- Step 9: I2C bus scan --- + if do_scan: + print("\n--- Step 9: I2C bus scan (0x03-0x77) ---") + print(" Scanning for all responding devices...") + devices = i2c_scan(dev) + if devices: + for addr, first_byte in devices: + label = "" + if addr == BCM4500_ADDR: + label = " ← BCM4500 demod" + elif addr == BCM3440_ADDR: + label = " ← BCM3440 tuner" + elif addr == 0x28: + label = " ← EEPROM? (0x51 wire >> 1)" + elif 0x50 <= addr <= 0x57: + label = " ← EEPROM range" + elif addr == EEPROM_ADDR: + label = " ← device 0x51 from FUN_CODE_10F2!" + print(f" 0x{addr:02X} (7-bit) = 0x{addr << 1:02X}/{addr << 1 | 1:02X} (wire) " + f"first byte: 0x{first_byte:02X}{label}") + else: + print(" No devices found!") + + # Try reading from device 0x51 specifically (calibration EEPROM) + print("\n --- Device 0x51 probe (calibration EEPROM) ---") + for try_addr in [EEPROM_ADDR, 0x50, 0x28, 0x29]: + try: + val = i2c_read(dev, try_addr, 0x00, length=16) + print(f" Addr 0x{try_addr:02X} reg 0x00 (16 bytes): {val.hex(' ')}") + # If we get data, try reading more + if not all(b == 0xFF for b in val): + val2 = i2c_read(dev, try_addr, 0x00, length=64) + print(f" Addr 0x{try_addr:02X} reg 0x00 (64 bytes):") + for row in range(0, len(val2), 16): + chunk = val2[row:row+16] + print(f" +{row:02X}: {chunk.hex(' ')}") + except usb.core.USBError: + print(f" Addr 0x{try_addr:02X}: no response") + else: + print("\n (use --i2c-scan or --dump-all for I2C bus scan)") + + # --- Summary --- + print("\n" + "=" * 60) + # Check for echo pattern on critical registers + pll_echo = (a9 is not None and a9 == 0xA8) or (aa is not None and aa == 0xAA) + if not sig_all_zero: + print("BCM4500 CORE IS ALIVE under stock firmware!") + print(" → Problem is in our custom firmware boot sequence") + if a9 is not None and aa is not None: + print(f" → PLL values to replicate: A9=0x{a9:02X} AA=0x{aa:02X} AB=0x{ab:02X}") + elif pll_echo: + print("BCM4500 CORE IS DEAD (echo pattern on PLL registers)") + print(" → FUN_CODE_10F2 may not have run, or EEPROM missing") + else: + print("BCM4500 CORE STATUS UNCERTAIN") + print(" → Check register values above for non-echo data") + print("=" * 60) + + +if __name__ == "__main__": + main()