^W górę

logo

Z każdym systemem informatycznym związane jest pojęcie typów danych. Wynika to ze specyfiki przechowywania informacji w pamięci operacyjnej. ARDUINO oparte na mikrokontrolerach z rodziny ATMEGA korzysta z podstawowych typów danych. Znajomość typów jest więc niezbędna do prawidłowego programowania układów. Poniżej zestawiono typy danych występujące w ArduinoIDE:

Nazwa typuRozmiarOpis
void 0 Typ nieważny stosowany podczas deklaracji funkcji, która nie zwraca wartości
char 1 bajt Znak ASCII np. 'A'. Char przechowuje informację o kodzie, czyli liczba 66 to 'B'. Typ przechowuje wartości od -128 do 127
unsigned char 1 bajt Znak ASCII zapisany jako liczba z przedziału 0 - 255
byte 1 bajt Liczba z przedziału 0 - 255
int 2 bajty Liczba z przedziału -32 768 do 32 767
unsigned int 2 bajty Liczba z przedziały 0 - 65535
word 2 bajty Liczba z przedziały 0 - 65535
long 4 bajty Liczba z przedziału -2 147 483 648 do 2 147 483 647
unsigned long 4 bajty Liczba z przedziału 0 - 4 294 967 295
float 4 bajty Liczba z przedziału -3.4028235E+38 do 3.4028235E+38
double 4 bajty W Arduino jak float
string ... Tablica znaków np. char Str1[15]; char Str2[8] = {'a', 'r', 'd', 'u', 'i', 'n', 'o'}; 
char Str3[8] = {'a', 'r', 'd', 'u', 'i', 'n', 'o', ''}; char Str4[]="arduino";
String ... Klasa String umożliwia wykonywanie operacji na obiektach tekstowych. Zostanie opisana w odrębnym artykule.
array ... Tablica dowolnego typu np. int tab[10]; char tekst[8]="arduino";

Początkujący programiści piszący oprogramowanie dla komputerów PC nie potrafią sobie jednoznacznie uświadomić, jak ważne jest dopasowanie odpowiedniego typu danych do przechowywanych w nich informacji. Wynika to z faktu, że wykorzystanie 4 bajtów zamiast jednego jest praktycznie niezauważalne w systemach wyposażonych w kilka GB pamięci operacyjnej RAM. W przypadku ARDUINO szybko okaże się, że oszczędzanie pamięci jest jedną z podstawowych cech dobrego programowania. Ilość dostępnej pamięci RAM szybko się kurczy i trzeba uważać w jaki sposób się ją wykorzystuje.

Przekształcenia pomiędzy typami danych

Często zdarza się, że należy zamienić jeden typ danych na drugi. Przykładowo zamieniamy znak na liczbę, lub liczbę na znak. W ArduinoIDE znajdziemy zestaw funkcji umożliwiających konwersję pomiędzy typami danych. Poniższa tabela przedstawia funkcje konwersji danych.

Nazwa funkcjiOpis
char(x) Zamienia x dowolnego typu na typ char
byte(x) Zamienia x dowolnego typu na typ byte
int(x) Zamienia x dowolnego typu na typ int
word(x) Zamienia x dowolnego typu na typ word
word(h,l) Zamienia parę liczb l,h na liczbę typu word (h-starszy bajt, l-młodszy bajt)
long(x) Zamienia x dowolnego typu na typ long
float(x) Zamienia x dowolnego typu na typ float

Zasięg zmiennych

ArduinoIDE jest wzorowany na C/C++. Przejął z tych języków również sposób traktowania zmiennych. Podczas pisania programu możemy stosować zmienne globalne lub zmienne lokalne. Zmienna globalna to taka, która zostanie zainicjowana przy uruchomieniu programu i jest dostępna z dowolnego miejsca - z dowolnej funkcji przez cały czas działania programu. Własność ta jest często zaletą - z każdej funkcji mamy dostęp do zmiennej bez konieczności przekazywania informacji jako parametru wywołania. W pewnych przypadkach jest to niestety wada. Wykorzystując gotowe funkcje może okazać się, że ta sama nazwa zmiennej wykorzystywana jest do różnych celów jednocześnie i program działa nieprawidłowo. Drugi typ zmiennej to zmienna lokalna. Definiujemy ją w ciele funkcji i jest dostępna wyłącznie z poziomu funkcji. Zmienna lokalna inicjowana jest podczas wywołania funkcji i niszczona po zakończeniu jej działania. Korzystanie ze zmiennych lokalnych zmniejsza zapotrzebowanie programu na pamięć operacyjną, komplikuje jednocześnie przekazywanie informacji pomiędzy poszczególnymi funkcjami programu.

Zdecydowanie zaleca się stosowanie zmiennych lokalnych oraz wywoływania funkcji z niezbędnymi parametrami jako prawidłową praktykę programistyczną. Zmienne globalne powinny być wykorzystywane z rozwagą w sytuacjach, w których istotne jest zachowanie stanu pewnego procesu przez cały okres działania programu. Poniższy kod ilustruje miejsce i sposób deklaracji zmiennych globalnych i zmiennych lokalnych:

char tablica[10]; // tablica globalna dostępna z dowolnego miejsca programu
void setup()
  {
    int a; // zmienna lokalna "a" dostępna z funkcji setup();
  }

int x; // zmienna globalna dostępna z dowolnego miejsca programu

void loop()
  {
    int a; // zmienna lokalna "a" dostępna z funkcji loop();
    char b; // zmienna lokalna "b" dostępna z funkcji loop();
  }

Jak widać na powyższym przykładzie zmienne zadeklarowane wewnątrz funkcji to zmienne lokalne, zmienne zadeklarowane poza ciałem funkcji to zmienne globalne. Zmienna "a" w funkcji setup() to zupełnie inna zmienna niż "a" z funkcji loop(). Przeanalizujmy kolejny kod programu. Funkcje Serial.begin i Serial.print umożliwiają wysłanie danych poprzez port szeregowy. Poniższy kod można skompilować i uruchomić, a wynik działania programu obserwować z pomocą monitora portu szeregowego dostępnego w ArduinoIDE.

void setup()
  {
   Serial.begin(9600);  //inicjalizacja portu szeregowego
  }
int a=10;               //zmienna globalna "a" o wartości 10
void loop()
  {
   Serial.print(a);     //wyświetlenie zmiennej "a"  - tu 10
   Serial.print(" ");
   int a=1;             //zmienna lokalna "a" o wartości 1
   Serial.println(a);   //wyświetlenie zmiennej "a"  - tu 1 
  }

W powyższym przykładzie mamy do czynienia z dodatkowym elementem - zmienną globalną i zmienną lokalną o tej samej nazwie. Kompilator nie zwróci błędu. Trzeba jednak pamiętać o problemach, jakie mogą wyniknąć z powyższego przykładu. Na początku funkcji loop() zmienna lokalna nie jest jeszcze zadeklarowana. Odwołując się do zmiennej "a" odwołujemy się do zmiennej globalnej, której wartość wynosi 10. Po deklaracji zmiennej lokalnej "a" wewnątrz funkcji loop() odwołujemy się do niej i otrzymamy wartość 1. Funkcja loop() jest wywoływana w systemie cyklicznie, więc za pomocą monitora portu szeregowego możemy zaobserwować naprzemienne pojawianie się liczb 10 i 1. Zmodyfikujemy nieznacznie kod:

void setup()
  {
   Serial.begin(9600);  //inicjalizacja portu szeregowego
  }
int a=10;               //zmienna globalna "a" o wartości 10
void loop()
  {
   Serial.print(a);     //wyświetlenie zmiennej "a"  - tu 10
   Serial.print(" ");
   int a=1;             //zmienna lokalna "a" o wartości 1
   a++;                 //zwiększenie wartości zmiennej "a" o jeden czyli a=2
   Serial.println(a);   //wyświetlenie zmiennej "a"  - tu 2 
  }

Jak widać na przykładzie zmiana wartości zmiennej "a" odnosi się do zmiennej lokalnej. Należy unikać stosowania zmiennych lokalnych i zmiennych globalnych o tych samych nazwach, ponieważ generuje to potencjalne problemy z prawidłowym działaniem programu.

Dyrektywy const, static, volatile

Dyrektywy kompilatora umożliwiają specjalne traktowanie pewnych elementów programistycznych i zastąpienie ich w procesie kompilacji lub wykonanie specjalnego kodu procesora. Dyrektywa const umożliwia utworzenie zmiennej "tylko do odczytu" czyli stałej. Przykłady stałych predefiniowanych znajdują się w innym artykule. Zamiennikiem dla const jest define. Twórcy ArduinoIDE zalecają stosowanie const. Poniżej przykłady dla stałych zdefiniowanych przez użytkownika:

const float pi=3.14;    // definicja stałej PI
const byte max=100; // wartość maksymalna=100

#define ledPin 13       // definicja pinu13 jako ledPin

Jak widać const wskazuje na typ, natomiast define przyporządkowuje nazwie odpowiednią wartość bez wskazywania typu. Brak średnika na końcu linii z define nie jest przeoczeniem, ale właściwą składnią zapożyczoną z C/C++. Dobrą praktyką wydaje się stosowanie define do oznaczenia słownego poszczególnych wejść/wyjść programowanego układu, natomiast dla pozostałych stałych wykorzystywanych w programie zaleca się korzystać z dyrektywy const.

Dyrektywa static umożliwia określenie sposobu inicjalizacji zmiennych lokalnych. Standardowo zmienna lokalna inicjalizowana jest podczas wywołania funkcji i niszczona po jej wykonaniu. Dodanie dyrektywy static spowoduje, że z punktu widzenia zasięgu zmienna pozostanie nadal zmienną lokalną, lecz  nie zostanie zniszczona po zakończeniu wykonywania funkcji. Przy kolejnym wywołaniu funkcji zmienna nie będzie już ponownie inicjalizowana, lecz będzie miała wartość z czasu poprzedniego zakończenia działania funkcji. Najlepiej sprawdzić to na przykładzie:

void setup()
   {
    Serial.begin(9600);
   }
void loop()
   {
    int a=1;
    static int b=1;  
    Serial.print(a);     // zawsze 1
    Serial.print(" ");   
    Serial.println(b);   // 1, 2, 3,...
    a++;
    b++;
   }

Po uruchomieniu programu okaże się, że zmienna "a" za każdym razem przyjmuje wartość początkową 1, a zmienna "b" wartości 1, 2, 3,....

Ostatnią ważną dyrektywą jest volatile. Należy z niej korzystać w sytuacji, gdy wartość zmiennej może zostać zmieniona za pomocą funkcji obsługi przerwania. Zrozumienie zasadności stosowania volatile nie jest proste. Należy wiedzieć jak obsługiwane są przerwania. Ogólnie obsługa przerwań polega na przerwaniu wykonywania właściwego programu, wykonaniu określonych instrukcji i powrocie do wykonywania programu zasadniczego. Jeżeli w przerwaniu zmieni się wartość zmiennej wykorzystywanej we właściwym programie, to może się okazać, że ta zmiana nie będzie w nim uwzględniona. W tym przypadku z pomocą przychodzi dyrektywa volatile, która wymusza odświeżenie wartości zmiennej po wykonaniu przerwania w programie głównym. W uproszczeniu należy zapamiętać, że zmienne wykorzystywane w przerwaniach powinny być deklarowane z dyrektywą volatile.

volatile int x;
...
void obsługa_przerwania()
   {
     x++;
   }

 sizeof()

Funkcja sizeof() zwraca ilość bajtów zajmowaną przez zmienną. 

void funkcja()
   {
    char tekst[]="to jest tekst";
    int x;
    x=sizeof(tekst);    // zmienna x przyjmie wartość 13
   }

sizeof() ułatwia przetwarzanie elementów tablic poprzez podanie ilości ich elementów. Poniższy przykład (przepisany z arduino.cc) pokazuje sposób praktycznego wykorzystania funkcji sizeof().

char myStr[] = "to jest test";
int i;

void setup()
   {
    Serial.begin(9600);
   }

void loop() 
   { 
    for(i = 0; i < sizeof(myStr) - 1; i++)
       {
        Serial.print(i, DEC);
        Serial.print(" = ");
        Serial.write(myStr[i]);
        Serial.println();
       }
   }

 

Copyright © 2005 - 2017 Katarzyna i Wojciech Płóciennik