Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Info

Cvičenie je zamerané na prácu s vývojovou doskou Arduino. Cvičenie je orientované na pokročilé programovanie mikroprocesora Atmega328p v prostredí ArduinoIDE. V rámci cvičenia bude na primeranej úrovní popísané použitie časovačov, prerušení a watchdogu. Študent po osvojení učiva pri úlohách, ktoré vyžadujú časové oneskorenia dokáže efektívne využívať čas procesora. Dokáže využiť externé a vnútorne prerušenie pre obsluhu funkcií s vysokou prioritou.  

  • Efektívne využívanie výpočtového výkonu  - už nikdy delay()!
  • Externé prerušenie - niekedy niet času nazvyš!
  • Watchdog - alebo ako nekontrolovane nezamrznúť
  • Prerušenie z časovača

Efektívne využívanie výpočtového výkonu  - už nikdy delay()!

Počas predchádzajúcich hodín sme konštatovali, že v Arduine môžeme používať funkciu delay(). Táto funkcia je veľmi obľúbená nakoľko umožňuje program na požadovaný čas "pozdržať". Používali sme ho napríklad v príklade blink resp. pri ošetrení zákmitov tlačidiel. Nevýhodou tejto funkcie je to, že procesor počas jej trvania doslova nerobí nič. Pritom by mohol vykonať obrovské množstvo užitočných operácií.  Ukážme si príklad, kedy sa budú striedavo zapínať a vypínať dve LED diódy, ktoré sú pripojené na piny č. 8 a 13.  Najprv s funkciou delay...

Code Block
languagecpp
linenumberstrue
void setup()
{
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
}
void loop()
{
    digitalWrite(13,HIGH);
    digitalWrite(8,LOW);
    delay(500); 
    digitalWrite(13,LOW);
    digitalWrite(8,HIGH);
    delay(500);
}

Je zrejmé, že taký kód bude pracovať pomerne spoľahlivo. Ale čo ak by sme toto blikanie chceli iba ako indikáciu behu Arduina a to by vykonávalo iné úlohy. Napríklad snímalo hodnoty na analógových portoch, komunikovalo cez I2C zbernicu, riadilo krokové motory a pod.? V takom prípade by asi nebolo veľmi rozumné zakaždým opakovaní funkcie loop() čakať 1 sekundu čakať na prebliknutie indikačných diód. Nasledujúci kód tento nedostatok rieši veľmi elegantným spôsobom.

V kóde vpravo si môžeme všimnúť, že kód pre blikanie diód sa vykoná len ak je splnená podmienka "millis()-prew_millis > 500". Zásadnú rolu v tejto podmienke hrá funkcia millis() [1]. Táto funkcia pri jej zavolaní vráti hodnotu času, ktorý ubehol od štartu Arduina. Tento čas je v milisekundách. V premennej  prew_millis je uložená hodnota millis() z predošlého vykonávania kódu, ktorý sa vykonáva vo funkcii if(). Z uvedeného je zrejmé, že  argument príkazu if() nadobúda pravdivý stav potom ako ubehne doba dlhšia ako 500ms.  Samotný stav, ktorý sa zapíše na digitálne piny je uložený v stavovej globálnej premennej (dá sa to vyriešiť aj elegantnejšie, ale  v tejto aplikácii to môže byť aj takto). 

Pre analogické využitie ale pre omnoho rýchlejšie deje je možné rovnakým spôsobom použiť funkciu micros(), ktorá vráti hodnotu v mikrosekundách.  

Register mikroprocesora, v ktorom sa zvyšuje hodnota času, ktorú vráti funkcia millis() pretečie po 232 ms, to predstavuje približne 49,7 dňa. Na túto skutočnosť je potrebné pamätať v prípade, že vaše zariadenie má fungovať dlhodobo a pomocou millis() neriešime iba blikanie diód alebo kmitanie tlačidiel. 

Ošetrenie zákmitov tlačidiel - debouncing

Rovnakým spôsobom je možné ošetriť aj tlačidlá  proti zákmitom. O zákmitoch sme hovorili na Cvičenie č. 10 - Arduino a jeho programovanie (Časť 1.) Jednoduchý kód pre obsluhu tlačidla na pripojeného na pin č. 3 je uvedený nižšie. V kóde sa pri stlačení tlačidla vykoná zmena v rozsvietení diód.

Code Block
languagecpp
linenumberstrue
#define BUTTON 3
bool STAV = 0;
unsigned long prew_millis=0;
void setup()
{
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(BUTTON,INPUT_PULLUP);
}
void loop()
{  /* Tu sa môže vykonávať obrovské množstvo operácií. */
  if( digitalRead(BUTTON)== 0 && (millis()-prew_millis > 250)) 
  								  // Tlačidlo sa zaznamená iba ak už prebehlo 250ms. 
   {                             // 250 ms je pre ošetrenie zákmitu viac než dosť
    prew_millis = millis();      // Aktualizuje sa čas kedy sa naposledy táto rutina vykonala
    digitalWrite(13,!STAV);                                           
    digitalWrite(8 , STAV);
    STAV = !STAV;                // Invertovanie stavu do budúce vykonania
   }   
Code Block
languagecpp
linenumberstrue
bool STAV = 0;               // Stavová premenná 
unsigned long prew_millis=0; // čas v ms 
void setup()
{
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
}
void loop()
{  /* Tu sa môže vykonávať obrovské množstvo operácií. */
  if(millis()-prew_millis > 500) // Rutina pre blikanie sa vykoná iba ak už prebehlo 500ms. 
   {
    prew_millis = millis();      // Aktualizuje sa čas kedy sa naposledy  ​táto rutina vykonal
    digitalWrite(13,!STAV);                                           
    digitalWrite(8 , STAV);
    STAV = !STAV;                // Invertovanie stavu do budúce vykonania
   }   
}


Externé prerušenie - niekedy niet času nazvyš!

V mikroprocesorovej technike sa často stretávame s takzvaným prerušením (interrupt). V podstate ide o mechanizmus, ktorý umožňuje prerušiť činnosť práve vykonávanej rutiny a vykonať niečo čo má vyššiu prioritu ako program, ktorý sa mikroprocesorom vykonáva v bežnej rutine. Prerušenie môže byť vyvolané vnútorne alebo z externe. Vnútorné prerušenia sú zvyčajne vyvolané časovačmi. Externe prerušenie je vyvolané zvonka, zmenou stavu na I/O pine. Táto zmena môže byť buď z HIGH na LOW alebo naopak. Arduino UNO má dva piny (D3 a D4), ktoré je možné použiť pre tento účel [2]. Keď príde prerušenie, procesor okamžite prestane vykonávať aktuálnu činnosť a vykoná sa predvolená funkcia. Potom ako sa dokončí prioritná funkcia, program pokračuje presne tam kde bol prerušený.

Kód zobrazený vpravo je aplikovaný na predošlí príklad. 

Zmena v porovnaní s predošlým príkladom nastáva v tom, že pri predošlom príklade sa stlačenie tlačidla vyhodnocovalo až potom ako sa vykonal kód pred príkazom if(), tento potencionálny kód je reprezentovaný komentárom: /*Tu sa môže vykonávať obrovské množstvo operácií.*/V príklade s využitím prerušenia sa sa stlačenie tlačidla vyhodnotí okamžite ako k nemu dôjde!

Prerušenie na pine aktivujeme pomocou nasledovného príkazu:


attachInterruptdigitalPinToInterrupt(pin), Funkcia_ktorá_sa_vykoná, režim );


  • Pre použitie prerušenia nie je potrebné zahrnúť žiadnu knižnicu (z pohľadu klasického programovania v ArduinoIDE). 
  • Podporované piny sú D3 a D4
  • Funkcia ktorá sa vykoná spĺňa nasledovné požiadavky:
    • je typu void
    • nemá vstupné argumenty
    • mala by byť čo najkratšia, má riešiť veľmi rýchle reakcie 
  • Režimy poznáme 4 (režim určuje aká udalosť vyvolá prerušenie)
    • Prechod z HIGH do LOW - FALLING (Vhodné ak je pin v INPUT_PULLUP móde)
    • Prechod z LOW do HIGH - RISING
    • Prechod z LOW do HIGH  alebo naopak - CHANGE
    • Stav logickej nuly (LOW) - LOW

V prípade, že počas behu programu chceme zrušiť prepojenie prerušenia na daný pin (väčšinou to nepotrebujeme) môžeme použiť príkaz:

detachInterrupt(digitalPinToInterrupt(pin))

Code Block
languagecpp
linenumberstrue
#define BUTTON 3
bool STAV = 0;
unsigned long prew_millis=0;
void setup()
{  
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(BUTTON,INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON), PRIORITNA_FCIA, FALLING);
}

void loop()
{  
  /* Tu sa môže vykonávať obrovské množstvo operácií ale keď príde prerušenie procesor
	 okamžite prestane vykonávať aktuálnu činnosť a vykoná sa predvolená funkcia. 
	 Potom ako sa dokončí prioritná funkcia program pokračuje presne tam kde bol prerušený. 
	 Aj prerušenia môžu mať rôzne priority.
  */
}

void PRIORITNA_FCIA()
{
  if(millis() - prew_millis > 250) // Ošetrenie proti zákmitom a tiež to rieši to, 
                                   // že používateľ nestihne tlačidlo uvoľniť skôr.  
  {
    prew_millis = millis();
    digitalWrite(13,!STAV);                                           
    digitalWrite(8 , STAV);
    STAV = !STAV; 
  }
}

Watchdog - alebo ako nezamrznúť







V mikroprocesorovej technike sa stretávame s pojmom  watchdog. Je to veľmi príznačný názov pre hardvérový časovať, ktorý je síce implementovaný vo vnútri procesora Atmega328p ale je na ňom kompletne nezávislý.  Časovač sa v anglosaskej literatúre nazýva Watchdog timer alebo skrátene WDT. Ak je tento časovať spustený, beží paralelne s procesorom. Jeho hlavnou úlohou je vyvolať reset procesora v prípade, ak sa procesor dostane do stavu zacyklenia alebo jednoducho vplyvom vonkajších vplyvov prestane pracovať v normálnom režime. Z tohto stavu sa môže dostať jedine pomocou resetu. Tiež je ho možné použiť pre vyvolanie prerušenia alebo prerušenia a následného resetu (kontrolné bity WDE, WDIF, WDIE) viac v [3]. WDT funguje na tom princípe, že čítač počíta impulzy ktoré prichádzajú z oscilátora. Čítač tento počet impulzov ukladá do registra. Pokiaľ tento register dosiahne maximálnu hodnotu pretečie a začína počítať znova od nuly. Moment kedy pretečie je ten moment, kedy WDT vyvolá prerušenie alebo reset. Pokiaľ procesor pracuje normálne, je schopný tento register ešte predtým ako pretečie vynulovať cez WATCHDOG RESET. Ak však procesor nepracuje ako má register pretečie a vyvolá sa reset. O pravidelné vynulovanie registra sa samozrejme vo svojom kóde musí postarať programátor. WDT pracuje s taktom 128kHz. Táto frekvencia sa následne pomocou deličiek (prescaler) znížená. To aké delenie sa použije je dané nastavením 4 bitov WDP0-WDP3. Keď ale budeme používať ArduinoIDE nemusíme sa zamýšľať ako dané kontrolné bity nastaviť. V princípe to znamená, že interval resetu je možné nastaviť na tieto hodnoty: 15ms, 32ms, 64ms, 0.125s, 0.25s, 0.5s, 1.0s, 2.0s, 4.0s, 8.0s. Pre prácu s WDT je potrebné použiť knižnicu avr/wdt.h. WDT sa inicializuje príkazom wdt_enable(WDTO_cas). Napríklad wdt_enable(WDTO_4S) alebo wdt_enable(WDTO_32MS). Pravidelný reset registra WDT sa vykonáva pomocou príkazu wdt_reset(). 

Nasledujúci kód demonštruje použitie WDT a neželaný reset procesora,  ktorý vznikol  chybou programátora (Uvažujeme prvú schému na tejto stránke).

Code Block
languagecpp
linenumberstrue
#include<avr/wdt.h> 
void setup()
{
  pinMode(13, OUTPUT);
  pinMode(8, OUTPUT);
  wdt_disable();  // Pokiaľ bol zapnutý WDT, tak sa vypne. 
                  // Toto je dôležité, hlavne vtedy ak máme nastavené veľmi krátke intervaly
                  // WDT pracuje nezávislé na procesore, a môže sa stať že ho nedovolí ani naprogramovať. 
  delay(2000);	  // Čakanie zabezpečí aby as WDT nespustil nižšie a nezabránil prípadnému nahraniu kódu. 
  for(int i = 0; i<20; i++) // Dióda na pine 13 bliká rýchlo
  {
    digitalWrite(13,!digitalRead(13));
    delay(150);
  }
   wdt_enable(WDTO_4S); // Nastavý sa interval 4S
}
void loop()
{ int pocet = 0;
  wdt_reset(); // WDT sa resetuje - tento príkaz je v kóde nutné umiestňovať na rozumných miestach.
 			
  while(pocet < 10) // Programátor pozabudol na to, že slučka sa bude vykonávať minimálne 5s WDT je nastavený na 4S
                    // Procesor sa bude neustále resetovať. Správne by v slučke mal byť vykonaný aj wdt_reset().
  { 
    digitalWrite(8,!digitalRead(8));
    delay(500);
    pocet++;   
  }
}

Prerušenie z časovača

Mikroprocesor Atmetga328p má okrem WDT aj tri ďalšie časovače. Dva 8 bitové časovače (Timer0, Timer2) a 16 bitoví časovač Timer1. Uvedený počet bitov reprezentuje veľkosť registra, v ktorom sa počítajú impulzy z oscilátora. Z uvedeného vyplýva, že Timer 0 a Timer2 môžu napočítať 255 impulzov, potom pretečú a Timer1 môže napočítať 65536 impulzov. Tieto časovače na rozdiel od WDT využívajú oscilátor procesora teda 16MHz (16000000 Hz). Táto frekvencia je podobne ako u WDT delená deličkami (prescalers), ktoré môžu frekvenciu deliť týmito hodnotami: 1(teda bez delenia), 8, 64, 256, 1024 pre Timer2 ešte navyše 32 a 128. Nastavenie deličky sa robí pomocou riadiacich bitov označených TCCR (Tento register je rozdelený na TCCRxA a TCCRxB). Oveľa viac detailov v literatúre [4, 5]. Počet impulzov je zaznamenaný v registri TCNTx (x=0, 1 a 2). Cieľová hodnota, ktorú má pred vyvolaním prerušenia časovač napočítať sa nastavuje v registri OCRx. Časovače majú aj ďalšie nastavenia, ktorými sa teraz nebudeme zaoberať. Problematika časovačov je pomerne zložitá a podrobne sa ňou budete zaoberať v 2. ročníku. Na Programovaní  si ale ukážeme ako časovač použiť. Projekt Arduino pripravil webstránku, na ktorej si môžete veľmi pohodlne vygenerovať nastavenie časovača, bez toho aby ste podrobne rozumeli práci s registrami a deličkami.  Tento web nájdete na  [65].

Len pre ilustráciu, ak chceme pomocou časovača Timer1 dosiahnuť prerušenie raz za sekundu (teda f = 1Hz) môžeme nastavenie konečného počtu impulzov OCR1A pri deličke s hodnotou D=1024 vypočítať nasledovne: 

  OCR1A =(f*16000000/D) = (1 * 16000000/1024) = 15 625;

Príklad:

prerobte príklad s dvoma blikajucími LED diódami tak, aby ich hodnota bola menená pomocou prerušenia z časovača Timer1. Frekvencia zmeny stavu LED diódy nech je 1Hz. Pre vygenerovanie konfigurácie použijeme [6].

Tam sa vygeneruje kód, ktorý okrem loop() a setup(), pozostáva z dvoch ďalších funkcii:

  • setupTimer1()  - Zavolá sa z funkcie setup() a nastaví registre TCCRxA, TCCRxB, TCNTx, OCRx
  • ISR(TIMER1_COMPA_vect) - Obsluha prerušenia. Tu sa uvedie kód, ktorý sa vykoná počas prerušenia.

Tieto si môžete skopírovať do svojich programov. Nižšie je uvedený program pre daný príklad. Komentáre, ktoré vygenerovala stránka sú odstránené.


Časovače Timer0 a Timer2 sú prepojené tiež s digitálnymi I/O vývodmi (na obr. port D a B). Pomocou týchto časovačov je zabezpečená činnosť hodín pre sériovú linku a generovanie PWM signálov.  Timer0 je používaný funkciami millis() a delay(). Preto by sme nemali zasahovať do jeho nastavenia. Timer2 je používaný napríklad funkciou tone()Timer1 je ten časovač, ktorý budeme väčšinou používať. 

Code Block
languagecpp
linenumberstrue
bool STAV = 0;
void setupTimer1() 
{ // Časovač spustí prerušenie raz za sekundu
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 15624;
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS12) | (1 << CS10);
  TIMSK1 |= (1 << OCIE1A);
  interrupts();
}
void setup()
{
   pinMode(13,OUTPUT);
   pinMode(8, OUTPUT);
   setupTimer1();
}
void loop()
{ 
  /* Tu sa môže vykonávať obrovské množstvo operácií ale keď príde prerušenie procesor
     okamžite prestane vykonávať aktuálnu činnosť a vykoná sa predvolená funkcia.
     Potom ako sa dokončí prioritná funkcia program pokračuje presne tam kde bol prerušený.
     V tomto prípade je prerušenie vyvolané každú sekundu.
  */
}
ISR(TIMER1_COMPA_vect) // Rutina prerušenia
{
    digitalWrite(13,!STAV);                                          
    digitalWrite(8 , STAV);
    STAV = !STAV;
}