Arduino мигане без закъснение “Blink Without Delay”

Arduino мигане без закъснение Blink Without Delay

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

Основи на мигането на светодиод

Класическият начин за мигане на светодиод с Arduino използва функцията delay(). Ето пример за прост код, който прави това:

void setup() {
  pinMode(13, OUTPUT); // Задаваме пин 13 като изход
}

void loop() {
  digitalWrite(13, HIGH); // Включваме светодиода
  delay(1000);            // Изчакваме 1000 милисекунди (1 секунда)
  digitalWrite(13, LOW);  // Изключваме светодиода
  delay(1000);            // Изчакваме още 1000 милисекунди
}

В този пример светодиодът, свързан към пин 13, ще мига със скорост 1 секунда включен, 1 секунда изключен. Функцията delay() спира изпълнението на програмата за определено време, което е подходящо за прости задачи. Но този подход има сериозни ограничения. По време на delay() нищо друго не може да се изпълнява от Arduino, което прави платформата “еднозадачна” (може да изпълнява само една задача в даден момент).

Многозадачно програмиране на Arduino

За да направим Arduino по-ефективно и да можем да изпълняваме повече от една задача едновременно, можем да използваме таймери вместо delay(). Това ни позволява да създадем многозадачна среда, където различни функции могат да се изпълняват с различни интервали от време, без да се прекъсва изпълнението на програмата. Стандартния код който ще срещнете за това решение е този отдолу или описания в тази статия:

const int ledPin = 6; // Пинът, към който е свързан светодиодът
unsigned long previousMillis = 0; // Променлива за съхранение на последното време на мигане
const long interval = 1000; // Интервалът между миганията в милисекунди (1 секунда)

void setup() {
  pinMode(ledPin, OUTPUT); // Задаваме пина за светодиода като изход
}

void loop() {
  unsigned long currentMillis = millis(); // Записваме текущото време

  // Проверяваме дали интервалът е изтекъл
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis; // Записваме текущото време като време на последното мигане

    // Превключваме състоянието на светодиода
    digitalWrite(ledPin, !digitalRead(ledPin));
  }
}

Проблемът тук е, че за да стартирате няколко функции с използването на таймер ще ви отнеме много писане и главоболия, защото кода ще стане “разхвърлян”.

Ние ще ви предоставим функция която ще замести голяма част от кода и така той ще стане подреден и разбираем.

Подобреният код за Arduino мигане без закъснение “Blink Without Delay”

Ето кодът, който реализира многозадачно мигане на три различни светодиода с различни интервали от време:

int led1 = 2;
int led2 = 4;
int led3 = 6;

//=========== Blink LED 1 ========\\
void led_1() {
  digitalWrite(led1, !digitalRead(led1));
}

//=========== Blink LED 2 ========\\
void led_2() {
  digitalWrite(led2, !digitalRead(led2));
}

//=========== Blink LED 3 ========\\
void led_3() {
  digitalWrite(led3, !digitalRead(led3));
}

//=========== TIMER ========\\
void executeWithInterval(unsigned long interval, void (*action)(), unsigned long &previousMillis) {
  unsigned long currentMillis = millis(); // Записваме текущото време

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis; // Записваме текущото време като време на последното изпълнение

    action();  // Изпълняваме предадената като параметър функция
  }
}

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
}

void loop() {
  
  static unsigned long previousMillis1 = 0;  // За led_1
  static unsigned long previousMillis2 = 0;  // За led_2
  static unsigned long previousMillis3 = 0;  // За led_3

  
  executeWithInterval(200, [](){ led_1(); }, previousMillis1); 
  executeWithInterval(600, [](){ led_2(); }, previousMillis2);
  executeWithInterval(1800, [](){  led_3(); }, previousMillis3);
}

Обяснение на кода

Деклариране на пинове и функции

В началото на кода декларираме три променливи, led1, led2 и led3, които съответстват на пиновете 2, 4 и 6. След това дефинираме три функции (led_1(), led_2(), led_3()), които ще управляват съответните светодиоди. Всяка от тези функции обръща състоянието на съответния светодиод, като използва digitalWrite() и digitalRead(). До тук предполагам, че е ясно! Ако не е мога да ви върна малко в основите на програмиране на Arduino в статията ни “Arduino IDE и Arduino UNO първaта ни програма“.

Таймерът executeWithInterval()

Ключовата част от кода е функцията executeWithInterval(). Тя приема три параметъра:

  1. interval – времето в милисекунди, през което функцията трябва да се изпълни.
  2. action – указател към функцията, която трябва да се изпълни.
  3. previousMillis – променлива, която съхранява времето на последното изпълнение на функцията.

Функцията executeWithInterval() изчислява времето, изминало от последното изпълнение на предадената функция. Ако това време надвишава зададения интервал, функцията се изпълнява и записва текущото време в previousMillis.

Функция setup()

Във функцията setup() задаваме пиновете като изходи с pinMode(). Това е стандартна практика за инициализация на пинове, които ще управляват светодиоди или други устройства.

Основният цикъл loop()

Във функцията loop() се изпълняват трите светодиода с различни интервали, като се използва функцията executeWithInterval(). За всяко извикване се създават статични променливи previousMillis1, previousMillis2, и previousMillis3, които съхраняват времето на последното изпълнение за съответните светодиоди.

  executeWithInterval(200, [](){ led_1(); }, previousMillis1);
  executeWithInterval(600, [](){ led_2(); }, previousMillis2);
  executeWithInterval(1800, [](){  led_3(); }, previousMillis3);

Тази част от кода демонстрира как се използва функцията executeWithInterval() за изпълнение на различни задачи с различни интервали от време. Всеки ред извиква функцията executeWithInterval(), като се предава определен интервал в милисекунди, анонимна функция, която извиква съответната функция за мигане на светодиод, и променлива, която съхранява времето на последното изпълнение на тази функция. Например, първият ред задава интервал от 200 милисекунди за мигане на светодиод 1, вторият – 600 милисекунди за светодиод 2, а третият – 1800 милисекунди за светодиод 3. Тази структура позволява светодиодите да мигат асинхронно, без да се блокира изпълнението на останалите задачи, като всяка задача се изпълнява в определеното време и със зададената честота.

Многозадачност и приоритети

Тази архитектура позволява на Arduino да изпълнява множество задачи, сякаш е многозадачна система. В нашия пример, всеки светодиод мига с различна честота:

  • Светодиод 1 мига на всеки 200ms.
  • Светодиод 2 мига на всеки 600ms.
  • Светодиод 3 мига на всеки 1800ms.

Тази гъвкавост ни позволява да задаваме приоритети на задачите. Например, ако един от светодиодите трябва да реагира по-бързо (на всеки 100ms), можем лесно да променим интервала за съответната функция, без да се нарушава останалата част от програмата.

Видео демонстрация на кода

Приложения на многозадачност в Arduino

С този подход можем да разширим функционалността на нашия Arduino проект, като добавим множество независими задачи, които се изпълняват паралелно. Например, можем да добавим сензори, които събират данни, докато светодиодите мигат, или да управляваме няколко мотора с различни скорости.

Многозадачността е изключително полезна при изграждането на сложни системи, където трябва да се управляват няколко различни компонента едновременно. Възможността да контролираме времето за изпълнение на всяка функция ни дава контрол върху поведението на цялата система и прави кода по-четим и лесен за поддръжка.

Заключение

Мигането на светодиоди е една от първите задачи, които всеки нов потребител на Arduino трябва да овладее. С използването на таймери вместо delay() можем да направим тази проста задача по-сложна и интересна, като превърнем Arduino в един вид многозадачна система. С този подход можем да управляваме различни задачи с различни приоритети, което значително увеличава функционалността и ефективността на нашите проекти. С подходяща структура и добро разбиране на таймерите и функцията millis(), можем да изградим сложни системи, които работят гладко и ефективно. Това е важна стъпка в развитието на всеки Arduino проект и отваря врати към по-сложни и амбициозни проекти в бъдеще.