Arduino a vykonanie úlohy periodicky za určitý čas.

Toto je jeden stále sa opakujúci problém s ktorým sa potýkajú ľudia ktorý začínajú programovať MCU. Sú len dve možnosti ako sa to dá urobiť - dobre a zle.
  1. Zlý spôsob pomocou funkcie delay()
    void loop()
    {
      delay(1000);                  // pockaj dany pocet milisekund
      .... urob nieco ....
    }
    
    Nevýhodou na ktorú za chvíľu všetci prídu je to, že funkcia delay() je takzvane blokujúca. To znamená, že pokým nenastane čas zadaný ako vstupný parameter, tak sa program zacyklí vo funkcii delay(). A nič iného sa nemôže vykonávať (okrem obsluhy prerušení).

    A prečo je to problém? Ak by naozaj išlo len o úlohu urob niečo každú sekundu, tak uvedený spôsob je funkčný robí presne to čo požadujeme. Problém je však ten, že zvyčajne chceme aby sa niečo robilo stále a každú sekundu chceme urobiť niečo špeciálne. S delay() sa takéto niečo nikdy nepodarí naprogramovať.

  2. Dobrý spôsob pomocou funkcie millis()
    unsigned long actionTime = 0;
    void loop()
    {
      unsigned long now;
      unsigned long elapsedTime;
      now = millis();                 //zisti kolko uplynulo milisekund od zapnutia
      elapsedTime = now - actionTime; //vypocitaj kolko uplynulo ms od poslednej akcie
      if (elapsedTime >= 1000){       //ak uplynuty cas je dlhsi ako jedna sekunda
        actionTime=now;               //zapametaj si novy cas akcie
        .... urob nieco ...           //urob akciu
      }
      
      ... rob nieco stale ...
    }
    
    Toto je správny spôsob ako urobiť požadovaný model správania. Nepoužíva sa žiadne blokujúce volanie a preto program v slučke loop beží neustále znova a znova.

Ako si to pokaziť.

Niekoľko spôsobov ktoré vyzerajú že fungujú. Ale v skutočnosti za určitých podmienok nefungujú.
  1. Miesto testu >= sa použije test ==
    unsigned long actionTime = 0;
    void loop()
    {
      unsigned long now;
      unsigned long elapsedTime;
      now = millis();                 //zisti kolko uplynulo milisekund od zapnutia
      elapsedTime = now - actionTime; //vypocitaj kolko uplynulo ms od poslednej akcie
      if (elapsedTime == 1000){       //ak uplynuty cas je jedna sekunda
        actionTime=now;               //zapametaj si novy cas akcie
        .... urob nieco ...           //urob akciu
      }
      
      ... rob nieco stale ...
    }
    
    Tento program nebude pracovať spoľahlivo ak "rob niečo stále" bude trvať okolo jednej milisekundy. V takomto prípade sa môže ľahko stať že elapsedTime nebude 1000, ale 1001 a preto k vykonaniu už nikdy nedôjde (nie je celkom pravda). Nebezpečné hlavne vtedy ak "rob niečo stále" nemá pevnú dobu vykonávania a za istých (zvyčajne chybových stavov) sa vykonanie pretiahne nad jednu milisekundu.
  2. Niekto potreboval aby sa činnost opakovala každé dve minúty.
    unsigned long actionTime = 0;
    void loop()
    {
      unsigned long now;
      unsigned long elapsedTime;
      now = millis();                 //zisti kolko uplynulo milisekund od zapnutia
      elapsedTime = now - actionTime; //vypocitaj kolko uplynulo ms od poslednej akcie
      if (elapsedTime >= 120000){     //ak uplynuty cas je dlhsi ako dve minúty
        actionTime=now;               //zapametaj si novy cas akcie
        .... urob nieco ...           //urob akciu
      }
      
      ... rob nieco stale ...
    }
    
    S prekvapením zistí že urob niečo sa opakuje ani nie raz za minútu. Keď skúsi číslo 60000, tak to naozaj funguje každú minútu. Tu je problém v tom že prekladač bežne predpokladá že ide o int. Ten je pre 8 bitové AVR definovaný s dĺžkou 16 bit. Maximálne číslo ktoré takto sa dá zapísať je 65535. Väčšie čísla sa odrežú bez varovania na dĺžku 16 bitov. Aby sa to nestalo, treba pridať k číslu suffix L ako long. Teda správne má test vyzerať: if (elapsedTime >= 120000L){
  3. Veľmi zákerný spôsob ako to sem-tam pokaziť
    unsigned long actionTime = 0;
    void loop()
    {
      unsigned long now;
      now = millis();                 //zisti kolko uplynulo milisekund od zapnutia
      if (now >= actionTime + 1000){  //ak uplynuty cas je vacsi ako cas poslednej akcie plus sekunda
        actionTime=now;               //zapametaj si novy cas akcie
        .... urob nieco ...           //urob akciu
      }
      
      ... rob nieco stale ...
    }
    
    Táto chyba je poriadne záludná. Chyba sa môže prejaviť iba počas jednej sekundy za 50 dní. Spočíva v tom že v intervale jedna sekundu pred pretečením počítadla vo funkcii millis() sa "urob niečo" môže robiť stále až do kým počítadlo nepretečie. K pochopeniu chyby si stačí uvedomiť že počet miest počítadla nie je nekonečný. Predstavte si tachometer auta ktorý má 6 miest. To znamená že vie zobraziť maximálne číslo 999 999. A teraz si predstavte že by ten tachometer vedel aj spočítať dve čísla. Koľko bude 999 000 + 1000? Malo by to byť 1 000 000. Lenže je tam iba 6 miest takže výsledok bude 0. No a ak teraz porovnáme 999 000 s výsledkom spočítania, teda s nulou tak dostaneme pravdivý výraz, a preto vykonáme "urob niečo" stále a zas a znova až dokým hodnota now nepretečie tiež.

    Premenná typu unsigned long nie je síce tachometer, ale je to niečo veľmi podobné. Je to 32 číslic vedľa seba. A pretože sme v digitálnom svete tak tie číslice môžu byť iba 0 a 1. Nasledovná vizualizácia ukazuje ako sa to bude správať. Východzí stav ktorý je zobrazený v registroch je práve ten okamih, kedy sa chyba spustí. Je to presne vtedy ak uplynie 4294967 sekund od zapnutia zariadenia (to je 49.71 dňa). V tomto okamihu sa normálne spustí kód v príkaze if. Výsledkom toho je že do premenej actionTime sa vloží číslo 4294967000. To by bolo zatiaľ v poriadku. Takže sa "urobí niečo" a potom sa urobí aj "niečo stále". Potom sa loop znova opakuje.

    Lenže súčet čísla v premennej actionTime a hodnoty 1000 pretiekol (výsledok je 704), ale now je stále 4294967000 a test je zase pravdivý. Takže sa znova vykoná niečo v príkaze if. Toto sa bude neustále opakovať dokiaľ now nepretečie na hodnotu 0. Teda po dobu 296ms. Či to bude presne takto ešte záleží od toho koľko trvá vykonanie oboch akcií. Tento rozbor predpokladá že obe akcie sa vykonajú za čas menší ako je jedna milisekunda. Ak to neplatí, uvedené správanie tiež neplatí úplne. Chyba nezmizne, ale jej výskyt a opakovanie bude ešte náhodnejšie.

    Či to vadí alebo nie zaleží od toho čo sa vykonáva a v akom časovom intervale. Napríklad majme mechanické hodiny ktoré sa posúvajú každú sekundu (alebo minútu). Potom raz za 50 dní pôjdu nejaký čas ako divé.
    Alebo to bude polievanie, ktoré je treba robiť raz za 24 hodín. Potom takýto program bude polievať celých 24 hodín raz za 50 dní.
    Obsah premenej actionTime a aj konštanta sa dá vo vizualizácii meniť buď kliknutím a zadaním hodnoty alebo rolovaním kolečkom na niektorom bite. Tak si môžete interaktívne odskúšať ako a kedy to pretečie.

    A takto to funguje ak je to naprogramované správne s odčítaním. V tomto prípade je zaujímavý okamih kedy dochádza k pretečeniu hodnoty now. Toho sa niektorí obávajú že sa stane niečo hrozného. V skutočnosti sa nestane vôbec nič a rozdiel sa vypočíta spravne aj keď sa bude vyhodnocovať výraz 0 - 4294967000. Toho sa dá docieliť vo vizualizácii pokrútením kolečkom myši na nultom bite premennej now smerom nahor. Je pekne vidno že tá celočíselná aritmetika nemá žiadny problém vypočítať správne uplynutý čas v milisekundách aj v takýchto prípadoch.

    Hodne dobrý znalec číslicovej techniky si tu tiež môže všimnúť, že v tomto prípade je použitie premenných dĺžky long na premenné actionTime, elapsedTime (a vlastne aj now) plytvaním miestom v pamäti. Bezproblémovo to bude fungovať aj s premennými typu int (dĺžka 16 bit v AVR svete).

Záver

Funkcia millis() sa dá ľahko použiť na časovanie opakujúcich sa alebo jednorazových časových úsekov v dĺžke od jednej milisekundy do maximalne 49 dní. Pomocou viacerých premenných je možné súčasne časovať aj viacero nezávislých dejov s rôznymi opakovaniami s presnosťou skoro až na pár milisekund. Pozor na to že millis() nepočíta milisekundu úplne presne v krátkodobom horizonte. Čo je dané použitou frekvenciou oscilátora v doske arduina a použitým spôsobom merania času 8 bitovým počítadlom MCU. Takže nie je vhodná napríklad pre stopky pri požadovanej presnosti okolo jednej milisekundy. Kto potrebuje naozaj milisekundovú presnosť, tak sa to dá docieliť použitím mierne iného spôsobu počítania času 16 bitovým počítadlom v režime auto reload módu. Ale to je už zase o trochu zložitejšia záležitosť do ktorej treba tiež poriadne vidieť. Linky kde sa to popisuje nebudem dávať. Kto chce ľahko si to nájde.