logo

Wielozadaniowość w arduino, czyli opóźnienie bez delay()!

Na początku gdy zaczynamy naszą zabawę z arduino wykorzystujemy funkcję delay() by opóźnić działanie naszego programu. Lecz załóżmy że chcemy podłączyć do naszego mikrokontrolera dwie diody i chcemy żeby jedna z nich zmieniła swój stan (zaświeciła się lub zgasła) co jedną sekundę, a druga z nich co dwie sekundy. Więc używając funkcji delay() możemy zapisać to tak:

#define LED_1 2
#define LED_2 3

//na początku diody są zgaszone
bool stan_LED_1 = false;
bool stan_LED_2 = false;

uint8_t licznik_LED_2 = 0;

void setup() {
  //ustawiamy piny do których podłączone są diody jako wyjście
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
}


void loop() {
  stan_LED_1 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
  digitalWrite(LED_1, stan_LED_1); //gasimy lub zapalamy diodę

  licznik_LED_2 ++; //zwiększamy wartość zmiennej o 1
  
  if(licznik_LED_2 >= 2){
    licznik_LED_2 = 0; //resetujemy licznik
    stan_LED_2 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_2, stan_LED_2); //gasimy lub zapalamy diodę
  }
  
  delay(1000); //opóźniamy działanie programu
 
}

Ale co jeżeli okaże się że potrzebujemy by pierwsza dioda migała szybciej np. zmieniała swój stan co pół sekundy? Możemy zmniejszyć opóźnienie do 500ms i co 4 przejście pętli zmieniać stan drugiej diody aby ona nadal migała w takich samych odstępach czasu.

#define LED_1 2
#define LED_2 3

//na początku diody są zgaszone
bool stan_LED_1 = false;
bool stan_LED_2 = false;

uint8_t licznik_LED_2 = 0;

void setup() {
  //ustawiamy piny do których podłączone są diody jako wyjście
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
}


void loop() {
  stan_LED_1 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
  digitalWrite(LED_1, stan_LED_1); //gasimy lub zapalamy diodę

  licznik_LED_2 ++; //zwiększamy wartość zmiennej o 1
  
  if(licznik_LED_2 >= 4){
    licznik_LED_2 = 0; //resetujemy licznik
    stan_LED_2 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_2, stan_LED_2); //gasimy lub zapalamy diodę
  }
  
  delay(500); //opóźniamy działanie programu
 
}

Idąc dalej dołóżmy jeszcze do naszej zabawy trzecią diodę i niech ona miga co 0,25 sekundy. No to co przeróbmy cały program i niech działa!

#define LED_1 2
#define LED_2 3
#define LED_3 4

//na początku diody są zgaszone
bool stan_LED_1 = false;
bool stan_LED_2 = false;
bool stan_LED_3 = false;

uint8_t licznik_LED_1 = 0;
uint8_t licznik_LED_2 = 0;

void setup() {
  //ustawiamy piny do których podłączone są diody jako wyjście
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
  pinMode(LED_3, OUTPUT);
}


void loop() {
  //zwiększamy wartość zmiennych o 1
  licznik_LED_1 ++; 
  licznik_LED_2 ++; 

  if(licznik_LED_1 >= 2){
    licznik_LED_1 = 0; //resetujemy licznik
    stan_LED_1 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_1, stan_LED_1); //gasimy lub zapalamy diodę
  }
  
  if(licznik_LED_2 >= 8){
    licznik_LED_2 = 0; //resetujemy licznik
    stan_LED_2 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_2, stan_LED_2); //gasimy lub zapalamy diodę
  }

  stan_LED_3 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
  digitalWrite(LED_3, stan_LED_3); //gasimy lub zapalamy diodę
  
  delay(250); //opóźniamy działanie programu
 
}

No dobra, po chwili spędzonej z kalkulatorem, nasz program działa poprawnie, diody migają tak jak chcemy. Ale jednak okazuje się że trzecia dioda miga zbyt szybko i chcemy by migała o 100 milisekund wolniej. To co, ponownie bierzemy kalkulator i znów chwila liczenia. Dobra ale jeżeli przyjdzie taka potrzeba by migać dziesięcioma diodami w kompletnie różnych odstępach czasu? Przecież nie będziemy wiecznie siedzieć i liczyć jaki odstęp czasu dobrać by nasze diody migały tak jak chcemy, a co dopiero gdyby przyszło nam zmienić częstotliwość migania kilku z tych dziesięciu diod? A jakby tak mikrokontroler liczył czas za nas? By my nie musieliśmy wcale używać kalkulatora? Nie ma żadnego problemu! Z pomocą przychodzi nam wbudowana w framework Arduino funkcja millis().

Funkcja millis()

Więc co robi funkcja millis()? Po prostu zwraca nam czas w milisekundach jaki upłynął od uruchomienia się mikrokontrolera do czasu jej wywołania. Dzięki niej możemy lepiej wykorzystać nasz mikrokontroler, zamiast czekać tak jak w przypadku używania funkcji delay(), nasz mikrokontroler może wykonywać inne działania nie marnując czasu na bezczynne czekanie. Funkcja millis() do określania czasu korzysta z wbudowanego w mikrokontroler timera (jest to coś w rodzaju stopera), dzięki temu odliczania przez nią czasu nie zatrzyma nawet delay(). Funkcja millis() jest zawsze dostępna do użycia w każdym programie, jeśli tylko korzystamy z frameworku Arduino.

Wartość jaką zwraca funkcja millis() jest typu unsigned long. Zazwyczaj używamy typu int (integer) lub jeszcze mniejszych typów zmiennych aby oszczędzać pamięć RAM naszego mikrokontrolera, której nie ma za dużo do dyspozycji. Więc należy uważać przy obliczeniach by liczyć na jednym typie zmiennych. W przeciwnym wypadku może powodować to błędy w obliczeniach! Więc musimy dać znać kompilatorowi że liczymy na typie unsigned long dopisując na końcu liczby UL (np. 100UL), lub jawnie konwertować inną zmienną do odpowiedniego typu dopisując przed nią w nawiasach “(unsigned long)”. Należy pamiętać że typ unsigned long jest typem zmiennej która może przyjmować tylko wartości dodatnie (nie przechowuje informacji o znaku liczby), więc należy zwracać uwagę na to jaką zmienną konwertujemy i czy nie konwertujemy przypadkiem wartości ujemnej.

Kolejną rzeczą na jaką trzeba zwrócić uwagę to fakt że nie ma typów zmiennych które mogłyby przechowywać liczby nieskończenie wielkie. Z tego powodu po jakimś czasie nastąpi overflow (przepełnienie). Więc co się stanie gdy zostawimy mikrokontroler włączony na bardzo długi czas? Odpowiedź jest bardzo prosta: odliczanie zacznie się od nowa i jeżeli program będzie dobrze napisany to nie wpłynie to na jego działanie. Ale też nie ma się o co martwić bo overflow wystąpi dopiero po niecałych 50 dniach działania mikrokontrolera.

Na to należy uważać

Możliwe że czasem będzie nam potrzebne zmienić częstotliwość PWM na niektórych pinach w naszym mikrokontrolerze. Dla płytek opartych o mikrokontroler ATmega328P* zmiana częstotliwości PWM’u na pinach D5 i D6 poprzez zmianę ustawienia preskalera (TCCR0B) będzie miała wpływ na zachowanie funkcji millis() oraz delay()! Odpowiednio zwiększając lub zmniejszając częstotliwość zwiększy lub zmniejszy się szybkość liczenia czasu przez millis() i delay(). Np dla 8 razy wyższej częstotliwości funkcja millis() w czasie 1s zwiększy zwracaną wartość nie o 1000, a o 8000, czyli będzie liczyć 8 razy szybciej. Analogicznie delay(8000) opóźni działanie programu nie o 8s a o 1s.

*Są to płytki między innymi Arduino Uno, Nano, Pro Mini itp. Dla płytek z innym mikrokontrolerem piny oraz timery mogą się różnić.

Blink bez delay()

Wracając do migania naszymi diodami. Będziemy potrzebować kilku zmiennych aby przechować informację kiedy ostatni raz każda z diod zmieniła swój stan. Więc program będzie wyglądać tak:

#define LED_1 2
#define LED_2 3
#define LED_3 4

//na początku diody są zgaszone
bool stan_LED_1 = false;
bool stan_LED_2 = false;
bool stan_LED_3 = false;


unsigned long now = 0;

//deklarujemy zmienne w których zapamiętamy ostatni czas zmiany stanu diody
unsigned long timer_LED_1 = 0;
unsigned long timer_LED_2 = 0;
unsigned long timer_LED_3 = 0;

void setup() {
  //ustawiamy piny do których podłączone są diody jako wyjście
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
  pinMode(LED_3, OUTPUT);

}

void loop() {
   now = millis(); //zapamiętujemy aktualny czas

  if(now - timer_LED_1 >= 500UL){ //sprawdzamy czy różnica czasu wynosi minimum 500ms
    timer_LED_1 = now;
    stan_LED_1 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_1, stan_LED_1); //gasimy lub zapalamy diodę
  }

  if(now - timer_LED_2 >= 2000UL){ //sprawdzamy czy różnica czasu wynosi minimum 2s
    timer_LED_2 = now;
    stan_LED_2 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_2, stan_LED_2); //gasimy lub zapalamy diodę
  }

  if(now - timer_LED_3 >= 350UL){ //sprawdzamy czy różnica czasu wynosi minimum 350ms
    timer_LED_3 = now;
    stan_LED_3 ^= 1; //zmieniamy stan zmiennej na przeciwny za pomocą operatora XOR
    digitalWrite(LED_3, stan_LED_3); //gasimy lub zapalamy diodę
  }
  
}

Do czego jeszcze możemy wykorzystać funkcję millis()?

Oczywiście poza tym że dzięki tej funkcji możemy wykonywać pewne fragmenty kodu cyklicznie (nie ogranicza się to tylko do migania diodami), możemy wykorzystać ją między innymi od wyeliminowania skutków drgania styków np przy przyciskach (czyli tak zwanego debouncing’u). Kiedyś sam napisałem taką klasę po to by uprościć sobie obsługę przycisków bez zbędnego opóźnienia wykonywania się reszty kodu programu:

class Button{
  uint8_t pin;
  bool last_state;
  unsigned long timmer, debounce;
  
  public:
    Button(uint8_t _pin,unsigned long _debounce = 10){
      pin = _pin;
      debounce = _debounce;
      pinMode(pin, INPUT_PULLUP);
    }
    bool keyDown(){
      if(millis()-timmer >= debounce){
        timmer = millis();
        bool state_now = !digitalRead(pin);
        if(last_state == false && state_now == true){
          last_state = state_now;
          return true;
        }else{
          last_state = state_now;
          return false;
        }
      }else{
        return false;
      }
    }
    bool keyUp(){
      if(millis()-timmer >= debounce){
        timmer = millis();
        bool state_now = !digitalRead(pin);
        if(last_state == true && state_now == false){
          last_state = state_now;
          return true;
        }else{
          last_state = state_now;
          return false;
        }
      }else{
        return false;
      }
    }
};

//tworzymy obiekt klasy Button podając pin na którym znajduje się przycisk zwierany do masy
Button btn1(2);
//możemy podać niestandardowy debounce time, domyślny czas to 10ms
Button btn2(3, 100);

void setup() {
  
}

void loop() {
  
  if(btn1.keyDown()){ //kiedy wciśnięto przycisk na pienie 2
    //fragment kodu ...
  }

  if(btn2.keyUp()){ //kiedy puszczono przycisk na pinie 3
    //fragment kodu ...
  }
  
}

Lecz na tym możliwości wykorzystania tej funkcji się nie kończą, przykładów gdzie można ją wykorzystać można by podawać w nieskończoność.


Możesz samodzielnie testować proste układy z arduino na stronie www.tinkercad.com.

Dziękuję za przeczytanie artykułu. :)