Посмотр рубрик

Суперцикл, loop()

Немного о программах #

Что есть программа на компьютере и программа на микроконтроллере? На компьютере у нас может быть много разных программ, их можно запускать и завершать в любой момент, а также удалять или устанавливать новые. Это возможно благодаря операционной системе (Windows, Linux, Mac) - главной программе для всех программ, "внутри" которой они и запускаются. Когда мы включаем компьютер, то сначала загружается операционная система, а затем мы запускаем условную программу и попадаем в её int main() (если она написана на Си). На МК всё гораздо проще - мы пишем программу не под операционную систему, а напрямую под МК, она одна будет находиться у него в памяти и запустится сразу же после подачи питания. Да, на МК тоже можно "поднять" операционную систему и запускать подпрограммы, в том числе с внешних носителей. Но микроконтроллер на то и "микро", что обычно решает более простые задачи, для которых не нужна громоздкая система - всё работает в рамках одной программы.

"Одна программа" не означает, что МК сможет делать только что-то одно - запрограммировать можно всё что угодно, программа - это просто код, он может выполнять разные задачи. Под задачей понимается некоторая самостоятельная часть программы, которая выполняет конкретную работу, например опрос кнопки, мигание светодиодом, обновление дисплея и т.д. Соответственно на макро уровне программа может быть однозадачной и многозадачной - рассмотрим, как это можно реализовать.

Суперцикл #

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

while (true) {
    if (клик_по_кнопке()) отправить_данные();
    мигать_светодиодом();
    читать_датчик();
}

Этот подход "родной" для чистого C/C++ и широко используется во встраиваемых системах и на слабых МК - это буквально самый легковесный способ, не требующий ни байта лишнего кода, а большинство остальных подходов работают на его основе.

Arduino loop #

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

"Под капотом" Arduino фреймворка основная программа выглядит примерно так - фреймворк делает какие-то свои дела и вызывает наш loop, а так как мы работаем на C++ - всё это лежит в файле main.cpp и помещено в функцию int main, т.е. в стандартную точку входа в программу на этом языке:

// main.cpp

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

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

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

Arduino loop() - не цикл! Это функция, которая вызывается в цикле внутри файлов фреймворка

Superloop polling #

Когда мы пишем многозадачную программу в суперцикле, то будет логично представить задачи в виде отдельных функций, которые вызываются из цикла, как в примере выше. При написании самих функций нужно понимать, что они будут вызываться по очереди, тысячи и десятки тысяч раз в секунду. Задачи выполняются не мгновенно, поэтому чем больше в цикле задач - тем реже они будут вызываться (речь о нано- и микросекундах, но сути это не меняет):

void loop() {
    if (клик_по_кнопке()) отправить_данные();
    мигать_светодиодом();
    читать_датчик();
}

Функция для вызова в суперцикле должна быть "прозрачной" и не содержать конструкций, блокирующих выполнение программы. В идеале она должна быть ещё и оптимизирована для скорости, т.е. банально не выполнять сложные вычисления при каждом вызове, не выделять-заполнять большие объёмы памяти и прочие очевидные долгие операции. Такие функции называют тикерами, они предназначены для вызова в суперцикле, а сам подход называется superloop polling - опрос в суперцикле. В библиотеках такие функции и методы часто содержат в названии слова tick, update, loop или poll.

Блокирующие конструкции #

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

Задержка #

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

// мигаем встроенным светодиодом

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

void blinkLED() {
    digitalWrite(LED_BUILTIN, HIGH);    // включить
    delay(500);                         // ждать 500 мс
    digitalWrite(LED_BUILTIN, LOW);     // выключить
    delay(500);                         // ждать 500 мс
}

void loop() {
    blinkLED();
}

Цикл ожидания #

Опрос кнопки через циклы ожидания:

// второй пин кнопки подключен к 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();
}

Сложные вычисления #

Начнём с простого примера - будем увеличивать число с шагом 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();
}

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

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

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

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

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

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

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

И так далее. Блокирующим задачам не место в суперцикле! Рассмотрим основные подходы к организации программы в таких условиях.

Зачем ждать, если можно проверить #

Основная природа блокирующего кода - ожидание события. В примерах выше мы ждём, когда выйдет время в задержке delay(), когда юзер нажмёт или отпустит кнопку, когда посчитаются все 500 синусов. Таких конструкций можно придумать бесконечное количество, но все они "исправляются" одной простой идеей - зачем ждать, если можно проверить... в следующий раз? Тикер вызывается десятки тысяч раз в секунду, то есть мы попадём в этот же код буквально через мгновение. Достаточно запоминать старое "состояние" и сравнивать его с новым. На этой идее работают:

Прерывания #

У МК есть очень мощный механизм - прерывания. Он позволяет обрабатывать аппаратные события непосредственно в момент их наступления. При наступлении прерывания программа переходит к функции-обработчику (там будет наш код), выполняет её и возвращается обратно. Прерывание может наступить в любой момент, например внутри цикла с суммированием синусов - программа всё равно мгновенно переключится на обработку события. Прерывания обычно можно настроить на изменение сигнала с пина, на отправку-приём байта по интерфейсу связи, а также на другие события периферии МК. Это позволяет делать некоторые вещи "в фоне", практически параллельно выполнению основной программы, без ожиданий и проверок условий. Подробнее - в уроке про прерывания.

Диспетчер задач #

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

  • GyverOS - простой диспетчер задач, вызывает функции с указанным для них периодом
  • Looper - очень мощный диспетчер-фреймворк с автоматической регистрацией задач, поддержкой событий и корутин

Операционная система #

Операционная система - более сложный вариант диспетчера задач, в котором задачи могут иметь приоритеты и выполняться "параллельно" друг другу. Здесь каждая задача - это независимый суперцикл, который выполняется параллельно остальным, даже если содержит блокирующий код. Писать проект с таким подходом гораздо проще, ведь можно буквально наваливать delay() в каждую задачу и не усложнять код машинами состояний.

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

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

Видео #

Полезные страницы #

(6 голосов)
Подписаться
Уведомить о
guest

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