How we fit a satellite flight computer in 68 KB of flash
ICARUS — our satellite OBC platform — runs the full stack (kernel + OBC app + ground-link protocols) in 68 KB of flash on a Cortex-M7. No Linux. No FreeRTOS. Here's why, and what it cost us to get here.
ICARUS Engineering - flight-software
- rtos
- icarus
- cortex-m
- obc
- embedded
Most satellite onboard computers run one of three things: Linux on an SBC (too big, too hungry, too much attack surface), FreeRTOS on a microcontroller (light, but you inherit a kernel written for thermostats), or a port of NASA cFS that weighs roughly as much as the spacecraft itself.
We didn’t want any of those. So we wrote our own flight software platform from scratch, targeted at a Cortex-M7 with 128 KB of flash, and fit everything — kernel, OBC application, CCSDS ground-link, 5-level FDIR, CFDP file transfer — in 68 KB.
This is the post about how that happened, what we gave up to get there, and why we think it’s the right shape for CubeSat-class missions.
Why 68 KB matters
68 KB is a number, not a philosophy. The philosophy underneath it is this: a satellite is a small computer that happens to be in space, and the software has to reflect that.
For a 3U CubeSat flying a primary payload, the OBC typically gets a small slice of a shared board. The entire flight-software budget — kernel, application, telemetry, fault handling, file transfer — often has to fit in the same chip that also runs the radio stack and power management. Every kilobyte you reserve for the OBC is a kilobyte the radio team doesn’t get.
Which means if your flight software platform needs 512 KB just to boot, you’ve already told the integrator to pick a bigger chip [fn] Or a bigger board. Or a bigger sat. Software cost compounds upward through every mechanical decision.. And bigger chips cost more, draw more power, and have a worse radiation response.
So fit matters. Not because 68 KB is a trophy — because the next engineer building a mission needs headroom for the things we didn’t write.
The alternatives, for context
Three things you’d normally reach for:
| Option | Typical flash | What it gives you | What it costs |
|---|---|---|---|
| Linux on SBC | Doesn’t apply — runs from storage | Rich userspace, standard toolchains, huge ecosystem | 256 MB+ RAM, a real MMU, an attack surface, and determinism that depends on which scheduler won the last config merge |
| FreeRTOS | ~5–10 KB | Preemptive scheduling, battle-tested, free | A kernel not written for flight. You build FDIR, CCSDS, file transfer, and ground tooling on top. |
| NASA cFS | ~100K LOC, huge flash | Everything, including the kitchen | Everything, including the kitchen. Fine for a Mars lander. Wrong shape for a 3U. |
cFS is the interesting one — it’s well-engineered, flight-proven, open-source, and clearly the right answer for a class of missions. It’s also hundreds of times the footprint, with a concept vocabulary (cFE, SB, EVS, CFE_ES) you have to fully internalize before writing useful code. For a two-person flight-software team building a short-lived CubeSat, that’s a mismatch.
The cheapest kernel feature is the one you didn’t write.
What “68 KB” actually contains
When we say “ICARUS fits in 68 KB,” this is what’s in there:
ICARUS Core (custom RTOS kernel)
├─ scheduler (priority + time-sliced) ≈ 4.1 KB
├─ MPU isolation + task context switch ≈ 2.3 KB
├─ system tick + deadline tracking ≈ 1.4 KB
├─ IPC (mailboxes + semaphores) ≈ 1.8 KB
└─ persistent BKPRAM driver ≈ 0.9 KB
OBC application layer
├─ CCSDS TC (telecommand) parser ≈ 3.6 KB
├─ CCSDS TM (telemetry) framer ≈ 2.4 KB
├─ 5-level hierarchical FDIR ≈ 7.8 KB
├─ onboard command sequencer ≈ 4.2 KB
├─ execution checkpoint + recovery ≈ 3.1 KB
├─ CFDP class-1 file transfer ≈ 6.5 KB
├─ HW CRC + SEU detection ≈ 1.2 KB
└─ power + thermal monitors ≈ 2.8 KB
Drivers + HAL
├─ UART / SPI / I2C / CAN ≈ 8.4 KB
├─ ADC + GPIO ≈ 1.1 KB
└─ flash controller (wear-levelled) ≈ 3.9 KB
Runtime glue
├─ C runtime (minimal newlib-nano) ≈ 6.2 KB
├─ startup + vector table ≈ 0.8 KB
└─ boot-loader stub ≈ 2.4 KB
Total: ≈ 64.9 KB (headroom + alignment → 68 KB on the map)
About ~5,600 lines of C across the OBC application layer, ~9 KB of which lives in ITCM (zero wait-state, deterministic) for the hot path.
The four decisions that did most of the work
1. No dynamic allocation, ever
ICARUS has zero malloc. All memory is statically reserved at link time: task control blocks, IPC buffers, FDIR event ring, CFDP file descriptors. This isn’t a dogma — it’s a footprint strategy. A heap implementation robust enough for flight adds 4–6 KB of code plus fragmentation bookkeeping. Skipping it buys us 10% of our entire flash budget.
It also means every task knows its worst-case memory cost at compile time, which simplifies FDIR enormously: there’s no “out-of-memory” fault class to handle because the condition can’t arise.
2. One scheduler, not six
FreeRTOS ships with half a dozen scheduling strategies (cooperative, preemptive, time-sliced, tickless-idle variations, etc.) because it targets every MCU from an AVR up. ICARUS picks one — priority-preemptive with bounded time slicing on ties — and deletes the others from the source.
Same argument for IPC: FreeRTOS has queues, semaphores, mutexes, event groups, stream buffers, and message buffers. ICARUS has mailboxes + counting semaphores, full stop. If you need something fancier, you build it on top of those two primitives explicitly, which usually reveals that you didn’t need the fancier thing.
3. FDIR is a state machine, not a framework
The 5-level hierarchical FDIR (fault detection, isolation, and recovery) is the single biggest piece of application code at 7.8 KB. It has to be, because FDIR is where a flight computer earns its name — everything else is plumbing.
But it’s not a framework. There’s no rules engine, no scripting language, no “user-configurable FDIR” abstraction layer. It’s a hand-written state machine with five levels, from “I saw a bit-flip, scrub RAM” up through “the task won’t come back, power-cycle the subsystem” up through “we’re losing the bus, restart the OBC.” Every transition is reviewable in a single C function.
Ground operators can load rules (thresholds, masks, recovery actions) via BKPRAM — but they can’t introduce new fault classes. That rigidity is a feature: an operator can’t accidentally brick the spacecraft by uploading a bad FDIR rule because the structure is frozen in ROM.
4. CFDP instead of SFTP or a custom protocol
File transfer was the one place we almost wrote our own thing. The path not taken: a bespoke chunked-upload protocol with our own retry logic. The path taken: CFDP class-1 (unacknowledged) with a thin class-2-style ACK layer on top of our own TM channel.
Why CFDP? Two reasons.
- It’s what every ground station already speaks. Using a CCSDS-family protocol means the operator team can integrate us into existing infrastructure instead of treating us as a weird snowflake.
- The class-1 profile is small. 6.5 KB for a complete flight-side implementation, because we don’t have to do segmentation or extensive error recovery at the CFDP layer — our TC/TM stack does that already.
What we gave up
Fitting in 68 KB has costs. We’re not going to pretend otherwise.
- No dynamic code loading. You can’t side-load a new task in flight. If we need a new capability on-orbit, it ships as a kernel update in the next OTA — which we support, but it’s more ceremony than a “push a module” workflow.
- Limited language runtime. No C++ exceptions, no RTTI, no virtual multiple inheritance. We use C for everything. Some teams find this limiting; we found it clarifying.
- One MCU family. ICARUS targets STM32H7 (and we’re porting to STM32U5 for radiation-tolerant variants). It’s not a portable kernel in the way FreeRTOS is. Trading portability for footprint is explicit.
- No POSIX compatibility layer. If your application code assumes
pthread_createor a filesystem at/, it won’t run on ICARUS without rewriting. Our OBC app layer was written native to the kernel; there’s no Linux-ish abstraction to port against.
What comes next
A few things on the roadmap that aren’t in the 68 KB today but will be:
- MISRA-C compliance — we’re close but not clean. Estimated footprint cost: negligible (MISRA is mostly style + defensive patterns, not more code).
- DO-178C DAL C documentation — not a code change, but a non-trivial documentation + trace-matrix lift. We’ve started the SRS/SDD work; targeting completion for the first operational mission.
- Radiation-tolerant STM32U5 port — the STM32U5 is newer and has better SEU characteristics for the kind of orbits we care about. Flash is slightly different; we expect ICARUS Core to port cleanly, the OBC app layer to need driver-level adjustments only.
The thing we didn’t say out loud
Hardware engineers love to mock the 1 MB-per-image web. Web engineers love to mock fragile firmware. Flight software sits in a third camp: everything has to fit, everything has to work, and nobody gets to ship a v2 patch to a dead satellite.
That last point is what drove every decision in this post. We’re not trying to win a byte-counting contest. We’re trying to leave headroom for the next team to write the code we didn’t know they’d need.
68 KB is the current answer. We’ll let you know when it changes.
Footnotes
The ICARUS Core kernel (open source, Apache-2.0) lives at github.com/ironhide23586/icarus-os-core. The OBC application layer is commercial per-mission. If you’re building a CubeSat and want to talk, say hi — we’ll send you a kernel benchmark for your hardware within the week.