Add v3.05.0 safety review documentation and Hamilton test suite docs

- firmware/custom-v305.mdx: watchdog, timeout protection, error codes,
  I2C return checks, do_tune() safety, DiSEqC fixes, hardware validation
- tools/safety-testing.mdx: 55-test adversarial suite covering DiSEqC
  abuse, tune parameter abuse, I2C address abuse, state machine violations,
  boundary conditions, rapid-fire stress, and invalid vendor commands
- Updated version comparison with v3.05 tab and architectural table
- Sidebar entries for both new pages
This commit is contained in:
Ryan Malloy 2026-02-17 13:52:23 -07:00
parent 7223fcf810
commit 8b4aacab81
4 changed files with 434 additions and 16 deletions

View file

@ -0,0 +1,181 @@
---
title: Custom Firmware v3.05
description: Safety-hardened firmware with software watchdog, timeout protection on all I2C/USB/GPIF paths, and comprehensive error reporting.
---
import { Badge, Aside, Steps } from '@astrojs/starlight/components';
Firmware v3.05.0 is the result of a systematic safety review applied to the entire codebase. Every infinite-spin loop has been replaced with timeout-protected equivalents, a software watchdog cuts LNB power if the main loop stalls, and every failure mode now sets a specific error code readable by the host. <Badge text="v3.05" variant="success" /> <Badge text="Safety" variant="caution" />
Built on the [v3.04 codebase](/firmware/custom-v301/) — same SDCC + fx2lib toolchain, same stock-compatible command set, plus all custom commands from [v3.02](/firmware/custom-v302/) through v3.04.
| Property | Value |
|----------|-------|
| Version ID | `0x030500` |
| Date | 2026-02-16 |
| Code size | 13,079 / 15,360 bytes (85%) |
| XRAM | 218 / 512 bytes (43%) |
| Stack | 132 bytes available |
| Source lines | 2,256 |
| New commands | None (safety hardening of existing paths) |
## Motivation
The SkyWalker-1 controls an LNB power supply capable of 750 mA at 18V with no hardware watchdog on the FX2LP. A firmware hang leaves the power supply energized indefinitely. Prior firmware versions contained multiple paths where a stuck I2C bus, an unresponsive GPIF, or a slow EP0 transfer could spin forever without recovery.
A structured safety review identified 21 issues across 4 severity levels:
| Severity | Count | Description |
|----------|-------|-------------|
| Critical | 3 | Infinite hangs in I2C bus scan, `do_tune()`, and DiSEqC Timer2 waits |
| High | 6 | Unprotected GPIF/EP0/EP2 waits, unchecked I2C returns in sweep functions |
| Medium | 7 | Missing error codes, payload validation, lock-bit magic numbers |
| Low | 5 | Code clarity, documentation, saturation guards |
## Software Watchdog
A Timer0-based software watchdog provides a safety backstop for any remaining or unknown hang paths.
**Architecture:**
- Timer0 runs in Mode 1 (16-bit, no auto-reload) at 4 MHz (48 MHz / 12)
- Overflow period: 16.384 ms per tick
- Watchdog window: 122 ticks = ~2 seconds
- The main loop calls `wdt_kick()` on every iteration to reset the counter
- Long-running operations (spectrum sweep, blind scan) call `wdt_kick()` inside their loops
**If the watchdog fires:**
<Steps>
1. Timer0 ISR sets `IOA` to cut LNB power (`PIN_PWR_EN` off, `PIN_PWR_DIS` on)
2. ISR sets `wdt_armed = 2` (fired state) — prevents re-arming
3. Main loop detects `wdt_armed == 2` on next iteration (if it recovers)
4. Main loop sets `last_error = ERR_WDT_FIRED` (0x0D) and clears `config_status`
5. Host can read `ERR_WDT_FIRED` via `GET_LAST_ERROR` (0xBC)
</Steps>
<Aside type="caution">
The watchdog shares `IOA` with the main loop via read-modify-write. To avoid RMW races between the ISR and main-loop GPIO operations, `wdt_armed` uses a tri-state protocol: `0` = off, `1` = armed, `2` = fired. The ISR only writes `IOA` once (on fire), and `wdt_kick()` refuses to re-arm when `wdt_armed == 2`.
</Aside>
## Timeout Protection
Every bare spin loop in the firmware has been replaced with a countdown-protected equivalent. These are the paths that could previously hang forever:
| Path | Location | Timeout | Error Code |
|------|----------|---------|------------|
| `i2c_wait_done()` | I2C DONE bit | 6,000 iterations | `ERR_I2C_TIMEOUT` |
| `i2c_wait_stop()` | I2C STOP bit | 6,000 iterations | `ERR_I2C_TIMEOUT` |
| `ep0_wait_data()` | EP0 BUSY bit | 6,000 iterations | `ERR_EP0_TIMEOUT` |
| `gpif_start()` | GPIF idle | 60,000 iterations | `ERR_GPIF_TIMEOUT` |
| `gpif_stop()` | GPIF idle | 60,000 iterations | `ERR_GPIF_TIMEOUT` |
| EP2 FIFO full | `do_param_sweep()` | 60,000 iterations | `ERR_EP2_TIMEOUT` |
| EP2 FIFO full | `do_spectrum_sweep()` | 60,000 iterations | `ERR_EP2_TIMEOUT` |
| Timer2 overflow | `diseqc_wait_ticks()` | 6,000 per tick | `ERR_DISEQC_TIMER` |
| I2C bus scan | `0xB4` handler | via `i2c_wait_done/stop` | `ERR_I2C_TIMEOUT` |
The `ep0_wait_data()` helper replaces 7 separate bare `while (EP0CS & bmEPBUSY)` loops in vendor command handlers. After the wait, each handler validates `EP0BCL` against the expected payload size.
## Error Codes
All error codes are set in the `last_error` variable and readable via `GET_LAST_ERROR` (0xBC). The error is sticky — it persists until overwritten by a new error.
| Code | Name | Meaning |
|------|------|---------|
| `0x00` | `ERR_OK` | No error |
| `0x01` | `ERR_I2C_TIMEOUT` | I2C DONE or STOP bit timeout |
| `0x02` | `ERR_I2C_NAK` | I2C slave did not ACK |
| `0x03` | `ERR_BCM_TIMEOUT` | BCM4500 poll-ready timeout |
| `0x04` | `ERR_BCM_NOT_READY` | BCM4500 not booted (tune attempted before boot) |
| `0x05` | `ERR_BCM_VERIFY` | BCM4500 register verify mismatch |
| `0x06` | `ERR_TUNE_FAIL` | Tune operation failed |
| `0x07` | `ERR_EP0_TIMEOUT` | EP0 data phase timeout or short payload |
| `0x08` | `ERR_GPIF_TIMEOUT` | GPIF did not reach idle state |
| `0x09` | `ERR_EP2_TIMEOUT` | EP2 FIFO full timeout during sweep |
| `0x0A` | `ERR_NOT_SUPPORTED` | Operation not supported (e.g., tone burst B) |
| `0x0B` | `ERR_DISEQC_LEN` | DiSEqC message length invalid |
| `0x0C` | `ERR_DISEQC_TIMER` | Timer2 overflow timeout during DiSEqC |
| `0x0D` | `ERR_WDT_FIRED` | Watchdog fired — LNB power was cut |
## I2C Return-Value Checks
All I2C write operations in sweep and scan functions now check return values. Failed writes skip to the next iteration instead of proceeding with stale data:
- `do_param_sweep()` — `bcm_indirect_write_block()` and `bcm_direct_write()` calls
- `do_spectrum_sweep()` — `bcm_indirect_write_block()` calls
- `do_blind_scan()` — `bcm_indirect_write_block()` and `bcm_direct_write()` calls
- `do_adaptive_blind_scan()` — same pattern
## do_tune() Safety
The `do_tune()` function was the most dangerous code path — it performed a 12-byte I2C write using the fx2lib `i2c_write()` function, which has no timeout protection. In v3.05.0:
- The fx2lib `i2c_write()` call is replaced with `i2c_write_multi_timeout()`, which applies the standard I2C timeout to every byte
- Both `bcm_poll_ready()` calls check their return values
- Both `bcm_direct_write()` calls check their return values
- An early guard rejects the tune if the BCM4500 is not booted (`ERR_BCM_NOT_READY`)
## DiSEqC Safety
- **Tone burst B** is explicitly rejected with `ERR_NOT_SUPPORTED` — the hardware only supports tone burst A (unmodulated carrier)
- **Invalid message lengths** (&lt;3 or &gt;6 bytes) set `ERR_DISEQC_LEN`
- **Timer2 waits** in `diseqc_wait_ticks()` have per-tick timeout protection — a stuck Timer2 no longer hangs the firmware
- `diseqc_tone_burst()` was refactored to use `diseqc_wait_ticks()` instead of three separate bare `while (!TF2)` loops
## Other Fixes
| Fix | Description |
|-----|-------------|
| `hp_changes` saturation | Counter capped at 0xFFFF to prevent u16 wrap-around |
| `stream_diag_poll()` | Only updates `sd_last_status` / `sd_last_lock` on successful I2C reads |
| `BCM_LOCK_BIT` constant | Replaces 4 hardcoded `0x20` literals with named constant |
| Hotplug data integrity | `hp_prev` bitmap is only updated after a successful scan completes |
| EP0BUF zero-fill | Commands 0x87 and 0xB7 zero-fill EP0BUF before indirect reads, ensuring I2C failures return zeros instead of stale buffer contents |
## Memory Budget
```
Code: 13,079 / 15,360 bytes (85%) +948 bytes from v3.04
XRAM: 218 / 512 bytes (43%) +2 bytes (wdt_counter, wdt_armed)
Stack: 132 bytes available unchanged
```
The safety additions cost 948 bytes of code space — well within the 2,281-byte margin. The 8051 stack starts at `0x7C` with 132 bytes, which is sufficient for the deepest call chain (vendor command → sweep → I2C helpers → Timer0 ISR).
## Hardware Validation
Firmware v3.05.0 was loaded onto physical hardware and tested:
| Test | Result |
|------|--------|
| Firmware load + re-enumeration | Pass |
| Version readback (0x92) | `v3.05.0 (2026-02-16)` |
| BCM4500 boot (0x89) | `STARTED \| FW_LOADED`, stage `COMPLETE` |
| I2C bus scan (0xB4) | Found BCM4500 (0x08), tuner (0x10), EEPROM (0x51) |
| Spectrum sweep (9502150 MHz) | 121 points, no hang |
| LNB power control (13V/18V/22kHz) | All config bits track correctly |
| DiSEqC 1.0 switch | No error |
| DiSEqC tone burst A | No error |
| DiSEqC 1.2 motor commands | No error |
| Tune attempt (no signal) | No hang, `last_error` stays OK |
| Signal monitor (no signal) | 0.0 dB SNR, no lock |
| Adversarial test suite | 55/55 passed |
| Watchdog false-fire | None observed |
See [Safety Testing](/tools/safety-testing/) for the full adversarial test methodology and results.
## What Changed from v3.04
| Feature | v3.04 | v3.05 |
|---------|-------|-------|
| Software watchdog | None | Timer0-based, 2-second window |
| I2C timeout paths | Helpers only | All paths including bus scan, DiSEqC |
| EP0 BUSY protection | None (bare spin) | `ep0_wait_data()` with timeout |
| GPIF idle protection | None (bare spin) | Timeout with `ERR_GPIF_TIMEOUT` |
| EP2 full protection | None (bare spin) | Timeout with `ERR_EP2_TIMEOUT` |
| `do_tune()` I2C | fx2lib `i2c_write()` (no timeout) | `i2c_write_multi_timeout()` |
| Error codes | 6 (`0x00``0x05`) | 14 (`0x00``0x0D`) |
| Payload validation | None | EP0BCL checked against expected size |
| DiSEqC Timer2 | Bare `while (!TF2)` | Timeout-protected `diseqc_wait_ticks()` |
| Sweep I2C checks | Unchecked writes | Returns checked, `continue` on fail |
| Adversarial testing | None | 55-test Hamilton suite |

View file

@ -5,12 +5,13 @@ description: Side-by-side comparison of all known SkyWalker-1 firmware revisions
import { Tabs, TabItem, Badge, Aside } from '@astrojs/starlight/components';
Five firmware versions have been analyzed through Ghidra reverse engineering and source code review. This page compares their architecture, features, and binary characteristics.
Six firmware versions have been analyzed through Ghidra reverse engineering and source code review. This page compares their architecture, features, and binary characteristics.
## Version Summary
| Firmware | Version ID | Build Date | Target PID | Functions | Binary Size | Stack Pointer |
|----------|-----------|------------|------------|-----------|-------------|---------------|
| Custom v3.05.0 | `0x030500` | 2026-02-16 | `0x0203` | N/A | 13,079 bytes (RAM) | `0x7B` |
| Custom v3.01.0 | `0x030100` | 2026-02-12 | `0x0203` | N/A | ~3 KB (RAM) | N/A |
| v2.13.01 (FW1) | `0x020D01` | 2010-03-12 | `0x0203` | 82-88 | 9,322 bytes | `0x50` |
| v2.13.02 (FW2) | `0x020D01` | 2010-03-12 | `0x0203` | 83 | 9,377 bytes | `0x50` |
@ -19,12 +20,42 @@ Five firmware versions have been analyzed through Ghidra reverse engineering and
| v2.06.04 | `0x020604` | 2007-07-13 | `0x0203` | 61 | 9,472 bytes | `0x72` |
<Aside type="note">
Rev.2 v2.10 targets PID `0x0202` (a different product line). All other versions target `0x0203` (SkyWalker-1). The custom v3.01.0 is compiled with SDCC + fx2lib and loaded into FX2 RAM, not flashed to EEPROM.
Rev.2 v2.10 targets PID `0x0202` (a different product line). All other versions target `0x0203` (SkyWalker-1). The custom firmware versions are compiled with SDCC + fx2lib and loaded into FX2 RAM, not flashed to EEPROM. Custom v3.05.0 is the safety-hardened release with software watchdog and comprehensive timeout protection.
</Aside>
## Version-by-Version Details
<Tabs>
<TabItem label="Custom v3.05">
### Custom v3.05.0 <Badge text="Custom" variant="success" /> <Badge text="Safety" variant="caution" />
Safety-hardened firmware with software watchdog and timeout protection on all code paths. Built on the v3.04 codebase with all features from v3.01 through v3.04.
| Property | Value |
|----------|-------|
| Version ID | `0x030500` |
| Build date | 2026-02-16 |
| Toolchain | SDCC + fx2lib |
| Source | `firmware/skywalker1.c` (2,256 lines) |
| Code size | 13,079 / 15,360 bytes (85%) |
| XRAM | 218 / 512 bytes (43%) |
| Stack | 132 bytes available |
| Error codes | 14 (`0x00`--`0x0D`) |
**Additions over v3.04:**
- Timer0 software watchdog (2-second window, cuts LNB power on stall)
- Timeout protection on all EP0, GPIF, EP2, and DiSEqC Timer2 spin loops
- `ep0_wait_data()` helper replacing 7 bare `while (EP0CS & bmEPBUSY)` loops
- EP0 payload length validation on all OUT vendor commands
- I2C return-value checks on all write operations in sweep/scan functions
- `do_tune()` rewritten with timeout-protected I2C and early guard for unbooted BCM4500
- 8 new error codes for observable failure modes
- 55-test Hamilton adversarial test suite (`test_hamilton.py`)
See the [Custom v3.05](/firmware/custom-v305/) page for the full safety review findings and hardware validation results.
</TabItem>
<TabItem label="Custom v3.01">
### Custom v3.01.0 <Badge text="Custom" variant="success" />
@ -147,20 +178,22 @@ The original SkyWalker-1 firmware extracted from the device's onboard EEPROM.
## Architectural Differences
| Feature | Custom v3.01 | v2.13 | Rev.2 v2.10 | v2.06 |
|---------|--------------|-------|-------------|-------|
| Vendor commands | 30 stock + 7 custom | 30 | 27 | 30 |
| INT0 handler | N/A (fx2lib ISR) | Demod polling | USB re-enum | USB re-enum |
| Demod probe at boot | Yes (with timeout) | Yes (40 attempts) | No | No |
| Retry loops | Yes (with timeout) | Yes (20-attempt) | No | No |
| HW revision detect | No | Yes (flag `_1_3`) | Yes (descriptor walker) | No |
| DiSEqC data pin | P0.7 | P0.0 | P0.4 | P0.7 |
| Config byte IRAM addr | C variable | `0x4F` | `0x4E` | `0x6D` |
| BCM4500 status poll | 1 register | 1 register | 3 registers | 3 registers |
| I2C timeout | 6000-count | None | None | None |
| Anti-tampering | No | Yes | No | No |
| New commands | 0xB0--0xB6 | 0x99, 0x9A, 0x9C | 0x99/0x9A proto | -- |
| 0x9D behavior | N/A | Conditional demod reset | N/A (out of range) | HW revision mode |
| Feature | Custom v3.05 | Custom v3.01 | v2.13 | Rev.2 v2.10 | v2.06 |
|---------|--------------|--------------|-------|-------------|-------|
| Vendor commands | 30 stock + 18 custom | 30 stock + 7 custom | 30 | 27 | 30 |
| INT0 handler | N/A (fx2lib ISR) | N/A (fx2lib ISR) | Demod polling | USB re-enum | USB re-enum |
| Demod probe at boot | Yes (with timeout) | Yes (with timeout) | Yes (40 attempts) | No | No |
| Retry loops | Yes (with timeout) | Yes (with timeout) | Yes (20-attempt) | No | No |
| HW revision detect | No | No | Yes (flag `_1_3`) | Yes (descriptor walker) | No |
| DiSEqC data pin | P0.7 | P0.7 | P0.0 | P0.4 | P0.7 |
| Config byte IRAM addr | C variable | C variable | `0x4F` | `0x4E` | `0x6D` |
| BCM4500 status poll | 1 register | 1 register | 1 register | 3 registers | 3 registers |
| I2C timeout | All paths (6000-count) | Helpers only (6000-count) | None | None | None |
| Software watchdog | Timer0 (2-second) | None | None | None | None |
| EP0/GPIF/EP2 timeout | All protected | None | None | None | None |
| Error codes | 14 | 6 | N/A | N/A | N/A |
| Anti-tampering | No | No | Yes | No | No |
| New commands | 0xB0--0xBE | 0xB0--0xB6 | 0x99, 0x9A, 0x9C | 0x99/0x9A proto | -- |
## Kernel Version Constants