Skip to content

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 boot function originally included a “bus reset” step before any I2C communication:

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.

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.

wValueActionResultKey Observation
0x80No-op: return status onlyWorksBaseline
0x81GPIO + power + delays (no I2C)WorksPower sequencing is correct
0x82GPIO + power + bmSTOP + I2C probeFailsbmSTOP corrupts I2C
0x83GPIO + power + bmSTOP + probe + initFailsSame root cause
0x84I2C probe only (chip already powered)WorksBCM4500 is alive
0x85GPIO + power + probe (no bmSTOP)WorksConfirms bmSTOP is the cause
  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.

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 is a single deletion. Remove the spurious STOP from the boot sequence:

Before (Broken)
/* "Reset" I2C bus */
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 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.

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 ... */
}
MetricValue
Boot time~90 ms total
config_status0x03 (STARTED + FW_LOADED)
boot_stage0xFF (COMPLETE)
Direct registers 0xA2-0xA8All return 0x02 (powered, not locked)
Signal lock0x00 (no lock — dish not aimed)
USB responsivenessNo hang; fully responsive throughout

The investigation was driven by a series of test scripts in the tools/ directory:

ScriptPurpose
test_boot_debug.pySends debug modes 0x80—0x83 sequentially
test_i2c_debug.pyPowers on via 0x81, runs bus scans, tests probe timing
test_i2c_isolate.pyTests whether re-reset or insufficient delay causes failure
test_i2c_pinpoint.pyThe definitive test: compares modes 0x84, 0x85, and 0x82

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:

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.