Add experimental I2C debugging and EEPROM analysis tools

One-off diagnostic scripts from experiments 0xD7-0xDB investigating
the I2C BERR deadlock. Documents the systematic elimination of
software-only recovery approaches:

- i2c_host_test.py: Proved 0xA0 register writes cannot drive I2C bus
- i2c_register_test.py: Tested I2C register writability from host
- i2c_recovery_boot.py: Attempted I2C state machine recovery via boot
- eeprom_flash_a0.py: Host-side EEPROM flash attempt (failed)
- boot_ab_test.py / boot_test.py: EEPROM boot reliability testing
- a8_autoclear_test.py: BCM4500 command register auto-clear behavior
- addr_gateway_test.py: BCM3440 gateway address routing analysis
- stock_fw_compare.py / stock_fw_test.py: Stock vs custom fw analysis
This commit is contained in:
Ryan Malloy 2026-02-20 10:57:10 -07:00
parent 97c1000d8b
commit 0d6facb321
16 changed files with 3279 additions and 0 deletions

View File

@ -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 ===')

128
tools/addr_gateway_test.py Normal file
View File

@ -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 ===')

194
tools/boot_ab_test.py Normal file
View File

@ -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()

154
tools/boot_deep_verify.py Normal file
View File

@ -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()

189
tools/boot_reg_probe.py Normal file
View File

@ -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 ===')

177
tools/boot_test.py Normal file
View File

@ -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 ===')

112
tools/eeprom_deep_scan.py Normal file
View File

@ -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 ===')

736
tools/eeprom_flash_a0.py Normal file
View File

@ -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()

108
tools/eeprom_pll_find.py Normal file
View File

@ -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 ===')

View File

@ -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 ===')

196
tools/i2c_host_test.py Normal file
View File

@ -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()

259
tools/i2c_recovery_boot.py Normal file
View File

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

126
tools/i2c_register_test.py Normal file
View File

@ -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!")

View File

@ -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 ===')

210
tools/stock_fw_compare.py Normal file
View File

@ -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 ===')

342
tools/stock_fw_test.py Normal file
View File

@ -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('<H', sig, 0)[0]
sig_all_zero = all(b == 0 for b in sig)
print(f" Signal strength (0x87): {sig.hex(' ')}")
print(f" SNR raw: 0x{snr_raw:04X} ({snr_raw}), all_zero: {sig_all_zero}")
except usb.core.USBError as e:
print(f" Signal strength read failed: {e}")
try:
lock = vendor_in(dev, GET_SIGNAL_LOCK, length=1)
print(f" Lock status (0x90): 0x{lock[0]:02X} {'LOCKED' if lock[0] else 'no lock'}")
except usb.core.USBError as e:
print(f" Lock status read failed: {e}")
# --- Step 6: FULL BCM4500 direct register dump (0xA0-0xBF) ---
# This captures the PLL/clock values written by FUN_CODE_10F2
print("\n--- Step 6: BCM4500 direct registers 0xA0-0xBF (post-boot) ---")
print(" (A0=config_mode, A9/AA/AB=PLL from EEPROM, A6/A7/A8=indirect)")
regs = dump_bcm4500_direct_regs(dev)
for reg in sorted(regs.keys()):
val = regs[reg]
if val is None:
print(f" 0x{reg:02X}: FAILED")
continue
# Check for echo pattern (dead core symptom)
echo_expected = min(reg & 0xFE, 0x7E) if reg > 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()