--- title: I2C STOP Corruption Bug description: Root cause analysis of the spurious I2C STOP condition that corrupted the FX2 controller state during boot. --- import { Steps, Aside, Badge } from '@astrojs/starlight/components'; During development of the custom firmware v3.01.0, the BOOT_8PSK (0x89) command caused the FX2 to hang for over 10 seconds, making the USB device completely unresponsive. The root cause was traced to a single line of code: a spurious I2C STOP condition issued when no transaction was active. ## The Problem The boot function originally included a "bus reset" step before any I2C communication: ```c title="Broken Code" I2CS |= bmSTOP; i2c_wait_stop(); ``` This pattern appears in various FX2 example code and seems reasonable -- send a STOP to ensure the I2C bus is in a known idle state before starting fresh. On the FX2's I2C controller hardware, this is incorrect. ## Root Cause Analysis The root cause was discovered through a series of incremental debug modes added to the BOOT_8PSK handler. Each mode executes a subset of the full boot sequence, isolating which step introduces the failure. ### Debug Mode Results | wValue | Action | Result | Key Observation | |--------|--------|--------|-----------------| | 0x80 | No-op: return status only | | Baseline | | 0x81 | GPIO + power + delays (no I2C) | | Power sequencing is correct | | 0x82 | GPIO + power + bmSTOP + I2C probe | | bmSTOP corrupts I2C | | 0x83 | GPIO + power + bmSTOP + probe + init | | Same root cause | | 0x84 | I2C probe only (chip already powered) | | BCM4500 is alive | | 0x85 | GPIO + power + probe (**no bmSTOP**) | | Confirms bmSTOP is the cause | ### Three Key Observations 1. **Mode 0x82 fails but mode 0x85 succeeds.** These modes are identical except that 0x82 issues `I2CS |= bmSTOP` before the probe and 0x85 does not. The bmSTOP is the only difference. 2. **Mode 0x84 succeeds immediately after 0x82 fails.** Mode 0x84 performs a plain I2C combined read with no GPIO manipulation or bus reset. If called after a failed 0x82, it succeeds. This proves the BCM4500 was alive and responding -- the FX2 I2C controller was in a bad state, not the bus or the slave. 3. **Raw I2C reads via command 0xB5 succeed after 0x82 fails.** Command 0xB5 uses the same `i2c_combined_read` function. Running it from the host after a failed 0x82 returns valid data from the BCM4500. ## What Happens Inside the FX2 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) - Subsequent START conditions fail to generate proper clock sequences - ACK detection from slaves becomes unreliable The Cypress TRM describes STOP as a step that follows a completed read or write transaction. It is not documented as a standalone bus-reset mechanism. ## The Fix The fix is a single deletion. Remove the spurious STOP from the boot sequence: ```c title="Before (Broken)" /* "Reset" I2C bus */ I2CS |= bmSTOP; i2c_wait_stop(); ``` ```c title="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 correct approach is to simply proceed with a new START condition. If the bus is idle (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. ## Corrected Boot Sequence ```c title="bcm4500_boot() -- Corrected" 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 respond */ if (!bcm_direct_read(BCM_REG_STATUS, &i2c_rd[0])) return FALSE; /* ... register init blocks follow ... */ } ``` ## Boot Results After Fix | Metric | Value | |--------|-------| | Boot time | ~90 ms total | | config_status | 0x03 (STARTED + FW_LOADED) | | boot_stage | 0xFF (COMPLETE) | | Direct registers 0xA2-0xA8 | All return 0x02 (powered, not locked) | | Signal lock | 0x00 (no lock -- dish not aimed) | | USB responsiveness | No hang; fully responsive throughout | ## Test Scripts The investigation was driven by a series of test scripts in the `tools/` directory: | Script | Purpose | |--------|---------| | `test_boot_debug.py` | Sends debug modes 0x80--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 modes 0x84, 0x85, and 0x82 | ## 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 holds SCL low (clock stretching) or a fault prevents bmDONE from asserting, the firmware spins forever. The custom firmware replaces all fx2lib I2C functions with timeout-protected wrappers: ```c title="Timeout-Protected I2C Waits" #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 at 48 MHz gives approximately 5--10 ms per wait, providing over 200x margin above the 22.5 us required for a single byte transfer at 400 kHz.