From 6e353c351fe67a1c846ce04abc04e26502c593ee Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 15 Feb 2026 18:24:45 -0700 Subject: [PATCH] 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. --- firmware/skywalker1.c | 257 +++++++++++++++++++++++++++++++- tools/skywalker_lib.py | 64 ++++++++ tui/src/skywalker_tui/bridge.py | 10 ++ tui/src/skywalker_tui/demo.py | 45 ++++++ 4 files changed, 372 insertions(+), 4 deletions(-) diff --git a/firmware/skywalker1.c b/firmware/skywalker1.c index 1078f27..adb4ae1 100644 --- a/firmware/skywalker1.c +++ b/firmware/skywalker1.c @@ -5,8 +5,9 @@ * Stock-compatible vendor commands (0x80-0x94) plus custom * spectrum sweep, raw demod access, blind scan (0xB0-0xB3), * hardware diagnostics (0xB4-0xB6), signal monitoring (0xB7-0xB9), - * and advanced commands: parameterized sweep (0xBA), adaptive - * blind scan (0xBB), error codes (0xBC), DiSEqC messaging (0x8D). + * advanced commands: parameterized sweep (0xBA), adaptive blind scan + * (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. */ @@ -62,6 +63,8 @@ #define PARAM_SWEEP 0xBA #define ADAPTIVE_BLIND_SCAN 0xBB #define GET_LAST_ERROR 0xBC +#define GET_STREAM_DIAG 0xBD +#define GET_HOTPLUG_STATUS 0xBE /* error codes (set by I2C helpers, read via 0xBC) */ #define ERR_OK 0x00 @@ -114,6 +117,25 @@ static __xdata BYTE last_error; /* Shared scratch buffer for vendor command case blocks (saves DSEG) */ 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. * 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; } +/* ---------- 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 ---------- */ static void gpif_start(void) { @@ -1433,8 +1575,8 @@ BOOL handle_vendorcommand(BYTE cmd) { /* 0x92: GET_FW_VERS -- return firmware version and build date */ case GET_FW_VERS: - EP0BUF[0] = 0x00; /* patch -> version 3.03.0 */ - EP0BUF[1] = 0x03; /* minor */ + EP0BUF[0] = 0x00; /* patch -> version 3.04.0 */ + EP0BUF[1] = 0x04; /* minor */ EP0BUF[2] = 0x03; /* major */ EP0BUF[3] = 0x0F; /* day = 15 */ EP0BUF[4] = 0x02; /* month = 2 */ @@ -1701,6 +1843,70 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BCL = 1; 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: return FALSE; } @@ -1762,6 +1968,21 @@ void main(void) { last_error = ERR_OK; 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 */ SYNCDELAY; @@ -1832,5 +2053,33 @@ void main(void) { handle_setupdata(); 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(); + } + } } } diff --git a/tools/skywalker_lib.py b/tools/skywalker_lib.py index aa30c3c..8eba328 100644 --- a/tools/skywalker_lib.py +++ b/tools/skywalker_lib.py @@ -63,6 +63,8 @@ CMD_MULTI_REG_READ = 0xB9 CMD_PARAM_SWEEP = 0xBA CMD_ADAPTIVE_BLIND_SCAN = 0xBB CMD_GET_LAST_ERROR = 0xBC +CMD_GET_STREAM_DIAG = 0xBD +CMD_GET_HOTPLUG_STATUS = 0xBE # Error codes (returned by CMD_GET_LAST_ERROR) ERR_OK = 0x00 @@ -789,6 +791,68 @@ class SkyWalker1: """Disable east/west soft 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(' 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(' 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 --- diff --git a/tui/src/skywalker_tui/bridge.py b/tui/src/skywalker_tui/bridge.py index 0621730..b4f994d 100644 --- a/tui/src/skywalker_tui/bridge.py +++ b/tui/src/skywalker_tui/bridge.py @@ -242,3 +242,13 @@ class USBBridge: def get_last_error_str(self) -> str: with self._lock: 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) diff --git a/tui/src/skywalker_tui/demo.py b/tui/src/skywalker_tui/demo.py index 2080efb..274ec58 100644 --- a/tui/src/skywalker_tui/demo.py +++ b/tui/src/skywalker_tui/demo.py @@ -576,6 +576,51 @@ class DemoDevice: rng = random.Random(start_reg) 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 --- def _power_at(self, freq_mhz: float, elapsed: float) -> float: