Полётный контроллер для ракеты

Главной задачей простейшего полётного контроллера модели ракеты является запуск системы спасения (выброс парашюта). Вторая задача — запись полётных данных, в минимальном виде — запись значений высоты через определённые интервалы времени. Затем по этим данным можно будет рассчитать скорость и ускорение ракеты, а также ответить на главный вопрос — на какую высоту взлетела ракета?

Предупреждение

Обе эти задачи важны. Ракета без системы спасения становится одноразовой, поскольку обычно разрушается при приземлении. Но это ещё полбеды. Свободно падающая ракета опасна для окружающих, поскольку, благодаря аэродинамической форме, может разогнаться до большой скорости и свалиться на чью-то дачу, теплицу, машину или, в худшем случае — на чью-то голову.

Полётные данные позволяют не только помериться, чья ракета взлетела выше, но и установить, соответствовал ли реальный полёт расчётным данным, то есть был ли он успешным или нет. Без записи данных можно только установить сам факт полёта ракеты.

Ракета, для которой делался этот контроллер, устроена следующим образом. В головной части размещён контроллер. За головной частью следует жёстко прикреплённый к ней парашютный отсек, в котором последовательно размещены вышибной пиропатрон, искрогаситель, поршень и парашют. В парашютный отсек вставлен двигательный отсек, он удерживается за счёт силы трения. Амортизационный фал соединяет между собой парашютный и двигательный отсеки, к нему же крепится парашют. После срабатывания пиропатрона давление образовавшихся газов толкает поршень, он выталкивает парашют и двигательный отсек. Ракета разделяется на две части, парашют раскрывается потоком воздуха.

В моём варианте контроллер должен определить момент запуска, затем до приземления или до истечения 1 минуты (что наступит раньше) каждую 1/10 секунды записывать данные высоты на флеш-карту, засечь точку апогея, и на 10 метров ниже апогея поджечь пиропатрон выброса парашюта.

Совет

Главным достоинством этого контроллера я считаю простую в реализации и программировании схему. Все компоненты достаточно дешёвы. Кроме того, он компактный и легко входит в корпус ракеты с внутренним диаметром 30 мм.

Недостатком также является простота. Контроллер не имеет средств связи и определения координат, и не решает проблему поиска приземлившейся ракеты. Полётные данные можно получить только после извлечения флешки.

Аппаратная часть

Электрическая схема полётного контроллера ракеты

Контроллер построен на базе Arduino Pro Mini 3,3 V. Эта плата имеет вход питания RAW и может питаться от обычного LiPo аккумулятора 3,7 V. Плата очень компактная. Она не имеет USB-разъёма, и для её программирования нужен USB-UART переходник.

Совет

Если переходника нет, но есть плата Arduino Uno, на которой процессор не припаян, а вставлен в слот, то процессор можно вынуть и плата станет переходником USB-UART.

Данные пишутся на micro-SD карту, вставленную в разъём модуля. Решение самое простое, но не лучшее, так как от ускорений и вибрации карта может выпасть.

Вышибной пиропатрон парашюта поджигается через стандартный силовой ключ на транзисторе MOSFET. Питание на поджиг подаётся от отдельного аккумулятора, чтобы в случае короткого замыкания (мало ли что может случиться при зажигании?) не просело питание на контроллере. Пиропатрон является частью схемы, но описывать его конструкцию я не буду.

RGB-светодиод сигнализирует о проблемах (если они возникнут) и о текущей стадии полёта (по мнению контроллера). Такая сигнализация хорошо помогает при «наземной» отладке. У меня он подключён анодом к цифровому пину, на котором всегда HIGH. На схеме я для наглядности нарисовал «правильное» подключение, но в листинге программы под питание выделен отдельный пин.

Данные о высоте приходят с датчика BMP280. Для работы с датчиком используется библиотека Adafruit_BMP280.h.

Сборка контроллера навесным монтажом

Примечание

Собрано всё навесным монтажом и укреплено на куске пластика. Сначала такая идея казалась удачным вариантом, это позволяло менять расположение деталей. Но в процессе сборки я пришёл к выводу, что лучше было бы спаять всё на макетной плате, на двух сторонах для компактности. Дело в том, что надёжно закрепить одну плату проще, чем четыре соединённых шлейфами компонента.

Параметры датчика BMP280

Руководств по работе с этим датчиком в Сети достаточно, здесь я бы хотел остановиться только на функции bmp.setSampling(), отвечающей, в том числе, за частоту измерений.

Параметры датчика, установленные по умолчанию:

bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,    // Режим работы
                Adafruit_BMP280::SAMPLING_X2,    // Точность измерения температуры
                Adafruit_BMP280::SAMPLING_X16,   // Точность измерения давления
                Adafruit_BMP280::FILTER_X16,     // Уровень фильтрации
                Adafruit_BMP280::STANDBY_MS_500); // Период пробуждения, мс

Первый параметр — режим работы датчика, имеет четыре варианта:

  • MODE_NORMAL — в данном режиме модуль циклически выходит из режима сна через установленный интервал времени. В активном состоянии он проводит измерения, сохраняет их в своей памяти и заново уходит в сон.

  • MODE_FORCED — в этом режиме датчик проводит измерения при получении команды от Arduino, после чего возвращается в состояние сна.

  • MODE_SLEEP — режим сна или пониженного энергопотребления.

  • MODE_SOFT_RESET_CODE — сброс на заводские настройки.

Второй и третий параметры — точность измерения температуры и давления, могут иметь следующие варианты:

  • SAMPLING_NONE — минимальная точность;

  • SAMPLING_X1 — точность АЦП 16 бит;

  • SAMPLING_X2 — точность АЦП 17 бит;

  • SAMPLING_X4 — точность АЦП 18 бит;

  • SAMPLING_X8 — точность АЦП 19 бит;

  • SAMPLING_X16 — точность АЦП 20 бит.

Четвёртый параметр, уровень фильтрации, принимает следующие значения:

  • FILTER_OFF — фильтр выключен;

  • FILTER_X2;

  • FILTER_X4;

  • FILTER_X8;

  • FILTER_X16 — максимальный уровень фильтрации.

Наконец, последний параметр отвечает за частоту измерений и имеет следующие варианты:

  • STANDBY_MS_1 — модуль просыпается каждую миллисекунду;

  • STANDBY_MS_63 — модуль просыпается каждые 63 миллисекунды;

  • STANDBY_MS_125 — модуль просыпается каждые 125 миллисекунд;

  • STANDBY_MS_250 — модуль просыпается каждые 250 миллисекунд;

  • STANDBY_MS_500 — модуль просыпается каждые 500 миллисекунд;

  • STANDBY_MS_1000 — модуль просыпается каждую секунду;

  • STANDBY_MS_2000 — модуль просыпается каждые 2 секунды;

  • STANDBY_MS_4000 — модуль просыпается каждые 4 секунды.

Параметры по умолчанию меня в целом устраивают, но так как контроллер должен производить 10 замеров в секунду, последний параметр я установил STANDBY_MS_63.

Программная часть

Итак, единственным «органом чувств» для контроллера является датчик давления BMP280. На основе данных с датчика можно определить, находится ли ракета ещё на земле или уже на земле, поднимается или снижается, пройдена ли точка апогея. Выводы делаются при сравнении текущей высоты с высотой старта и максимальной высотой полёта (апогеем).

При включении питания контроллер подключается к датчику BMP280 и micro-SD карте, открывает файл лога, и если эти операции прошли успешно, переходит к основной программе. В случае неудачи сигнализирует светодиодом. Всё это упаковано в функцию setup().

Работа основной части программы, записанной в функцию loop(), основана на смене стадий. Номер стадии хранится в переменной. При каждом повторе функции loop() программа обращается к этой переменной, чтобы определить стадию. Также при каждом повторе функции loop() производится замер высоты.

Стадии меняются последовательно.

Стадии полёта ракеты
  • Первая стадия — время до старта, начинается после выполнения функции setup(). Условие перехода ко второй стадии — текущая высота больше высоты старта на 10 метров.

  • Вторая стадия — полёт до точки апогея. Высота максимума сравнивается с текущей, и если она меньше, то приравнивается к текущей. Условие перехода к третьей стадии — текущая высота меньше максимальной.

  • Третья стадия — свободное падение на 10 метров ниже апогея. Условие перехода к четвёртой части — текущая высота меньше максимальной на 10 метров.

  • Четвёртая стадия — спуск на парашюте. При наступлении этой стадии на 2 секунды подаётся питание на поджиг пиропатрона для выброса парашюта. Условие перехода к пятой стадии — текущая высота меньше, чем высота старта + 10 метров.

  • Пятая стадия — ракета на земле. Продолжается до выключения или перезагрузки контроллера.

Блок-схема смены стадий полёта

Высота полёта записывается в файл в формате CSV каждые 100 мс во время выполнения стадий 2, 3 и 4.

Совет

Полёт ракеты с этим контроллером прошёл успешно. Анализ лога выявил ошибку в программе (в приложенном листинге она исправлена).

Готовая ракета

Исходный код

#include <SPI.h>
#include <Wire.h>
#include <SD.h>
#include <Adafruit_BMP280.h>

//////////
// пины //
#define redPin 2
#define greenPin 3
#define bluePin 4
#define firePin 6     // пин поджига
#define ledPin 7      // общий + на светодиод
#define CSPin 10      // пин SD карты

// переменные //
bool readyOK = 0;     // готовность
byte stage = 1;       // стадии полета
                      // 1 - стадия ожидания
                      // 2 - стадия активного полета
                      // 3 - стадия спуска
                      // 4 - стадия раскрытия парашюта
                      // 5 - стадия посадки
// высота //
float startAlt;       // высота старта
float maxAlt = 0;     // максимальная высота
float curAlt;         // текущая высота

// парашют //
unsigned long chuteTimer = 0; // таймер парашюта

// лог //
unsigned long logTimer = 0;   // таймер лога
unsigned long logTime = 0;    // таймер лога
int logCounter = 0;           // счетчик лога
int logLimit = 100;           // максимальное количество замеров
int logPeriod = 100;          // интервал между замерами
File logFile;                 // файл лога
Adafruit_BMP280 bmp;          // I2C

void setup() {
   pinMode(redPin, OUTPUT);
   pinMode(greenPin, OUTPUT);
   pinMode(bluePin, OUTPUT);
   pinMode(firePin, OUTPUT);
   pinMode(ledPin, OUTPUT);

   digitalWrite(redPin, HIGH);       // светодиод красный погашен
   digitalWrite(greenPin, HIGH);     // светодиод зеленый погашен
   digitalWrite(bluePin, HIGH);      // светодиод синий погашен
   digitalWrite(ledPin, HIGH);       // общий пин

   //////////////////////////////
   // бессмысленая иллюминация //
   digitalWrite(redPin, LOW);
   delay(1000);
   digitalWrite(redPin, HIGH);
   digitalWrite(greenPin, LOW);
   delay(1000);
   digitalWrite(greenPin, HIGH);
   digitalWrite(bluePin, LOW);
   delay(1000);
   digitalWrite(bluePin, HIGH);
   Serial.begin(9600);

   //////////////////////
   // заводим SD карту //
   if (!SD.begin(CSPin)) {
     while (1) {
       for (int i = 0; i < 2; i++) {
         digitalWrite(redPin, LOW);
         delay(250);
         digitalWrite(redPin, HIGH);
         delay(250);
       }
       delay(750);
     }
   }

   //////////////////////
   // заводим барометр //
   if (!bmp.begin()) {
     while (1){
       for (int i = 0; i < 3; i++) {
         digitalWrite(redPin, LOW);
         delay(250);
         digitalWrite(redPin, HIGH);
         delay(250);
       }
       delay(750);
     }
   }

   //////////////////////////////
   // открываем файл на запись //
   logFile = SD.open("log.csv", FILE_WRITE);
   if (!logFile){
     while (1){
       for (int i = 0; i < 4; i++) {
         digitalWrite(redPin, LOW);
         delay(250);
         digitalWrite(redPin, HIGH);
         delay(250);
       }
       delay(750);
     }
   }

   // если все завелось
   digitalWrite(redPin, HIGH);
   digitalWrite(greenPin, LOW);
   readyOK = 1;

   /* Default settings from datasheet. */
   bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                   Adafruit_BMP280::SAMPLING_X2,
                   Adafruit_BMP280::SAMPLING_X16,
                   Adafruit_BMP280::FILTER_X16,
                   Adafruit_BMP280::STANDBY_MS_500);
                   startAlt = bmp.readAltitude(1013.25);

   /////////////////////////////
   // новые параметры датчика //
   bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                   Adafruit_BMP280::SAMPLING_X2,
                   Adafruit_BMP280::SAMPLING_X16,
                   Adafruit_BMP280::FILTER_OFF,
                   Adafruit_BMP280::STANDBY_MS_63);
} // конец setup

void loop() {
  if (!readyOK){
    return;
  }

  ///////////////////
  // высота ракеты //
  curAlt = bmp.readAltitude(1013.25);
  if (maxAlt < curAlt){
    maxAlt = curAlt;
  }

  /////////////////////
  // выброс парашюта //
  if ((stage == 4) && (chuteTimer == 0)){
    chuteTimer = millis();
    digitalWrite(firePin, HIGH);
  }
  if ((millis() - chuteTimer) >= 2000){
    digitalWrite(firePin, LOW);
  }

  /////////////////////////////
  // запись в лог по таймеру //
  if (((stage == 2) || (stage == 3) || (stage == 4)) && ((millis() - logTimer) >= logPeriod)) {
     logTimer = millis();
     logFile.print(logTimer / 1000);
     logFile.print(";");
     logFile.print(curAlt);
     logFile.println(";");
     logCounter++;
  }

  /////////////////////////////////
  // стадия 5 - окончание полета //
  if (stage == 5){
    logFile.print("Maximum Alt. = ");
    logFile.print(maxAlt);
    logFile.println(" m");
    logFile.println("");
    logTime = millis() - logTime;
    logFile.close();
    while (1) {
      if ((millis() - chuteTimer) >= 2000){
        digitalWrite(firePin, LOW);
      }
      digitalWrite(bluePin, HIGH);
      delay(500);
      digitalWrite(bluePin, LOW);
      delay(500);
    }
  }

  //////////////////////
  //// смена стадий ////

  // стадия ожидания //
  if (stage == 1) {
    if ((curAlt - startAlt) >= 10){
      stage = 2;
      logTime = millis();
      digitalWrite(redPin, HIGH);
      digitalWrite(greenPin, HIGH);
      digitalWrite(bluePin, HIGH);
      digitalWrite(bluePin, LOW);
    }
  }
  // стадия активного полета //
  else if (stage == 2) {
    if (curAlt < maxAlt){
      stage = 3;
      digitalWrite(redPin, HIGH);
      digitalWrite(greenPin, HIGH);
      digitalWrite(bluePin, HIGH);
      digitalWrite(bluePin, LOW);
    }
  }
  // стадия спуска //
  else if (stage == 3) {
    if ((maxAlt - curAlt) >= 10){
      stage = 4;
      digitalWrite(redPin, HIGH);
      digitalWrite(greenPin, HIGH);
      digitalWrite(bluePin, HIGH);
      digitalWrite(bluePin, LOW);
    }
  }
  // стадия раскрытия парашюта //
  else if (stage == 4) {
    if ((curAlt - startAlt) <= 10){
      stage = 5;
    }
    digitalWrite(redPin, HIGH);
    digitalWrite(greenPin, HIGH);
    digitalWrite(bluePin, HIGH);
  }
  // переполнение счетчика лога //
  if (logCounter >= logLimit){
    stage = 5;
  }
}