I2C STOP Corruption Bug
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
Section titled “The Problem”The boot function originally included a “bus reset” step before any I2C communication:
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
Section titled “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
Section titled “Debug Mode Results”| wValue | Action | Result | Key Observation |
|---|---|---|---|
| 0x80 | No-op: return status only | Works | Baseline |
| 0x81 | GPIO + power + delays (no I2C) | Works | Power sequencing is correct |
| 0x82 | GPIO + power + bmSTOP + I2C probe | Fails | bmSTOP corrupts I2C |
| 0x83 | GPIO + power + bmSTOP + probe + init | Fails | Same root cause |
| 0x84 | I2C probe only (chip already powered) | Works | BCM4500 is alive |
| 0x85 | GPIO + power + probe (no bmSTOP) | Works | Confirms bmSTOP is the cause |
Three Key Observations
Section titled “Three Key Observations”-
Mode 0x82 fails but mode 0x85 succeeds. These modes are identical except that 0x82 issues
I2CS |= bmSTOPbefore the probe and 0x85 does not. The bmSTOP is the only difference. -
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.
-
Raw I2C reads via command 0xB5 succeed after 0x82 fails. Command 0xB5 uses the same
i2c_combined_readfunction. Running it from the host after a failed 0x82 returns valid data from the BCM4500.
What Happens Inside the FX2
Section titled “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
Section titled “The Fix”The fix is a single deletion. Remove the spurious STOP from the boot sequence:
/* "Reset" I2C bus */I2CS |= bmSTOP;i2c_wait_stop();/* 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
Section titled “Corrected Boot Sequence”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
Section titled “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
Section titled “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
Section titled “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:
#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.