From 834c2bd9ee8b792cac0bbcc1c529f36a30e6ab8f Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 16 Feb 2026 03:41:08 -0700 Subject: [PATCH] Add software watchdog and timeout protection for all I2C/USB paths (firmware v3.05.0) Safety review identified infinite-hang paths in do_tune(), GPIF streaming, EP0/EP2 FIFO waits, and the 0xB4 I2C bus scan. The firmware controls an LNB power supply (750mA at 18V) on a Cypress FX2LP with no hardware watchdog. Key changes: - Software watchdog via Timer0 ISR (~2s timeout, cuts LNB power on stall) - Replace fx2lib i2c_write() in do_tune() with timeout-protected helper - ep0_wait_data() helper replaces 7 bare EP0 BUSY spin loops - GPIF idle wait and EP2 FIFO full wait now have timeouts - 0xB4 I2C bus scan uses i2c_wait_done()/i2c_wait_stop() instead of bare spins - Return-value checks on all I2C writes in sweep/scan functions - EP0 payload length validation on all vendor commands with data phase - Zero-fill EP0BUF before indirect reads (0x87, 0xB7) for deterministic output - i2c_wait_stop() now sets last_error on timeout - New error codes: ERR_TUNE_FAIL through ERR_DISEQC_LEN - BCM_LOCK_BIT constant replaces hardcoded 0x20 in lock checks - DiSEqC Tone Burst B rejected with ERR_NOT_SUPPORTED - DiSEqC message length error sets ERR_DISEQC_LEN - hp_changes counter saturates instead of wrapping - stream_diag_poll() only updates status on successful I2C reads - do_tune() forward declaration eliminates implicit-function warning - do_tune() sets ERR_BCM_NOT_READY when demod not booted - wdt_kick() in all long-running sweep/scan loops Code: 13,057 / 15,360 bytes (85%). XRAM: 218 / 512 bytes (43%). Stack: 132 bytes free. Zero new SDCC warnings. --- firmware/skywalker1.c | 279 +++++++++++++++++++++++++++++++++--------- 1 file changed, 220 insertions(+), 59 deletions(-) diff --git a/firmware/skywalker1.c b/firmware/skywalker1.c index adb4ae1..5e0eda8 100644 --- a/firmware/skywalker1.c +++ b/firmware/skywalker1.c @@ -33,6 +33,7 @@ /* BCM4500 status registers */ #define BCM_REG_STATUS 0xA2 #define BCM_REG_LOCK 0xA4 +#define BCM_LOCK_BIT 0x20 /* BCM4500 lock detect bit in register 0xA4 */ /* BCM commands */ #define BCM_CMD_READ 0x01 @@ -73,6 +74,12 @@ #define ERR_I2C_ARB_LOST 0x03 #define ERR_BCM_NOT_READY 0x04 #define ERR_BCM_TIMEOUT 0x05 +#define ERR_TUNE_FAIL 0x06 +#define ERR_EP0_TIMEOUT 0x07 +#define ERR_GPIF_TIMEOUT 0x08 +#define ERR_EP2_TIMEOUT 0x09 +#define ERR_NOT_SUPPORTED 0x0A +#define ERR_DISEQC_LEN 0x0B /* configuration status byte bits */ #define BM_STARTED 0x01 @@ -136,6 +143,11 @@ 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 */ +/* Software watchdog: Timer0 ISR decrements; on zero, LNB power is cut. + * volatile: shared between ISR (interrupt 1) and main loop. */ +static volatile __xdata BYTE wdt_counter; +static volatile __xdata BYTE wdt_armed; + /* * BCM4500 register initialization data extracted from stock v2.06 firmware. * FUN_CODE_0ddd writes these 3 blocks to BCM4500 indirect registers (page 0) @@ -163,6 +175,8 @@ static const __code BYTE bcm_init_block2[] = { * SCL low (clock stretching), the master waits forever without this. */ #define I2C_TIMEOUT 6000 +#define GPIF_TIMEOUT 60000 /* GPIF idle wait (~15ms at 4MHz tick) */ +#define EP2_TIMEOUT 60000 /* EP2 drain wait */ static BOOL i2c_wait_done(void) { WORD timeout = I2C_TIMEOUT; @@ -178,8 +192,25 @@ static BOOL i2c_wait_done(void) { static BOOL i2c_wait_stop(void) { WORD timeout = I2C_TIMEOUT; while (I2CS & bmSTOP) { - if (--timeout == 0) + if (--timeout == 0) { + last_error = ERR_I2C_TIMEOUT; return FALSE; + } + } + return TRUE; +} + +/* + * Wait for EP0 data phase to complete (host -> device transfer). + * Replaces bare `while (EP0CS & bmEPBUSY)` spin loops with timeout. + */ +static BOOL ep0_wait_data(void) { + WORD timeout = I2C_TIMEOUT; + while (EP0CS & bmEPBUSY) { + if (--timeout == 0) { + last_error = ERR_EP0_TIMEOUT; + return FALSE; + } } return TRUE; } @@ -314,6 +345,39 @@ fail: return FALSE; } +/* ---------- Software watchdog ---------- */ + +/* + * Kick the watchdog timer — resets the countdown to ~2 seconds. + * Must be called from the main loop at least once per period. + */ +static void wdt_kick(void) { + if (wdt_armed == 1) wdt_counter = 122; +} + +/* + * Initialize Timer0 as a ~16ms periodic interrupt for the software + * watchdog. At 48MHz/12 = 4MHz timer clock with 16-bit overflow, + * period = 65536 / 4MHz = 16.384ms. 122 decrements × 16.384ms ≈ 2s. + * + * Mode 1 (16-bit) does NOT auto-reload. After overflow the counter + * wraps to 0x0000 and keeps counting — which is exactly the reload + * value we want, so no manual reload is needed in the ISR. If the + * start value is ever changed to non-zero, add a reload in the ISR. + * + * CKCON bit 3 (T0M) controls Timer0 clock; bit 5 (T2M) is Timer2. + * DiSEqC uses Timer2 — do not touch bit 3 from DiSEqC code. + */ +static void wdt_init(void) { + TMOD = (TMOD & 0xF0) | 0x01; /* Timer0 Mode 1 (16-bit) */ + CKCON &= ~0x08; /* Timer0 clk = 48MHz/12 = 4MHz (bit 3 only) */ + TH0 = 0x00; TL0 = 0x00; /* full-count: 0x0000 to 0xFFFF = 16.384ms */ + wdt_armed = 1; + wdt_kick(); + ET0 = 1; /* Enable Timer0 interrupt */ + TR0 = 1; /* Start Timer0 */ +} + /* * Write one byte to a BCM4500 direct I2C register (subaddr). */ @@ -570,7 +634,7 @@ static void i2c_hotplug_scan(void) { /* Count per-device changes (not per-byte) */ hp_byte = hp_diff; while (hp_byte) { - hp_changes++; + if (hp_changes < 0xFFFF) hp_changes++; hp_byte &= (hp_byte - 1); } } @@ -617,18 +681,17 @@ static void stream_diag_poll(void) { ((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]; + if (i2c_combined_read(BCM4500_ADDR, BCM_REG_STATUS, 1, &sd_rd[0])) + sd_last_status = sd_rd[0]; + if (i2c_combined_read(BCM4500_ADDR, BCM_REG_LOCK, 1, &sd_rd[1])) + 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_had_sync && !(sd_last_lock & BCM_LOCK_BIT)) { if (sd_sync_loss < 0xFFFF) sd_sync_loss++; } - sd_had_sync = (sd_last_lock & 0x20) ? 1 : 0; + sd_had_sync = (sd_last_lock & BCM_LOCK_BIT) ? 1 : 0; } } @@ -664,9 +727,18 @@ static void gpif_start(void) { /* Assert P3.5 low (BCM4500 TS enable) briefly */ IOD &= ~0x20; - /* Wait for GPIF idle */ - while (!(GPIFTRIG & 0x80)) - ; + /* Wait for GPIF idle with timeout */ + { + WORD gp_timeout = GPIF_TIMEOUT; + while (!(GPIFTRIG & 0x80)) { + if (--gp_timeout == 0) { + last_error = ERR_GPIF_TIMEOUT; + IOD |= 0x20; + config_status &= ~BM_ARMED; + return; + } + } + } IOD |= 0x20; @@ -688,9 +760,16 @@ static void gpif_stop(void) { EP2FIFOBCH = 0xFF; SYNCDELAY; - /* Wait for GPIF idle */ - while (!(GPIFTRIG & 0x80)) - ; + /* Wait for GPIF idle with timeout */ + { + WORD gp_timeout = GPIF_TIMEOUT; + while (!(GPIFTRIG & 0x80)) { + if (--gp_timeout == 0) { + last_error = ERR_GPIF_TIMEOUT; + break; /* proceed with cleanup regardless */ + } + } + } /* Skip/discard partial EP2 packet */ OUTPKTEND = 0x82; @@ -714,7 +793,7 @@ static void gpif_stop(void) { static void diseqc_tone_burst(BYTE sat_b) { BYTE i; - (void)sat_b; /* both A and B send 22kHz burst for now */ + if (sat_b) { last_error = ERR_NOT_SUPPORTED; return; } /* Configure Timer2 auto-reload */ /* CKCON.T2M = 0 -> Timer2 clk = 48MHz/12 = 4MHz */ @@ -822,8 +901,10 @@ static void diseqc_send_byte(BYTE val) { static void diseqc_send_message(BYTE len) { static __xdata BYTE dm_i, dm_saved_tone; - if (len < 3 || len > 6) + if (len < 3 || len > 6) { + last_error = ERR_DISEQC_LEN; return; + } /* Save current 22 kHz tone state */ dm_saved_tone = IOA & PIN_22KHZ; @@ -874,6 +955,10 @@ static void diseqc_send_message(BYTE len) { IOA &= ~PIN_22KHZ; } +/* Forward declaration: do_tune() is defined after the sweep functions + * but called from do_param_sweep(). */ +static void do_tune(void); + /* ---------- Parameterized sweep (0xBA) ---------- */ /* @@ -918,6 +1003,7 @@ static void do_param_sweep(void) { ps_cur = ps_start; while (ps_cur <= ps_stop) { + wdt_kick(); /* sweep is progressing, not hung */ /* * Set up a tune payload in EP0BUF for do_tune(): * [0..3] = symbol_rate (LE), [4..7] = freq (LE), [8] = mod, [9] = fec @@ -957,8 +1043,15 @@ static void do_param_sweep(void) { SYNCDELAY; ps_buf_idx = 0; - while (EP2CS & bmEPFULL) - ; + { + WORD ep2_to = EP2_TIMEOUT; + while (EP2CS & bmEPFULL) { + if (--ep2_to == 0) { + last_error = ERR_EP2_TIMEOUT; + return; + } + } + } } ps_cur += ps_step; @@ -1016,20 +1109,30 @@ static BOOL do_adaptive_blind_scan(void) { abs_sr_cur = abs_sr_min; while (abs_sr_cur <= abs_sr_max) { + wdt_kick(); /* scan is progressing, not hung */ /* Program SR and frequency into BCM4500 */ i2c_buf[0] = (BYTE)(abs_sr_cur >> 24); i2c_buf[1] = (BYTE)(abs_sr_cur >> 16); i2c_buf[2] = (BYTE)(abs_sr_cur >> 8); i2c_buf[3] = (BYTE)(abs_sr_cur); - bcm_indirect_write_block(0x00, i2c_buf, 4); + if (!bcm_indirect_write_block(0x00, i2c_buf, 4)) { + abs_sr_cur += abs_sr_step; + continue; + } i2c_buf[0] = (BYTE)(abs_freq >> 24); i2c_buf[1] = (BYTE)(abs_freq >> 16); i2c_buf[2] = (BYTE)(abs_freq >> 8); i2c_buf[3] = (BYTE)(abs_freq); - bcm_indirect_write_block(0x00, i2c_buf, 4); + if (!bcm_indirect_write_block(0x00, i2c_buf, 4)) { + abs_sr_cur += abs_sr_step; + continue; + } - bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) { + abs_sr_cur += abs_sr_step; + continue; + } /* Quick AGC pre-check if enabled */ if (abs_quick_dwell > 0) { @@ -1056,7 +1159,7 @@ static BOOL do_adaptive_blind_scan(void) { /* Check lock */ abs_lock_val = 0; bcm_direct_read(BCM_REG_LOCK, &abs_lock_val); - if (abs_lock_val & 0x20) { + if (abs_lock_val & BCM_LOCK_BIT) { EP0BUF[0] = (BYTE)(abs_freq); EP0BUF[1] = (BYTE)(abs_freq >> 8); EP0BUF[2] = (BYTE)(abs_freq >> 16); @@ -1120,6 +1223,7 @@ static void do_spectrum_sweep(void) { cur_freq = start_freq; while (cur_freq <= stop_freq) { + wdt_kick(); /* sweep is progressing, not hung */ /* * Program frequency into BCM4500 via indirect write. * The BCM4500 expects big-endian frequency bytes at page 0. @@ -1129,7 +1233,10 @@ static void do_spectrum_sweep(void) { i2c_buf[1] = (BYTE)(cur_freq >> 16); i2c_buf[2] = (BYTE)(cur_freq >> 8); i2c_buf[3] = (BYTE)(cur_freq); - bcm_indirect_write_block(0x00, i2c_buf, 4); + if (!bcm_indirect_write_block(0x00, i2c_buf, 4)) { + cur_freq += step_khz; + continue; + } /* Wait for demod to settle */ delay(10); @@ -1155,8 +1262,15 @@ static void do_spectrum_sweep(void) { ss_buf_idx = 0; /* Wait for the buffer to be taken by host */ - while (EP2CS & bmEPFULL) - ; + { + WORD ep2_to = EP2_TIMEOUT; + while (EP2CS & bmEPFULL) { + if (--ep2_to == 0) { + last_error = ERR_EP2_TIMEOUT; + return; + } + } + } } cur_freq += step_khz; @@ -1212,6 +1326,7 @@ static BOOL do_blind_scan(void) { sr_cur = sr_min; while (sr_cur <= sr_max) { + wdt_kick(); /* scan is progressing, not hung */ /* * Program frequency (BE) and symbol rate (BE) into BCM4500. * We write both in a single block: 4 bytes SR + 4 bytes freq. @@ -1220,16 +1335,25 @@ static BOOL do_blind_scan(void) { i2c_buf[1] = (BYTE)(sr_cur >> 16); i2c_buf[2] = (BYTE)(sr_cur >> 8); i2c_buf[3] = (BYTE)(sr_cur); - bcm_indirect_write_block(0x00, i2c_buf, 4); + if (!bcm_indirect_write_block(0x00, i2c_buf, 4)) { + sr_cur += sr_step; + continue; + } i2c_buf[0] = (BYTE)(freq_khz >> 24); i2c_buf[1] = (BYTE)(freq_khz >> 16); i2c_buf[2] = (BYTE)(freq_khz >> 8); i2c_buf[3] = (BYTE)(freq_khz); - bcm_indirect_write_block(0x00, i2c_buf, 4); + if (!bcm_indirect_write_block(0x00, i2c_buf, 4)) { + sr_cur += sr_step; + continue; + } /* Issue tune command */ - bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) { + sr_cur += sr_step; + continue; + } /* Wait for acquisition attempt */ delay(100); @@ -1237,7 +1361,7 @@ static BOOL do_blind_scan(void) { /* Check lock */ bs_lock_val = 0; bcm_direct_read(BCM_REG_LOCK, &bs_lock_val); - if (bs_lock_val & 0x20) { + if (bs_lock_val & BCM_LOCK_BIT) { /* Locked -- report back via EP0 */ EP0BUF[0] = (BYTE)(freq_khz); EP0BUF[1] = (BYTE)(freq_khz >> 8); @@ -1276,8 +1400,10 @@ static void do_tune(void) { static __xdata BYTE tune_i; static __xdata BYTE tune_data[13]; /* 12 data + 1 scratch for reg addr */ - if (!(config_status & BM_STARTED)) + if (!(config_status & BM_STARTED)) { + last_error = ERR_BCM_NOT_READY; return; + } /* * Byte-reverse symbol rate (LE->BE) into tune_data[0..3] @@ -1309,17 +1435,21 @@ static void do_tune(void) { } /* Poll BCM4500 for readiness */ - bcm_poll_ready(); + if (!bcm_poll_ready()) + return; /* Write page 0 */ - bcm_direct_write(BCM_REG_PAGE, 0x00); + if (!bcm_direct_write(BCM_REG_PAGE, 0x00)) + return; - /* Write all configuration data to BCM4500 data register */ - tune_data[12] = BCM_REG_DATA; /* borrow byte past data (safe: 13 bytes in xdata) */ - i2c_write(BCM4500_ADDR, 1, &tune_data[12], 12, tune_data); + /* Write all 12 configuration bytes to BCM4500 data register (0xA7). + * Uses our timeout-protected multi-byte write instead of fx2lib i2c_write(). */ + if (!i2c_write_multi_timeout(BCM4500_ADDR, BCM_REG_DATA, 12, tune_data)) + return; /* Execute indirect write */ - bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE); + if (!bcm_direct_write(BCM_REG_CMD, BCM_CMD_WRITE)) + return; /* Wait for command completion */ bcm_poll_ready(); @@ -1355,8 +1485,8 @@ BOOL handle_vendorcommand(BYTE cmd) { /* EP0 data phase: wait for 10 bytes from host */ EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 10) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_tune(); return TRUE; @@ -1370,6 +1500,9 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BCL = 6; return TRUE; } + /* Zero-fill before reads so I2C failures return zeros, not stale data */ + EP0BUF[0] = 0; EP0BUF[1] = 0; EP0BUF[2] = 0; + EP0BUF[3] = 0; EP0BUF[4] = 0; EP0BUF[5] = 0; /* Read signal quality via indirect registers */ bcm_indirect_read(0x00, &EP0BUF[0]); bcm_indirect_read(0x01, &EP0BUF[1]); @@ -1549,8 +1682,8 @@ BOOL handle_vendorcommand(BYTE cmd) { /* EP0 data phase: receive message bytes */ EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < (BYTE)wlen) { last_error = ERR_EP0_TIMEOUT; return TRUE; } /* Copy message from EP0BUF to diseqc_msg buffer */ { BYTE di; @@ -1575,10 +1708,10 @@ 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.04.0 */ - EP0BUF[1] = 0x04; /* minor */ + EP0BUF[0] = 0x00; /* patch -> version 3.05.0 */ + EP0BUF[1] = 0x05; /* minor */ EP0BUF[2] = 0x03; /* major */ - EP0BUF[3] = 0x0F; /* day = 15 */ + EP0BUF[3] = 0x10; /* day = 16 */ EP0BUF[4] = 0x02; /* month = 2 */ EP0BUF[5] = 0x1A; /* year - 2000 = 26 */ EP0BCH = 0; @@ -1597,8 +1730,8 @@ BOOL handle_vendorcommand(BYTE cmd) { /* EP0 data phase: wait for 10 bytes from host */ EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 10) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_spectrum_sweep(); return TRUE; @@ -1624,8 +1757,8 @@ BOOL handle_vendorcommand(BYTE cmd) { /* EP0 data phase: wait for 16 bytes from host */ EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 16) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_blind_scan(); return TRUE; @@ -1640,8 +1773,7 @@ BOOL handle_vendorcommand(BYTE cmd) { /* Try START + address + write, see if ACK comes back */ I2CS |= bmSTART; I2DAT = a << 1; /* write direction */ - while (!(I2CS & bmDONE)) - ; + if (!i2c_wait_done()) break; if (I2CS & bmACK) { /* Device responded at this address */ byte_idx = a >> 3; @@ -1649,8 +1781,7 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BUF[byte_idx] |= (1 << bit); } I2CS |= bmSTOP; - while (I2CS & bmSTOP) - ; + if (!i2c_wait_stop()) break; } EP0BCH = 0; EP0BCL = 16; @@ -1739,6 +1870,9 @@ BOOL handle_vendorcommand(BYTE cmd) { * in a single USB transfer instead of 3 separate reads. */ case SIGNAL_MONITOR: { BYTE sm_val; + /* Zero-fill before reads so I2C failures return zeros, not stale data */ + EP0BUF[0] = 0; EP0BUF[1] = 0; EP0BUF[2] = 0; + EP0BUF[3] = 0; EP0BUF[4] = 0; EP0BUF[5] = 0; /* SNR: indirect regs 0x00-0x01 */ bcm_indirect_read(0x00, &EP0BUF[0]); bcm_indirect_read(0x01, &EP0BUF[1]); @@ -1778,8 +1912,8 @@ BOOL handle_vendorcommand(BYTE cmd) { BYTE dwell = (BYTE)wval; EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 10) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_tune(); if (dwell > 0) delay(dwell); @@ -1822,8 +1956,8 @@ BOOL handle_vendorcommand(BYTE cmd) { case PARAM_SWEEP: EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 16) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_param_sweep(); return TRUE; @@ -1831,8 +1965,8 @@ BOOL handle_vendorcommand(BYTE cmd) { case ADAPTIVE_BLIND_SCAN: EP0BCL = 0; SYNCDELAY; - while (EP0CS & bmEPBUSY) - ; + if (!ep0_wait_data()) return TRUE; + if (EP0BCL < 18) { last_error = ERR_EP0_TIMEOUT; return TRUE; } do_adaptive_blind_scan(); return TRUE; @@ -1866,7 +2000,9 @@ BOOL handle_vendorcommand(BYTE cmd) { EP0BUF[9] = sd_last_lock; EP0BUF[10] = (config_status & BM_ARMED) ? 1 : 0; EP0BUF[11] = sd_had_sync; - /* Reset counters if wValue=1 */ + /* Reset counters if wValue=1. + * Non-atomic read+reset is safe: single-threaded main loop, + * ISR only sets got_sud (never touches diag counters). */ if (wval == 1) { sd_poll_count = 0; sd_overflow_count = 0; @@ -1960,6 +2096,27 @@ void hispeed_isr(void) __interrupt (HISPEED_ISR) { CLEAR_HISPEED(); } +/* Software watchdog Timer0 ISR: fires every ~16.384ms. + * If the main loop stops kicking, cut LNB power for safety. + * wdt_armed states: 0=off, 1=armed, 2=fired (power was cut). */ +void timer0_isr(void) __interrupt (1) { + if (wdt_armed != 1) return; + if (wdt_counter > 0) { + wdt_counter--; + } else { + /* Main loop stalled — cut LNB power for safety. + * IOA RMW race note: if the main loop is genuinely hung (which + * it must be for wdt_counter to reach 0), it is not executing + * IOA modifications. If by rare coincidence we interrupt an IOA + * RMW, the main loop's stale write re-enables power — but then + * wdt_armed=2 prevents wdt_kick() from rearming, so the next + * ISR tick exits immediately and the main loop's own guard + * checks (BM_STARTED etc.) will prevent further I2C activity. */ + IOA = (IOA & ~PIN_PWR_EN) | PIN_PWR_DIS; + wdt_armed = 2; /* fired — main loop must not re-arm */ + } +} + /* ---------- Main ---------- */ void main(void) { @@ -2048,7 +2205,11 @@ void main(void) { EA = 1; /* global interrupt enable */ + wdt_init(); /* start software watchdog (~2s timeout) */ + while (TRUE) { + wdt_kick(); /* main loop alive — reset watchdog */ + if (got_sud) { handle_setupdata(); got_sud = FALSE;