From d9f51548e0b0f547c2aea982c373cb2c359b69e7 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 12 Feb 2026 10:34:15 -0700 Subject: [PATCH] Fix BCM4500 boot: spurious I2C STOP corrupted FX2 controller Removed I2CS bmSTOP "bus reset" from bcm4500_boot() and debug modes. Sending STOP with no active transaction puts the FX2 I2C controller into an inconsistent state where subsequent START+ACK detection fails. Root cause identified through incremental debug modes (wValue 0x80-0x85) on live hardware: mode 0x82 (with bmSTOP) fails, mode 0x85 (identical but without bmSTOP) succeeds. Raw I2C reads confirm BCM4500 is alive the entire time -- only the controller state is corrupted. BCM4500 now boots successfully in ~90ms. Three I2C devices found on bus: 0x08 (BCM4500), 0x10 (tuner/LNB), 0x51 (EEPROM). Also in this commit: - Timeout-protected I2C functions replacing fx2lib bare while loops - I2C bus scan and debug mode infrastructure - Kernel driver blacklist for dvb_usb_gp8psk - Test tools for incremental boot debugging - Technical findings documented in docs/boot-debug-findings.md --- docs/boot-debug-findings.md | 257 ++++++++++++++++++++++ firmware/skywalker1.c | 426 +++++++++++++++++++++++++++++++----- tools/test_boot.py | 171 +++++++++++++++ tools/test_boot_debug.py | 127 +++++++++++ tools/test_i2c_debug.py | 118 ++++++++++ tools/test_i2c_isolate.py | 126 +++++++++++ tools/test_i2c_pinpoint.py | 122 +++++++++++ 7 files changed, 1296 insertions(+), 51 deletions(-) create mode 100644 docs/boot-debug-findings.md create mode 100644 tools/test_boot.py create mode 100644 tools/test_boot_debug.py create mode 100644 tools/test_i2c_debug.py create mode 100644 tools/test_i2c_isolate.py create mode 100644 tools/test_i2c_pinpoint.py diff --git a/docs/boot-debug-findings.md b/docs/boot-debug-findings.md new file mode 100644 index 0000000..713898d --- /dev/null +++ b/docs/boot-debug-findings.md @@ -0,0 +1,257 @@ +# BOOT_8PSK Debugging Findings + +Technical reference for the BCM4500 demodulator boot sequence on the Genpix SkyWalker-1 (Cypress FX2 CY7C68013A + Broadcom BCM4500), firmware v3.01.0. Documents the root cause analysis of a firmware hang during I2C initialization and the fixes applied. + +**Hardware:** Genpix SkyWalker-1 USB 2.0 DVB-S receiver +**MCU:** Cypress CY7C68013A (FX2LP), 8051 core at 48MHz +**Demodulator:** Broadcom BCM4500 +**Firmware:** Custom v3.01.0 (SDCC + fx2lib) +**I2C bus speed:** 400kHz + +--- + +## The Problem + +Custom firmware v3.01.0 implements vendor command `BOOT_8PSK` (bRequest=0x89, wValue=1), which powers on the BCM4500 demodulator and initializes it via I2C. When first tested, this command caused the FX2 firmware to hang for over 10 seconds, making the USB device completely unresponsive -- no vendor command would return, and the host-side USB stack would report timeout errors. + +The initial suspicion was infinite I2C loops. The fx2lib I2C library uses bare `while` loops that poll hardware status bits with no timeout: + +```c +// fx2lib/lib/i2c.c -- original code +while ( !(I2CS & bmDONE) && !cancel_i2c_trans); +``` + +The `cancel_i2c_trans` variable is intended as an external abort mechanism, but nothing in the firmware sets it during normal operation. If the I2C controller never asserts `bmDONE` (for example, because a slave is holding SCL low), the firmware spins indefinitely in this loop. + +Adding I2C timeout protection (described below) eliminated the infinite-hang symptom, but the boot sequence still failed: the BCM4500 probe read returned NACK, and all three register initialization blocks failed. + +## Root Cause: Spurious I2C STOP Condition + +The boot function originally included a so-called "I2C bus reset" step before any I2C communication: + +```c +I2CS |= bmSTOP; +i2c_wait_stop(); +``` + +This pattern appears in various FX2 example code and seems reasonable on its face -- send a STOP condition to ensure the I2C bus is in a known idle state before starting fresh. On the FX2's I2C controller hardware, this is incorrect. + +### Incremental Debug Modes + +The root cause was discovered through a series of incremental debug modes added to the `BOOT_8PSK` vendor command handler. Each mode executes a subset of the full boot sequence, isolating which step introduces the failure: + +| wValue | Action | Result | +|--------|--------|--------| +| `0x80` | No-op: return `config_status` and `boot_stage` only | Works | +| `0x81` | GPIO + power + delays only (no I2C at all) | Works | +| `0x82` | GPIO + power + `bmSTOP` + I2C probe read | **Fails** | +| `0x83` | GPIO + power + `bmSTOP` + probe + init block 0 | **Fails** (same root cause) | +| `0x84` | `bcm_direct_read` only (no GPIO, chip already powered) | Works | +| `0x85` | GPIO + power + reset, **no** `bmSTOP`, then probe | Works | + +Three observations clinch the diagnosis: + +1. **Mode 0x82 fails but mode 0x85 succeeds.** These two modes are identical except that 0x82 issues `I2CS |= bmSTOP` before the probe read and 0x85 does not. The `bmSTOP` is the only difference, and it is the only thing that breaks I2C. + +2. **Mode 0x84 succeeds immediately after 0x82 fails.** Mode 0x84 calls `bcm_direct_read` with no GPIO manipulation or bus reset -- just a plain I2C combined read. If called after a failed 0x82, it succeeds. This proves two things: the BCM4500 is alive and responding on I2C, and the `i2c_combined_read` function itself is correct. The failure in 0x82 is not a timing or power issue. + +3. **Raw I2C reads via vendor command 0xB5 succeed after 0x82 fails.** Command 0xB5 uses the same `i2c_combined_read` function as `bcm_direct_read`. Running it from the host side after a failed 0x82 returns valid data from the BCM4500. This confirms the chip was alive the whole time -- the FX2's I2C controller was in a bad state, not the bus or the slave. + +The test scripts that drove this investigation are in the `tools/` directory: + +- `test_boot_debug.py` -- sends debug modes 0x80 through 0x83 sequentially +- `test_i2c_debug.py` -- powers on via 0x81, runs bus scans, tests probe timing +- `test_i2c_isolate.py` -- tests whether re-reset or insufficient delay causes failure +- `test_i2c_pinpoint.py` -- the definitive test: compares 0x84, 0x85, and 0x82 + +### What Happens Inside the FX2 I2C Controller + +The FX2's I2C master controller is a hardware peripheral accessed through the `I2CS`, `I2DAT`, and `I2CTL` SFRs. The controller implements an I2C state machine in silicon. Writing `bmSTOP` to `I2CS` instructs the hardware to generate a STOP condition (SDA rising while SCL is high). + +When no I2C transaction is active -- no prior START has been issued, and the bus is idle -- writing `bmSTOP` puts the controller into an inconsistent internal state. The `bmSTOP` bit may not clear properly (it is supposed to self-clear when the STOP condition completes on the bus), and subsequent START conditions fail to generate proper clock sequences or detect ACK from slaves. + +The Cypress TRM (EZ-USB Technical Reference Manual) does not explicitly warn against this, but the I2C chapter describes STOP as a step that follows a completed read or write transaction. It is not documented as a standalone bus-reset mechanism. + +The correct way to ensure a clean I2C bus state on the FX2 is to simply proceed with a new START condition. If the bus is idle (which it will be after power-on or after the previous transaction completed normally), the START succeeds and the controller enters its normal operating state. The hardware handles bus arbitration automatically on START. + +## The Fix + +The fix is a single deletion. Remove the spurious STOP from the boot sequence: + +```c +/* BEFORE (broken): */ +I2CS |= bmSTOP; +i2c_wait_stop(); + +/* AFTER (correct): */ +/* NOTE: Do NOT send I2CS bmSTOP here. Sending STOP when no transaction + * is active corrupts the FX2 I2C controller state, causing subsequent + * START+ACK detection to fail. The I2C bus will be in a clean state + * when we reach the probe step -- any prior transaction ended with STOP. */ +``` + +The corrected `bcm4500_boot()` function proceeds directly from GPIO/power setup to the I2C probe read without any bus-reset step: + +```c +static BOOL bcm4500_boot(void) { + boot_stage = 1; + cancel_i2c_trans = FALSE; + + /* P3.7, P3.6, P3.5 HIGH (idle state for control lines) */ + IOD |= 0xE0; + + /* Assert BCM4500 hardware RESET (P0.5 LOW) */ + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + + /* No I2CS bmSTOP here -- see note above */ + + /* Power on: P0.1 HIGH (enable), P0.2 LOW (disable off) */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + + boot_stage = 2; + delay(30); /* power settle */ + + IOA |= PIN_BCM_RESET; /* release reset */ + delay(50); /* BCM4500 POR + mask ROM boot */ + + boot_stage = 3; + /* I2C probe -- if this fails, the chip didn't come out of reset */ + if (!bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) + return FALSE; + + /* ... register init blocks follow ... */ +} +``` + +## I2C Timeout Protection + +Even with the `bmSTOP` fix, timeout protection on all I2C operations is essential. The FX2's I2C controller has no hardware timeout -- if a slave device holds SCL low (clock stretching), or if an electrical fault prevents `bmDONE` from asserting, the firmware will spin forever in a polling loop. + +### The Problem with fx2lib + +The fx2lib `i2c_write()` and `i2c_read()` functions poll `bmDONE` and `bmSTOP` with loops like: + +```c +while ( !(I2CS & bmDONE) && !cancel_i2c_trans); +``` + +The `cancel_i2c_trans` flag is declared as `volatile __xdata BOOL` and is set to `FALSE` at the start of each transaction. The library documentation says firmware can set it to `TRUE` from an interrupt to abort a stuck transaction. In practice, nothing in the firmware sets it, so these loops are effectively: + +```c +while (!(I2CS & bmDONE)); // infinite if bmDONE never asserts +``` + +### Timeout-Protected Replacements + +The custom firmware replaces all fx2lib I2C functions with timeout-protected wrappers: + +```c +#define I2C_TIMEOUT 6000 + +static BOOL i2c_wait_done(void) { + WORD timeout = I2C_TIMEOUT; + while (!(I2CS & bmDONE)) { + if (--timeout == 0) + return FALSE; + } + return TRUE; +} + +static BOOL i2c_wait_stop(void) { + WORD timeout = I2C_TIMEOUT; + while (I2CS & bmSTOP) { + if (--timeout == 0) + return FALSE; + } + return TRUE; +} +``` + +A `WORD` counter of 6000, decremented in a tight SDCC-compiled loop at 48MHz (4 clocks per 8051 machine cycle, ~12 MIPS), gives approximately 5-10ms per wait. At 400kHz I2C, a single byte transfer (9 clock pulses) takes 22.5 microseconds, so this timeout provides well over 200x margin for normal operations while still bounding the worst case. + +All BCM4500 I2C operations -- `i2c_combined_read`, `i2c_write_timeout`, `i2c_write_multi_timeout` -- use these timeout-protected waits and return `FALSE` on timeout, allowing the caller to report failure rather than hanging the firmware. + +## Kernel Driver Race Condition + +The `dvb_usb_gp8psk` kernel module auto-loads via udev when VID:PID `09C0:0203` appears on the USB bus. This happens every time the FX2 re-enumerates after firmware load. The kernel driver races with the test tools and sends its own `BOOT_8PSK` command (along with other initialization), which interferes with debugging. + +Symptoms of this race condition: +- Test scripts report "resource busy" or "entity not found" errors +- The BCM4500 enters an unexpected state because the kernel driver partially initialized it +- The kernel driver detaches from the device mid-test + +The fix is to blacklist the module: + +``` +# /etc/modprobe.d/blacklist-gp8psk.conf +blacklist dvb_usb_gp8psk +blacklist gp8psk_fe +``` + +After creating this file, run `sudo modprobe -r dvb_usb_gp8psk gp8psk_fe` to unload any currently-loaded instances. The blacklist prevents udev from auto-loading the module on device insertion, giving test tools exclusive access. + +## I2C Bus Scan Results + +Vendor command `0xB4` performs a full 7-bit I2C bus scan by attempting a START + address + WRITE to every address from 0x01 to 0x77 and checking for ACK. Three devices were found: + +| Address | Identity | +|---------|----------| +| `0x08` | BCM4500 demodulator. Status register `0xA2` returns valid data. This is the primary device for all demodulator operations. | +| `0x10` | Likely the tuner or LNB controller. The SkyWalker-1 uses a separate tuner IC (accessed through the BCM4500 in normal operation, but also directly addressable on the shared I2C bus). | +| `0x51` | Likely a configuration EEPROM. Many DVB-S receivers store tuner calibration data or device serial numbers in a small I2C EEPROM at addresses in the 0x50-0x57 range. | + +The BCM4500's 7-bit I2C address of `0x08` corresponds to 8-bit wire addresses of `0x10` (write) and `0x11` (read). + +## BCM4500 Boot Results After Fix + +With the `bmSTOP` removed, the full boot sequence completes reliably: + +- **Boot time:** ~90ms total (30ms power settle + 50ms post-reset delay + ~10ms I2C init) +- **config_status:** `0x03` (STARTED | FW_LOADED) +- **boot_stage:** `0xFF` (COMPLETE) +- **Direct registers 0xA2-0xA8:** All return `0x02` (powered, not locked -- expected without a satellite signal) +- **Signal lock:** `0x00` (no lock -- dish not aimed at satellite) +- **Signal strength:** All zeros (same reason) +- **USB responsiveness:** No hang. The firmware remains fully responsive to vendor commands throughout boot and afterward. + +## Firmware v3.01.0 Boot Sequence (Corrected) + +The complete boot sequence as implemented in `bcm4500_boot()`: + +1. **Assert BCM4500 RESET** -- Drive P0.5 LOW. This holds the BCM4500's digital logic in reset while power is applied. + +2. **Power on** -- Set P0.1 HIGH (power enable), P0.2 LOW (power disable off). The SkyWalker-1 has complementary power control pins. + +3. **delay(30ms)** -- Allow the power supply to settle and reach regulation. The stock firmware uses the same delay. + +4. **Release RESET** -- Drive P0.5 HIGH. The BCM4500 begins its internal power-on reset (POR) and mask ROM boot sequence. + +5. **delay(50ms)** -- Wait for the BCM4500's POR and internal initialization to complete. The chip needs time for its internal oscillator to stabilize and mask ROM to execute. + +6. **I2C probe** -- Read direct register `0xA2` (status) to verify the chip is alive and responding on I2C. If this fails, the boot aborts. + +7. **Write init block 0** -- 7 bytes to BCM4500 indirect page 0, starting at register `0x06`. Written via the `0xA6`/`0xA7`/`0xA8` indirect register protocol. Data: `{0x06, 0x0b, 0x17, 0x38, 0x9f, 0xd9, 0x80}`. + +8. **Write init block 1** -- 8 bytes to page 0, starting at register `0x07`. Data: `{0x07, 0x09, 0x39, 0x4f, 0x00, 0x65, 0xb7, 0x10}`. + +9. **Write init block 2** -- 3 bytes to page 0, starting at register `0x0F`. Data: `{0x0f, 0x0c, 0x09}`. + +10. **Set config_status** -- OR in `BM_STARTED | BM_FW_LOADED` (`0x03`). Subsequent vendor commands (tuning, signal strength readout, etc.) check this flag before operating. + +The three initialization blocks were extracted from disassembly of the stock v2.06 firmware's `FUN_CODE_0ddd` routine, which performs the same indirect register writes. + +## FX2 Hardware Recovery Note + +The FX2's CPUCS register at address `0xE600` controls the 8051 CPU's run/halt state. It is accessible via the standard vendor request bRequest=0xA0 (RAM read/write) even when the user firmware is completely hung in an infinite loop. + +This works because bRequest=0xA0 is handled by the FX2 silicon's boot ROM, not by firmware. The boot ROM's USB handler runs in a hardware-priority context that preempts the 8051's main loop. Writing `0x01` to CPUCS halts the CPU, new firmware can be loaded into RAM, and writing `0x00` starts it again. + +This means `fw_load.py` can reload firmware over a hung device without requiring a physical USB unplug/replug or power cycle. For iterative firmware development, this is significant -- a failed boot attempt that hangs the firmware can be recovered from the host side in seconds: + +```bash +sudo python3 tools/fw_load.py load firmware/build/skywalker1.ihx --wait 3 +``` + +The load sequence halts the CPU (CPUCS=0x01), writes new code into RAM, then restarts the CPU (CPUCS=0x00). The device re-enumerates with the new firmware. diff --git a/firmware/skywalker1.c b/firmware/skywalker1.c index fe9d0d2..48b4060 100644 --- a/firmware/skywalker1.c +++ b/firmware/skywalker1.c @@ -65,28 +65,78 @@ #define BM_ARMED 0x80 /* GPIO pin definitions for v2.06 hardware */ -#define PIN_22KHZ 0x08 /* P0.3 */ -#define PIN_LNB_VOLT 0x10 /* P0.4 */ -#define PIN_DISEQC 0x80 /* P0.7 */ +#define PIN_PWR_EN 0x02 /* P0.1 -- power supply enable */ +#define PIN_PWR_DIS 0x04 /* P0.2 -- power supply disable */ +#define PIN_22KHZ 0x08 /* P0.3 */ +#define PIN_LNB_VOLT 0x10 /* P0.4 */ +#define PIN_BCM_RESET 0x20 /* P0.5 -- BCM4500 hardware reset (active LOW) */ +#define PIN_DISEQC 0x80 /* P0.7 */ /* configuration status byte -- stored in ordinary variable */ static volatile BYTE config_status; +/* boot progress tracker for diagnostics (0=not started, 1-6=step, 0xFF=done) */ +static volatile BYTE boot_stage; + /* ISR flag */ volatile __bit got_sud; /* I2C scratch buffers in xdata */ -static __xdata BYTE i2c_buf[8]; +static __xdata BYTE i2c_buf[16]; static __xdata BYTE i2c_rd[8]; +/* + * BCM4500 register initialization data extracted from stock v2.06 firmware. + * FUN_CODE_0ddd writes these 3 blocks to BCM4500 indirect registers (page 0) + * via the A6/A7/A8 control interface during BOOT_8PSK. + */ +static const __code BYTE bcm_init_block0[] = { + 0x06, 0x0b, 0x17, 0x38, 0x9f, 0xd9, 0x80 +}; +static const __code BYTE bcm_init_block1[] = { + 0x07, 0x09, 0x39, 0x4f, 0x00, 0x65, 0xb7, 0x10 +}; +static const __code BYTE bcm_init_block2[] = { + 0x0f, 0x0c, 0x09 +}; +#define BCM_INIT_BLOCK0_LEN 7 +#define BCM_INIT_BLOCK1_LEN 8 +#define BCM_INIT_BLOCK2_LEN 3 + /* ---------- BCM4500 I2C helpers ---------- */ +/* + * I2C timeout: ~5ms at 48MHz CPU clock (4 clocks/cycle, ~12 MIPS). + * At 400kHz I2C, one byte = 22.5us; 5ms gives >200x margin. + * The FX2 I2C controller has no hardware timeout -- if a slave holds + * SCL low (clock stretching), the master waits forever without this. + */ +#define I2C_TIMEOUT 6000 + +static BOOL i2c_wait_done(void) { + WORD timeout = I2C_TIMEOUT; + while (!(I2CS & bmDONE)) { + if (--timeout == 0) + return FALSE; + } + return TRUE; +} + +static BOOL i2c_wait_stop(void) { + WORD timeout = I2C_TIMEOUT; + while (I2CS & bmSTOP) { + if (--timeout == 0) + return FALSE; + } + return TRUE; +} + /* * Combined I2C write-read with repeated START (no STOP between * write and read phases). Many I2C devices including the BCM4500 * require this pattern instead of separate write+stop/read+stop. * - * Sequence: START → addr+W → reg → RESTART → addr+R → data → STOP + * Sequence: START -> addr+W -> reg -> RESTART -> addr+R -> data -> STOP */ static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { BYTE i; @@ -95,23 +145,23 @@ static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { /* START + write address */ I2CS |= bmSTART; I2DAT = addr << 1; - while (!(I2CS & bmDONE)) - ; + if (!i2c_wait_done()) + goto fail; if (!(I2CS & bmACK)) goto fail; /* Write register address */ I2DAT = reg; - while (!(I2CS & bmDONE)) - ; + if (!i2c_wait_done()) + goto fail; if (!(I2CS & bmACK)) goto fail; /* REPEATED START + read address */ I2CS |= bmSTART; I2DAT = (addr << 1) | 1; - while (!(I2CS & bmDONE)) - ; + if (!i2c_wait_done()) + goto fail; if (!(I2CS & bmACK)) goto fail; @@ -123,8 +173,8 @@ static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { tmp = I2DAT; for (i = 0; i < len; i++) { - while (!(I2CS & bmDONE)) - ; + if (!i2c_wait_done()) + goto fail; if (i == len - 2) I2CS |= bmLASTRD; if (i == len - 1) @@ -132,25 +182,84 @@ static BOOL i2c_combined_read(BYTE addr, BYTE reg, BYTE len, BYTE *buf) { buf[i] = I2DAT; } - while (I2CS & bmSTOP) - ; + i2c_wait_stop(); return TRUE; fail: I2CS |= bmSTOP; - while (I2CS & bmSTOP) - ; + i2c_wait_stop(); + return FALSE; +} + +/* + * I2C write with timeout -- writes addr+reg+data without using fx2lib, + * so we have full control over timeout behavior. + * Sends: START -> (addr<<1) -> reg -> data -> STOP + */ +static BOOL i2c_write_timeout(BYTE addr, BYTE reg, BYTE val) { + /* START + write address */ + I2CS |= bmSTART; + I2DAT = addr << 1; + if (!i2c_wait_done()) goto fail; + if (!(I2CS & bmACK)) goto fail; + + /* Register address */ + I2DAT = reg; + if (!i2c_wait_done()) goto fail; + if (!(I2CS & bmACK)) goto fail; + + /* Data byte */ + I2DAT = val; + if (!i2c_wait_done()) goto fail; + + /* STOP */ + I2CS |= bmSTOP; + i2c_wait_stop(); + return TRUE; + +fail: + I2CS |= bmSTOP; + i2c_wait_stop(); + return FALSE; +} + +/* + * Multi-byte I2C write with timeout. + * Sends: START -> (addr<<1) -> reg -> data[0..len-1] -> STOP + */ +static BOOL i2c_write_multi_timeout(BYTE addr, BYTE reg, BYTE len, + __xdata BYTE *data) { + BYTE i; + + I2CS |= bmSTART; + I2DAT = addr << 1; + if (!i2c_wait_done()) goto fail; + if (!(I2CS & bmACK)) goto fail; + + I2DAT = reg; + if (!i2c_wait_done()) goto fail; + if (!(I2CS & bmACK)) goto fail; + + for (i = 0; i < len; i++) { + I2DAT = data[i]; + if (!i2c_wait_done()) goto fail; + } + + I2CS |= bmSTOP; + i2c_wait_stop(); + return TRUE; + +fail: + I2CS |= bmSTOP; + i2c_wait_stop(); return FALSE; } /* * Write one byte to a BCM4500 direct I2C register (subaddr). - * This writes to the I2C register directly, not through the - * indirect protocol. */ static BOOL bcm_direct_write(BYTE reg, BYTE val) { - i2c_buf[0] = val; - return i2c_write(BCM4500_ADDR, 1, ®, 1, i2c_buf); + return i2c_write_timeout(BCM4500_ADDR, reg, val); } /* @@ -167,11 +276,10 @@ static BOOL bcm_direct_read(BYTE reg, BYTE *val) { * [0xA6] = page, [0xA7] = data, [0xA8] = 0x03 (write cmd) */ static BOOL bcm_indirect_write(BYTE reg, BYTE val) { - BYTE start_reg = BCM_REG_PAGE; i2c_rd[0] = reg; i2c_rd[1] = val; i2c_rd[2] = BCM_CMD_WRITE; - return i2c_write(BCM4500_ADDR, 1, &start_reg, 3, i2c_rd); + return i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_PAGE, 3, i2c_rd); } /* @@ -181,12 +289,11 @@ static BOOL bcm_indirect_write(BYTE reg, BYTE val) { * Then read the result from 0xA7. */ static BOOL bcm_indirect_read(BYTE reg, BYTE *val) { - BYTE start_reg = BCM_REG_PAGE; - /* page, placeholder data, read command — written to A6,A7,A8 in one shot */ + /* page, placeholder data, read command -- written to A6,A7,A8 in one shot */ i2c_rd[0] = reg; i2c_rd[1] = 0x00; i2c_rd[2] = BCM_CMD_READ; - if (!i2c_write(BCM4500_ADDR, 1, &start_reg, 3, i2c_rd)) + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_PAGE, 3, i2c_rd)) return FALSE; delay(1); return i2c_combined_read(BCM4500_ADDR, BCM_REG_DATA, 1, val); @@ -197,15 +304,10 @@ static BOOL bcm_indirect_read(BYTE reg, BYTE *val) { * Page select, then N data bytes to 0xA7, then commit with 0x03. */ static BOOL bcm_indirect_write_block(BYTE page, __xdata BYTE *data, BYTE len) { - BYTE reg; - - reg = BCM_REG_PAGE; - i2c_buf[0] = page; - if (!i2c_write(BCM4500_ADDR, 1, ®, 1, i2c_buf)) + if (!bcm_direct_write(BCM_REG_PAGE, page)) return FALSE; - reg = BCM_REG_DATA; - if (!i2c_write(BCM4500_ADDR, 1, ®, len, data)) + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, len, data)) return FALSE; if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) @@ -220,16 +322,134 @@ static BOOL bcm_indirect_write_block(BYTE page, __xdata BYTE *data, BYTE len) { */ static BOOL bcm_poll_ready(void) { BYTE i, val; - for (i = 0; i < 20; i++) { + for (i = 0; i < 10; i++) { if (bcm_direct_read(BCM_REG_CMD, &val)) { if (!(val & 0x01)) return TRUE; } - delay(5); + delay(2); } return FALSE; } +/* + * Write one block of initialization data to BCM4500 indirect registers. + * Replicates FUN_CODE_0ddd's per-iteration I2C sequence from stock firmware: + * 1. Write 0x00 to reg 0xA6 (page select = page 0) + * 2. Write data[0..len-1] to reg 0xA7 (data buffer, auto-increment) + * 3. Write 0x00 to reg 0xA7 (trailing zero -- stock firmware sends this) + * 4. Write 0x03 to reg 0xA8 (commit indirect write) + * 5. Wait for BCM4500 to finish processing + */ +static BOOL bcm_write_init_block(const __code BYTE *data, BYTE len) { + BYTE i; + + /* Page select = 0 */ + if (!bcm_direct_write(BCM_REG_PAGE, 0x00)) + return FALSE; + + /* Copy block data from code space to xdata scratch buffer */ + for (i = 0; i < len; i++) + i2c_buf[i] = data[i]; + + /* Write data bytes to 0xA7 */ + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, len, i2c_buf)) + return FALSE; + + /* Trailing zero to 0xA7 (stock firmware does this as separate write) */ + if (!bcm_direct_write(BCM_REG_DATA, 0x00)) + return FALSE; + + /* Commit: write command 0x03 to 0xA8 */ + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) + return FALSE; + + /* Wait for BCM4500 to process the write */ + return bcm_poll_ready(); +} + +/* + * BCM4500 full boot sequence, reverse-engineered from stock firmware + * FUN_CODE_1D4F (reset/power) + FUN_CODE_0ddd (register init). + * + * GPIO sequence from disassembly: + * P3 |= 0xE0 -- P3.7, P3.6, P3.5 HIGH (control lines idle) + * P0 &= ~0x20 -- P0.5 LOW = assert BCM4500 hardware RESET + * I2C bus reset + * P0.1 set, P0.2 clr -- power supply enable + * delay(30) -- wait for power settle + * P0 |= 0x20 -- P0.5 HIGH = release BCM4500 from RESET + * Write 3 register initialization blocks + */ +static BOOL bcm4500_boot(void) { + boot_stage = 1; /* Stage 1: GPIO setup */ + + /* Ensure fx2lib I2C functions won't spin forever */ + cancel_i2c_trans = FALSE; + + /* P3.7, P3.6, P3.5 HIGH (idle state for control lines) */ + IOD |= 0xE0; + + /* Assert BCM4500 hardware RESET (P0.5 LOW) */ + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + + /* NOTE: Do NOT send I2CS bmSTOP here. Sending STOP when no transaction + * is active corrupts the FX2 I2C controller state, causing subsequent + * START+ACK detection to fail. The I2C bus will be in a clean state + * when we reach the probe step -- any prior transaction ended with STOP. */ + + /* Power on: P0.1 HIGH (enable), P0.2 LOW (disable off) */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + + boot_stage = 2; /* Stage 2: power settled, releasing reset */ + + /* Wait for power supply to settle (stock firmware uses 30 iterations) */ + delay(30); + + /* Release BCM4500 from RESET (P0.5 HIGH) */ + IOA |= PIN_BCM_RESET; + + /* Wait for BCM4500 internal POR and mask ROM boot to complete */ + delay(50); + + boot_stage = 3; /* Stage 3: I2C probe */ + + /* Verify BCM4500 is alive on I2C before attempting register init. + * If we can't read a direct register, the chip didn't come out of reset. */ + if (!bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) + return FALSE; + + boot_stage = 4; /* Stage 4: register init block 0 */ + + /* Initialize BCM4500 registers -- 3 blocks from stock firmware */ + if (!bcm_write_init_block(bcm_init_block0, BCM_INIT_BLOCK0_LEN)) + return FALSE; + + boot_stage = 5; /* Stage 5: register init block 1 */ + + if (!bcm_write_init_block(bcm_init_block1, BCM_INIT_BLOCK1_LEN)) + return FALSE; + + boot_stage = 6; /* Stage 6: register init block 2 */ + + if (!bcm_write_init_block(bcm_init_block2, BCM_INIT_BLOCK2_LEN)) + return FALSE; + + boot_stage = 0xFF; /* Success */ + return TRUE; +} + +/* + * BCM4500 shutdown -- reverse of boot. + * From stock firmware FUN_CODE_1D4F shutdown path at 0x1D93. + */ +static void bcm4500_shutdown(void) { + /* Power off: P0.1 LOW (enable off), P0.2 HIGH (disable) */ + IOA = (IOA & ~PIN_PWR_EN) | PIN_PWR_DIS; +} + /* ---------- GPIF streaming ---------- */ static void gpif_start(void) { @@ -658,21 +878,120 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BCL = 6; return TRUE; - /* 0x89: BOOT_8PSK -- initialize BCM4500 demodulator */ + /* 0x89: BOOT_8PSK -- initialize BCM4500 demodulator + * wValue=0: shutdown + * wValue=1: full boot (reset + power + register init) + * wValue=0x80: debug -- return boot_stage only (no-op) + * wValue=0x81: debug -- GPIO setup + delays only + * wValue=0x82: debug -- GPIO + I2C probe only + * wValue=0x83: debug -- GPIO + I2C probe + 1 init block */ case BOOT_8PSK: - if (wval) { - /* Power on: scan for BCM4500 at address 0x10 */ - val = 0; - if (bcm_direct_read(BCM_REG_STATUS, &val)) { - config_status |= BM_STARTED; - config_status |= BM_FW_LOADED; + if (wval == 0x80) { + /* Debug: no-op, just return current state */ + } else if (wval == 0x81) { + /* Debug: GPIO only, no I2C */ + boot_stage = 1; + IOD |= 0xE0; + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + boot_stage = 2; + delay(30); + IOA |= PIN_BCM_RESET; + delay(50); + boot_stage = 0xA1; /* success marker for debug 0x81 */ + } else if (wval == 0x82) { + /* Debug: GPIO + probe read (same as 0x85 now -- bmSTOP removed) */ + boot_stage = 1; + IOD |= 0xE0; + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + /* bmSTOP removed -- corrupts FX2 I2C controller */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + boot_stage = 2; + delay(30); + IOA |= PIN_BCM_RESET; + delay(50); + boot_stage = 3; + if (bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) { + EP0BUF[2] = i2c_rd[0]; + boot_stage = 0xA2; + } else { + EP0BUF[2] = 0xEE; + boot_stage = 0xE3; /* failed at I2C probe */ + } + } else if (wval == 0x83) { + /* Debug: GPIO + probe + first init block */ + boot_stage = 1; + cancel_i2c_trans = FALSE; + IOD |= 0xE0; + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + /* bmSTOP removed -- corrupts FX2 I2C controller */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + boot_stage = 2; + delay(30); + IOA |= PIN_BCM_RESET; + delay(50); + boot_stage = 3; + if (!bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) { + boot_stage = 0xE3; + } else { + boot_stage = 4; + if (bcm_write_init_block(bcm_init_block0, BCM_INIT_BLOCK0_LEN)) + boot_stage = 0xA3; + else + boot_stage = 0xE4; + } + } else if (wval == 0x84) { + /* Debug: I2C-only probe, no GPIO (assumes chip already powered) */ + boot_stage = 3; + if (bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) { + EP0BUF[2] = i2c_rd[0]; + boot_stage = 0xA4; + } else { + EP0BUF[2] = 0xEE; + boot_stage = 0xE3; + } + } else if (wval == 0x85) { + /* Debug: Same as 0x82 but WITHOUT I2C bus reset (no bmSTOP) */ + boot_stage = 1; + IOD |= 0xE0; + OEA |= PIN_BCM_RESET; + IOA &= ~PIN_BCM_RESET; + /* NOTE: no I2CS bmSTOP here, unlike 0x82 */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS); + IOA = (IOA & ~PIN_PWR_DIS) | PIN_PWR_EN; + boot_stage = 2; + delay(30); + IOA |= PIN_BCM_RESET; + delay(50); + boot_stage = 3; + if (bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) { + EP0BUF[2] = i2c_rd[0]; + boot_stage = 0xA5; + } else { + EP0BUF[2] = 0xEE; + boot_stage = 0xE3; + } + } else if (wval) { + if (bcm4500_boot()) { + config_status |= (BM_STARTED | BM_FW_LOADED); + } else { + bcm4500_shutdown(); + config_status &= ~(BM_STARTED | BM_FW_LOADED); } } else { - config_status &= ~BM_STARTED; + bcm4500_shutdown(); + config_status &= ~(BM_STARTED | BM_FW_LOADED); } EP0BUF[0] = config_status; + EP0BUF[1] = boot_stage; EP0BCH = 0; - EP0BCL = 1; + EP0BCL = 3; return TRUE; /* 0x8A: START_INTERSIL -- enable LNB power supply */ @@ -736,10 +1055,10 @@ BOOL handle_vendorcommand(BYTE cmd) { /* 0x92: GET_FW_VERS -- return firmware version and build date */ case GET_FW_VERS: - EP0BUF[0] = 0x01; /* patch -> version 3.00.1 */ - EP0BUF[1] = 0x00; /* minor */ + EP0BUF[0] = 0x00; /* patch -> version 3.01.0 */ + EP0BUF[1] = 0x01; /* minor */ EP0BUF[2] = 0x03; /* major */ - EP0BUF[3] = 0x0B; /* day = 11 */ + EP0BUF[3] = 0x0C; /* day = 12 */ EP0BUF[4] = 0x02; /* month = 2 */ EP0BUF[5] = 0x1A; /* year - 2000 = 26 */ EP0BCH = 0; @@ -973,11 +1292,16 @@ void main(void) { /* Configure I2C: 400kHz */ I2CTL = bm400KHZ; - /* Configure GPIO output enables for LNB/tone/DiSEqC (v2.06 pin map) */ - OEA |= (PIN_22KHZ | PIN_LNB_VOLT | PIN_DISEQC); /* P0.3, P0.4, P0.7 output */ + /* Configure GPIO output enables (v2.06 pin map): + * P0.1=power_en, P0.2=power_dis, P0.3=22kHz, P0.4=LNB, + * P0.5=BCM_reset, P0.7=DiSEqC/streaming */ + OEA |= (PIN_PWR_EN | PIN_PWR_DIS | PIN_22KHZ | PIN_LNB_VOLT | + PIN_BCM_RESET | PIN_DISEQC); - /* Initial GPIO state: LNB off, tone off, DiSEqC idle */ - IOA = 0x84; /* P0.7=1 (idle), P0.2=1 (BCM4500 control) */ + /* Initial GPIO state matches stock firmware: + * P0.7=1 (DiSEqC idle), P0.5=0 (BCM4500 held in reset), + * P0.2=1 (power disable), all others LOW */ + IOA = 0x84; IOD = 0xE1; /* P3.7:5=1 (controls idle), P3.0=1 */ /* EP2 is bulk IN (0x82), 512 byte, double-buffered */ diff --git a/tools/test_boot.py b/tools/test_boot.py new file mode 100644 index 0000000..04f1e0d --- /dev/null +++ b/tools/test_boot.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Test BOOT_8PSK on SkyWalker-1 with custom firmware v3.01.0""" + +import usb.core +import usb.util +import sys +import time + +def find_device(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("Device not found!") + sys.exit(1) + return dev + +def setup_device(dev): + """Detach kernel driver and set configuration.""" + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + print("Detached kernel driver from interface 0") + except Exception as e: + print(f"Driver detach note: {e}") + + try: + dev.set_configuration() + except usb.core.USBError: + # Already configured, that's fine + pass + +def main(): + dev = find_device() + setup_device(dev) + + # GET_FW_VERS (0x92) + print("=" * 50) + ret = dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6) + major, minor, patch = ret[2], ret[1], ret[0] + day, month, year = ret[3], ret[4], ret[5] + 2000 + print(f"Firmware: v{major}.{minor:02d}.{patch} ({year}-{month:02d}-{day:02d})") + + # GET_8PSK_CONFIG (0x80) + ret = dev.ctrl_transfer(0xC0, 0x80, 0, 0, 1) + print(f"Config before boot: 0x{ret[0]:02X}") + + # BOOT_8PSK (0x89) with wValue=1 + print() + print("=" * 50) + print("Sending BOOT_8PSK(1)...") + print(" (This triggers: P0.5 reset, power on, 3-block register init)") + print() + + try: + ret = dev.ctrl_transfer(0xC0, 0x89, 1, 0, 3, timeout=10000) + except usb.core.USBError as e: + print(f"BOOT_8PSK USB error: {e}") + print("The device may have timed out during init.") + print("Trying to read config status anyway...") + try: + ret = dev.ctrl_transfer(0xC0, 0x80, 0, 0, 1) + print(f"Config after attempted boot: 0x{ret[0]:02X}") + except: + print("Device not responding. May need power cycle.") + sys.exit(1) + + status = ret[0] + stage = ret[1] if len(ret) > 1 else 0 + stage_names = { + 0: "NOT_STARTED", 1: "GPIO_SETUP", 2: "PWR_SETTLED", + 3: "I2C_PROBE", 4: "INIT_BLK0", 5: "INIT_BLK1", + 6: "INIT_BLK2", 0xFF: "COMPLETE" + } + flags = [] + if status & 0x01: flags.append("STARTED") + if status & 0x02: flags.append("FW_LOADED") + if status & 0x04: flags.append("INTERSIL") + if status & 0x08: flags.append("DVB_MODE") + if status & 0x10: flags.append("22KHZ") + if status & 0x20: flags.append("SEL18V") + if status & 0x40: flags.append("DC_TUNED") + if status & 0x80: flags.append("ARMED") + print(f"BOOT_8PSK response: 0x{status:02X} [{' | '.join(flags) if flags else 'none'}]") + print(f"Boot stage: 0x{stage:02X} [{stage_names.get(stage, 'UNKNOWN')}]") + + if status & 0x03 == 0x03: + print() + print("*** BCM4500 BOOT SUCCESS! ***") + print() + + # Read direct I2C registers + print("BCM4500 direct registers (via I2C_RAW_READ 0xB5):") + for reg in [0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8]: + try: + r = dev.ctrl_transfer(0xC0, 0xB5, 0x08, reg, 1) + print(f" Reg 0x{reg:02X} = 0x{r[0]:02X}") + except Exception as e: + print(f" Reg 0x{reg:02X}: ERROR {e}") + + # Read indirect registers through our protocol + print() + print("BCM4500 indirect registers (via RAW_DEMOD_READ 0xB1):") + for page in range(16): + try: + r = dev.ctrl_transfer(0xC0, 0xB1, page, 0, 1) + print(f" Page 0x{page:02X} = 0x{r[0]:02X}") + except Exception as e: + print(f" Page 0x{page:02X}: ERROR {e}") + + # I2C diagnostic + print() + print("I2C diagnostic (0xB6) for page 0x00:") + try: + r = dev.ctrl_transfer(0xC0, 0xB6, 0x00, 0, 8) + labels = ["wr_A6", "rb_A6", "wr_A8", "rb_A8", + "rb_A7", "fin_A6", "fin_A7", "fin_A8"] + for i, lab in enumerate(labels): + print(f" {lab}: 0x{r[i]:02X}") + except Exception as e: + print(f" ERROR: {e}") + + # Signal strength + print() + try: + r = dev.ctrl_transfer(0xC0, 0x87, 0, 0, 6) + print(f"Signal strength: {' '.join(f'{b:02X}' for b in r)}") + except Exception as e: + print(f"Signal strength ERROR: {e}") + + # Signal lock + try: + r = dev.ctrl_transfer(0xC0, 0x90, 0, 0, 1) + print(f"Signal lock: 0x{r[0]:02X}") + except Exception as e: + print(f"Signal lock ERROR: {e}") + + else: + print() + print("*** BOOT FAILED ***") + print() + + # I2C bus scan + print("I2C bus scan:") + try: + r = dev.ctrl_transfer(0xC0, 0xB4, 0, 0, 16) + addrs = [] + for bi in range(16): + for bit in range(8): + if r[bi] & (1 << bit): + addrs.append(bi * 8 + bit) + if addrs: + print(f" Found devices at: {[f'0x{a:02X}' for a in addrs]}") + else: + print(" No I2C devices found!") + except Exception as e: + print(f" Scan error: {e}") + + # Try raw I2C reads anyway + print() + print("Raw I2C reads to BCM4500 (0x08):") + for reg in [0xA2, 0xA4, 0xA6, 0xA7, 0xA8]: + try: + r = dev.ctrl_transfer(0xC0, 0xB5, 0x08, reg, 1) + print(f" Reg 0x{reg:02X} = 0x{r[0]:02X}") + except Exception as e: + print(f" Reg 0x{reg:02X}: ERROR {e}") + + print() + print("=" * 50) + +if __name__ == "__main__": + main() diff --git a/tools/test_boot_debug.py b/tools/test_boot_debug.py new file mode 100644 index 0000000..59dcbb4 --- /dev/null +++ b/tools/test_boot_debug.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Incremental BOOT_8PSK debug tester for SkyWalker-1. + +Sends debug boot modes (wValue=0x80..0x83) one at a time to isolate +which stage of the BCM4500 boot sequence hangs the FX2 firmware. + +Usage: + sudo python3 test_boot_debug.py # run all debug stages + sudo python3 test_boot_debug.py 0x82 # run only stage 0x82 +""" + +import usb.core +import usb.util +import sys +import time + +BOOT_8PSK = 0x89 + +def find_device(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("Device not found!") + sys.exit(1) + return dev + +def setup_device(dev): + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except Exception: + pass + try: + dev.set_configuration() + except usb.core.USBError: + pass + +def decode_stage(stage): + names = { + 0x00: "NOT_STARTED", + 0x01: "GPIO_SETUP", + 0x02: "PWR_SETTLED", + 0x03: "I2C_PROBE", + 0x04: "INIT_BLK0", + 0x05: "INIT_BLK1", + 0x06: "INIT_BLK2", + 0xA1: "DEBUG_GPIO_OK", + 0xA2: "DEBUG_PROBE_OK", + 0xA3: "DEBUG_BLK0_OK", + 0xE3: "DEBUG_PROBE_FAIL", + 0xE4: "DEBUG_BLK0_FAIL", + 0xFF: "COMPLETE", + } + return names.get(stage, f"UNKNOWN(0x{stage:02X})") + +def test_mode(dev, wval, label, timeout_ms=3000): + """Send a debug boot mode and read 3-byte response.""" + print(f"\n{'─' * 50}") + print(f" Testing wValue=0x{wval:02X}: {label}") + print(f"{'─' * 50}") + + t0 = time.monotonic() + try: + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, wval, 0, 3, timeout=timeout_ms) + except usb.core.USBError as e: + elapsed = (time.monotonic() - t0) * 1000 + print(f" FAILED after {elapsed:.0f}ms: {e}") + # Try to see if device is still alive + try: + dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6, timeout=1000) + print(" Device still responds to GET_FW_VERS") + except: + print(" Device is HUNG (no response to GET_FW_VERS)") + return None + elapsed = (time.monotonic() - t0) * 1000 + + status = ret[0] + stage = ret[1] if len(ret) > 1 else 0 + probe = ret[2] if len(ret) > 2 else 0 + + print(f" Response in {elapsed:.0f}ms:") + print(f" config_status: 0x{status:02X}") + print(f" boot_stage: 0x{stage:02X} [{decode_stage(stage)}]") + print(f" probe_byte: 0x{probe:02X}") + return ret + +def main(): + dev = find_device() + setup_device(dev) + + # Verify firmware is responding + try: + ret = dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6, timeout=2000) + major, minor, patch = ret[2], ret[1], ret[0] + print(f"Firmware: v{major}.{minor:02d}.{patch}") + except usb.core.USBError as e: + print(f"GET_FW_VERS failed: {e}") + print("Device may be hung. Try reloading firmware with fw_load.py.") + sys.exit(1) + + ret = dev.ctrl_transfer(0xC0, 0x80, 0, 0, 1) + print(f"Config: 0x{ret[0]:02X}") + + # Parse optional argument for single-stage testing + single_stage = None + if len(sys.argv) > 1: + single_stage = int(sys.argv[1], 0) + + stages = [ + (0x80, "No-op: return current state only"), + (0x81, "GPIO setup + power + delays (no I2C)"), + (0x82, "GPIO + I2C bus reset + BCM4500 probe read"), + (0x83, "GPIO + I2C probe + write init block 0"), + ] + + for wval, label in stages: + if single_stage is not None and wval != single_stage: + continue + result = test_mode(dev, wval, label) + if result is None: + print("\n*** STOPPING: device not responding ***") + break + + print(f"\n{'=' * 50}") + print("Debug complete.") + +if __name__ == "__main__": + main() diff --git a/tools/test_i2c_debug.py b/tools/test_i2c_debug.py new file mode 100644 index 0000000..143a313 --- /dev/null +++ b/tools/test_i2c_debug.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""I2C debug tool for SkyWalker-1. + +First powers on the BCM4500 via GPIO debug mode (0x81), then: +1. Runs I2C bus scan (0xB4) to find any devices +2. Tries raw I2C reads (0xB5) to common BCM4500 addresses +3. Tests different post-reset delays +""" + +import usb.core +import usb.util +import sys +import time + +BOOT_8PSK = 0x89 + +def find_device(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("Device not found!") + sys.exit(1) + return dev + +def setup_device(dev): + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except Exception: + pass + try: + dev.set_configuration() + except usb.core.USBError: + pass + +def main(): + dev = find_device() + setup_device(dev) + + # Verify firmware + ret = dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6, timeout=2000) + major, minor, patch = ret[2], ret[1], ret[0] + print(f"Firmware: v{major}.{minor:02d}.{patch}") + + # Step 1: Power on BCM4500 via GPIO-only debug mode + print("\n--- Step 1: Power on BCM4500 (GPIO mode 0x81) ---") + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x81, 0, 3, timeout=3000) + print(f" GPIO setup: stage=0x{ret[1]:02X}") + + # Step 2: I2C bus scan immediately + print("\n--- Step 2: I2C bus scan (immediately after power-on) ---") + try: + ret = dev.ctrl_transfer(0xC0, 0xB4, 0, 0, 16, timeout=5000) + addrs = [] + for bi in range(16): + for bit in range(8): + if ret[bi] & (1 << bit): + addrs.append(bi * 8 + bit) + if addrs: + print(f" Found devices at: {[f'0x{a:02X}' for a in addrs]}") + else: + print(" No I2C devices found!") + except usb.core.USBError as e: + print(f" Bus scan error: {e}") + + # Step 3: Wait longer and scan again + print("\n--- Step 3: Wait 500ms and scan again ---") + time.sleep(0.5) + try: + ret = dev.ctrl_transfer(0xC0, 0xB4, 0, 0, 16, timeout=5000) + addrs = [] + for bi in range(16): + for bit in range(8): + if ret[bi] & (1 << bit): + addrs.append(bi * 8 + bit) + if addrs: + print(f" Found devices at: {[f'0x{a:02X}' for a in addrs]}") + else: + print(" No I2C devices found!") + except usb.core.USBError as e: + print(f" Bus scan error: {e}") + + # Step 4: Try raw I2C reads to various addresses + print("\n--- Step 4: Raw I2C reads (0xB5) to likely BCM4500 addresses ---") + # BCM4500 could be at different addresses depending on pin strapping + # Common: 0x08 (AD=low), 0x0A (AD=high), or even other addresses + candidates = [0x08, 0x09, 0x0A, 0x0B, 0x10, 0x11, 0x68, 0x69, 0x60, 0x61] + for addr in candidates: + for reg in [0xA2, 0x00]: + try: + r = dev.ctrl_transfer(0xC0, 0xB5, addr, reg, 1, timeout=1000) + print(f" Addr 0x{addr:02X} Reg 0x{reg:02X} = 0x{r[0]:02X} <--- RESPONDS!") + except usb.core.USBError: + print(f" Addr 0x{addr:02X} Reg 0x{reg:02X} = (no response)") + + # Step 5: Check I2C bus state + print("\n--- Step 5: I2C controller state ---") + try: + # Read I2CTL and I2CS by inspecting them through a known-working address + # Actually, we can just observe what happens when we try reads + print(" (Bus scan and raw reads above show bus health)") + except Exception as e: + print(f" Error: {e}") + + # Step 6: Try I2C probe via debug mode 0x82 again with a delay + print("\n--- Step 6: Debug probe (0x82) after additional 1s delay ---") + time.sleep(1.0) + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x82, 0, 3, timeout=3000) + stage = ret[1] + probe = ret[2] + if stage == 0xA2: + print(f" PROBE SUCCESS! BCM4500 status = 0x{probe:02X}") + else: + print(f" Probe failed: stage=0x{stage:02X} probe=0x{probe:02X}") + + print("\nDone.") + +if __name__ == "__main__": + main() diff --git a/tools/test_i2c_isolate.py b/tools/test_i2c_isolate.py new file mode 100644 index 0000000..4369c78 --- /dev/null +++ b/tools/test_i2c_isolate.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Isolate whether bcm_direct_read is broken or if re-reset causes the failure. + +Test sequence: +1. Power on BCM4500 with 0x81 (GPIO only) +2. Wait 1s for chip to settle +3. Confirm chip alive via raw read 0xB5 +4. Try bcm_direct_read via debug mode 0x82 (which RE-RESETS the chip) +5. Immediately try raw read 0xB5 again (is chip alive after 0x82's reset?) +6. Wait various delays and retry raw reads + +This tells us if the issue is bcm_direct_read vs insufficient post-reset delay. +""" + +import usb.core +import usb.util +import sys +import time + +BOOT_8PSK = 0x89 + +def find_device(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("Device not found!") + sys.exit(1) + return dev + +def setup_device(dev): + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except Exception: + pass + try: + dev.set_configuration() + except usb.core.USBError: + pass + +def raw_read(dev, addr, reg, label=""): + """Read via 0xB5 raw I2C handler.""" + try: + r = dev.ctrl_transfer(0xC0, 0xB5, addr, reg, 1, timeout=1000) + val = r[0] + ok = val != 0xFF + mark = "OK" if ok else "no-resp" + print(f" {label}Raw read addr=0x{addr:02X} reg=0x{reg:02X} → 0x{val:02X} ({mark})") + return val, ok + except usb.core.USBError as e: + print(f" {label}Raw read addr=0x{addr:02X} reg=0x{reg:02X} → USB ERROR: {e}") + return None, False + +def main(): + dev = find_device() + setup_device(dev) + + ret = dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6, timeout=2000) + major, minor, patch = ret[2], ret[1], ret[0] + print(f"Firmware: v{major}.{minor:02d}.{patch}\n") + + # --- Test A: Verify BCM4500 alive from cold --- + print("=" * 55) + print("TEST A: Power on BCM4500, wait, then raw read") + print("=" * 55) + print(" Sending 0x81 (GPIO power on + reset release)...") + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x81, 0, 3, timeout=3000) + print(f" GPIO done: stage=0x{ret[1]:02X}") + + print(" Waiting 1000ms for BCM4500 to settle...") + time.sleep(1.0) + + raw_read(dev, 0x08, 0xA2, "After 1s: ") + + # --- Test B: Now try bcm_direct_read (which re-resets) --- + print() + print("=" * 55) + print("TEST B: Run debug mode 0x82 (re-resets + probe via bcm_direct_read)") + print("=" * 55) + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x82, 0, 3, timeout=3000) + stage = ret[1] + probe = ret[2] + if stage == 0xA2: + print(f" bcm_direct_read SUCCEEDED: status=0x{probe:02X}") + else: + print(f" bcm_direct_read FAILED: stage=0x{stage:02X} probe=0x{probe:02X}") + + # --- Test C: Immediately try raw read after 0x82 (same I2C function, no reset) --- + print() + print("=" * 55) + print("TEST C: Immediately try raw read 0xB5 (same i2c_combined_read)") + print("=" * 55) + raw_read(dev, 0x08, 0xA2, "Immediate: ") + + # --- Test D: Wait and retry at various intervals --- + print() + print("=" * 55) + print("TEST D: Raw reads with increasing delays after 0x82's reset") + print("=" * 55) + for delay_ms in [100, 200, 500, 1000, 2000]: + time.sleep(delay_ms / 1000.0) + raw_read(dev, 0x08, 0xA2, f"After {delay_ms}ms: ") + + # --- Test E: Redo power-on without reset, then probe --- + print() + print("=" * 55) + print("TEST E: Run 0x81 again (re-power), wait 1s, then 0x82") + print("=" * 55) + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x81, 0, 3, timeout=3000) + print(f" GPIO done: stage=0x{ret[1]:02X}") + time.sleep(1.0) + raw_read(dev, 0x08, 0xA2, "After 0x81+1s: ") + + print(" Now running 0x82 (re-reset + probe)...") + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x82, 0, 3, timeout=3000) + stage = ret[1] + probe = ret[2] + if stage == 0xA2: + print(f" bcm_direct_read SUCCEEDED: status=0x{probe:02X}") + else: + print(f" bcm_direct_read FAILED: stage=0x{stage:02X} probe=0x{probe:02X}") + + print("\n" + "=" * 55) + print("Analysis complete.") + +if __name__ == "__main__": + main() diff --git a/tools/test_i2c_pinpoint.py b/tools/test_i2c_pinpoint.py new file mode 100644 index 0000000..feaf7b4 --- /dev/null +++ b/tools/test_i2c_pinpoint.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Pinpoint which element in mode 0x82 causes bcm_direct_read to fail. + +Test sequence: +1. Power on via 0x81, confirm alive with raw read +2. 0x84: bcm_direct_read ONLY (no GPIO, no reset, no bus reset) +3. 0x85: GPIO + reset + power but NO I2C bus reset (no bmSTOP) +4. 0x82: GPIO + I2C bus reset + reset + power + probe (the one that fails) +""" + +import usb.core +import usb.util +import sys +import time + +BOOT_8PSK = 0x89 + +def find_device(): + dev = usb.core.find(idVendor=0x09C0, idProduct=0x0203) + if not dev: + print("Device not found!") + sys.exit(1) + return dev + +def setup_device(dev): + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except Exception: + pass + try: + dev.set_configuration() + except usb.core.USBError: + pass + +def decode_stage(stage): + names = { + 0x00: "NOT_STARTED", 0xA1: "GPIO_OK", 0xA2: "PROBE_OK(0x82)", + 0xA3: "BLK0_OK", 0xA4: "PROBE_OK(0x84)", 0xA5: "PROBE_OK(0x85)", + 0xE3: "PROBE_FAIL", 0xE4: "BLK0_FAIL", + } + return names.get(stage, f"0x{stage:02X}") + +def test_boot_mode(dev, wval, label, timeout_ms=3000): + print(f"\n{'─' * 55}") + print(f" Mode 0x{wval:02X}: {label}") + print(f"{'─' * 55}") + + t0 = time.monotonic() + try: + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, wval, 0, 3, timeout=timeout_ms) + except usb.core.USBError as e: + elapsed = (time.monotonic() - t0) * 1000 + print(f" TIMEOUT after {elapsed:.0f}ms: {e}") + return None + elapsed = (time.monotonic() - t0) * 1000 + + stage = ret[1] + probe = ret[2] + ok = stage not in (0xE3, 0xE4) + status_str = "SUCCESS" if ok else "FAILED" + print(f" {status_str} in {elapsed:.0f}ms") + print(f" stage=0x{stage:02X} [{decode_stage(stage)}] probe=0x{probe:02X}") + return ret + +def raw_read(dev, addr, reg): + try: + r = dev.ctrl_transfer(0xC0, 0xB5, addr, reg, 1, timeout=1000) + return r[0] + except: + return None + +def main(): + dev = find_device() + setup_device(dev) + + ret = dev.ctrl_transfer(0xC0, 0x92, 0, 0, 6, timeout=2000) + major, minor, patch = ret[2], ret[1], ret[0] + print(f"Firmware: v{major}.{minor:02d}.{patch}") + + # Step 1: Power on via GPIO-only mode + print("\n=== STEP 1: Power on BCM4500 (mode 0x81) ===") + ret = dev.ctrl_transfer(0xC0, BOOT_8PSK, 0x81, 0, 3, timeout=3000) + print(f" GPIO setup done, stage=0x{ret[1]:02X}") + time.sleep(1.0) + + # Confirm alive + val = raw_read(dev, 0x08, 0xA2) + print(f" Raw read 0x08:0xA2 = 0x{val:02X}" if val is not None else " Raw read FAILED") + + # Step 2: Test 0x84 (I2C read ONLY, no GPIO manipulation) + test_boot_mode(dev, 0x84, "bcm_direct_read ONLY (no GPIO, chip already powered)") + + # Confirm still alive + val = raw_read(dev, 0x08, 0xA2) + print(f" Raw read after 0x84: 0x{val:02X}" if val is not None else " Raw read FAILED") + + # Step 3: Test 0x85 (GPIO + reset but NO I2C bus reset) + test_boot_mode(dev, 0x85, "GPIO + reset + power, NO bmSTOP (no I2C bus reset)") + + # Confirm still alive + time.sleep(0.1) + val = raw_read(dev, 0x08, 0xA2) + print(f" Raw read after 0x85: 0x{val:02X}" if val is not None else " Raw read FAILED") + + # Step 4: For comparison, test 0x82 (the one that fails) + test_boot_mode(dev, 0x82, "GPIO + I2C bmSTOP + reset + power + probe") + + # Confirm still alive + val = raw_read(dev, 0x08, 0xA2) + print(f" Raw read after 0x82: 0x{val:02X}" if val is not None else " Raw read FAILED") + + print(f"\n{'=' * 55}") + print("Analysis complete.") + print() + print("If 0x84 works → bcm_direct_read is fine, issue is in reset/GPIO sequence") + print("If 0x84 fails → bcm_direct_read itself has a bug") + print("If 0x85 works → I2CS bmSTOP (I2C bus reset) is the culprit in 0x82") + print("If 0x85 fails → re-reset of BCM4500 needs more delay") + +if __name__ == "__main__": + main()