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:
parent
7223fcf810
commit
8b4aacab81
@ -97,6 +97,7 @@ export default defineConfig({
|
|||||||
{ label: 'FW2.13 Variants', slug: 'firmware/fw213-variants' },
|
{ label: 'FW2.13 Variants', slug: 'firmware/fw213-variants' },
|
||||||
{ label: 'Custom v3.01', slug: 'firmware/custom-v301' },
|
{ label: 'Custom v3.01', slug: 'firmware/custom-v301' },
|
||||||
{ label: 'Custom v3.02', slug: 'firmware/custom-v302' },
|
{ label: 'Custom v3.02', slug: 'firmware/custom-v302' },
|
||||||
|
{ label: 'Custom v3.05', slug: 'firmware/custom-v305' },
|
||||||
{ label: 'Storage Formats', slug: 'firmware/storage-formats' },
|
{ label: 'Storage Formats', slug: 'firmware/storage-formats' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -120,6 +121,7 @@ export default defineConfig({
|
|||||||
{ label: 'Debugging', slug: 'tools/debugging' },
|
{ label: 'Debugging', slug: 'tools/debugging' },
|
||||||
{ label: 'TS Analyzer', slug: 'tools/ts-analyzer' },
|
{ label: 'TS Analyzer', slug: 'tools/ts-analyzer' },
|
||||||
{ label: 'Spectrum Analysis', slug: 'tools/spectrum-analysis' },
|
{ label: 'Spectrum Analysis', slug: 'tools/spectrum-analysis' },
|
||||||
|
{ label: 'Safety Testing', slug: 'tools/safety-testing' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
181
site/src/content/docs/firmware/custom-v305.mdx
Normal file
181
site/src/content/docs/firmware/custom-v305.mdx
Normal 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** (<3 or >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 (950–2150 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 |
|
||||||
@ -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';
|
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
|
## Version Summary
|
||||||
|
|
||||||
| Firmware | Version ID | Build Date | Target PID | Functions | Binary Size | Stack Pointer |
|
| 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 |
|
| 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.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` |
|
| 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` |
|
| v2.06.04 | `0x020604` | 2007-07-13 | `0x0203` | 61 | 9,472 bytes | `0x72` |
|
||||||
|
|
||||||
<Aside type="note">
|
<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>
|
</Aside>
|
||||||
|
|
||||||
## Version-by-Version Details
|
## Version-by-Version Details
|
||||||
|
|
||||||
<Tabs>
|
<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">
|
<TabItem label="Custom v3.01">
|
||||||
|
|
||||||
### Custom v3.01.0 <Badge text="Custom" variant="success" />
|
### 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
|
## Architectural Differences
|
||||||
|
|
||||||
| Feature | Custom v3.01 | v2.13 | Rev.2 v2.10 | v2.06 |
|
| Feature | Custom v3.05 | Custom v3.01 | v2.13 | Rev.2 v2.10 | v2.06 |
|
||||||
|---------|--------------|-------|-------------|-------|
|
|---------|--------------|--------------|-------|-------------|-------|
|
||||||
| Vendor commands | 30 stock + 7 custom | 30 | 27 | 30 |
|
| Vendor commands | 30 stock + 18 custom | 30 stock + 7 custom | 30 | 27 | 30 |
|
||||||
| INT0 handler | N/A (fx2lib ISR) | Demod polling | USB re-enum | USB re-enum |
|
| 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 (40 attempts) | No | No |
|
| Demod probe at boot | Yes (with timeout) | Yes (with timeout) | Yes (40 attempts) | No | No |
|
||||||
| Retry loops | Yes (with timeout) | Yes (20-attempt) | No | No |
|
| Retry loops | Yes (with timeout) | Yes (with timeout) | Yes (20-attempt) | No | No |
|
||||||
| HW revision detect | No | Yes (flag `_1_3`) | Yes (descriptor walker) | No |
|
| HW revision detect | No | No | Yes (flag `_1_3`) | Yes (descriptor walker) | No |
|
||||||
| DiSEqC data pin | P0.7 | P0.0 | P0.4 | P0.7 |
|
| DiSEqC data pin | P0.7 | P0.7 | P0.0 | P0.4 | P0.7 |
|
||||||
| Config byte IRAM addr | C variable | `0x4F` | `0x4E` | `0x6D` |
|
| Config byte IRAM addr | C variable | C variable | `0x4F` | `0x4E` | `0x6D` |
|
||||||
| BCM4500 status poll | 1 register | 1 register | 3 registers | 3 registers |
|
| BCM4500 status poll | 1 register | 1 register | 1 register | 3 registers | 3 registers |
|
||||||
| I2C timeout | 6000-count | None | None | None |
|
| I2C timeout | All paths (6000-count) | Helpers only (6000-count) | None | None | None |
|
||||||
| Anti-tampering | No | Yes | No | No |
|
| Software watchdog | Timer0 (2-second) | None | None | None | None |
|
||||||
| New commands | 0xB0--0xB6 | 0x99, 0x9A, 0x9C | 0x99/0x9A proto | -- |
|
| EP0/GPIF/EP2 timeout | All protected | None | None | None | None |
|
||||||
| 0x9D behavior | N/A | Conditional demod reset | N/A (out of range) | HW revision mode |
|
| 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
|
## Kernel Version Constants
|
||||||
|
|
||||||
|
|||||||
202
site/src/content/docs/tools/safety-testing.mdx
Normal file
202
site/src/content/docs/tools/safety-testing.mdx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
title: Safety Testing
|
||||||
|
description: Hamilton adversarial test suite for firmware safety validation — operator error, invalid inputs, state machine violations, and rapid-fire stress.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Badge, Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Hamilton adversarial test suite (`test_hamilton.py`) validates that the firmware survives every kind of wrong input an operator or buggy host software could send. Named after Margaret Hamilton's methodology at MIT Instrumentation Laboratory — she tested "what if the astronaut pushes the wrong button?" and found bugs that saved Apollo missions. <Badge text="v3.05" variant="success" />
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo python3 tools/test_hamilton.py
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Requires `pyusb`, root access, and firmware v3.05.0+ loaded. The device must have the BCM4500 booted before running. The test handles boot/shutdown sequences internally.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Test Philosophy
|
||||||
|
|
||||||
|
Traditional hardware testing validates the happy path — correct inputs producing correct outputs. Adversarial testing validates the *unhappy* path: what happens when the host sends garbage? The firmware must never:
|
||||||
|
|
||||||
|
1. **Hang** — every code path must terminate in bounded time
|
||||||
|
2. **Corrupt state** — invalid inputs must not leave the device in an inconsistent state
|
||||||
|
3. **Fire the watchdog** — the main loop must stay responsive
|
||||||
|
4. **Become unresponsive** — the device must accept new commands after any error
|
||||||
|
|
||||||
|
Each test follows the same pattern:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
1. Read `last_error` before the operation
|
||||||
|
2. Send the invalid/abusive command
|
||||||
|
3. Verify the device is still alive (`GET_FW_VERS` returns `3.05.0`)
|
||||||
|
4. Read `last_error` after — check it matches expectations
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="DiSEqC">
|
||||||
|
|
||||||
|
### Category 1: DiSEqC Message Abuse
|
||||||
|
|
||||||
|
Tests the DiSEqC messaging path with invalid parameters. DiSEqC uses Timer2-based Manchester encoding on the 22 kHz carrier — a stuck timer would hang the firmware without the v3.05.0 `diseqc_wait_ticks()` timeout fix.
|
||||||
|
|
||||||
|
| Test | Input | Expected |
|
||||||
|
|------|-------|----------|
|
||||||
|
| 1a. Tone burst B | `wValue=1` | `ERR_NOT_SUPPORTED` (0x0A) |
|
||||||
|
| 1b. Tone burst 0xFF | `wValue=0xFF` | `ERR_NOT_SUPPORTED` (0x0A) |
|
||||||
|
| 1c. Too short | 2-byte payload | No hang |
|
||||||
|
| 1d. Too long | 8-byte payload | No hang |
|
||||||
|
| 1e. Empty | 0-byte payload | No hang |
|
||||||
|
| 1f. Recovery | Valid 4-byte message | No hang, correct execution |
|
||||||
|
| 1g. Motor halt | DiSEqC 1.2 `E0 31 60` (no motor connected) | No hang |
|
||||||
|
| 1h. Drive 255 steps | DiSEqC 1.2 `E0 31 68 FF` | No hang |
|
||||||
|
| 1i. Bogus USALS | DiSEqC 1.2 `E0 31 6E FF FF` | No hang |
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Tune">
|
||||||
|
|
||||||
|
### Category 2: Tune Parameter Abuse
|
||||||
|
|
||||||
|
Sends tuning commands with out-of-range parameters. The BCM4500 demodulator receives these values directly over I2C — the firmware must not hang regardless of what the host sends.
|
||||||
|
|
||||||
|
| Test | Input | Expected |
|
||||||
|
|------|-------|----------|
|
||||||
|
| 2a. Zero symbol rate | `SR=0` | No hang |
|
||||||
|
| 2b. Max symbol rate | `SR=0xFFFFFFFF` | No hang |
|
||||||
|
| 2c. Zero frequency | `freq=0` | No hang |
|
||||||
|
| 2d. Max frequency | `freq=0xFFFFFFFF` | No hang |
|
||||||
|
| 2e. Invalid modulation | `mod=0xFF` | No hang |
|
||||||
|
| 2f. Invalid FEC | `fec=0xFF` | No hang |
|
||||||
|
| 2g. Truncated (4 bytes) | 4 of 10 bytes sent | `ERR_EP0_TIMEOUT` (0x07) |
|
||||||
|
| 2h. Single byte | 1 of 10 bytes sent | `ERR_EP0_TIMEOUT` (0x07) |
|
||||||
|
| 2i. All zeros | 10 zero bytes | No hang |
|
||||||
|
| 2j. All 0xFF | 10 × `0xFF` | No hang |
|
||||||
|
|
||||||
|
Tests 2g and 2h specifically validate the `ep0_wait_data()` + `EP0BCL` payload length check. The firmware waits for the EP0 data phase, but when the host sends fewer bytes than expected, the timeout fires and the command is safely aborted.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="I2C">
|
||||||
|
|
||||||
|
### Category 3: I2C Address Space Abuse
|
||||||
|
|
||||||
|
Sends I2C reads and writes to non-existent or invalid addresses. Before v3.05.0, the bus scan command (`0xB4`) used bare `while (!(I2CS & bmDONE))` loops that would hang if any address didn't ACK.
|
||||||
|
|
||||||
|
| Test | Input | Expected |
|
||||||
|
|------|-------|----------|
|
||||||
|
| 3a. Addr 0x7F | Non-existent I2C device | `ERR_I2C_NAK` (0x02) |
|
||||||
|
| 3b. Addr 0x00 | General call address | No hang |
|
||||||
|
| 3c. Page 0xFF | BCM4500 indirect read, invalid page | No hang |
|
||||||
|
| 3d. Count=0 | Multi-reg read with zero count | No hang |
|
||||||
|
| 3e. Count=255 | Multi-reg read exceeding 64 max | No hang (clamped) |
|
||||||
|
| 3f. Write to 0x7F | Raw I2C write to non-existent device | No hang |
|
||||||
|
| 3g. Reserved reg | BCM4500 direct read of reg 0xFF | No hang |
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="State Machine">
|
||||||
|
|
||||||
|
### Category 4: State Machine Violations
|
||||||
|
|
||||||
|
Tests operations in the wrong order — the classic "what if the astronaut pushes the wrong button?" scenario.
|
||||||
|
|
||||||
|
| Test | Scenario | Expected |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 4a. Double boot | `BOOT_8PSK(1)` when already booted | No hang |
|
||||||
|
| 4b. Tune with BCM off | Power off BCM4500, then tune | `ERR_BCM_NOT_READY` (0x04) |
|
||||||
|
| 4c. Signal read, BCM off | Signal monitor with demod powered down | No hang |
|
||||||
|
| 4d. Bus scan, BCM off | I2C bus scan with demod powered down | No hang |
|
||||||
|
| 4e. Hotplug, BCM off | Force hotplug rescan with demod off | No hang |
|
||||||
|
| 4f. Recovery | Re-boot BCM4500 after power cycle | `STARTED \| FW_LOADED` |
|
||||||
|
| 4g. Arm + disarm | Arm streaming then immediately disarm | No hang |
|
||||||
|
| 4h. Disarm, not armed | Disarm when already disarmed | No hang |
|
||||||
|
| 4i. Rapid boot toggle | Off → on → off → on in quick succession | No hang |
|
||||||
|
|
||||||
|
Test 4b is the most important: it proves the `do_tune()` early guard works. Without the v3.05.0 fix, tuning with a powered-off BCM4500 would attempt I2C writes to a device that isn't there, potentially hanging on the bus.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Boundaries">
|
||||||
|
|
||||||
|
### Category 5: Boundary and Buffer Abuse
|
||||||
|
|
||||||
|
Tests USB transfer size mismatches — requesting more or fewer bytes than the firmware produces.
|
||||||
|
|
||||||
|
| Test | Scenario | Expected |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 5a. 0 bytes from GET_CONFIG | Request 0 bytes (firmware sends 1) | No hang |
|
||||||
|
| 5b. 64 bytes from GET_CONFIG | Request 64 bytes (firmware sends 1) | No hang |
|
||||||
|
| 5c. 64 bytes from GET_LAST_ERROR | Request 64 bytes (firmware sends 1) | No hang |
|
||||||
|
| 5d. 1 byte from GET_FW_VERS | Request 1 byte (firmware sends 6) | No hang |
|
||||||
|
| 5e. 1 byte from GET_STREAM_DIAG | Request 1 byte (firmware sends 12) | No hang |
|
||||||
|
| 5f. STREAM_DIAG wval=0xFFFF | Non-standard wValue | No hang |
|
||||||
|
| 5g. HOTPLUG wval=0xFFFF | Non-standard wValue | No hang |
|
||||||
|
|
||||||
|
The USB layer handles size mismatches — the firmware writes its full response to EP0BUF regardless of how many bytes the host requested. The host-side libusb may return an `Overflow` error, but the device remains stable.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Stress">
|
||||||
|
|
||||||
|
### Category 6: Rapid-Fire Stress
|
||||||
|
|
||||||
|
Sends many commands in quick succession to test for timing-dependent failures.
|
||||||
|
|
||||||
|
| Test | Operations | Typical Time |
|
||||||
|
|------|-----------|--------------|
|
||||||
|
| 6a. Config reads | 200 × `GET_CONFIG` | ~9 ms |
|
||||||
|
| 6b. Error reads | 50 × `GET_LAST_ERROR` | ~2 ms |
|
||||||
|
| 6c. Signal monitors | 30 × `SIGNAL_MONITOR` | ~276 ms |
|
||||||
|
| 6d. Voltage toggles | 40 × `SET_LNB_VOLTAGE` | ~2 ms |
|
||||||
|
| 6e. DiSEqC messages | 10 × 4-byte DiSEqC switch | ~1,473 ms |
|
||||||
|
|
||||||
|
Single-byte vendor commands (config, error) complete in under 0.05 ms. Signal monitors are slower (~9 ms each) because they perform multiple I2C reads. DiSEqC messages are the slowest due to Timer2-based bit-bang encoding.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Invalid Cmds">
|
||||||
|
|
||||||
|
### Category 7: Invalid Vendor Commands
|
||||||
|
|
||||||
|
Sends vendor command codes that don't exist in the firmware.
|
||||||
|
|
||||||
|
| Test | Command | Expected |
|
||||||
|
|------|---------|----------|
|
||||||
|
| 7a-f. Unknown codes | `0xFF`, `0x01`, `0x50`, `0xFE`, `0x00`, `0x79` | USB STALL |
|
||||||
|
| 7g. Wrong direction | `GET_CONFIG` as OUT | No hang |
|
||||||
|
| 7h. Oversized payload | 64-byte payload to `GET_CONFIG` | No hang |
|
||||||
|
|
||||||
|
Unknown commands correctly return a USB STALL (the EP0 error response per USB spec). The firmware's `handle_vendorcommand()` returns FALSE for unrecognized request codes, which causes fx2lib to send the STALL handshake.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
The full test suite runs in under 30 seconds on hardware:
|
||||||
|
|
||||||
|
```
|
||||||
|
================================================================
|
||||||
|
HAMILTON ADVERSARIAL TEST — FINAL RESULTS
|
||||||
|
-----------------------------------------
|
||||||
|
Tests passed: 55
|
||||||
|
Tests failed: 0
|
||||||
|
Device alive: True
|
||||||
|
Final error: 0x08 [GPIF_TIMEOUT]
|
||||||
|
Watchdog fired: No
|
||||||
|
Verdict: PASS
|
||||||
|
================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The final `GPIF_TIMEOUT` error is expected — it comes from the arm/disarm toggle test (4g). The GPIF state machine hadn't fully started before it was told to stop. The error code correctly reports what happened, and the device remains fully operational.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Running the Tests
|
||||||
|
|
||||||
|
The test can be run after any firmware change to verify safety properties are preserved:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Load firmware and run tests
|
||||||
|
sudo python3 tools/fw_load.py load firmware/build/skywalker1.ihx --wait 5
|
||||||
|
sudo python3 tools/test_hamilton.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The test automatically boots the BCM4500, runs all 55 tests, and powers everything down. If any test fails, the device state and error code are reported. If the device becomes unresponsive, the test halts immediately and reports which operation killed it.
|
||||||
Loading…
x
Reference in New Issue
Block a user