Add I2C hot-plug detection and streaming diagnostics (firmware v3.04.0)

Phase D firmware hardening: vendor commands 0xBD (streaming diagnostics)
and 0xBE (I2C hot-plug detection) with Python library, bridge, and demo
support. All I2C operations use timeout-protected helpers, BCM4500 reads
are rate-limited during streaming, and frame counter reads use atomic
read-verify-reread pattern. Counters saturate instead of wrapping.
This commit is contained in:
Ryan Malloy 2026-02-15 18:24:45 -07:00
parent 592898dd7a
commit 6e353c351f
4 changed files with 372 additions and 4 deletions

View File

@ -5,8 +5,9 @@
* Stock-compatible vendor commands (0x80-0x94) plus custom * Stock-compatible vendor commands (0x80-0x94) plus custom
* spectrum sweep, raw demod access, blind scan (0xB0-0xB3), * spectrum sweep, raw demod access, blind scan (0xB0-0xB3),
* hardware diagnostics (0xB4-0xB6), signal monitoring (0xB7-0xB9), * hardware diagnostics (0xB4-0xB6), signal monitoring (0xB7-0xB9),
* and advanced commands: parameterized sweep (0xBA), adaptive * advanced commands: parameterized sweep (0xBA), adaptive blind scan
* blind scan (0xBB), error codes (0xBC), DiSEqC messaging (0x8D). * (0xBB), error codes (0xBC), DiSEqC messaging (0x8D), streaming
* diagnostics (0xBD), and I2C hot-plug detection (0xBE).
* *
* SDCC + fx2lib toolchain. Loaded into FX2 RAM for testing. * SDCC + fx2lib toolchain. Loaded into FX2 RAM for testing.
*/ */
@ -62,6 +63,8 @@
#define PARAM_SWEEP 0xBA #define PARAM_SWEEP 0xBA
#define ADAPTIVE_BLIND_SCAN 0xBB #define ADAPTIVE_BLIND_SCAN 0xBB
#define GET_LAST_ERROR 0xBC #define GET_LAST_ERROR 0xBC
#define GET_STREAM_DIAG 0xBD
#define GET_HOTPLUG_STATUS 0xBE
/* error codes (set by I2C helpers, read via 0xBC) */ /* error codes (set by I2C helpers, read via 0xBC) */
#define ERR_OK 0x00 #define ERR_OK 0x00
@ -114,6 +117,25 @@ static __xdata BYTE last_error;
/* Shared scratch buffer for vendor command case blocks (saves DSEG) */ /* Shared scratch buffer for vendor command case blocks (saves DSEG) */
static __xdata BYTE vc_diag[8]; static __xdata BYTE vc_diag[8];
/* I2C hot-plug detection: previous and current bus scan bitmaps (16 bytes each) */
static __xdata BYTE hp_prev[16]; /* last completed scan */
static __xdata BYTE hp_curr[16]; /* working scan buffer */
static __xdata WORD hp_changes; /* cumulative device change events */
static __xdata BYTE hp_added; /* devices added in last scan */
static __xdata BYTE hp_removed; /* devices removed in last scan */
static __xdata BYTE hp_scan_ok; /* 1 after first scan completes */
/* Streaming diagnostics counters */
static __xdata DWORD sd_poll_count; /* main-loop poll cycles while armed */
static __xdata WORD sd_overflow_count; /* EP2 FULL events detected */
static __xdata WORD sd_sync_loss; /* BCM4500 transport sync losses */
static __xdata BYTE sd_last_status; /* last BCM4500 status register */
static __xdata BYTE sd_last_lock; /* last BCM4500 lock register */
static __xdata BYTE sd_had_sync; /* had sync in previous poll */
/* Main loop timing: USB frame counter for periodic tasks */
static __xdata WORD hp_last_frame; /* frame counter at last I2C scan */
/* /*
* BCM4500 register initialization data extracted from stock v2.06 firmware. * BCM4500 register initialization data extracted from stock v2.06 firmware.
* FUN_CODE_0ddd writes these 3 blocks to BCM4500 indirect registers (page 0) * FUN_CODE_0ddd writes these 3 blocks to BCM4500 indirect registers (page 0)
@ -490,6 +512,126 @@ static void bcm4500_shutdown(void) {
IOA = (IOA & ~PIN_PWR_EN) | PIN_PWR_DIS; IOA = (IOA & ~PIN_PWR_EN) | PIN_PWR_DIS;
} }
/* ---------- I2C hot-plug detection ---------- */
/*
* Scan all I2C addresses 0x01-0x77, storing result as 16-byte bitmap
* in hp_curr[]. Then compare with hp_prev[] to detect changes.
* Must NOT be called while streaming (I2C bus contention with GPIF).
*/
static void i2c_hotplug_scan(void) {
static __xdata BYTE hp_a, hp_byte, hp_bit, hp_diff;
/* Save current as previous before starting new scan.
* This keeps hp_prev valid between scans so the host can read
* both bitmaps and see the actual transition. */
for (hp_a = 0; hp_a < 16; hp_a++)
hp_prev[hp_a] = hp_curr[hp_a];
/* Clear current scan buffer */
for (hp_a = 0; hp_a < 16; hp_a++)
hp_curr[hp_a] = 0;
/* Probe each 7-bit address using timeout-protected I2C ops */
for (hp_a = 1; hp_a < 0x78; hp_a++) {
I2CS |= bmSTART;
I2DAT = hp_a << 1; /* write direction */
if (!i2c_wait_done())
goto hp_abort; /* I2C hung — abandon scan */
if (I2CS & bmACK) {
hp_byte = hp_a >> 3;
hp_bit = hp_a & 0x07;
hp_curr[hp_byte] |= (1 << hp_bit);
}
I2CS |= bmSTOP;
if (!i2c_wait_stop())
goto hp_abort;
}
/* Compare with previous scan (only after first successful scan) */
if (hp_scan_ok) {
hp_added = 0;
hp_removed = 0;
for (hp_a = 0; hp_a < 16; hp_a++) {
hp_diff = hp_curr[hp_a] ^ hp_prev[hp_a];
if (hp_diff) {
/* Count new devices (in curr but not prev) */
hp_byte = hp_diff & hp_curr[hp_a];
while (hp_byte) {
hp_added++;
hp_byte &= (hp_byte - 1); /* Kernighan clear-lowest */
}
/* Count removed devices (in prev but not curr) */
hp_byte = hp_diff & hp_prev[hp_a];
while (hp_byte) {
hp_removed++;
hp_byte &= (hp_byte - 1);
}
/* Count per-device changes (not per-byte) */
hp_byte = hp_diff;
while (hp_byte) {
hp_changes++;
hp_byte &= (hp_byte - 1);
}
}
}
}
hp_scan_ok = 1;
return;
hp_abort:
/* I2C timeout during scan — issue STOP and bail out.
* hp_curr is partial so we don't update hp_prev. */
I2CS |= bmSTOP;
last_error = ERR_I2C_TIMEOUT;
}
/* ---------- Streaming diagnostics poll ---------- */
/*
* Called from main loop while streaming (BM_ARMED set).
* Checks EP2 FIFO overflow and BCM4500 transport sync state.
*/
/* Rate-limit I2C reads: only poll BCM4500 every SD_I2C_INTERVAL polls.
* At ~100K main-loop iterations/sec, 4096 every 40ms fast enough
* for meaningful sync-loss detection without saturating the I2C bus. */
#define SD_I2C_INTERVAL 4096
static void stream_diag_poll(void) {
static __xdata BYTE sd_rd[2]; /* dedicated I2C buffer for diag reads */
/* Saturate at max instead of wrapping (I1 fix) */
if (sd_poll_count < 0xFFFFFFFF)
sd_poll_count++;
/* EP2 FIFO full check is a pure SFR read — no I2C, always safe */
if (EP2CS & bmEPFULL) {
if (sd_overflow_count < 0xFFFF)
sd_overflow_count++;
}
/* Rate-limited BCM4500 I2C reads for sync tracking.
* Only attempt if BCM is booted and interval elapsed. */
if ((config_status & BM_FW_LOADED) &&
((WORD)sd_poll_count & (SD_I2C_INTERVAL - 1)) == 0) {
sd_rd[0] = 0;
sd_rd[1] = 0;
i2c_combined_read(BCM4500_ADDR, BCM_REG_STATUS, 1, &sd_rd[0]);
i2c_combined_read(BCM4500_ADDR, BCM_REG_LOCK, 1, &sd_rd[1]);
sd_last_status = sd_rd[0];
sd_last_lock = sd_rd[1];
/* Detect sync loss: had lock (bit 5) previously, lost it now */
if (sd_had_sync && !(sd_last_lock & 0x20)) {
if (sd_sync_loss < 0xFFFF)
sd_sync_loss++;
}
sd_had_sync = (sd_last_lock & 0x20) ? 1 : 0;
}
}
/* ---------- GPIF streaming ---------- */ /* ---------- GPIF streaming ---------- */
static void gpif_start(void) { static void gpif_start(void) {
@ -1433,8 +1575,8 @@ BOOL handle_vendorcommand(BYTE cmd) {
/* 0x92: GET_FW_VERS -- return firmware version and build date */ /* 0x92: GET_FW_VERS -- return firmware version and build date */
case GET_FW_VERS: case GET_FW_VERS:
EP0BUF[0] = 0x00; /* patch -> version 3.03.0 */ EP0BUF[0] = 0x00; /* patch -> version 3.04.0 */
EP0BUF[1] = 0x03; /* minor */ EP0BUF[1] = 0x04; /* minor */
EP0BUF[2] = 0x03; /* major */ EP0BUF[2] = 0x03; /* major */
EP0BUF[3] = 0x0F; /* day = 15 */ EP0BUF[3] = 0x0F; /* day = 15 */
EP0BUF[4] = 0x02; /* month = 2 */ EP0BUF[4] = 0x02; /* month = 2 */
@ -1701,6 +1843,70 @@ BOOL handle_vendorcommand(BYTE cmd) {
EP0BCL = 1; EP0BCL = 1;
return TRUE; return TRUE;
/* 0xBD: GET_STREAM_DIAG -- streaming diagnostics counters
* Returns 12 bytes:
* [0-3] u32 LE poll_count (main-loop poll cycles while armed)
* [4-5] u16 LE overflow_count (EP2 FIFO full events)
* [6-7] u16 LE sync_loss (BCM4500 lock-lost transitions)
* [8] u8 last_status (BCM4500 register 0xA2)
* [9] u8 last_lock (BCM4500 register 0xA4)
* [10] u8 armed (1 if currently streaming)
* [11] u8 had_sync (1 if currently locked)
* wValue=1 to reset counters after read. */
case GET_STREAM_DIAG:
EP0BUF[0] = (BYTE)(sd_poll_count);
EP0BUF[1] = (BYTE)(sd_poll_count >> 8);
EP0BUF[2] = (BYTE)(sd_poll_count >> 16);
EP0BUF[3] = (BYTE)(sd_poll_count >> 24);
EP0BUF[4] = (BYTE)(sd_overflow_count);
EP0BUF[5] = (BYTE)(sd_overflow_count >> 8);
EP0BUF[6] = (BYTE)(sd_sync_loss);
EP0BUF[7] = (BYTE)(sd_sync_loss >> 8);
EP0BUF[8] = sd_last_status;
EP0BUF[9] = sd_last_lock;
EP0BUF[10] = (config_status & BM_ARMED) ? 1 : 0;
EP0BUF[11] = sd_had_sync;
/* Reset counters if wValue=1 */
if (wval == 1) {
sd_poll_count = 0;
sd_overflow_count = 0;
sd_sync_loss = 0;
}
EP0BCH = 0;
EP0BCL = 12;
return TRUE;
/* 0xBE: GET_HOTPLUG_STATUS -- I2C device change detection
* Returns 36 bytes:
* [0-15] current I2C bus bitmap (16 bytes)
* [16-17] u16 LE change_count (cumulative change events)
* [18] u8 devices_added (in last scan)
* [19] u8 devices_removed (in last scan)
* [20-35] previous I2C bus bitmap (16 bytes)
* wValue=1 to reset change counter after read.
* wValue=2 to force immediate rescan. */
case GET_HOTPLUG_STATUS: {
static __xdata BYTE hp_i;
if (wval == 2) {
/* Force rescan (only when not streaming) */
if (!(config_status & BM_ARMED))
i2c_hotplug_scan();
}
for (hp_i = 0; hp_i < 16; hp_i++)
EP0BUF[hp_i] = hp_curr[hp_i];
EP0BUF[16] = (BYTE)(hp_changes);
EP0BUF[17] = (BYTE)(hp_changes >> 8);
EP0BUF[18] = hp_added;
EP0BUF[19] = hp_removed;
for (hp_i = 0; hp_i < 16; hp_i++)
EP0BUF[20 + hp_i] = hp_prev[hp_i];
if (wval == 1)
hp_changes = 0;
EP0BCH = 0;
EP0BCL = 36;
return TRUE;
}
default: default:
return FALSE; return FALSE;
} }
@ -1762,6 +1968,21 @@ void main(void) {
last_error = ERR_OK; last_error = ERR_OK;
got_sud = FALSE; got_sud = FALSE;
/* Initialize hot-plug detection state */
hp_changes = 0;
hp_added = 0;
hp_removed = 0;
hp_scan_ok = 0;
hp_last_frame = 0;
/* Initialize streaming diagnostics */
sd_poll_count = 0;
sd_overflow_count = 0;
sd_sync_loss = 0;
sd_last_status = 0;
sd_last_lock = 0;
sd_had_sync = 0;
REVCTL = 0x03; /* NOAUTOARM + SKIPCOMMIT */ REVCTL = 0x03; /* NOAUTOARM + SKIPCOMMIT */
SYNCDELAY; SYNCDELAY;
@ -1832,5 +2053,33 @@ void main(void) {
handle_setupdata(); handle_setupdata();
got_sud = FALSE; got_sud = FALSE;
} }
/* Periodic tasks using USB frame counter (125us/frame at HS).
* USBFRAMEH:USBFRAMEL wraps every 2048ms (16384 frames).
* We check every ~8000 frames (~1 second).
*
* The two SFRs are incremented by USB hardware asynchronously,
* so we read H-L-H and retry if H changed (carry propagation). */
{
WORD cur_frame;
BYTE fh;
do {
fh = USBFRAMEH;
cur_frame = ((WORD)fh << 8) | USBFRAMEL;
} while (fh != USBFRAMEH);
/* Streaming diagnostics: poll every main-loop iteration when armed */
if (config_status & BM_ARMED) {
stream_diag_poll();
}
/* I2C hot-plug: scan every ~1s, but only when NOT streaming
* (I2C bus contention with GPIF would corrupt data) */
if (!(config_status & BM_ARMED) &&
((WORD)(cur_frame - hp_last_frame) > 8000)) {
hp_last_frame = cur_frame;
i2c_hotplug_scan();
}
}
} }
} }

View File

@ -63,6 +63,8 @@ CMD_MULTI_REG_READ = 0xB9
CMD_PARAM_SWEEP = 0xBA CMD_PARAM_SWEEP = 0xBA
CMD_ADAPTIVE_BLIND_SCAN = 0xBB CMD_ADAPTIVE_BLIND_SCAN = 0xBB
CMD_GET_LAST_ERROR = 0xBC CMD_GET_LAST_ERROR = 0xBC
CMD_GET_STREAM_DIAG = 0xBD
CMD_GET_HOTPLUG_STATUS = 0xBE
# Error codes (returned by CMD_GET_LAST_ERROR) # Error codes (returned by CMD_GET_LAST_ERROR)
ERR_OK = 0x00 ERR_OK = 0x00
@ -789,6 +791,68 @@ class SkyWalker1:
"""Disable east/west soft limits.""" """Disable east/west soft limits."""
self.send_diseqc_message(diseqc_disable_limits()) self.send_diseqc_message(diseqc_disable_limits())
# -- Streaming diagnostics (v3.04+) --
def get_stream_diag(self, reset: bool = False) -> dict:
"""Read streaming diagnostics counters (0xBD).
Returns dict with poll_count, overflow_count, sync_loss,
last_status, last_lock, armed, had_sync.
Set reset=True to clear counters after read.
"""
wval = 1 if reset else 0
data = self._vendor_in(CMD_GET_STREAM_DIAG, value=wval, length=12)
poll_count = struct.unpack_from('<I', data, 0)[0]
overflow_count = struct.unpack_from('<H', data, 4)[0]
sync_loss = struct.unpack_from('<H', data, 6)[0]
return {
"poll_count": poll_count,
"overflow_count": overflow_count,
"sync_loss": sync_loss,
"last_status": data[8],
"last_lock": data[9],
"armed": bool(data[10]),
"had_sync": bool(data[11]),
}
# -- I2C hot-plug detection (v3.04+) --
def get_hotplug_status(self, reset: bool = False,
force_scan: bool = False) -> dict:
"""Read I2C hot-plug detection status (0xBE).
Returns dict with current/previous bus bitmaps, change count,
devices added/removed in last scan, and decoded address lists.
Set reset=True to clear change counter.
Set force_scan=True to trigger immediate I2C rescan.
"""
wval = 2 if force_scan else (1 if reset else 0)
data = self._vendor_in(CMD_GET_HOTPLUG_STATUS, value=wval, length=36)
current_bitmap = bytes(data[0:16])
changes = struct.unpack_from('<H', data, 16)[0]
added = data[18]
removed = data[19]
previous_bitmap = bytes(data[20:36])
return {
"current_bitmap": current_bitmap,
"previous_bitmap": previous_bitmap,
"changes": changes,
"added": added,
"removed": removed,
"current_devices": _bitmap_to_addrs(current_bitmap),
"previous_devices": _bitmap_to_addrs(previous_bitmap),
}
def _bitmap_to_addrs(bitmap: bytes) -> list[int]:
"""Convert 16-byte I2C address bitmap to list of 7-bit addresses."""
addrs = []
for byte_idx in range(16):
for bit in range(8):
if bitmap[byte_idx] & (1 << bit):
addrs.append((byte_idx << 3) | bit)
return addrs
# --- DiSEqC 1.2 command builders --- # --- DiSEqC 1.2 command builders ---

View File

@ -242,3 +242,13 @@ class USBBridge:
def get_last_error_str(self) -> str: def get_last_error_str(self) -> str:
with self._lock: with self._lock:
return self._dev.get_last_error_str() return self._dev.get_last_error_str()
def get_stream_diag(self, reset: bool = False) -> dict:
with self._lock:
return self._dev.get_stream_diag(reset=reset)
def get_hotplug_status(self, reset: bool = False,
force_scan: bool = False) -> dict:
with self._lock:
return self._dev.get_hotplug_status(reset=reset,
force_scan=force_scan)

View File

@ -576,6 +576,51 @@ class DemoDevice:
rng = random.Random(start_reg) rng = random.Random(start_reg)
return bytes(rng.randint(0, 255) for _ in range(count)) return bytes(rng.randint(0, 255) for _ in range(count))
def get_stream_diag(self, reset: bool = False) -> dict:
"""Simulated streaming diagnostics."""
self._sd_poll_count = getattr(self, '_sd_poll_count', 0) + random.randint(50, 200)
self._sd_overflow_count = getattr(self, '_sd_overflow_count', 0)
if random.random() < 0.02: # 2% chance of overflow per call
self._sd_overflow_count += 1
sig = self.signal_monitor()
is_locked = sig["locked"]
result = {
"poll_count": self._sd_poll_count,
"overflow_count": self._sd_overflow_count,
"sync_loss": getattr(self, '_sd_sync_loss', 0),
"last_status": 0x42,
"last_lock": 0x20 if is_locked else 0x00,
"armed": True,
"had_sync": is_locked,
}
if reset:
self._sd_poll_count = 0
self._sd_overflow_count = 0
self._sd_sync_loss = 0
return result
def get_hotplug_status(self, reset: bool = False,
force_scan: bool = False) -> dict:
"""Simulated I2C hot-plug detection."""
# Standard SkyWalker-1 I2C devices: BCM4500 (0x08), EEPROM (0x50),
# tuner (0x61), LNB controller (0x08 shares)
bitmap = bytearray(16)
for addr in [0x08, 0x50, 0x51, 0x61]:
bitmap[addr >> 3] |= (1 << (addr & 0x07))
current = bytes(bitmap)
previous = current # no changes in demo
addrs = [0x08, 0x50, 0x51, 0x61]
result = {
"current_bitmap": current,
"previous_bitmap": previous,
"changes": 0,
"added": 0,
"removed": 0,
"current_devices": addrs,
"previous_devices": addrs,
}
return result
# --- Internal signal model --- # --- Internal signal model ---
def _power_at(self, freq_mhz: float, elapsed: float) -> float: def _power_at(self, freq_mhz: float, elapsed: float) -> float: