Информации из предыдущих уроков уже достаточно для того, чтобы начать писать простенькие программы - опрашивать кнопки, переключать реле, мигать светодиодами и так далее. Для полного счастья не хватает самих принципов написания сложной программы с большим количеством логики, тем более - многозадачной.
Самым простым и базовым способом организации программы является суперцикл (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
- они не будут мешать друг другу.
Нельзя мешать тикерам, если используется подобный подход: в суперцикле не должно быть долгих задержек и блокирующих циклов - они будут мешать коду в тикерах, который рассчитан на постоянный вызов