loop #
Как обсуждалось ранее, функция loop
вызывается на всём протяжении работы программы. Как часто она это делает? По сути - с максимальной скоростью, на которую способен процессор. "Вне" этой функции он выполняет некоторые системные задачи, поэтому loop
вызывается с чуть меньшей скоростью - порядка десятков и сотен тысяч раз в секунду. "Под капотом" Arduino фреймворка основная программа выглядит примерно так - программа делает какие-то свои дела и вызывает наш loop
:
// main.cpp
int main() {
// системный код
setup();
for (;;) {
// системный код
loop();
}
}
Если писать без Arduino-фреймворка, то ваша программа скорее всего будет выглядеть именно так - сначала код, выполянемый при запуске, затем суперцикл.
Одна задача #
В однозадачной программе всё довольно просто - действия выполняются по порядку и не мешают друг другу. В такой программе можно использовать простые, удобные и понятные блокирующие конструкции: delay
, циклы с задержками, циклы ожидания - всё то, что нельзя использовать в многозадачной программе с суперциклом. Более того - если переходить на операционную систему, то там в задаче тоже можно всё это использовать и не задумываться об остальных задачах - это забота ОС, она позволяет писать более простой код, который будет работать.
Задержка #
Например, классическое мигание светодиодом через задержку 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();
}
Запустите данный пример - новые значения будут появляться в мониторе буквально пару раз в секунду! Вычисления занимают очень много времени, если запустить эту задачу одновременно со светодиодом или кнопкой - всё опять же сломается.
Несколько задач #
Для того, чтобы написать многозадачную программу в рамках суперцикла, нужно соблюдать одно условие: задачи должны выполняться за минимальное время - быть "прозрачными", неблокирующими выполнение кода. Этому могут мешать как минимум три момента, с которыми мы уже столкнулись выше.
Асинхронная задача (функция) выполняется быстро, не останавливаясь на ожидание времени или событий, такой код также называется неблокирующим. В свою очередь синхронный или блокирующий код содержит долго выполняющиеся конструкции, например временные задержки и циклы ожидания, т.е. он буквально блокирует выполнение основной программы на значительное время
В следующих уроках этой темы рассмотрим типовые конструкции и алгоритмы, позволяющие писать асинхронный код для суперцикла и обходить блокирующие участки. Код станет сложнее и объёмнее - но это плата за универсальность и многозадачность:
- Задержки. Решение: в некоторых случаях - функция yield(), но в основном это программный таймер
- Циклы ожидания. Решение: машина состояний и флаги
Тикер #
Во многих библиотеках используется "тикер" - неблокирующая функция, которую нужно вызывать как можно чаще - в loop
. Внутри неё реализована вся логика, необходимая для работы библиотеки в условиях суперцикла. Это может быть опрос кнопки, счёт времени или проверка соединений клиентов с вебсервером. Например, стандартный Serial
и его метод проверки входящих данных:
void loop() {
if (Serial.available()) {
// данные доступны
}
}
Таким образом, в рамках суперцикла вся программа может состоять из набора таких тикеров в loop
- они не будут мешать друг другу.
Нельзя мешать тикерам, если используется подобный подход: в суперцикле не должно быть долгих задержек и блокирующих циклов - они будут мешать коду в тикерах, который рассчитан на постоянный вызов
Выбор подхода (шуточный) #
Видео #
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками
