Typy danych w Arduino. Rzutowanie typów.

Typy danych w Arduino. Rzutowanie typów.

Cześć! Witam Cię w kolejnym lekko wakacyjnym wpisie. Dziś zajmiemy się omówieniem tematu, który już częściowo omówiliśmy we wcześniejszych wpisach, nigdy jednak nie zagłębiliśmy się w szczegóły. Mianowicie, dziś poznamy dokładnie typy danych w Arduino. Pokażę Ci też w jaki sposób można wykonać tak zwane rzutowanie, czyli konwersję typów danych. Wiedza o typach danych pomoże Ci pisać lepiej zoptymalizowany kod. Dzięki niej będziesz w stanie zaoszczędzić pamięć programu oraz zapewnić szybsze jego działanie. Nie przedłużając więc wstępu przejdźmy od razu do omawiania typów danych dostępnych w Arduino.

Typy danych liczbowych w Arduino

Wiesz zapewne, że komputery operują na zerach i jedynkach. Nic więc dziwnego, że programując najczęściej spotkasz się z różnymi typami danych liczbowych. Dane liczbowe najczęściej dzielimy na typy całkowitoliczbowe oraz zmiennoprzecinkowe.

Dane całkowitoliczbowe

Najpopularniejszym i zarazem najistotniejszym typem danych w programowaniu są dane całkowitoliczbowe, które służą oczywiście do wykonywania operacji na liczbach całkowitych. Musisz wiedzieć, że w programowaniu mikrokontrolerów dane całkowitoliczbowe mają szczególne znaczenie. Wynika to z tego, że operacje na nich są wykonywane przez procesor znacznie szybciej niż w przypadku danych zmiennoprzecinkowych. W przypadku prostych programów tego typu różnica prędkości może nie mieć znaczenia, lecz w przypadku operacji na wielkich zbiorach danych lub programach, które muszą działać jak najszybciej, na przykład odczyty czujników w pojazdach, może to być jedna z najbardziej podstawowych metod optymalizacji.  Możesz jednak zapytać się dlaczego potrzebujemy wielu różnych typów danych reprezentujących ten sam rodzaj informacji. Odpowiedź jest dosyć prosta – aby umożliwić optymalizację wykorzystania pamięci mikrokontrolera. Poszczególne typy danych umożliwiają przechowywanie różnych zakresów wartości liczbowych, a co za tym idzie wymagają zajęcia różnej ilości pamięci programu. Poniżej znajdziesz opis podstawowych typów danych całkowitoliczbowych w Arduino.

byte

Najmniejszym typem danych całkowitoliczbowych jest byte, który umożliwia przechowywanie ośmiobitowej wartości liczbowej bez znaku, czyli wartości od 0 do 255. Ten typ danych przyda Ci się na przykład przy oprogramowywaniu transmisji za pomocą małych ramek danych. W zmiennych tego typu warto też zapisywać numery portów mikrokontrolera.

word

Nieco rzadziej spotykanym typem danych jest word, który jest bardziej pojemną wersją typu byte. W zmiennej tego typu możesz zapisać 16-bitową wartość od 0 do 65535. Każda zmienna typu word zajmuje więc 2 bajty pamięci.

short

Typ short, podobnie jak word przechowuje 16-bitową wartość liczbową, jednak jeden bit poświęcony jest na znak. Dzięki temu w zmiennej tego typu możesz zapisać wartości od -32768 do 32767.

int

Dotarliśmy do najpopularniejszego typu danych całkowitoliczbowych. int – od angielskiego integer, czyli całkowity, jest podstawowym typem danych liczbowych. Wiąże się z nim niestety również najwięcej wyjątków. W zależności od platformy sprzętowej, zmienna typu int może przechowywać liczby z różnego zakresu. Kiedy korzystamy z płytki Arduino Uno lub innych wykorzystujących mikrokontroler ATMega, zmienna ta zajmuje 2 bajty pamięci, czyli można na niej zapisać 16 bitowe wartości od -32768 do 32767. Jeżeli ten sam program skompilujesz na płytkę Arduino Due, Zero lub MKR1000, każda zmienna typu int zajmie 4 bajty pamięci i umożliwi przechowanie 32 bitowej wartości od -2,147,483,648 do 2,147,483,647.

W przypadku typu int, dokumentacja Arduino informuje również, że liczby ujemne są uzyskiwane w wyniku kodu uzupełnienia do dwóch, o którym możesz poczytać tutaj. Powoduje to pewne komplikacje przy wykorzystywaniu operacji przesunięcia bitowego w prawo, jednak ze względu na to, że nie omawiałem jeszcze operacji bitowych, pominiemy to zagadnienie. Powoduje to też możliwe występowanie nieprzewidzianych zachowań w przypadku przekroczenia zakresu, powinno się więc unikać wykorzystywania tego zjawiska jako sposobu realizacji algorytmów.

long

Zmienne typu long umożliwiają przechowywanie 4 bajtów danych, czyli zapis wartości od -2,147,483,648 do 2,147,483,647. Wartości typu long wymagają dodania litery L po wartości (przykład poniżej), co informuje kompilator, że ma traktować wartość jako typ long, a nie typ int. Pamiętaj o tym szczególnie w sytuacji, gdzie zapisujesz działanie z użyciem wartości liczbowych, a nie zmiennych! Unikniesz godzin spędzonych na poszukiwanie błędu.

unsigned int

Jest to zmienna typu int, bez użycia bitu znaku (z ang. unsigned – bez znaku). Umożliwia zapis wyłącznie dodatnich wartości – na płytce Arduino Uno z zakresu od 0 do 65535, na płytkach Due, Zero, MKR1000 od 0 do 4,294,967,295. W przypadku zmiennych bez znaku pojawiają się jednak dwie istotne różnice. Pierwszą z nich jest zachowanie w przypadku przekroczenia zakresu – jeżeli wartość zmiennej unsigned przekroczy wartość maksymalną, to przyjmie wartość 0. Jest to przydatne w przypadku tworzenia liczników. Zmienne unsigned są również użyteczne, jeśli oczekujesz operacji na wartościach nieco większych niż zakres zmiennej ze znakiem.

Musisz jednak zachować szczególną ostrożność wykorzystując zmienne bez znaku w działaniach matematycznych. Główną zasadą, którą kieruje się mikrokontroler przy wykonywaniu obliczeń, jest wykonywanie operacji zgodnych z typem zmiennej wynikowej. Oznacza to, że jeśli zmienna wynikowa jest typu int, to nawet jeśli będziemy wykonywali działanie na dwóch zmiennych typu unsigned int, zostaną one przeprowadzone według zasad zgodnych z typem int. Jeżeli jednak działanie będzie bardziej złożone i będzie wymagało obliczania wyników pośrednich, nie możemy mieć gwarancji jaka logika zostanie zastosowana. Obrazuje to poniższy przykład:

unsigned long

Zmienna tego typu działa analogicznie do unsigned int. Umożliwia przechowywanie wartości od 0 do 4,294,967,295. Pozwolę sobie tutaj na małą ciekawostkę. Może Ci się wydawać, że maksymalna wartość zmiennej typu unsigned long jest bardzo duża. Pisząc akapit o zmiennej long zastanawiałem się jaka liczba może posłużyć jako przykład wartości tego typu. Pierwszą wartością o jakiej pomyślałem jest dług publiczny Polski, okazało się jednak, że wartość ta (1 000 000 000 000 zł) przekracza maksymalną wartość zmiennej typu unsigned long ponad 2000 razy. Smutne, ale prawdziwe. Nie przejmuj się jednak, operacje na tak dużych liczbach nie są niemożliwe w Arduino, kiedyś na pewno się tym zajmiemy! 😉

Analogicznie do zmiennych typu long, wartości typu unsigned long wymagają dodania liter UL po wartości (przykład poniżej):

Dane zmiennoprzecinkowe

Dane zmiennoprzecinkowe (z ang. floating point data) umożliwiają w dużym uproszczeniu przechowywanie wartości całkowitych razem z częścią ułamkową. W praktyce, różnica pomiędzy tym jak mikroprocesor pracuje z typami całkowitoliczbowymi i zmiennoprzecinkowymi jest bardzo duża. Temat ten jest dosyć złożony i zasługiwałby na oddzielny wpis, jednak możesz poczytać o tym jak komputery obliczają wartości tego typu na Wikipedii. Zapamiętaj tylko, że operacje arytmetyczne na wartościach zmiennoprzecinkowych są znacznie wolniejsze niż na liczbach całkowitych. Wiedz też, że ze względu na sposób wyznaczania reprezentacje wartości zmiennoprzecinkowych są przybliżone – pojedyncza wartość tego typu może reprezentować pewien zakres wartości rzeczywistych wokół tej liczby. Z tych względów czasami sprowadza się operacje na wartościach zmiennoprzecinkowych do działania na liczbach całkowitych.

Pomimo tych wszystkich wad, wartości zmiennoprzecinkowe są bardzo często wykorzystywane do reprezentowania wartości analogowych, na przykład odczytów z czujników obrazujących wielkości fizyczne.

float

Podstawowym typem danych zmiennoprzecinkowych w Arduino jest float. Umożliwia on przechowywanie 32-bitowych wartości od -3.4028235E+38 do 3.4028235E+38. Oznacza to również, że każda zmienna typu float zajmuje 4 bajty pamięci programu.

Nieco ciekawsze są właściwości zmiennych typu float jeśli chodzi o operacje matematyczne. Pierwszą informacją, którą musisz zapamiętać jest fakt, że przy próbie przypisania wartości typu float do zmiennej typu całkowitoliczbowego część dziesiętna zostanie bez żadnego ostrzeżenia zaokrąglona do zera. Skutkuje to również problemami z dzieleniem – zawsze używaj wtedy wartości typu float. Możesz to zapewnić przez dopisanie wprost części dziesiętnej, nawet jeśli wynosi ona zero (np. 1.0), lub poprzez tzw. rzutowanie typów, o czym opowiem później.

Z niedokładności wartości typu float wynikają też problemy z bezpośrednim porównywaniem wartości. Przykładowo wynik dzielenia 6.0 przez 3.0 niekoniecznie musi być równy 2.0. Dlatego zamiast operatora równoważności == lepiej zastosować porównanie wartości bezwzględnej różnicy dwóch wartości z pewną małą wartością stałą. Dokumentacja Arduino stwierdza, że dokładność zmiennych typu float wynosi 6-7 cyfr. Dotyczy to wszystkich cyfr liczby, nie tylko tych po przecinku.

double

Dane typu double w C umożliwiają operowanie na wartościach niecałkowitych o dwukrotnie większej precyzji niż przy danych typu float. Oznacza to, że za pomocą zmiennych double możemy operować na liczbach do 15-16 cyfr. Oprócz zwiększonej precyzji zmienne typu double mają też znacznie szerszy zakres. W przypadku Arduino musisz jednak wiedzieć, że stosowanie typu double ma sens jedynie przy wykorzystaniu płytek Arduino Due i innych nowszych. Zmienna double przechowuje wtedy 64 bity danych. W przypadku Arduino Uno zmienna typu double ma taką samą dokładność jak float.

Pozostałe typy danych

bool

Zmienne bool umożliwiają przechowywanie wartości logicznych true (prawda) lub false (fałsz). Musisz wiedzieć, że wartość true rozumiana jest przez procesor jako 1, a false jako 0. Jest to często wykorzystywana własność zmiennych typu bool.

boolean

Typ danych boolean jest niestandardowym typem danych oferowanym przez Arduino. Dokumentacja zaleca stosowanie tego typu zamiast bool, jednocześnie stwierdzając, że jego działanie jest identyczne. Jedną z istotnych różnic jest fakt, że środowisko programistyczne prawidłowo „pokoloruje” użycie składni boolean, pomijając za to użycie słowa kluczowego bool.

char

O zmiennych typu char mówiliśmy sporo przy okazji pracy nad nadajnikiem kodu Morse’a. Umożliwiają one przechowywanie znaków tekstowych, które umieszczane są pomiędzy apostrofami (np. ‚A’). Znaki te przechowywane są jako jednobajtowe wartości liczbowe z zakresu od 0 do 255. Tablica kodów ASCII pozwoli Ci sprawdzić jaka wartość kodu odpowiada konkretnym znakom. Teoretycznie umożliwia to wykonywanie operacji arytmetycznych z wykorzystaniem znaków, jest to jednak odradzane. Zdecydowanie czytelniej i bezpieczniej będzie jeśli użyjesz zmiennej typu byte, która umożliwi zapis tego samego zakresu wartości.

unsigned char

Podobnie jak char, przechowuje jednobajtowe zmienne znakowe, są one jednak reprezentowane jako liczby bez znaku. Jeśli naprawdę uważnie prześledzisz wpisy o nadajniku kodu Morse’a znajdziesz tam przykład w którym ten typ danych może być przydatny. Umożliwi Ci on dostęp do tak zwanej rozszerzonej tabeli kodów, w której znajdują się na przykład kody polskich znaków diakrytycznych (np. ą, ę). Musisz jednak wiedzieć, że w przeciwieństwie do podstawowych kodów ASCII, kody rozszerzone mogą mieć różne znaczenie w zależności od wykorzystywanego przez system kodowania. Oznacza to, że korzystanie z tego może być bardzo problematyczne.

string

Dotarliśmy do najbardziej złożonego typu danych dostępnego w języku C i Arduino! Zmienne typu string (z ang. łańcuch) przechowują łańcuchy tekstowe. Przydadzą Ci się one do przechowywania skomplikowanych danych w formie przystosowanej do prezentacji użytkownikowi. Łańcuchy tekstowe są reprezentowane jako tablice znaków (czyli typ char[]) zakończonych symbolem NULL (‚\0’). Oznacza to, że jeśli chcesz utworzyć tablicę znaków char, aby wykorzystywać ją jako typ string, musisz pamiętać o utworzeniu tablicy większej o jeden znak niż Twój tekst. Znak ten umożliwia funkcjom takim jak Serial.print() wykryć koniec przesyłanej wiadomości. Istnieje wiele sposobów inicjalizacji zmiennych typu string, znajdziesz je poniżej:

Najczęściej spotkasz jednak sposób inicjalizacji podany w czwartym przykładzie, z wykorzystaniem cudzysłowów informujących kompilator o wykorzystywaniu zmiennej typu string.

Jeśli będziesz musiał wykorzystać bardzo długi napis umieszczony w kodzie programu, możesz podzielić go na mniejsze części, jak na przykładzie poniżej. Zwiększy to czytelność Twojego kodu. Zwróć uwagę, że średnik jest wtedy dopisany dopiero na końcu ostatniej linijki zawierającej łańcuch tekstowy!

String()

Typ String, jest obiektowym typem danych wprowadzonym w języku Arduino. O programowaniu obiektowym opowiem wkrótce, wyjaśnię Ci wtedy masę trudnych słów takich jak „instancja”, „referencja” i tak dalej. 😉 Na tę chwilę wystarczy, że będziesz wiedział, że typ ten jest rozbudowaną wersją typu podstawowego i oferuje różne dodatkowe funkcje ułatwiające operacje z wykorzystaniem łańcuchów tekstowych. O typach string i String możesz również poczytać na początku tego wpisu.

void

Typ void (z ang. pusty/próżny) wykorzystywany jest w deklaracjach funkcji. Oznacza on, że funkcja nie zwraca żadnej wartości.

size_t

Enigmatycznie wyglądający typ size_t służy do prezentowania rozmiaru obiektu wyrażonego w bajtach. Jest to typ danych zwracany na przykład przez operacje Serial.print(), która zwraca ilość bajtów zapisanych przez port szeregowy. W Arduino możesz również użyć funkcji sizeof(), podając jako argument dowolną zmienną. Funkcja ta zwróci ilość bajtów jakie zajmuje dana zmienna lub tablica danych.

Rzutowanie (konwersja) typów danych

Wszystkie podstawowe typy danych liczbowych w Arduino możesz poddać operacji tak zwanego rzutowania, czyli inaczej konwersji. Przykładowo zmienną typu int możesz zamienić w zmienną typu float. Wykonuje się to poprzez umieszczenie nazwy pożądanego typu w nawiasach okrągłych przed nazwą zmiennej. Przykład rzutowania zmiennej typu int na typ float znajdziesz poniżej:

Możesz się spotkać również z zapisem rzutowania w następujący sposób:

Jest to jednak mniej uniwersalny zapis i będę go raczej unikał. Pamiętaj również, że wykonując rzutowanie typu na mniej pojemny możemy przekroczyć zakres zmiennej lub pozbawić się informacji o części dziesiętnej liczby. Może to doprowadzić do ciężkich do wyłapania błędów.

Podsumowanie

Znowu udało nam się dotrzeć do końca! 😉 Muszę przyznać, że nie było lekko, myślałem, że ten wpis będzie znacznie krótszy i prostszy. Niestety typy danych mają swoje wyjątki oraz pułapki. Warto być ich świadomym, żeby uniknąć bardzo trudnych do zdebugowania błędów. Mam nadzieję, że wybaczysz mi taki bardziej teoretyczny i szczerze mówiąc nudnawy wpis. Jest to jednak ostatni krok przed przejściem do praktyki i tworzeniem ciekawych projektów, obiecuję! 🙂 Widzimy się już wkrótce w kolejnym wpisie, na który szykuję coś ekstra!

Źródła

Obrazek wyróżniający: https://www.flickr.com/photos/christiaancolen/20607150556   Creative Commons Share Alike

 

Podziel się stroną ze znajomymi! :)

Dodaj komentarz