sobota, 22 kwietnia 2017

C - Przydatne rady oraz pułapki dla programowania mikrokontrolerów

Ten post chciałbym poświęcić na opisanie kilku przydatnych elementów, rad, pułapek jakie można spotkać podczas programowania w C.

Instrukcja If:


Instrukcja warunkowa if nie wymaga opisania, należy przy niej pamiętać, że w przypadku takiej instrukcji:

  1. int i;
  2. if(i)
  3. {
  4.     //........
  5. }


Elementy zdefiniowane do wykonania wewnątrz instrukcji nie zostaną wykonane gdy i będzie równa 0. Wiele osób zapomina przy tym, że wartości ujemne też spowodują wykonanie instrukcji if:

  1. #include <stdio.h>
  2.      
  3. int main(void)
  4. {
  5.     int i = -4;
  6.      
  7.     if(i)
  8.     {
  9.         printf("instrukcja if");
  10.     }
  11.     else
  12.     {
  13.         printf("instrukcja else");
  14.     }
  15.     // your code goes here
  16.     return 0;
  17. }

W przypadku bardzo krótkiego programu przedstawionego powyżej na ekranie pojawi się napis "instrukcja if".

Pętla for:


Tutaj chodzi o efektywność kodu zwłaszcza na mikrokontrolery 8 bitowe. Dla układów z rdzeniem 8051 program znacznie lepiej zoptymalizuje się kod, który zamiast pętli while będzie wykorzystywał pętle for. Dodatkowo lepsze rozwiązanie jest nie zwiększanie zmiennej w tej pętli tylko jej zmniejszanie. Przechodzenie od wartości najwyższej do najniższej. Spowodowane jest to pewnymi instrukcjami znajdującymi się w mikrokontrolerach z rdzeniem 8051, które pozwalają na znacznie łatwiejsze operowanie w czasie zmniejszania wartości.

W przypadku mikrokontrolerów 32 bitowych takie zmiany nie mają większego sensu. Szybkość jego działania będzie właściwie taka sama.

Typy danych:


Dużo lepszym rozwiązaniem niż korzystanie z typów standardowych np int jest wykorzystywanie typów danych zdeklarowanych razem z rozmiarem:

Bez znaku: uint8_t, uint16_t, uint32_t, uint64_t
Ze znakiem: int8_t, int16_t, int32_t, uint64_t

Dzięki temu znacznie łatwiej odnaleźć się w rozmiarze danych, oraz pozwoli to na pewne zdeklarowanie rozmiaru. W niektórych kompilator typ int może mieć różne długości. Dodatkowo zyskuje się możliwość bezproblemowego przenoszenia kodu pomiędzy mikrokontrolerami oraz programami.

Dodatkowo można jeszcze dobrać odpowiednią ilość bitów np. jeśli dany typ jest za duży, a najbliższy mniejszy za mały to można skorzystać z takiej deklaracji:

  1. uint16_t data:10;

Funkcja printf:


Nie należy wykorzystywać funkcji printf, sprintf itp. na mikrokontrolerach 8 bitowych, w celu przekierowania strumienia np. na port COM (printf), bądź w celu przygotowania bufora (sprintf). Powodem tego jest bardzo duże zużycie pamięci przez układ. Te funkcje bardzo mocno go obciążają, oraz znacząco ograniczają możliwości dodania dodatkowych elementów do programu. Należy przygotować własne funkcje wykonujące tylko te operacje, które są potrzebne aby uzyskać taki sam efekt działania. Pozwoli to znacząco rozbudować sam projekt o dodatkowe funkcje w programie.

Zmienna volatile:


Jest to zmienna, która nie podlega optymalizacji. Należy ją stosować w przerwaniach, kiedy jej zmiana może nastąpić nieoczekiwanie, i gdy kompilator nie przewidział jej zmiany w danym momencie. Wszystkie zmienne będą działać na takiej zasadzie gdy parametr optymalizacji będzie ustawiony na zero. Spowoduje to jedna występowanie mało optymalnego kodu. Podobnie będzie się działo w przypadku oznaczenia wszystkich zmiennych jako volatile, tak na wszelki wypadek. Najlepiej stosować ten modyfikator tylko wtedy gdy jest on rzeczywiście potrzebny.

Typ volatile należy stosować dla wartości które dotyczą rejestrów mikrokontrolera, modyfikowanych podczas przerwań oraz wartości dostępnych przez wiele zadań, które są wykorzystywane przez system czasu rzeczywistego, zwłaszcza gdy jest ona dostępna przez wiele operacji.

Zamienianie dwóch wartości:

W celu zamiany dwóch wartości można posłużyć się dodatkową zmienną, co by wyglądało następująco:

  1. #include <stdio.h>
  2. #include <stdint.h>
  3. int main(void)
  4. {
  5.     uint8_t zmienna = 30;
  6.     uint8_t zmienna2 = 50;
  7.     uint8_t tmp = 0;
  8.     printf("Zmienna: %u, zmienna2: %u, tmp %u\n", zmienna, zmienna2, tmp);
  9.     tmp = zmienna;
  10.     zmienna = zmienna2;
  11.     zmienna2 = tmp;
  12.     printf("Zmienna: %u, zmienna2: %u, tmp %u\n", zmienna, zmienna2, tmp);
  13.     return 0;
  14. }

Druga opcja, którą chciałbym tutaj przedstawić jest użycie operator EXOR. Pozwoli to na wykorzystywanie zmiennych, które przechowują potrzebne wartości, bez konieczności deklaracji zmiennej dodatkowej.

  1. #include <stdio.h>
  2. #include <stdint.h>
  3. int main(void)
  4. {
  5.     uint8_t zmienna = 30;
  6.     uint8_t zmienna2 = 50;
  7.     printf("Zmienna: %u, zmienna2: %u\n", zmienna, zmienna2);
  8.     zmienna = zmienna ^ zmienna2;
  9.     zmienna2 = zmienna ^ zmienna2;
  10.     zmienna = zmienna ^ zmienna2;
  11.     printf("Zmienna: %u, zmienna2: %u\n", zmienna, zmienna2);
  12.     return 0;
  13. }

Ustawianie i zerowanie bitów:


Jednym z prostszych sposobów jest wykorzystywanie następującej definicji:

  1. #define setBit(data, numberToSet)       ((data) |= 1UL << (numberToSet))
  2. #define clearBit(data, numberToClear)   ((data) &= ~(1UL << (numberToClear)))

Do makra podawana jest zmienna wraz z numerem bitu do ustawienia bądź wyczyszczenia.

Nazwy plików:


Tutaj nie tyle o programowaniu ile o kompilatorach. Należy pamiętać, żeby w przypadku plików .c, .h ich nazwy w systemie linux były pisane z małej litery. Gdy będą napisane z dużej, kompilator może tego pliku nie znaleźć. Taki problem nie występuje w systemie windows.

Przekazywanie wartości do makr:

Należy uważać podczas przekazywania wartości do makr zdefiniowanych za pomocą dyrektywy preprocesora #define. Chodzi mi tutaj o wartości z parametrami inkrementacji oraz dekrementacji. Zaprezentuje to na poniższym krótkim programie:

  1. #define kmax(A, B) ((A) > (B) ? (A) : (B))
  2. #define kmin(A, B) ((A) > (B) ? (B) : (A))
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6.     int data = 4;
  7.     int data2 = 8;
  8.     int test = 0;
  9.  
  10.     test = kmax(data++, data2++);
  11.  
  12.     printf("data: %d; data2: %d test: %d\n", data, data2, test);
  13.  
  14.     test = kmin(data++, data2++);
  15.  
  16.     printf("data: %d; data2: %d test: %d\n", data, data2, test);
  17.  
  18.     return 0;
  19. }

Oto wynik jego działania:

Jak można zaobserwować na rysunku prezentującym wynik operacji. Wartości data oraz data2 zostały zmodyfikowane więcej razy niż można się było spodziewać. Wywołane makro zadziałało następująco:

((data++) > (data2++) ? (data++) : (data2++))

Czyli najpierw zwiększone zostanie data i data2, a następnie przy wybraniu wyniku postinkrementacja nastąpi po przypisaniu wyniku do zmiennej test. Dla bezpieczeństwa najlepiej żadnej tego typu deklaracji do makra.