A 1U CubeSat-scale avionics research platform built around a hardware-enforced watchdog supervisor, an attitude determination pipeline, and a forensic black-box logging system. The goal is to demonstrate deterministic fault recovery in a multi-MCU embedded system where a silicon-level supervisor operates independently of the software it monitors.
Targeting VDAT (VLSI Design and Test), India.
Software watchdogs (IWDG, WWDG, task monitors) share the same fault domain as the software they protect. If the OS or scheduler locks up, the watchdog lock up with it. This project moves the watchdog function into a dedicated eFPGA that runs its own clock domain and has direct access to the MCU reset line. The flight computer cannot disable or defer it.
The secondary problem this addresses is post-fault observability. When a satellite crashes and reboots, the question is always: what was happening in the last few seconds before the fault? This project builds toward a non-volatile "black box" ring buffer that captures the last 60 seconds of sensor data before any hardware-triggered reset.
+---------------------+ UART (115200) +---------------------+
| Shrike-Lite | =========================> | ESP32-C6 (Glyph) |
| (RP2040) | <========================= | (PCBCupid) |
| | [TLM_PACKET/CMD_INGEST] | |
| - MicroPython | | - ESP-IDF |
| - MPU6050 (I2C1) | HB_TICK | - WiFi / MQTT |
| - Comp. Filter | | - NVS Forensics |
| - Flashes FPGA | | - HB to FPGA |
+---------------------+ +---------------------+
| |
| SPI (bitstream flash) GPIO14 (5Hz toggle)
| |
v v
+------------------------------+ +---------------------+
| SLG47910 eFPGA | | SLG47910 eFPGA |
| (xio_guardian) | <============| hb_in (PIN 18) |
| | HW_RESET | |
| - 50 MHz internal clock | ============>| rst_out (PIN 17) |
| - 5s heartbeat timeout | to RP2040 RUN pin |
| - Hard RST on flatline +------------------------------+
+------------------------------+
+---------------------+ ESP-NOW (failover) +---------------------+
| ESP32-C6 (Glyph) | =========================> | Relay Node |
| | [SWARM_RELAY] | (Standalone ESP32)|
+---------------------+ +---------------------+
|
| WiFi / MQTT
|
v
+---------------------+
| Ground Hub |
| (Laptop) |
| - Mosquitto broker |
| - ROS2 bridge |
| - SCADA terminal |
+---------------------+
| Component | Part | Role |
|---|---|---|
| Mission computer | Shrike-Lite (RP2040) | ADCS, IMU read, FPGA flash, UART telemetry |
| Comms node | ESP32-C6 "Glyph" (PCBCupid) | WiFi, MQTT uplink, heartbeat to FPGA, forensics |
| Watchdog supervisor | SLG47910 eFPGA (Dialog/Renesas) | Hardware-only fault detection, RST trigger |
| Sensor | MPU6050 | 3-axis accel + gyro for attitude estimation |
| Relay node | Standalone ESP32 | ESP-NOW fallback when WiFi link is unavailable |
Boot sequence:
- Flash the SLG47910 bitstream over SPI (
shrike.flash("watch_4.bin")) - Initialize UART1 at 115200 baud (TX: RP_IO8, RX: RP_IO9)
- Initialize I2C1 at 400 kHz (SDA: RP_IO14, SCL: RP_IO15), wake MPU6050
- Run 200-sample static calibration to compute pitch and roll offsets
- Enter 20 Hz main loop: read raw IMU, apply complementary filter, send
pitch,roll\nover UART
Complementary filter:
pitch = 0.98 * (pitch + gyro_rate_x * dt) + 0.02 * acc_pitch
roll = 0.98 * (roll + gyro_rate_y * dt) + 0.02 * acc_roll
Gyroscope sensitivity set to 131 LSB/deg/s (default +/-250 dps range).
Four modules:
heartbeat.c
Toggles GPIO14 at 5 Hz from a pinned FreeRTOS task (priority 10). This is the signal the FPGA monitors. heartbeat_stop() kills the task and drives the pin hard LOW, guaranteeing a flatline that the FPGA detects within 5 seconds.
forensics.c On every boot:
- Reads
boot_countandlast_resetfrom NVS partition"xio" - After a confirmed MQTT connection, calls
forensics_record_boot()which increments the counter and capturesesp_reset_reason() - Publishes boot count and last failure message to
guardian/status
This means the NVS record is only committed on a clean, network-verified boot. A crash loop does not inflate the count until a successful recovery is confirmed.
mqtt_layer.c Connects to a Mosquitto broker. Publishes to three topics:
guardian/status-- boot count + failure reason JSONguardian/telemetry--{"pitch": x.xx, "roll": x.xx}at 20 Hzguardian/report-- anomaly events (reserved)
Subscribes to guardian/command. The kill command writes a reason string to NVS, stops the heartbeat, and deliberately hangs -- forcing the FPGA watchdog to trigger a hardware reset. This is the fault injection mechanism.
adcs_telemetry_task (main.c)
UART1 ingestion task. Reads from GPIO20 (RX from Shrike), accumulates bytes into a line buffer until \n, then sscanfs the pitch,roll pair and forwards it as JSON to MQTT. Buffer is 2048 bytes to handle the burst during Shrike boot and calibration.
Boot sequence in app_main:
forensics_init() -> heartbeat_init() -> wifi_init_sta() -> mqtt_wait() ->
forensics_record_boot() -> publish_status() -> launch adcs_task
Heartbeat starts before WiFi to prevent the FPGA from triggering a reset during the network bring-up window.
module xio_guardian (
input wire osc_clk, // 50 MHz internal clock
input wire hb_in, // Heartbeat from ESP32 (PIN 18)
output reg rst_out // RST to RP2040 RUN pin (PIN 17)
);
reg [27:0] count = 0;
reg last_hb = 0;
always @(posedge osc_clk) begin
if (hb_in != last_hb) begin
count <= 0;
last_hb <= hb_in;
rst_out <= 1; // System active
end else if (count >= 250000000) begin
rst_out <= 0; // 5s elapsed, assert reset
end else begin
count <= count + 1;
end
end
endmoduleThe counter is 28 bits. At 50 MHz, 250,000,000 ticks = 5 seconds. Any toggle on hb_in resets the counter. The FPGA has no dependency on the software stack it supervises. The bitstream is compiled for the SLG47910 and stored as FPGA_bitstream_MCU.bin. Shrike loads it over SPI at boot.
A Python ROS2 node (GuardianBridge) that bridges MQTT to the ROS2 graph:
| MQTT topic | ROS2 topic | Direction |
|---|---|---|
guardian/status |
/guardian/status |
satellite -> ground |
guardian/telemetry |
/guardian/telemetry |
satellite -> ground |
guardian/report |
/guardian/report |
satellite -> ground |
guardian/command |
/guardian/command |
ground -> satellite |
Any message arriving on guardian/report triggers an error-level log in the ROS2 node, surfacing crash events to the operator terminal immediately.
| Path | Label | Description |
|---|---|---|
| RP2040 -> FPGA | [HB_TICK] |
Heartbeat toggle signal |
| RP2040 <-> ESP32-C6 | [TLM_PACKET] |
IMU telemetry stream (UART) |
| RP2040 <-> ESP32-C6 | [CMD_INGEST] |
Command downlink to RP2040 |
| FPGA -> RP2040 / ESP32 | [HW_RESET] |
Hardware reset assertion |
| Source | Label | Description |
|---|---|---|
| ESP32-C6 | [NOMINAL_HK] |
Housekeeping status (boot count, uptime) |
| Relay node | [SWARM_RELAY] |
ESP-NOW relayed packets |
| RP2040 | [ADCS_DATA] |
Pitch / roll telemetry |
| RP2040 | [URGENT: CRASH] |
Fault report with pre-crash context |
| RP2040 | [EVT_ANOMALY] |
Non-fatal anomaly event |
| ALL | [CMD_ACK] |
Command acknowledgement |
xio_sat/
|
+-- watchdog_poc/
| +-- main.py # Shrike-Lite MicroPython firmware (entry point)
| +-- esp_idf_hb/
| | +-- main/
| | +-- main.c # ESP32-C6 app_main, ADCS task
| | +-- heartbeat.c/h # 5Hz GPIO heartbeat to FPGA
| | +-- forensics.c/h # NVS boot count + reset reason
| | +-- mqtt_layer.c/h # MQTT publish/subscribe, kill command
| | +-- wifi.c/h # Station mode WiFi, event group sync
| +-- esp_hb/ # Earlier Arduino/micro-ROS prototype (superseded)
| +-- IMU_test/ # Standalone IMU validation sketch
| +-- watchdog/ # Early watchdog Arduino sketch (superseded)
| +-- fpga_logic/ # Early FPGA logic tests
|
+-- avionics/
| +-- supervisor_fpga/
| +-- xio_guardian/
| +-- ffpga/
| +-- src/main.v # Watchdog Verilog (xio_guardian module)
| +-- build/ # Compiled bitstreams (MCU, FLASH, OTP variants)
|
+-- ground_hub/
+-- guardian_bridge/
+-- guardian_bridge/
+-- guardian_bridge_node.py # ROS2 <-> MQTT bridge node
- Flash MicroPython firmware to the RP2040 via UF2 (hold BOOTSEL, drag UF2)
- Copy
watchdog_poc/main.pyand theshrikelibrary to the device filesystem - Copy
watch_4.bin(FPGA bitstream) to the device root - The script runs automatically on power-up
cd watchdog_poc/esp_idf_hb
idf.py set-target esp32c6
idf.py build
idf.py flash monitorSet WiFi credentials and MQTT broker IP in wifi.h and mqtt_layer.c before building.
The bitstream is pre-compiled. shrike.flash("watch_4.bin") loads it over SPI from Shrike at boot. To recompile, open xio_guardian.ffpga in the Dialog/Renesas GreenPAK Designer tool and synthesize from ffpga/src/main.v.
cd ground_hub
colcon build
source install/setup.bash
ros2 run guardian_bridge bridge_nodeRequires a Mosquitto broker running on localhost (sudo apt install mosquitto).
- FPGA watchdog operational: 5s timeout, hard RST via RP2040 RUN pin
- ESP32-C6 heartbeat task: 5 Hz GPIO toggle, survives WiFi bring-up window
- Forensics layer: NVS boot count + reset reason, only committed on clean recovery
- Fault injection via
killcommand: MQTT -> heartbeat stop -> FPGA triggers reset -> NVS records reason -> next boot publishes it - Shrike MicroPython: FPGA flash at boot, MPU6050 wake and calibration, 20 Hz complementary filter, UART stream
- ESP32-C6 UART ingestion: parses
pitch,roll\nfrom Shrike, forwards as JSON to MQTT - ROS2 ground bridge: routes status, telemetry, report topics; command relay to ESP32
- Flash ring buffer: architecture defined (60s non-volatile sensor log before any FPGA reset), not yet implemented in firmware
[URGENT: CRASH]pipeline:heartbeat_stop()and NVS write exist, but the pre-crash data capture andguardian/reportpublish are not wired up[EVT_ANOMALY]events: label defined in schema, no publisher yet
- Relay node (standalone ESP32): ESP-NOW failover when WiFi link is unavailable,
[SWARM_RELAY]publish [CMD_INGEST]on RP2040: command parser on the Shrike side, currently one-directional- SGP4 orbital propagation on RP2040
- Latency measurement tooling: time delta from
[EVT_ANOMALY]to[HW_RESET] - Reliability heatmap:
[CMD_ACK]loss rate during fault injection phases - Relay success rate benchmark:
[NOMINAL_HK]vs[SWARM_RELAY]signal strength vs. distance
- Deterministic fault recovery for satellite avionics
- Uninterrupted edge nodes (agricultural IoT, remote industrial)
- Hardware-in-the-loop (HIL) testing rig for fault-tolerant system validation
- Resilient swarm constellations with mesh relay fallback