Na ostatnich zajęciach kontynuowaliśmy naukę obsługi ledów przez monitor szeregowy. Tym razem nie ograniczaliśmy się do sterowania wszystkimi ledami jednocześnie, a dążyliśmy do możliwości dowolnego włączania i wyłączania pojedynczych lampek. Podczas tych zajęć uczyliśmy się sprytnego (zaawansowanego?) wykorzystania tablic.
Switch … case
Na zajęciach wykorzystaliśmy układy przygotowane tydzień temu. Podłączyliśmy je w taki sam sposób. Do naszego pierwszego programu wykorzystaliśmy warunek wielokrotnego wyboru, czyli składnię switch … case.
Na początku kodu standardowo podłączamy każdy z ledów do pinu cyfrowego. Następnie tworzymy pętlę if, która sprawdza, czy są dane (bajty) na porcie szeregowym do odczytania przez Arduino – linia #11. W 12 linii wczytujemy jeden bajt i przypisujemy go do zmiennej znakowej c (omawialiśmy to tydzień temu). W celu umożliwienia sterowania pojedynczymi lampkami za pomocą konkretnych liter w 13 linii tworzymy przełącznik switch zależny właśnie od zmiennej c. W liniach 14-23 tworzymy warunki, czyli przypisujemy do wybranych znaków (u nas: a b c d e A B C D i E) włączanie lub wyłączanie danego LED-a. Fajne jest to, że do bufora możemy od razu wpisać całe „zdanie” a nie tylko pojedynczy znak. Arduino będzie odczytywać znak-po-znaku (linia 12), a my zobaczymy daną sekwencję włączania/wyłączania LED-ów na płytce. Aby uatrakcyjnić ten fragment etap zabawy z Arduino i LED-ami dodaliśmy specjalny case z wartośćią #, który tworzy przerwę – pauzę (linia 24).
#define MAX 5 int piny[5]={2,3,4,5,6}; int i; char c; void setup(){ for(i=0;i<MAX;i++) pinMode(piny[i],OUTPUT); Serial.begin(9600); } void loop(){ if(Serial.available()>0){ c=Serial.read(); switch(c){ case 'a': digitalWrite(piny[0],HIGH);break; case 'A': digitalWrite(piny[0],LOW);break; case 'b': digitalWrite(piny[1],HIGH);break; case 'B': digitalWrite(piny[1],LOW);break; case 'c': digitalWrite(piny[2],HIGH);break; case 'C': digitalWrite(piny[2],LOW);break; case 'd': digitalWrite(piny[3],HIGH);break; case 'D': digitalWrite(piny[3],LOW);break; case 'e': digitalWrite(piny[4],HIGH);break; case 'E': digitalWrite(piny[4],LOW);break; case '#': delay(200);break; }}}
Właśnie dzięki nowemu symbolowi # (pauza) było możliwe wpisywanie sekwencji (=”zdań”) typu abcde#####A#B#C#D#E (jednoczesne włączenie wszystkich 5-ciu LEDów, odczekanie sekundy a następnie wyłączenie, z krótkimi przerwami, po kolei LED-ów). Inne sekwencje to, np. abcde##E##D##C##B##A##abcde###ABCDE###abcde###ABCDE (włączenie wszystkich, wyłączenie po kolei wszystkich w odwrotnej kolejności a na koniec dwukrotne „mrygnięcie” wszystkimi na raz LED-ami).
Obserwacje/uwagi
Powyższy program jest prosty ale ciągle efektowny – dzięki wprowadzeniu pauzy (#). Jednak z informatycznego punktu widzenia cierpi na następujące problemy:
- chcąc dodać więcej LED-ów musimy jak „małpa” skopiować linie 14-15 dodając nowe literki do sterowania. Zauważamy jednak pewną regularność w oprogramowaniu każdej literki (linie 14-23 niewiele różnią się od siebie). Może nie ma w tym nic złego, ale czy nie da się tego jakoś lepiej zaprogramować?
- jak rozbudować program o możliwość sterowania literkami w ten sposób, że dana literka włącza LED-a gdy był on wyłączony, a wyłącza gdy był on włączony? Jak na razie do włączania używamy małych liter a do wyłączania dużych – to chyba zbyteczna rozrzutność.
Zaczynamy od rozwiązania pierwszego problemu i przechodzimy do wykorzystania tablic. Tablice pojawiają się tu w sposób naturalny – przyglądając się liniom 14-23 zauważamy, że włączamy/wyłaczamy LED-y podpięte do portów cyfrowych Arduino zapisanych w tablicy piny[0,1,2,3,4]. Przy czym pierwszy LED podłączony jest do portu piny[0], drugi do portu piny[1] i tak dalej. Jak więc dobrać odpowiedni indeks tablicy do konkretnego LED-a?
ASCII
Dobranie indeksu tablicy piny[] będzie bazować na kodowaniu znaków ASCII. Dane wczytywane przez Serial.read() to w rzeczywistości bajty, które możemy interpretować jako literki (typ char) lub jako liczby (typ int). Możemy więc patrzeć na literkę d jak znak 'd’ (typ char) lub jak na kod ASCII wynoszący 100 (liczba całkowita, typ int). Dlaczego liczba 100? Przypatrz się uważnie tablicy kodów ASCII z poprzedniego linku (kolumna DEC) lub tutaj. Kolejna literka po d to e – czyli kod ASCII 101 i tak dalej. Co więcej, możemy odejmować literki od siebie, bo to będzie zrozumiałe jako… odejmowanie liczb całkowitych! Tak więc d-a oznacza 100-97, czyli 3. Jesli więc umówimy się, że pod literką a mamy sterowanie pierwszego LEDa, pod b drugiego i tak dalej – to właśnie różnica wczytanej literki i znaku a da nam dobrze określenie indeksu tablicy dla konkretnego LED-a! Zapisane jest to w linii #6 poniższego kodu:
void loop(){ if(Serial.available()>0){ c=Serial.read(); Serial.print("Wczytałem znak = "); Serial.println(c); int idx=c-'a'; Serial.print("indeks ="); Serial.print(idx); if(idx>=0&&idx<5){ //gdy mamy 5 LEDow Serial.print("Włączam / wyłączam LED-a nr"); Serial.print(idx); //dalsza część kodu } }
Na małą uwagę zasługuje jeszcze linia #9 – sprawdzenie, czy indeks nie jest większy niż liczba podłączonych LED-ów (oczywiste) oraz czy indeks jest większy od zera. To drugie może nastąpić gdy, np. omyłkowo wpiszem z klawiatury znak [ (o numerze ASCII 91), więc w wyniku odejmowania mamy nieistnieący element tablicy o indeksie 91-97=-6.
Tablica – zapamiętanie stanów pinów
Drugi problem na naszej liście to zapamiętanie stanów portów cyfrowych Arduino. Chodzi o to, aby po odczytaniu danej literki włączyć LED-a gdy był on w stanie wyłączonym, i wyłączyć – gdy był on włączony. Arduino nie ma jakiejś specjalnej funkcji do „zapytania się” o aktualny stan portu, dlatego więc musimy zrobić do samodzielnie. Wykorzystamy pamięć operacyjną płytki Arduino, czyli stany portów będziemy zapisywać w zmiennych. Może do tego służyć zmienna typu logicznego bool. Przechowuje ona tylko dwie wartości: 0, czyli fałsz (false), oraz liczbę różną od zera, czyli prawdę (true). Pewnie nie ma nic złego w utworzeniu pięciu takich zmiennych dla naszych pięciu LED-ów, ale gdy podłączymy ich 20? 30? Dlatego ponownie używamy tablice:
bool stan[]={false,false,false,false,false}; void loop(){ if(Serial.available()>0){ c=Serial.read(); int idx=c-'a'; if(idx>=0&&idx<5)//gdy mamy 5 LEDow if(stan[idx]==true){ digitalWrite(piny[idx],HIGH); stan[idx]=false; } else{ digitalWrite(piny[idx],LOW); stan[idx]=true; } else//jesli idx rozny od zera to moze... nasza pauza? if(c=='#') delay(200);
Na początku (linia #1) wprowadzamy zmienną tablicową stan z informacją o wyłączeniu wszystkich LED-ów (pięć false-ów). Kluczowe są linie #7-14, gdzie sprawdzamy stan portu i jeśli jest on włączony (wartość true) to wyłączamy LED-a i zmieniamy stan na false (linia #9), a gdy jest wyłączony (wartość false – u nas „w przeciwnym przypadku” linia #11) to włączamy LED-a i także zmieniamy stan portu – tym razem na true (linia #13).
Program ponownie akceptuje całe sekwencje rozkazów („zdania”) a dodatkowo nie potrzebuje już oddzielnej literki dla włączenia (poprzednio mała literka) i wyłączenia (poprzednio duża literka) LED-a. Piszemy więc zdania typu abcde######edcba##a##a##b##b##c##c##b##b i obserwujemy płytkę stykową.
Podsumowanie
Wszystko dało się tak prosto zapisać dzięki tablicom oraz sprytnie wyliczonemu indeksowi tablicy (kodowanie ASCII). Jak widać indeks wykorzystaliśmy dwukrotnie – raz w odniesieniu do włączania/wyłączania LED-a (funkcja digitalWrite i LEDy podłączone do portów zapisanych w tablicy piny) a drugi raz do zapisu stanu portu cyffrowego Arduino (tablica stan). Siłę tego programy docenimy wówczas, gdy podłączymy dużo LEDów (np. 20) i jedyne rozbudowanie tego kodu polegać będzie na… zmienie stałej MAX w pierwszej linii programu – a nie dopisywaniu prawie identycznych linii kodu dla każdego nowego LED-a (i jego stanu!).
Proszę przemyśleć dzisiejszą lekcję, bo za tydzień ponownie spotykamy się z tablicami (no i z wyświetlaczem siedmiosegmentowym).
(c) Ewelina & KG