Fyzikální kabinet FyzKAB
Články FUNDUINO: Multi-function Shield Funduino: LED zobrazovače pomocí přerušení

Funduino: LED zobrazovače pomocí přerušení

Po delší době se vracíme ke školnímu výukovému shieldu MFS (Multi Function Shield) Funduino, který se nejen hodí pro první seznámení s modulem Arduino, ale občas se hodí na jednoduchou aplikaci, kdy je třeba jen tak něco „spíchnout“.

V dnešním článku se konkrétně zaměříme na čtveřici LED zobrazovačů, kterými je MFS Funduino osazeno. Rádi bychom vyřešili obsluhu těchto zobrazovačů tak, abychom je neustále nemuseli v hlavní smyčce obnovovat. Právě neustálý „refresh“ zobrazovačů výrazně omezuje strategii tvorby kódu. Kupříkladu nelze v hlavním kódu použít jakoukoliv časově náročnější operaci nebo řešit čekací dobu klasickým příkazem delay().

Cíle našeho dnešního snažení tedy bude vyřešení obsluhy LED zobrazovačů „tak nějak na pozadí“ běhu programu běžícího v hlavní nekonečné smyčce.

Stávající obsluha LED zobrazovačů

V předcházejících článcích o MFS Funduino jsme řešili ovládání LED zobrazovačů pomocí následující funkce display, kterou ale musíme neustále volat v hlavní smyčce. Pro funkčnost této funkce je použito několik globálních polí.

// data pro zobrazeni cislic 0-9
unsigned char Dis_table[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};
unsigned char Dis_buf[] = {0xF1, 0xF2, 0xF4, 0xF8};    // adresy segmentovek
unsigned char disbuff[] = {0, 0, 0, 0}; // hodnoty pro zobrazeni

Pole proměnných Dis_table obsahuje hodnoty odpovídající rozsvícení jednotlivých segmentů LED zobrazovačů (segmentovek) pro číselné symboly. Například první hodnota (index pole 0) odpovídá stavu rozsvícení symbolu „0“. Při rozsvěcení jednotlivých LED segmentů zobrazovačů na MFS Funduino platí, že hodnotou 0 se LED rozsvítí, naopak hodnotou 1 zhasne. Jak získat hodnotu odpovídající zobrazení symbolu „0“ při odesílání dat v dále používaném režimu MSBFIRST (Most Significant Bit First – Nejvýznamnější bit jako první) ukazuje níže uvedený obrázek.

LED Display - nastaveni

Pro zhasnutí segmentů G a DP je třeba nastavit bity odpovídající hodnotě 64 a 128 na hodnotu 1. Zbylé bity odpovídající segmentům A–F jsou rozsvíceny, tedy nastaveny na hodnoty 0. V šestnáctkové soustavě, ve které jsou hodnoty do pole zapsány, odpovídá hodnota 192 zápisu: 0xC0. Obdobně jsou připraveny hodnoty pro zobrazení dalších symbolů, tedy 1, 2, … 9. Zároveň platí, že definice každého symbolu má v poli stejný index, jako je symbol, který má zobrazovat. Chceme-li tedy zobrazit symbol 0, odešleme na segmentovku položku pole s indexem 0 Dis_table[0].

V uvedené části kódu též vidíme, že zde máme i pole Dis_buf a disbuff. První pole (tj. Dis_buf) obsahuje adresy jednotlivých LED zobrazovačů (bráno zleva doprava). Pole disbuff slouží jako buffer hodnot, které se budou na jednotlivé LED zobrazovače neustále zapisovat (indexy pole opět brány zleva doprava). Výchozí hodnoty {0, 0, 0, 0} tedy odpovídají rozsvícení všech LED segmentů.

Samotná funkce display pak vypadá takto:

void display(void) {
   for(char i = 0; i <= 3; i++) {  // probehne segmenty a zobrazi hodnoty
     digitalWrite(4, LOW);    // zapis do latch pameti
     shiftOut(8, 7, MSBFIRST, Dis_table[disbuff[i]]);
     shiftOut(8, 7, MSBFIRST, Dis_buf[i] );    // adr. zobrazovace
     digitalWrite(4, HIGH);   // zobraz zapsana data
     delay(2);    // pred dalsim segmentem pockej 2 ms
   }
}

Jak vidíme, je zde čtyřikrát zapsána kombinace čísel odpovídající rozsvícení LED segmentů a adresy LED zobrazovače, do dvojice obslužných posuvných registrů. Po ukončení zápisu (pin 4 převeden do stavu HIGH) se data z registru zapíší na výstup a rozsvítí zadaný LED zobrazovač. Následuje 2 ms pauza a pokračuje zápis do dalšího LED zobrazovače. Rozsvícením následujícího LED zobrazovače se ten předchozí zhasne. Chceme-li tedy zobrazovat všechny čtyři LED zobrazovače, musíme je rozsvěcet tak rychle, aby to oko pozorovatele nestihlo zaznamenat.

Jakmile by se program v modulu Arduino jakkoliv „zadrhl“ a nestihl by se včas rozsvítit LED výstup, pak by dotčený LED zobrazovač blikl (případně by blikla celá čtveřice). A to je to, co právě nechceme.

Hledáme nové řešení obsluhy LED zobrazovačů

V článku věnovanému přerušení časovače na modulu Arduino jsme si říkali, že přerušení časovače nám umožňuje nezávisle spouštět tzv. rutinu obsluhy přerušení (funkce ISR). Řešení se nám tedy přímo nabízí. Tím, že převedeme předchozí funkci display na tuto ISR funkci, bude docházet k jejímu pravidelnému spouštění (podobně jako když byla vnořena do hlavní nekonečné smyčky programu) a máme vše vyřešeno. Nebo ne?

Zkusíme se nad tím zamyslet! Aby LED zobrazovače viditelně neblikaly, zvolíme frekvenci jejich cyklického rosvěcení například 50 krát za 1 vteřinu. Jinými slovy budeme funkci display spouštět každých 20 milisekund. Výkonná část funkce display trvá: 1 μs nastavení registrů pro začátek zápisu, 2× 24 μs za zápis do posuvných registrů (dva registry po 8 μs zápis dat, 8× 2 μs vygenerování hodinového pulzu pro každý registr), 1 μs nastavení registrů pro ukončení zápisu. Pokud nejsou při těchto operacích dodržovány nějaké časovací pauzy (např. při generování hodinového signálu) měla by tato část celkem trvat 50 μs. I kdybychom celou situaci nadhodnotili, můžeme říci, že za 0,1 ms je hotovo. Jenže pak to přijde! Ve funkci display následuje příkaz delay(2), který procesoru vystaví na celé 2 ms stopku. A  pak se to celé ještě třikrát opakuje pro další tři LED zobrazovače. Takže se v celé funkci méně než 0,5 ms pracuje a 8 ms čeká. Jestliže by se každých 20 ms spustila rutina, která bude 8 ms čekat, tak výrazně ovlivníme běh celého programu. To nemluvě o tom, že kdyby se čekalo ve funkci obsluhy přerušení, je celý procesor po tuto dobu odstaven a neběží nic!

To je mimochodem ten důvod, proč se všude píše, že funkce obsluhy přerušení NESMÍ obsahovat žádné zbytečné zdržování.

Dobrá, tak ty „čekačky“ vynecháme!

Ani toto není dobrý nápad… Opět si trochu projdeme časování takto navrženého řešení. Nastane přerušení a funkce display během cca 0,1 ms rozsvítí první LED zobrazovač. Čekací příkaz je nyní vynechán, proto se pokračuje s obsluhou druhého LED zobrazovače (první zobrazovač zatím svítí). Během následující 0,1 ms jsou data připravena pro druhý zobrazovač, první zobrazovač zhasíná a druhý se rozsvěcí. První zobrazovač, stejně tak druhý, ale stejně i ten třetí tedy bude svítit po dobu maximálně 0,1 ms. U čtvrtého zobrazovače to bude trochu jinak. Jeho samotné rozsvícení nic nového nepřinese, ale jeho zhasnutí ano. Zatím, co první tři zobrazovače svítili (každý) 0,1 ms, poslední se rozsvítí a k jeho zhasnutí dojde až zase při rozsvěcení zobrazovače prvního – tedy až při dalším zavolání přerušení. To je skoro 20 ms. Vizuálně tedy bude poslední LED zobrazovač svítit výrazně více.

Tak to budeme spouštět častěji, aby se ten poslední zobrazovač dříve zhasl a ty předchozí se rozsvítily častěji!

Je pravda, že při nastavení frekvence spouštění asi tak 5000× za vteřinu by se svit LED zobrazovačů vizuálně měl vyrovnat. První tři zobrazovače stále svítí asi 0,1 ms (každý), čtvrtý necelých 0,2 ms. Je to sice dvojnásobek, ale lidské oko by se mohlo rádo nechat ošidit. Takže hotovo? Ten, kdo si myslí, že máme hotovo, se ale sakramensky mýlí!

Chtěli jsme ovládat LED zobrazovače tak nějak „na pozadí“ jiné práce. Ale nezabírá nám nyní to „pozadí“ nějak moc času pro to „popředí“? Jinými slovy, nedostali jsme se nakonec do situace, jako byla ta s tím čekáním?

Nyní každých 0,2 ms zastavíme celý procesor. A to na dobu 0,1 ms pro první LED zobrazovač, 0,1 ms pro druhý… A při případném rozsvěcení třetího LED zobrazovače bychom už zase měli spouštět funkci ISR. Tady nám asi tak akorát pomáhá fakt, že pokud běží ISR, procesor modulu Arduino (asi) zakazuje spuštění tohoto samého přerušení. Takže bychom asi nakonec byli schopni rozsvítit i třetí a čtvrtý zobrazovač. Ale rozhodně nám nezbyde čas na cokoliv jiného. (A vůbec je to totální „prasárna“!)

Nové řešení obsluhy LED zobrazovačů

Je asi už jasné, že pouhé převedení funkce display na ISR funkci je cesta špatného řešení! Chceme-li začít používat přerušení, musíme začít myslet jinak.

Filozofie přerušení je přece jasná: Nastane-li přerušení, je třeba něco hodně rychle udělat a konec! Zpět do hlavního programu.

Viděli jsme, že čtvrtý zobrazovač vlastně svítil dobře (tedy do chvíle, než jsme se zbláznili a nastavili frekvenci přerušení na šílených 5000 Hz). Nešlo by tedy obdobným způsobem rozsvítit i ostatní LED zobrazovače? Třeba při prvním volání přerušení rozsvítit jen první LED zobrazovač. Ten bude svítit do dalšího zavolání přerušení, kdy rozsvítíme druhý zobrazovač. A zase nic jiného. Hlavní program běží a čeká se na třetí přerušení. Až nastane, rozsvítíme třetí LED zobrazovač… Stejně tak časem rozsvítíme čtvrtý a pak zase první zobrazovač a tak dále. Okamžiky, kdy uvolníme procesor pro běh hlavní programové smyčky, nám slouží jako „čekačka“. Zároveň jsme zkrátili dobu běhu ISR funkce asi tak na čtvrtinu, neboť vždy rozsvěcíme jen jediný LED zobrazovač. Pro rozsvěcení LED zobrazovačů budeme potřebovat asi frekvenci 50 Hz, tedy pro čtyři zobrazovače budeme volat přerušení s frekvencí 200 Hz.

Jak jsme na tom s časem? Funkci ISR voláme každých 5 ms. Doba trvání ISR funkce je asi 0,1 ms, což jsou asi 2 % z tohoto času. Pro běh hlavního programu nám tedy zbývá 98 % původních prostředků. Všechny LED zobrazovače svítí stejnou dobu. To není špatný výsledek!

Jdeme programovat!

Nejdříve se podíváme, jak vytvořit ISR funkci pro obsluhu přerušení, pak se podíváme na to, jak nastavit samotné přerušení časovače. Následující část výsledného kódu nyní jen ukazuje řešení ISR a její pomocnou globální proměnnou:

volatile char Dis_No;

 

ISR(TIMER2_COMPA_vect) {          // timer compare interrupt service routine
    digitalWrite(4, LOW);    // zapis do latch pameti
    shiftOut(8, 7, MSBFIRST, disbuff[Dis_No]); // data pro zobrazovac
    shiftOut(8, 7, MSBFIRST, Dis_buf[Dis_No]);    // adr. zobrazovace
    digitalWrite(4, HIGH); // zobraz zapsana data
    Dis_No = (Dis_No+1) % 4;
}

Podívejme se do těla funkce ISR. Vlastně jde o klasické odeslání hodnoty zobrazovacích dat pole disbuff na jeden LED zobrazovač. Který zobrazovač to bude, určuje globální proměnné Dis_No, která musí být deklarována v hlavní části programu a je deklarována jako volatile, což zabrání kompilátoru, aby jakýmkoliv způsobem optimalizovat její užití. Hodnota této proměnné bude na začátku nastavena na hodnotu 0, tím se tedy odešlou na zobrazovač zobrazovací data disbuff[0] a adresa LED zobrazovače bude odpovídat hodnotě v Dis_buf[0]. To odpovídá prvnímu LED zobrazovači. Těsně před ukončením ISR funkce se proměnná Dis_No zvýší o jedničku modulu 4 (tedy postupně cyklicky nabývá hodnot 0, 1, 2 a 3). Při dalším zavolání funkce přerušení bude v proměnné Dis_No hodnota 1, takže dojde k nastavení druhého LED zobrazovače. Stejně tak při dalším volání se nastaví 3., pak 4. zobrazovač. „Přetečením“ proměnné Dis_No z hodnoty 3 zpět na 0 se opět dostáváme k nastavení prvního zobrazovače.

Nastavení zobrazované hodnoty na LED zobrazovačích

Víme-li jak se ve finálním kódu budou obsluhovat LED zobrazovače, je třeba si ukázat, jak budeme v programu zadávat hodnoty (znaky), které se budou na LED zobrazovačích zobrazovat. V tomto případě se držíme původního konceptu, tedy, že zobrazovací data jsou v poli disbuff. Budeme-li tedy chtít na prvním zobrazovači zobrazit symbol „0“, zapíšeme do proměnné disbuff[0] hodnotu 192 (resp. 0xC0). Na toto místo můžeme zapisovat libovolné hodnoty z intervalu 0–255, což pak odpovídá zapnutí/vypnutí jednotlivých LED segmentů. Například pokud budeme chtít LED zobrazovač vypnout, zapíšeme na něj hodnotu 255 (resp. 0xFF).

Pochopitelně si některé hodnoty můžeme připravit – například ty pro znaky 0–9. K tomu můžeme použít pole Dis_table, jeho použití už známe z předešlého použití.

Přerušení časovače

Rozhodneme-li se nastavit přerušení (spouštění ISR 200× za sekundu) pomocí standardního způsobu nastavení časovače, mohl by ukázkový program vypadat následovně:

unsigned char Dis_buf[] = {0xF1, 0xF2, 0xF4, 0xF8};    // adresy segmentovek
unsigned char disbuff[] = {0xFF, 0xFF, 0xFF, 0xFF}; // hodnoty pro zobrazeni
volatile char Dis_No;

// data pro zobrazeni cislic 0-9
unsigned char Dis_table[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};

ISR(TIMER2_COMPA_vect) {          // timer compare interrupt service routine
    digitalWrite(4, LOW);    // zapis do latch pameti
    shiftOut(8, 7, MSBFIRST, disbuff[Dis_No]);   // data pro zobrazovac
    shiftOut(8, 7, MSBFIRST, Dis_buf[Dis_No] );  // adr. zobrazovace
    digitalWrite(4, HIGH); // zobraz zapsana data
    Dis_No = (Dis_No+1) % 4;
}

void LED_vystup(char A, char B, char C, char D) {
  disbuff[0]= A;   // prvni zobrazovac
  disbuff[1]= B;   // druhy zobrazovac
  disbuff[2]= C;   // treti zobrazovac
  disbuff[3]= D;   // ctvrty zobrazovac
}

void setup() {
  pinMode(4, OUTPUT);   // nastaveni pro zobrazovace
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  Dis_No = 0;

  Serial.begin(115200);
  while (!Serial);

  // initialize Timer2
  noInterrupts();    // zakaz vsech preruseni (kdyz se v tom ted vrta)
  TCCR2A = 0;
  TCCR2B = 0;
  TCNT2  = 0;

  OCR2A = 77;               // hodnota shody 16MHz/1024/200Hz (77 odpovida 200,32 Hz)
  TCCR2A |= (1 << WGM21);   // zapnuti CTC rezimu (vynulovaci citace při shode)
  TCCR2B |= (1 << CS22) | (1 << CS21) | (1 << CS20); // nastaveni delicky timeru2 na 1024
  TIMSK2 |= (1 << OCIE2A);  // zapnuti preruseni shody (s reg OCR2A)
  interrupts();             // opet povolene všechna preruseni
}

void loop() {
  Serial.println("Jsem hlavni smycka a delam si co chci!");
  int i = random(0, 9999);   // vydeneruje se nahodne cislo 0-9999
  int tisice = i / 1000;
  int stovky = i % 1000 / 100;
  int desitky = i % 100 / 10;
  int jednotky = i % 10;

  LED_vystup(Dis_table[tisice], Dis_table[stovky], Dis_table[desitky], Dis_table[jednotky]);
  delay(5000);
}

Vysvětlení kódu

Pro přerušení byl zvolen na modulu Arduino Uno časovač Timer2, který jinak ovládá PWM signál na pinech 3 a 11 (tímto jsme se definitivně o PWM signál na těchto pinech připravili). Druhou obětí naší volby časovače Timer2 je zrušení funkčnosti příkazu Tone(), který jinak tento časovač využívá. To je důležité vědět, neboť až budeme chtít na MFS Funduino použít tento způsob ovládání LED zobrazovačů a zároveň využívat služeb vestavěného bzučáku, tak to nebude možné. Kdybychom však pro naši přerušovací funkci použili časovač Timer1, přijdeme o možnost ovládat Servo (jeho knihovna využívá tento časovač). A pokud bychom použili časovač Timer0, budeme časovačích funkcí jako je delay() nebo millis(). Tyto a jiné informace o přerušení byly popsány v našem článku Arduino: Použití přerušení.

S frekvencí asi 200 Hz je pravidelně časovačem spouštěna funkce obsluhy přerušení ISR(TIMER2_COMPA_vect), která postupně rozsvěcuje LED zobrazovače. Její název ani vstupní parametr neměňme! Hlavní nekonečná smyčka postupně generuje čísla v rozmezí 0 až 9999. Pak do proměnných tisíce, stovky, desítky a jednotky vypočítá číslici, která se má na daném LED zobrazovači rozsvítit. Pro nastavení dat pro rozsvícení jednotlivých LED zobrazovačů slouží funkce LED_vystup, která dělá je to, že zadané parametry A, B, C D zapíše po globálního pole zobrazovačů disbuff. Pomocné pole Dis_table obsahuje definice jednotlivých symbolů 0–9.

Výsledný kód by měl na LED zobrazovačích MFS Funduino po pěti vteřinách zobrazovat náhodná čísla z intervalu 0–9999. Zároveň, abychom viděli, že přerušení nijak zvlášť nezatěžuje běh hlavního programu, se na sériový port vypisuje text „Jsem hlavni smycka a delam si co chci“, který můžeme pak sledovat pomocí sériového monitoru.

Pokud nás děsí surově syrové nastavení přerušení časovače, můžeme použít některé z knihoven, které práci s přerušením časovače ulehčují. Kupříkladu doporučujeme knihovnu TimerInterrupt vývojáře Khoi Hoang.

Výsledný kód by pak mohl vypadat kupříkladu takto:

#define USE_TIMER_2     true
#define TIMER_FREQ_HZ   200

#include "TimerInterrupt.h"  // https://github.com/khoih-prog/TimerInterrupt
volatile char Dis_No;

unsigned char Dis_buf[] = {0xF1, 0xF2, 0xF4, 0xF8};    // adresy segmentovek
unsigned char disbuff[] = {0xFF, 0xFF, 0xFF, 0xFF}; // hodnoty pro zobrazeni

// data pro zobrazeni cislic 0-9
unsigned char Dis_table[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};

void TimerHandler() {
    digitalWrite(4, LOW);    // zapis do latch pameti
    shiftOut(8, 7, MSBFIRST, disbuff[Dis_No]);   // data pro zobrazovac
    shiftOut(8, 7, MSBFIRST, Dis_buf[Dis_No] );  // adr. zobrazovace
    digitalWrite(4, HIGH); // zobraz zapsana data
    Dis_No = (Dis_No+1) % 4;
}

void LED_vystup(char A, char B, char C, char D) {
  disbuff[0]= A;   // prvni zobrazovac
  disbuff[1]= B;   // druhy zobrazovac
  disbuff[2]= C;   // treti zobrazovac
  disbuff[3]= D;   // ctvrty zobrazovac
}

void setup() {
  pinMode(4, OUTPUT);   // nastaveni pro zobrazovace
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  Dis_No = 0;

  Serial.begin(115200);
  while (!Serial);

  ITimer2.init();     // Init timer ITimer2
  if (!ITimer2.attachInterrupt(TIMER_FREQ_HZ, TimerHandler))  // zadani frekvence volani
    Serial.println("Problem s nastavenim casovace!");
}

void loop() {
  Serial.println("Jsem hlavni smycka a delam si co chci!");
  int i = random(0, 9999);
  int tisice = i / 1000;
  int stovky = i % 1000 / 100;
  int desitky = i % 100 / 10;
  int jednotky = i % 10;

  LED_vystup(Dis_table[tisice], Dis_table[stovky], Dis_table[desitky], Dis_table[jednotky]);
  delay(5000);
}

Hlavní části kódu jsou vlastně stejné. Jednou změnou je nastavení přerušení. Funkce ISR se nyní jmenuje TimerHandler() a nemá žádný vstupní parametr. Frekvence volání funkce obsluhy přerušení je uložena direktivě TIMER_FREQ_HZ, stejně jako je nastaveno číslo použitého časovače direktivou USE_TIMER_2 true. V sekci setup je pak inicializován objekt ITimer2, pomocí jehož metody attachInterrupt je nastavena frekvence volání přerušení a zadána funkce, která bude sloužit jako ISR (zde funkce TimerHandler). To je z oblasti nastavení přerušení vše. V tomto směru nám použití knihovny TimerInterrupt celý kód z hlediska použití přerušení časovače nejen zjednodušilo, ale asi i funkčně osvětlilo.

pohled na MFS Funduino

Ať již použijeme první nebo druhý kód měli bychom získat možnost určité tvůrčí svobody z hlediska vytváření dalších kódů modul Arduino s nasazeným rozšířením MSF Funduino. Nyní se již nemusíme starat o režii vykreslování symbolů na čtveřici LED zobrazovačů. Vždy, když budeme chtít na nějakém LED zobrazovači zobrazovaný symbol změnit, zapíšeme do pomocného globálního pole disbuff. O zbytek, tedy zobrazení a pravidelné „refreshování“ se stará nastavení přerušení časovače Timer2. Možná by se tuto agendu řízení LED zobrazovačů hodilo umístit do pomocné knihovny a pak ji jen je svým projektům připojovat, ale to je již na každém vývojáři, jak si svůj projekt upraví. Cílem tohoto článku bylo ukázat možné využití přerušení časovače pro shield MFS Funduino a také trochu ukázat, jak je třeba přemýšlet při tvorbě programů s užitím časovače modulu Arduino.

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!