Sterowanie ledami za pomocą klawiatury – ASCII i (znowu) tablice

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