Einen Raumfahrzeug-Computersimulator mit Claude Code bauen
Ich wollte verstehen, wie Computer auf Raumfahrzeugen funktionieren. Die Echtzeit-Planung, die Strahlungshärtung, die verzögerungstolerante Vernetzung, die Mars-Rover mit der Erde in Verbindung hält. Mit Artemis II am Horizont (die erste bemannte Mondmission seit über 50 Jahren) schien es der richtige Zeitpunkt zu sein, tiefer einzutauchen. Also baute ich einen Simulator. Von Grund auf. Mit Claude Code als meinem Pair-Programmer.
Keine physische Hardware. Alles läuft in QEMU und Docker auf einem Laptop. Code auf GitHub.
flowchart LR
QEMU["QEMU (Cortex-M3)"] -->|UART socket| Bridge[uart_bridge.py]
Bridge -->|bpsendfile| SC[Spacecraft Node]
SC -->|LTP + 5s delay| GS[Ground Station]
subgraph Docker
SC
GS
end
flowchart TB
QEMU["QEMU (Cortex-M3)"] -->|UART socket| Bridge[uart_bridge.py]
Bridge -->|bpsendfile| SC["Spacecraft Node (Docker)"]
SC -->|LTP + 5s delay| GS["Ground Station (Docker)"]
Warum Dieses Projekt
Ich wollte etwas wirklich Schwieriges lernen. Etwas, bei dem die Konzepte unbekannt und das Werkzeug unnachgiebig sind. C-Compiler, die auf ARM abzielen. Linker-Skripte. Speicherabgebildete Ein-/Ausgabe. Interrupt-Vektortabellen.
Claude Code hat das möglich gemacht. Nicht weil es den gesamten Code schrieb, sondern weil es erklärte, was ein Linker-Skript tut, während wir eines schrieben, sodass die Erklärung im eigentlichen Problem verankert war, das ich löste. Dasselbe gilt für volatile, FreeRTOS-Prioritäts-Preemption und LTP-Neuübertragungstimer.
Der Fahrplan
Ich teilte das Projekt in vier Phasen auf, jede auf der letzten aufbauend:
| Phase | Was | Schlüsselkonzepte |
|---|---|---|
| Bare Metal | ARM-Kreuzkompilierung, UART-Ausgabe, Interrupt-Handler | Speicherabgebildete E/A, SysTick-Timer, Start-Assembly |
| FreeRTOS | Tasks, Warteschlangen, Watchdogs, Prioritätsinversion | Deterministisches Scheduling, Mutex-Protokolle, Preemption |
| DTN | Zwei-Knoten-Netzwerk, degradierte Verbindungen, CFDP, Kontaktgraph-Routing | Bundle-Protokoll, Store-and-Forward, LTP-Zuverlässigkeit |
| Integration | Vollständige Telemetrie-Pipeline mit Mars-Entfernungsverzögerungen | UART-Brücke, tc netem, synchronisiertes ION OWLT |
Jede Phase hat automatisierte Tests. Jeder Meilenstein ist ein PR mit bestandenem CI.
Phase 1: Bare Metal
Die erste Herausforderung war, überhaupt etwas zum Laufen zu bringen. ARM-Kreuzkompilierung, die auf einen Cortex-M3 (MPS2-AN385) in QEMU abzielt. Kein Betriebssystem, keine Standardbibliothek, kein printf.
Claude half mir, die Startsequenz zu verstehen: die Vektortabelle, den Reset-Handler, das Kopieren von .data von Flash in RAM, das Nullen von .bss. Dinge, die passieren, bevor main() überhaupt ausgeführt wird.
Der erste Erfolg war ein einzelnes Zeichen auf einer UART-Konsole:
#define UART0_DR (*(volatile uint32_t *)0x40004000)
void uart_putc(char c) {
UART0_DR = c;
}
Zwei Zeilen C. Keine Bibliotheken. Nur ein Byte an eine Speicheradresse schreiben, die zufällig mit einem seriellen Port verdrahtet ist. Es fühlte sich an wie direkt mit der Maschine zu sprechen.
Danach: SysTick-Timer-Interrupts, Interrupt-Handler, strukturierte Ausgabe. Die SysTick-Demo konfiguriert einen Hardware-Timer, der bei 2 Hz feuert und 10 Ticks zählt:
SysTick interrupt demo
======================
Ticking at 2 Hz for 5 seconds (10 ticks)...
tick 1
tick 2
tick 3
tick 4
tick 5
tick 6
tick 7
tick 8
tick 9
tick 10
Done — 5 seconds counted by interrupt.
Keine verlorenen Ticks. Keine extra Ticks. Die CPU schläft zwischen Interrupts und die Hardware weckt sie genau im richtigen Moment auf. Deterministische Ausführung aus dem Bare Metal.
Phase 2: FreeRTOS
Mit funktionierendem Bare Metal fügte ich FreeRTOS hinzu, ein Echtzeit-Betriebssystem, das auf allem läuft, von medizinischen Geräten bis hin zu Satelliten.
Die Übungen wurden schrittweise aufgebaut:
- Zwei Tasks mit unterschiedlichen Raten. Grundlegendes Multitasking.
- Warteschlangen-basierte Kommunikation. Daten sicher zwischen Tasks teilen.
- Watchdog-Timer. Hängende Tasks erkennen und davon erholen.
- Sensor-Pipeline. Vier Sensoren mit unterschiedlichen Raten, die eine Verarbeitungskette speisen.
- Prioritätsinversion. Den klassischen RTOS-Bug auslösen und beheben.
Hier startet die Sensor-Pipeline. Beachten Sie, wie der 10-Hz-Gyroskop den Datenstrom dominiert, mit dem 1-Hz-Temperaturlesegerät, das bei Tick 1001 eingeschoben wird:
FreeRTOS Sensor Pipeline Demo
=============================
[GYRO] Sensor online (10 Hz, priority 4)
[PROC] Processor online (priority 3)
[TELEM] Telemetry online (priority 2)
[TEMP] Sensor online (1 Hz, priority 1)
[TELEM] #000 GYRO: 300 at tick 100
[TELEM] #001 GYRO: 300 at tick 200
...
[TELEM] #009 GYRO: 300 at tick 1000
[TELEM] #010 TEMP: 1991 at tick 1001
[TELEM] #011 GYRO: 300 at tick 1100
Das ist prioritätsbasierte Preemption in Aktion. Das Gyroskop läuft mit Priorität 4, der Temperatursensor mit Priorität 1. Die Temperaturmessung kommt nur durch, wenn das Gyroskop die CPU nicht belegt.
Die Prioritätsinversionsübung war die lehrreichste. Ich erstellte drei Tasks, bei denen ein niedrigprioritärer Task einen Mutex hält, den ein hochprioritärer Task benötigt, und ein mittelprioritärer Task beide aushungert. Die Lösung: Prioritätsvererbung, bei der FreeRTOS den niedrigprioritären Task vorübergehend anhebt, damit er den Mutex schneller freigeben kann.
Das ist der Bug, der die Mars-Pathfinder-Mission 1997 fast getötet hätte. Es selbst zu bauen ließ die Lehrbucherklärung einrasten.
Phase 3: Verzögerungstolerante Vernetzung
TCP/IP funktioniert nicht im Weltraum. Hin-und-Rückzeiten zum Mars reichen von 6 bis 44 Minuten. Verbindungen fallen stundenlang aus, wenn Planeten das Signal verdecken. TCPs Annahme einer kontinuierlichen Verbindung mit niedriger Latenz bricht vollständig zusammen.
NASA JPLs Antwort ist DTN, oder Verzögerungstolerante Vernetzung. Das Bundle-Protokoll speichert Daten lokal und leitet sie Hop für Hop weiter, wenn Verbindungen verfügbar werden. Es ist genau für die Bedingungen konzipiert, die das Internet kaputt machen.
Ich baute NASAs ION-Implementierung in Docker und führte zunehmend schwierigere Tests durch:
- Grundlegende Konnektivität. Zwei Knoten tauschen Bundles aus.
- Degradierte Verbindungen.
tc netemfügt 500ms Latenz, 25% Paketverlust und vollständige Ausfälle hinzu. - CFDP-Dateiübertragung. Zuverlässige Dateiübertragung mit Integritätsprüfungen.
- Kontaktgraph-Routing. Bundles in der Warteschlange während Verbindungslücken, geliefert wenn Fenster sich öffnen.
Der intermittierende Verbindungstest erzählt die ganze DTN-Geschichte in ein paar Ausgabezeilen:
Test: intermittent link (send during outage, deliver on recovery)...
qdisc: qdisc netem root refcnt 2 limit 1000 loss 100%
confirmed: bundle queued (link is down)
restoring link...
PASS: bundle held during outage
PASS: bundle delivered after link recovery
Das ist Store-and-Forward in Aktion. Das Bundle wird gesendet, während die Verbindung vollständig tot ist, 100% Paketverlust. Es sitzt in der Warteschlange des lokalen Knotens. In dem Moment, in dem wir die netem-Regel löschen und die Konnektivität wiederherstellen, überträgt LTP neu und das Bundle kommt am anderen Ende an. TCP hätte längst aufgegeben.
Phase 4: Integration
Die letzte Phase verbindet alles. FreeRTOS-Firmware generiert Telemetrie in QEMU. Ein Python-Brücken-Skript liest die UART-Ausgabe und injiziert sie als DTN-Bundles. Die Bundles durchqueren ein verzögertes Netzwerk, um eine Bodenstation zu erreichen.
Die Firmware startet und beginnt sofort zu streamen:
# Spacecraft Telemetry Firmware v1.0
# ===================================
# GYRO sensor online (10 Hz, priority 4)
# Processor online (priority 3)
# Telemetry online (priority 2)
# TEMP sensor online (1 Hz, priority 1)
# BATT sensor online (0.5 Hz, priority 1)
# SUN sensor online (2 Hz, priority 1)
$TELEM,0000,GYRO,300,100
$TELEM,0001,GYRO,300,200
...
$TELEM,0005,SUN,801,501
...
$TELEM,0011,TEMP,1991,1001
$TELEM,0012,SUN,801,1001
Vier Sensoren mit vier verschiedenen Raten. Man kann sehen, wie das 10-Hz-Gyroskop die meisten Messwerte produziert, mit dem 2-Hz-Sonnensensor, der 1-Hz-Temperatur und der 0,5-Hz-Batterie dazwischen. Die Brücke fasst diese alle 2 Sekunden in DTN-Bundles zusammen.
Der Mars-Verzögerungstest beweist, dass die gesamte Pipeline unter realistischen Bedingungen funktioniert:
Running Mars-delay end-to-end tests...
PASS: tc netem delay 5s applied on spacecraft
PASS: bridge exited cleanly
PASS: bridge read telemetry lines
PASS: bridge sent >= 1 bundle (got 12)
PASS: ground station received files (got 8)
PASS: received telemetry lines (got 224)
PASS: telemetry CSV format valid (5 fields)
INFO: delay observable (8 delivered at bridge exit < 12 sent)
7 passed, 0 failed
12 Bundles gesendet, aber nur 8 geliefert, als die Brücke beendete. Der Rest war noch unterwegs durch die 5-Sekunden-Verzögerung. Das ist die Verzögerung, die real ist, nicht in Software simuliert.
Die Mars-Verzögerungssimulation verwendet zwei synchronisierte Mechanismen:
- tc netem fügt 5 Sekunden echter Netzwerklatenz zu beiden Containern hinzu
- ION-Bereichstabellen setzen die gleiche 5-Sekunden-Einweglichtlaufzeit, damit LTP-Neuübertragungstimer korrekt sind
Beide müssen übereinstimmen. Wenn die tatsächliche Verzögerung 5 Sekunden beträgt, ION aber glaubt, es sei 1 Sekunde, überträgt LTP aggressiv neu und überflutet die Verbindung. Das richtig hinzubekommen lehrte mich mehr über Protokolldesign als jedes Lehrbuchkapitel über Zuverlässigkeit.
Mit Claude Code arbeiten
Dieses Projekt hätte mich ohne Claude Monate gekostet. Nicht weil der Code komplex ist (die meisten Dateien haben unter 300 Zeilen), sondern weil die Lernkurve für jedes Gebiet steil ist.
Was gut funktioniert hat:
- Domänenübergreifendes Wissen. Das Projekt umfasst C, Python, ARM-Assembly, Docker, ION DTN, FreeRTOS, QEMU, tc netem und LTP. Keine einzelne Person kennt all diese Gebiete gut. Claude konnte zwischen ihnen wechseln.
- Testgetriebene Meilensteine. Jede Phase hat automatisierte Tests. Claude half dabei, Assertions zu schreiben, die echtes Verhalten verifizieren, nicht nur “es kompiliert”.
- PR-basierter Workflow. Jeder Meilenstein ist ein separater Branch und PR mit CI. Claude Codes Worktree-Unterstützung hielt das sauber. Isolierte Branches, keine versehentliche Kreuzkontaminierung.
Was Sorgfalt erforderte:
- Embedded C ist unnachgiebig. Off-by-One-Fehler in Linker-Skripten geben keinen Stack-Trace. Sie geben einen Hard Fault oder stille Korruption. Claudes Vorschläge mussten sorgfältig anhand der Hardware-Dokumentation überprüft werden.
- ION DTN-Dokumentation ist spärlich. Claudes Trainingsdaten enthalten nicht die tiefen ION-Interna. Ich verließ mich mehr auf den ION-Quellcode und Konfigurationsbeispiele als auf Claudes Erklärungen zu ION-spezifischem Verhalten.
Die Zahlen
| Metrik | Wert |
|---|---|
| Gesamte Tests | 50+ Assertions in 7 Test-Suites |
| Sprachen | C, Python, Bash |
| Hardware | Keine (QEMU + Docker) |
| Zeilen C | ~1.200 (Firmware + Bare Metal) |
| Zeilen Python | ~1.500 (Tests + Brücke + DTN-Skripte) |
| ION DTN-Konfiguration | ~350 Zeilen in 6 .rc-Dateien |
Ausprobieren
Alles ist Open Source und läuft auf jedem Linux-Rechner mit QEMU und Docker:
sudo apt install gcc-arm-none-eabi qemu-system-arm build-essential
git clone https://github.com/granda/spacecraft-computing-sim
cd spacecraft-computing-sim
make -C bare-metal run # bare metal UART output
make -C freertos run # FreeRTOS sensor pipeline
make -C dtn test # two-node DTN network
make -C integration test-bridge # full telemetry pipeline
make -C integration test-mars-delay # Mars-distance delays
Keine physische Hardware. Keine Cloud-Dienste. Nur ein Laptop und Neugier darüber, wie Computer auf Raumfahrzeugen funktionieren.