View Categories

Суперцикл и многозадачность

Информации из предыдущих уроков уже достаточно для того, чтобы начать писать простенькие программы - опрашивать кнопки, переключать реле, мигать светодиодами и так далее. Для полного счастья не хватает самих принципов написания сложной программы с большим количеством логики, тем более - многозадачной.

Самым простым и базовым способом организации программы является суперцикл (superloop) - вся программа помещается в бесконечный цикл, который выполняется раз за разом с большой скоростью. В этом цикле могут опрашиваться кнопки, читаться датчики, отправляться данные и так далее. Знакомо? Всё верно, в Arduino фреймворке нам предлагается работать именно таким образом - в функции loop.

Суперцикл - это не буквально "цикл", это подход к организации программы. В реализации это может быть как непосредственно цикл, так и функция, которая вызывается всё время на протяжении работы программы

Второй вариант - использование операционной системы (ОС), где каждая задача - это независимый цикл, писать проект с таким подходом гораздо проще. В рамках данного раздела уроков мы не рассматриваем популярные ОС (например FreeRTOS), т.к. они довольно тяжёлые для слабых МК, требуют знания дополнительной теории и обладания навыками работы с суперциклом - так что первым делом нужно всё равно освоиться в нём.

loop #

Как обсуждалось ранее, функция loop вызывается на всём протяжении работы программы. Как часто она это делает? По сути - с максимальной скоростью, на которую способен процессор. "Вне" этой функции он выполняет некоторые системные задачи, поэтому loop вызывается с чуть меньшей скоростью - порядка десятков и сотен тысяч раз в секунду. "Под капотом" Arduino фреймворка основная программа выглядит примерно так - программа делает какие-то свои дела и вызывает наш loop:

// main.cpp

int main() {
    // системный код
    setup();

    for (;;) {
        // системный код
        loop();
    }
}

Если писать программу под МК без Arduino-фреймворка, то ваш код будет выглядеть именно так - сначала код, выполянемый при запуске, затем цикл.

Одна задача #

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

Задержка #

Например, классическое мигание светодиодом через задержку - задержка тормозит выполнение программы, чтобы задать нужную скорость переключения светодиода:

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void blinkLED() {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
    delay(500);
}

void loop() {
    blinkLED();
}

Задержка позволяет сделать программу простой и понятной - она работает последовательно так, как написана.

Ожидание #

Или опрос кнопки (кнопка подключена на D3 и GND), в котором нужно поймать сам факт нажатия: например дождаться его в цикле, просигналить и точно так же дождаться отпускания:

#define BTN_PIN 3

void setup() {
    Serial.begin(115200);
    pinMode(BTN_PIN, INPUT_PULLUP);
}

void checkButton() {
    while (digitalRead(BTN_PIN));       // пока кнопка не нажата
    Serial.println("Кнопка нажата");

    while (!digitalRead(BTN_PIN));      // пока кнопка нажата
    //Serial.println("Кнопка отпущена");
}

void loop() {
    checkButton();
}

Аналогично задержке, цикл ожидания позволяет сделать программу простой и понятной - она работает последовательно так, как написана.

Но если мы захотим соединить две этих задачи, то ничего не получится - кнопка будет мешать светодиоду.

// ...

void loop() {
    blinkLED();
    checkButton();
}

Даже если получится решить ситуацию с кнопкой - ей начнёт мешать светодиод: пока программа ждёт задержку светодиода - можно пропустить нажатие кнопки!

Вычисления #

Следующий момент - долгие сложные вычисления. Начнём с простого - будем увеличивать число с шагом 1 и выводить его в порт:

void setup() {
    Serial.begin(115200);
}

int x = 0;

void loop() {
    x++;
    Serial.println(x);
}

Если открыть монитор порта - можно увидеть, как с огромной скоростью появляются числа. В данном случае вывод в порт будет занимать больше времени, чем увеличение числа. Теперь давайте представим, что нам нужно 500 раз вычислить синус. Чтобы компилятор не оптимизировал вычисления - считать будем синус случайного числа:

void setup() {
    Serial.begin(115200);
}

void calcSin() {
    float x = 0;

    for (int i = 0; i < 500; i++) {
        x += sin(random(100));
    }

    Serial.println(x);
}

void loop() {
    calcSin();
}

Запустите данный пример - новые значения будут появляться в мониторе буквально пару раз в секунду! Вычисления занимают очень много времени, если запустить эту задачу одновременно со светодиодом или кнопкой - всё опять же сломается.

Многозадачность #

Для того, чтобы написать многозадачную программу в рамках суперцикла, нужно соблюдать одно условие: задачи должны выполняться за минимальное время - быть "прозрачными", асинхронными, неблокирующими выполнение кода. Этому могут мешать как минимум три момента, с которыми мы уже столкнулись выше:

  • Задержки
  • Циклы ожидания
  • Долгие вычисления

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

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

Тикер #

Во многих библиотеках используется "тикер" - неблокирующая функция, которую нужно вызывать как можно чаще - в loop. Внутри неё реализована вся логика, необходимая для работы библиотеки в условиях суперцикла. Это может быть опрос кнопки, счёт времени или проверка соединений клиентов с вебсервером. Например, стандартный Serial и его метод проверки входящих данных:

void loop() {
    if (Serial.available()) {
        // данные доступны
    }
}

Таким образом, вся программа может состоять из набора таких тикеров в loop - они не будут мешать друг другу.

Нельзя мешать тикерам, если используется подобный подход: в суперцикле не должно быть долгих задержек и блокирующих циклов - они будут мешать коду в тикерах, который рассчитан на постоянный вызов

Выбор подхода (шуточный) #

Видео #

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх