Tablice i pętle programowe. Nadajnik Morse’a – cz. 3

Cześć! W poprzedniej części kursu udało nam się wreszcie napisać kompletny „słownik” umożliwiający zamianę znaków alfabetu łacińskiego na kod Morse’a. Ostatnim elementem, którego brakuje w naszym projekcie jest możliwość wygodnego kodowania dłuższych wiadomości. W realizacji tego zadania pomogą nam tablice i pętle programowe, które są niesamowicie użytecznymi i interesującymi elementami języka C. Zanim jednak się z nimi zapoznamy, wspomnimy jeszcze krótko o nowym typie danych.

Typ danych String

Dłuższe teksty w języku C oraz Arduino reprezentowane są przez zmienne typu String (ang. ciąg/łańcuch znaków). Zmienne typu String są wykorzystywane najczęściej do przesyłania i wyświetlania komunikatów, które są skierowane do użytkownika naszego programu, pełnią więc bardzo istotną funkcję. Z danymi tego typu spotkaliśmy się już w poprzednich częściach kursu, kiedy wyświetlaliśmy teksty w monitorze portu szeregowego. Stosowaliśmy wtedy tekst zapisany pomiędzy znakami cudzysłowu. Nieświadomie używałeś wtedy zmiennej typu String, pewnie już domyślasz się więc, że deklaracja zmiennej tego typu może wyglądać następująco:

String helloWorld = "Hello world!";

String, czy string???

W Arduino możesz napotkać zmienne String pisane z wielkiej jak i z małej litery (String oraz string). Jest to dosyć skomplikowane zagadnienie, które może być mocno dezorientujące. Spróbuję jednak wytłumaczyć Ci chociaż pobieżnie co jest powodem tej różnorodności, jaka jest różnica pomiędzy tymi dwoma typami danych i z którego będziemy korzystać. Pewnie zauważyłeś już, że wszystkie nazwy podstawowych typów danych w C są zapisywane z małej litery  (int, float, double itp.). Nie inaczej jest z typem string, który w podstawowej wersji z języka C zapisywany jest z małej litery.

Arduino dodaje jednak do języka C typ danych String. Tworząc zmienną typu String tworzymy tak naprawdę obiekt. O tym czym są w programowaniu obiekty, jak się nimi posługiwać i czemu są podstawą nowoczesnego programowania dowiemy się w jednej z przyszłych części kursu, ponieważ jest to bardzo obszerny temat. Wiedz tylko, że obiekt może zawierać charakterystyczne dla siebie różne użyteczne funkcje, które mogą na przykład modyfikować przechowywane w nim dane. W naszym przykładzie będzie to funkcja, która zamienia wszystkie znaki w obiekcie typu String na małe litery. Te wszystkie bonusy dostajemy jednak kosztem większego zużycia pamięci programu. Będziemy musieli o tym pamiętać, jeżeli znajdziemy się w potrzebie optymalizacji naszego kodu.

Zagadnienia optymalizacyjne i różne problemy dotyczące wykorzystania obiektów typu String są źródłem licznych kontrowersji. Z tego względu bardziej doświadczeni użytkownicy Arduino odradzają wykorzystanie obiektów typu String w programach. My jednak zaryzykujemy i skorzystamy z oferowanych przez Arduino udogodnień, w przyszłości spróbujemy jednak uniknąć korzystania z nich i zadbamy o optymalizację i stabilność naszego kodu. 😉

Zmienne typu String a tablice

W uproszczeniu możemy powiedzieć, że zmienne typu String są rozumiane przez Arduino, jako tablica zmiennych typu char. Umożliwia to łatwe odwoływanie się do poszczególnych liter łańcucha tekstowego. Ale czym właściwie jest tablica w programowaniu?

Tablice

Pamiętasz być może, jak porównałem wartości zmienne do szufladek. Trzymając się tej analogii, tablice danych możesz wyobrazić sobie jako regał, składający się z wielu ponumerowanych szufladek rozmieszczonych jedna nad drugą. Szufladki te będziemy nazywać komórkami tablicy. Tablica (z ang. array) jest strukturą danych, która umożliwia gromadzenie wielu zmiennych tego samego typu w jednym miejscu. Tablice, w szczególności w połączeniu z pętlami programowymi, umożliwiają prostą, zwięzłą i czytelną inicjalizację dużych ilości danych oraz późniejsze wygodne operowanie na nich.

Tworzenie tablic

Poniżej znajdziesz kilka sposobów tworzenia tablic:

 int liczbyCalkowite[10]; //deklaracja bez przypisania wartosci
 int numeryPortow[] = {2, 3, 7}; //inicjalizacja bez podania rozmiaru tablicy, kompilator sam policzy elementy i utworzy tablice o odpowiedniej wielkosci
 float wartosciCzujnikow[6] = {2.1, 3.4, -0.8, 3.0, 5.2}; //inicjalizacja czesci elementow tablicy
 char message[6] = "hello"; //inicjalizujac tablice wartosci typu char musimy dodac miejsce na znak 0 oznaczajacy koniec lancucha tekstowego

Pierwszy przykład pokazuje deklarację tablicy liczb całkowitych (int) o dziesięciu elementach. Możesz zauważyć, że zapis ten jest całkiem podobny do deklaracji pojedynczej zmiennej typu całkowitego. Jedynym elementem kodu jaki musimy dodać, są nawiasy kwadratowe, między którymi podajemy liczbę całkowitą większą od zera, która określa rozmiar tablicy (ilość szufladek w regale).

Drugi przykład pokazuje inicjalizację tablicy trzech elementów typu int.  Jak widzisz, przypisanie wartości następuje przez podanie nawiasów klamrowych i wypisanie elementów tablicy oddzielonych przecinkiem. Jeśli w momencie deklaracji tablicy przypisujemy jej wszystkie elementy, nie musimy podawać rozmiaru tablicy w nawiasach kwadratowych. Bierz jednak pod uwagę, że nie będzie można później zwiększyć rozmiaru tej tablicy w prosty sposób.

Przykład trzeci pokazuje deklarację tablicy sześcioelementowej wraz z inicjalizacją części z jej wartości. Przy takiej operacji należy zachować szczególną uwagę, bo nie wiadomo co znajdzie się w ostatniej komórce tabeli póki jej prawidłowo nie zapełnimy danymi (najczęściej będzie to jednak wartość 0).

Przykład czwarty jest nieco zawiły. Ma on zaprezentować jak kompilator rozumie wartości typu String. Jeżeli chcemy wprost zapisać wartość String jako tablicę wartości typu char, musi ona zawierać o jeden element więcej niż ilość znaków. Jest to związane z tym, że obiekty typu String są zakończone domyślnie znakiem null (’\0′) który oznacza symbolicznie koniec łańcucha tekstowego. Znak ten umożliwia różnym komendom, takim jak Serial.print() wykryć koniec przesyłanego łańcucha tekstowego.

Korzystanie z tablic

Wspominałem już, że tablice są jak regały z ponumerowanymi kolejno szufladkami. Numery szufladek zwane są indeksami i umożliwiają proste odnoszenie się do poszczególnych komórek tablicy. W tym momencie muszę Cię jednak ostrzec, ponieważ znajduje się tu jedna z pułapek, która potrafi napsuć krwi początkującym programistom. Otóż,  Komórki tablicy indeksowane są od ZERA (0)!!! . Jeśli będziesz próbował utworzyć tablicę o rozmiarze 10, a następnie spróbujesz odwołać się do indeksu o numerze 10, mogą stać się dwie rzeczy. Pierwszą z nich jest odczyt danych z następnej komórki pamięci znajdującej się za ostatnim elementem tablicy. Możesz wtedy otrzymać totalnie losową wartość, która doprowadzi do nieprawidłowego działania programu. W jeszcze gorszym przypadku nastąpi tzw. „crash„, czyli nieoczekiwane zakończenie działania programu. Oba typy błędów są bardzo trudne do wyśledzenia i dostrzeżenia, nie są też zazwyczaj komunikowane jako błąd przez środowisko programistyczne w momencie kompilacji. Dlatego powtórzę jeszcze raz, żebyś dobrze zapamiętał lub zapamiętała:

 Komórki tablicy indeksowane są od ZERA (0)!!! 

Dobrze, czas wreszcie dowiedzieć się jak zapisywać i odczytywać wartości w poszczególnych komórkach tablicy. 😉

Zapisywanie wartości

Następne przykłady będą korzystały z tablicy wartosciCzujnikow zainicjalizowanej w przykładach omawianych przeze mnie wcześniej. Aby zmodyfikować wartość komórki tabeli wartosciCzujnikow o indeksie 0 wystarczy, że napiszemy:

wartosciCzujnikow[0] = 36.7;

Jak widzisz, wystarczy podać nazwę tablicy oraz numer indeksu w nawiasach kwadratowych, a następnie użyć operatora przypisania do zapisania wartości (w tym przypadku 36.7).

Odczytywanie wartości

W celu odczytania wartości z komórki tabeli musimy również posłużyć się nawiasami kwadratowymi:

float wartosc = wartosciCzujnikow[3];

Pętle programowe

Wreszcie mamy za sobą kawał materiału, który miał rolę wprowadzenia do głównych gwiazd dzisiejszej części kursu – pętli programowych. Najprościej ujmując, pętle służą do wielokrotnego wykonywania tego samego zestawu instrukcji. Są one bardzo ważnym elementem języków programowania, ponieważ umożliwiają wykonywanie złożonych zadań, składających się z powtarzalnych czynności za pomocą krótkiego, zwięzłego kodu. Istnieje kilka typów instrukcji, umożliwiających pisanie pętli programowej. Ich wspólną cechą jest to, że wykonują one zestaw instrukcji, dopóki pewien warunek logiczny jest spełniony. Słowem kluczowym jest tutaj słowo „dopóki”, które wprowadza nas do pierwszego typu pętli.

while

Pierwszym typem pętli z którym się zapoznamy, jest while. Jej nazwa oznacza właśnie „dopóki”. Przyjrzyjmy się jej składni:

while (warunek) {
  // instrukcje
}

Jak widzisz, składnia tej instrukcji jest bardzo prosta. Za słowem kluczowym while podajemy w nawiasach okrągłych warunek logiczny, który jest sprawdzany PRZED każdym wykonaniem zestawu instrukcji zawartych w nawiasach klamrowych. Dodam, że instrukcje zawarte w nawiasach klamrowych nazywane są też ciałem pętli (analogicznie do ciała funkcji). Zwróć uwagę, że instrukcje zawarte w ciele pętli muszą w jakiś sposób modyfikować wartość zmiennej, od której zależy warunek logiczny, inaczej stworzymy tak zwaną pętlę nieskończoną. Poniżej znajdziesz przykład wypisywania wartości od 1 do 100 poprzez port szeregowy za pomocą pętli while.

int zmienna = 1;
while (zmienna <= 100) {
  Serial.println(zmienna);
  zmienna++;
}

Zauważ, że gdybyś chciał napisać taki kod ręcznie, zająłby on 200 linii kodu. Powodzenia w szukaniu błędów w takim kodzie! 😉

do..while

Pętla do..while (z ang. wykonuj dopóki) swoją konstrukcją i działaniem bardzo przypomina poprzednio poznaną pętlę. Różnica polega na tym, że warunek logiczny sprawdzany jest PO wykonaniu instrukcji zawartych w ciele pętli. Oznacza to, że ciało pętli zawsze zostanie wykonane co najmniej jeden raz. Jej składnia prezentuje się następująco:

do {
  // instrukcje
} while (warunek);

Jest ona nieznacznie bardziej skomplikowana, po słowie kluczowym do następują klamry, w których zapisujemy instrukcje wykonywane przez pętle. Za nawiasem klamrowym podajemy słowo kluczowe while, a po nim w nawiasach okrągłych podajemy warunek logiczny sprawdzany po wykonaniu instrukcji. Ten sam przykład wypisywania liczb od 1 do 100 zapisany za pomocą pętli do..while wygląda następująco:

int zmienna = 1;
do {
 Serial.println(zmienna);
 zmienna++;
} while (zmienna <= 100);

Zwróć uwagę na konieczność dodania średnika za nawiasem okrągłym obejmującym sprawdzany warunek logiczny.

for

Ostatnim, ale chyba najciekawszym typem pętli jest for (z ang. dla, na przykład „dla każdego z…”). Jej składnia jest najbardziej skomplikowana, jednak wciąż dosyć łatwo można się z nią oswoić. Daje też ona zarazem największe możliwości programistom. Szybko staje się też ona ulubioną pętlą programistów, którzy zwykle nadużywają jej, choć często ten sam kod można zapisać zwięźlej za pomocą poprzednich typów pętli. Składnię pętli for znajdziesz poniżej:

for (inicjalizacja; warunek; inkrementacja) {
  // instrukcje
}

Nie przejmuj się, jeśli czujesz się zdezorientowany patrząc na to, wyjaśnimy działanie tej instrukcji na przykładzie identycznym jak w poprzednich przypadkach.

for (int zmienna = 1; zmienna <= 100; zmienna++) {
  Serial.println(zmienna);
}

Po słowie kluczowym for umieszczone są okrągłe nawiasy, w środku których umieszczane są trzy typy instrukcji oddzielane średnikami. Pierwszą instrukcją jest inicjalizacja zmiennej, która może być wykorzystywana przez instrukcje wykonywane w ciele pętli. Inicjalizacja ta dokonywana jest tylko raz, przy każdym początku wykonywania pętli. Następną instrukcją jest warunek logiczny, od którego zależy, czy zostanie wykonane ciało funkcji. Jeżeli jest on spełniony, to wykonywane są instrukcje zawarte w nawiasach klamrowych. Finalnie wykonywana jest instrukcja będąca trzecim składnikiem nawiasów okrągłych, czyli inkrementacja. Powoduje ona zmianę wartości zmiennej sterującej ilością wykonań pętli. Zazwyczaj spotkasz się tutaj właśnie z operatorem inkrementacji (++), ale w zależności od potrzeb możesz tu wykonać dowolną inną operację arytmetyczną.

W następnej części kursu znajdziesz więcej przykładów i kilka wyzwań – zadań, które będziesz musiał wykonać z pomocą pętli i tablic. Pomogą Ci one zrozumieć i osiągnąć mistrzostwo w wykorzystywaniu pętli programowych! 😉 Póki co przejdźmy jednak do naszego projektu nadajnika kodu Morse’a.

Konwersja wiadomości

W poprzedniej części kursu zaprogramowaliśmy słownik tłumaczący litery i cyfry na kod Morse’a. Kod programu, od którego dzisiaj zaczniemy znajdziesz tutaj. Przypomnę, że naszym zadaniem na dziś jest dopisanie funkcji, która będzie przyjmowała argument typu String. Argument ten będzie wiadomością, którą będziemy chcieli zakodować. Funkcja ta jest dość skomplikowana i zawiera wiele nowych elementów kodu, dlatego poniżej znajdziesz jej całą treść, którą wyjaśnię linijka po linijce. Kompletny kod projektu dostępny jest standardowo w repozytorium kodu.

void convertStringToMorseCode(String message) {
  message.toLowerCase();
  for (int i = 0; i < message.length(); i++) {
    char characterToConvert = message[i];
    convertToMorseCode(characterToConvert);
  }
  turnOff(messagePause);
}

W pierwszej linii ciała funkcji, dokonywana jest konwersja kodowanej wiadomości. Jest to wspomniane już przeze mnie wcześniej wykorzystanie funkcji „wbudowanej” w typ String. Funkcja ta, wywoływana jest „na obiekcie” typu String. Oznacza to w tym przypadku, że modyfikuje ona jego wartość nie zwracając żadnej wartości. Nie musisz na razie dokładnie rozumieć tego zagadnienia. Wiedz jednak, że istnieją takie użyteczne funkcje jak toLowerCase() oraz toUpperCase(), która z kolei zamienia wszystkie litery na ich wielkie odpowiedniki.

W kolejnej linii widzimy wykorzystanie pętli for, która odczytuje kolejne znaki kodowanej wiadomości. Zwróć uwagę na fragment message.length(). Domyślasz się zapewne, że to kolejna użyteczna funkcja charakterystyczna dla obiektów typu String. Z jej pomocą możesz sprawdzić ile znaków zawiera tekst zapisany w zmiennej. Jest to bardzo użyteczne właśnie przy tworzeniu warunków kontrolujących ilość wykonań pętli.

Wewnątrz pętli for, pobierane są kolejne znaki wiadomości, które przechowywane są w zmiennej typu char. Zwróć uwagę na wykorzystanie nawiasów kwadratowych oraz zmiennej inicjalizowanej w pętli for do „wyjmowania” wartości z kolejnych szufladek tablicy. Kolejne litery wykorzystujemy jako argument w wywołaniu napisanej przez nas poprzednim odcinku funkcji dokonującej zamiany na kod Morse’a.

Możliwości rozszerzenia projektu

Na tym zakończymy wspólną pracę nad naszym projektem nadajnika kodu Morse’a. Przypomnę, że całość kodu projektu znajdziesz tutaj. Pomyśl nad tym w jaki sposób mógłbyś rozszerzyć funkcjonalność projektu. Dobrym pierwszym krokiem, który powinieneś już być w stanie samemu wykonać byłoby dodanie wysyłania wiadomości do zakodowania przez UART. Podpowiedzi jak to zrobić znajdziesz w części kursu dotyczącej portu szeregowego. Zdradzę Ci również, że w jednej z następnych części kursu będziemy pracowali nad obsługą zapytań sieciowych, dzięki którym moglibyśmy zrobić prosty komunikator!

W prosty sposób możesz również udoskonalić projekt podłączając brzęczyk piezo (z wbudowanym generatorem). Plus brzęczyka podłącz do wyjścia zasilania 3.3V na płytce, natomiast minus podłącz do wyjścia oznaczonego D4. Zobaczysz wtedy, że Twój nadajnik będzie jednocześnie mrygał diodą i nadawał sygnały dźwiękowe brzęczykiem.

Zastanów się też do czego mógłbyś zastosować taki układ. Może razem z brzęczykiem zamkniesz go w małej obudowie, i po podłączeniu do powerbanka, będziesz miał awaryjny sygnalizator sygnału SOS? Mam nadzieję, że nigdy Ci się on jednak nie przyda. Możesz też wykorzystać nadajnik do bezdźwięcznej komunikacji na odległość, samodzielnie wymyśl w jakich sytuacjach mogłoby Ci się takie urządzenie przydać. 😉

Rozwiązywanie problemów

W dzisiejszej części kursu zapoznaliśmy się z tablicami i pętlami programowymi. Żeby uniknąć błędów w działaniu programu, musisz zachować szczególną ostrożność i pamiętać o tym, że tablice są indeksowane od zera. Jeżeli się pomylisz, bardzo łatwo doprowadzić do przekroczenia zakresu indeksów tablicy co spowoduje błędy w logice programu. Uważaj też, aby nie utworzyć przypadkiem pętli nieskończonej przez brak modyfikowania zmiennej, od której zależna jest ilość wywołań pętli.

Podsumowanie

Uff, wreszcie dotarliśmy do końca naszego pierwszego wspólnego projektu! 🙂 Po drodze nauczyliśmy się wielu nowych rzeczy, poznaliśmy liczne instrukcje sterujące, tablice i nowe typy danych. Te nowe elementy języka C, pozwolą stworzyć Ci mnóstwo fascynujących projektów. Aby jednak w pełni wykorzystać możliwości naszego mikrokontrolera, wypadałoby się z nim zapoznać nieco dokładniej oraz nauczyć się korzystać z jego wejść i wyjść. W następnej części kursu zajmiemy się nieco dokładniejszym zapoznaniem z pętlami programowymi.Tak jak wspominałem, przedstawię nieco więcej, bardziej skomplikowanych przykładów ich wykorzystania wraz z zadaniami do samodzielnego wykonania. Dowiemy się też jak można dokładniej kontrolować działanie pętli. W dalszej kolejności zajmiemy się nauką obsługi wejść i wyjść i zaczniemy pracować nad projektem zdalnie sterowanej lampki! 🙂 Do zobaczenia przy kolejnych ekscytujących przygodach z kodem! 🙂

Podziel się stroną ze znajomymi! :)

Dodaj komentarz