Embedded systems · real-time firmware
I crashed the network chip 11 times. The control loop didn’t miss a deadline.
Kestrel-RT is a real-time runtime I wrote in C for a 48 MHz Arm Cortex-M4F, from the reset vector up. Phase 2 moved networking onto the board's ESP32-S3 and treated it as untrusted. In one 10,003,440-period soak with 11 forced crashes of that chip, the real-time side had 0 resets and 0 deadline misses; separate campaigns with Wi-Fi floods and 15,000 malformed frames also caused 0 resets and 0 critical misses.
At a glance
- 10.43M
- Phase 1: control periods in one 2.9 h soak, 0 deadline misses (10,429,500)
- 11
- Phase 2: forced ESP32-S3 crashes in one 10,003,440-period soak, 0 controller resets
- 0
- Phase 2: of 15,000 malformed frames accepted, in a separate fault campaign
- +3.3 µs
- control start p99, the measured cost of the network link
The problem
A robot controller puts a hard-timed control loop next to code that misbehaves: networking, logging, telemetry. I wanted to own every layer of that on one microcontroller, from the reset handler to the scheduler to the drivers, and measure what each design choice actually costs.
What I built
- Runtime
- Own startup and linker script, a tickless fixed-priority scheduler on a 64-bit timebase, PendSV context switching, unprivileged tasks under 8 MPU regions, no heap.
- Fault handling
- Crash records that survive the reset, a contained fault parks one task while control keeps running, and a supervisor feeds the watchdog only while the critical tasks vote.
- I/O and tracing
- Register-level DMA for all UART traffic; telemetry drops records (counted) instead of blocking; a binary flight recorder.
- Network isolation
- The ESP32-S3 reaches the controller only through a DMA ring and a budgeted task: 300 µs per job, sessions, a sequence window, replay rejection, 400 frames/s rate limit.
Results
- Phase 1 soak
- One 2.9 h run on the physical board: 10,429,500 control periods with 0 misses, 0 overruns, 0 faults across all six tasks; control start latency p99 45.0 µs, max 50.5 µs.
- Faults
- 17/17 injected cases on the final firmware (earlier runs 16/17, traced to UART receive overruns and fixed): MPU violations, bus and usage faults, stack overflow, asserts, hangs. Contained faults cost control 0 deadlines once a handler bug that missed one was fixed.
- Comparison
- Same workload on FreeRTOS 11.1.0, Zephyr 4.2.0 and a superloop. Under 7x overload Kestrel's control task degraded to half rate while safety, sensor and comms kept their deadlines; the FreeRTOS baseline missed every control job and starved the other tasks.
- Phase 2 soak
- One 10,003,440-period run with a forced ESP32-S3 panic every 15 minutes (11 in total): 0 controller resets, 0 deadline misses on any task, 0 protocol desyncs.
- Phase 2 campaigns
- Separate short runs: Wi-Fi flood, CPU and heap exhaustion, an 83,711-frame mailbox flood, 15,000 malformed frames (0 accepted), a held-low line and reboot loops, each with 0 controller resets and 0 critical misses.
The crash, as the board logged it
-- force an ESP32-S3 panic (NULL write on the ESP)
[live] ESP reply: ok crash
[live] ESP gone: no telemetry for 2.2 s (ESP reboot, Wi-Fi rejoin, new session)
[live] telemetry resumed: RA4 epoch 585065 (same RA4 boot), link session 46789 -> 13097
[live] RA4 control ran through it: +2,300 jobs in 2.3 s (1 ms period), misses 0 -> 0 What did not work
The failures are kept in the repository with root causes. The network chip's download mode once produced about 1.17 million receive-error interrupts on the controller. The first link budget let a job reach 507 µs against a 400 µs gate. The protocol desynchronized permanently after 15 s of silence. Each was fixed and has a regression test.
Limitations
- Trade-off
- FreeRTOS had lower control-start latency (p50 15.9 vs 24.6 µs). Kestrel spends that time on per-job deadline accounting and MPU switching.
- Security
- Fault isolation, not a security boundary: the ESP32-S3 still controls the controller's reset and debug lines.
- Scope
- Measured on one board, with no motor driven. The cause of the +3.3 µs link cost is not yet isolated.