Создание симулятора бортового компьютера космического аппарата с Claude Code

Я хотел понять, как работают компьютеры космических аппаратов. Планирование в реальном времени, защита от радиации, устойчивые к задержкам сети, которые поддерживают связь марсоходов с Землёй. С приближением Artemis II (первой пилотируемой лунной миссии за более чем 50 лет) пришло время погрузиться в эту тему. Поэтому я создал симулятор. С нуля. С Claude Code в роли напарника-программиста.

Никакого физического оборудования. Всё запускается в QEMU и Docker на ноутбуке. Код на 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)"]

Почему именно этот проект

Я хотел изучить что-то действительно сложное. Что-то, где концепции незнакомы, а инструменты беспощадны. Компиляторы C, нацеленные на ARM. Скрипты компоновщика. Ввод/вывод с отображением в память. Таблицы векторов прерываний.

Claude Code сделал это возможным. Не потому что написал весь код, а потому что объяснял, что делает скрипт компоновщика, пока мы его писали, так что объяснение было основано на реальной задаче, которую я решал. То же самое для volatile, вытеснения по приоритету FreeRTOS и таймеров повторной передачи LTP.

Дорожная карта

Я разбил проект на четыре фазы, каждая из которых строилась на предыдущей:

ФазаЧтоКлючевые концепции
Bare MetalARM-кросс-компиляция, вывод UART, обработчики прерыванийВвод/вывод с отображением в память, таймер SysTick, стартовый ассемблер
FreeRTOSЗадачи, очереди, сторожевые таймеры, инверсия приоритетовДетерминированное планирование, протоколы мьютекса, вытеснение
DTNДвухузловая сеть, деградированные каналы, CFDP, маршрутизация по графу контактовBundle Protocol, хранение и пересылка, надёжность LTP
ИнтеграцияПолный телеметрический конвейер с задержками на расстоянии МарсаМост UART, tc netem, синхронизированный ION OWLT

Каждая фаза имеет автоматизированные тесты. Каждый этап — это PR с пройденным CI.

Фаза 1: Bare Metal

Первой задачей было заставить хоть что-то работать. ARM-кросс-компиляция для Cortex-M3 (MPS2-AN385) в QEMU. Нет ОС, нет стандартной библиотеки, нет printf.

Claude помог мне понять последовательность запуска: таблица векторов, обработчик сброса, копирование .data из флэш в ОЗУ, обнуление .bss. Всё это происходит до того, как main() начнёт выполняться.

Первой победой стал единственный символ, появившийся на UART-консоли:

#define UART0_DR  (*(volatile uint32_t *)0x40004000)

void uart_putc(char c) {
    UART0_DR = c;
}

Две строки C. Никаких библиотек. Просто запись байта по адресу памяти, который оказался подключён к последовательному порту. Это ощущалось как прямой разговор с машиной.

Далее: прерывания таймера SysTick, обработчики прерываний, структурированный вывод. Демонстрация SysTick настраивает аппаратный таймер для срабатывания с частотой 2 Гц и считает 10 тиков:

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.

Ни одного пропущенного тика. Ни одного лишнего тика. Процессор спит между прерываниями, и аппаратура будит его точно в нужный момент. Детерминированное выполнение с bare metal.

Фаза 2: FreeRTOS

После успешной работы bare metal я добавил FreeRTOS — операционную систему реального времени, работающую на всём, от медицинских устройств до спутников.

Упражнения строились постепенно:

  1. Две задачи с разными частотами. Базовая многозадачность.
  2. Коммуникация на основе очередей. Безопасный обмен данными между задачами.
  3. Сторожевой таймер. Обнаружение зависших задач и восстановление после них.
  4. Сенсорный конвейер. Четыре датчика с разными частотами, питающих цепочку обработки.
  5. Инверсия приоритетов. Воспроизведение и устранение классической ошибки RTOS.

Вот запуск сенсорного конвейера. Обратите внимание, как гироскоп на 10 Гц доминирует в потоке, с показанием температуры на 1 Гц, протискивающимся на такте 1001:

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

Это вытеснение по приоритету в действии. Гироскоп работает с приоритетом 4, датчик температуры — с приоритетом 1. Показание температуры проходит только тогда, когда гироскоп не занимает процессор.

Упражнение по инверсии приоритетов было наиболее поучительным. Я создал три задачи, где задача с низким приоритетом удерживает мьютекс, нужный задаче с высоким приоритетом, а задача со средним приоритетом морит голодом обе. Решение: наследование приоритетов, при котором FreeRTOS временно повышает задачу с низким приоритетом, чтобы та могла быстрее освободить мьютекс.

Это та самая ошибка, которая чуть не погубила миссию Mars Pathfinder в 1997 году. Построив её самостоятельно, я понял объяснение из учебника.

Фаза 3: Устойчивые к задержкам сети

TCP/IP не работает в космосе. Время туда и обратно до Марса составляет от 6 до 44 минут. Каналы пропадают на часы, когда планеты заслоняют сигнал. Предположение TCP о непрерывном соединении с низкой задержкой полностью рушится.

Ответ NASA JPL — DTN (Delay-Tolerant Networking). Bundle Protocol хранит данные локально и пересылает их поузлово, когда каналы становятся доступными. Он разработан именно для условий, которые ломают интернет.

Я собрал реализацию NASA ION в Docker и запустил всё более сложные тесты:

Тест прерывистого канала рассказывает всю историю DTN в нескольких строках вывода:

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

Это хранение и пересылка в действии. Bundle отправляется, пока канал полностью мёртв, потеря пакетов 100%. Он ждёт в очереди локального узла. В момент, когда мы снимаем правило netem и восстанавливаем связность, LTP осуществляет повторную передачу, и bundle прибывает на другой конец. TCP давно бы сдался.

Фаза 4: Интеграция

Финальная фаза соединяет всё вместе. Прошивка FreeRTOS генерирует телеметрию в QEMU. Python-скрипт-мост читает вывод UART и вставляет его как DTN-пакеты. Пакеты проходят через сеть с задержкой и достигают наземной станции.

Прошивка запускается и немедленно начинает передачу:

# 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

Четыре датчика с четырьмя разными частотами. Можно видеть, что гироскоп на 10 Гц производит большинство показаний, с датчиком солнца на 2 Гц, температурой на 1 Гц и батареей на 0,5 Гц, чередующимися между ними. Мост группирует их в DTN-пакеты каждые 2 секунды.

Тест задержки до Марса доказывает, что весь конвейер работает в реалистичных условиях:

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 пакетов, но к моменту завершения моста доставлено только 8. Остальные ещё находились в пути через 5-секундную задержку. Это задержка настоящая, а не симулированная программно.

Симуляция задержки до Марса использует два синхронизированных механизма:

  1. tc netem добавляет 5 секунд реальной сетевой задержки обоим контейнерам
  2. Таблицы диапазонов ION устанавливают то же 5-секундное одностороннее время распространения света, чтобы таймеры повторной передачи LTP были корректными

Оба должны совпадать. Если реальная задержка составляет 5 секунд, но ION думает, что 1 секунда, LTP агрессивно повторяет передачу и переполняет канал. Правильная настройка этого научила меня большему о проектировании протоколов, чем любая глава учебника о надёжности.

Работа с Claude Code

Этот проект занял бы у меня месяцы без Claude. Не потому что код сложен (большинство файлов не превышают 300 строк), а потому что кривая обучения для каждой области крутая.

Что работало хорошо:

Что требовало осторожности:

Цифры

МетрикаЗначение
Всего тестов50+ утверждений в 7 наборах тестов
ЯзыкиC, Python, Bash
ОборудованиеНет (QEMU + Docker)
Строк C~1 200 (прошивка + bare metal)
Строк Python~1 500 (тесты + мост + скрипты DTN)
Конфигурация ION DTN~350 строк в 6 файлах .rc

Попробуйте

Всё это открытый исходный код, работающий на любой Linux-машине с QEMU и 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

Никакого физического оборудования. Никаких облачных сервисов. Просто ноутбук и любопытство о том, как работают компьютеры космических аппаратов.