poniedziałek, 14 marca 2011

Techniki programowania uC: Smocze zasady


Autor: BlueDraco
Redakcja: Dondu


Przedstawiam zbiór zasad poprawnego projektowania oprogramowania, który powstał na bazie moich doświadczeń z praktyki zawodowej.

Nie ma reguł bez wyjątków – dla niemal każdego z poniższych „przykazań” można wskazać przypadek, gdy niestosowanie go będzie uzasadnione – sam w wielu projektach nie stosowałem tej, czy innej zasady.

Jeśli jednak napisałeś mniej niż 20 programów i uważasz, że wiele z tych reguł nie ma zastosowania do Twojego projektu, to prawdopodobnie jesteś w błędzie, a działanie Twojego oprogramowania może być dalekie od Twoich oczekiwań.





I. Struktura oprogramowania


I.1. Opóźnienia programowe

Nie używaj opóźnień programowych w postaci funkcji z pętlami („delay”) nigdzie poza sekwencją inicjującą działanie mikrokontrolera, wykonywaną jednorazowo przy starcie urządzenia.


I.2. Timer

W każdym projekcie, nawet najprostszym, użyj timera zgłaszającego przerwania ze stałą częstotliwością. Nawet nie wiesz, jak bardzo jest potrzebny.


I.3. Jeden timer wystarczy

Nie używaj dwóch timerów zgłaszających przerwania ze stałą częstotliwością jeśli programujesz mikrokontroler z jednopoziomowym systemem przerwań (np. taki jak AVR Mega lub Tiny).


I.4. Żadnego oczekiwania w obsłudze przerwania

Aktywne oczekiwanie stosuj wyłącznie w kodzie o najniższym priorytecie (pętli zdarzeń, a w przypadku wielopoziomowego systemu przerwań i programu bez pętli zdarzeń – w obsłudze przerwania o najniższym priorytecie).

Nie czekaj w obsłudze przerwania jeżeli poza procedurą obsługi przerwania w systemie jest jeszcze inny kod o takim samym lub niższym priorytecie wykonania (np. pętla główna lub inne przerwanie o takim samym priorytecie).


I.5. Nie obniżaj priorytetu

Nie obniżaj programowo priorytetu procesora. Na przykład w funkcjach przerwania w mikrokontrolerach AVR, nie używaj ISR_NO_BLOCK, bo narażasz się na kłopoty.


I.6. Zbędne przerwania

Nie używaj procedur obsługi przerwań, w których jedyną akcją jest ustawienie znacznika wystąpienia zdarzenia lub odczyt danej z rejestru do pojedynczej zmiennej skalarnej. Jeśli tak napisałeś program – to znaczy, że w ogóle nie potrzebujesz tego przerwania.


I.7. Zmiany stanu portów

Jeżeli dwa różne fragmenty kodu działające na różnych priorytetach (np. program główny i procedura obsługi przerwania) wykonują operacje logiczne lub arytmetyczne na tej samej danej lub tym samym porcie – zawsze blokuj przerwania na czas wykonania takiej operacji przez kod o niższym priorytecie (program główny).


I.8. ADC i przerwania

Jeśli korzystasz z przetwornika analogowo-cyfrowego mikrokontrolera i nie jest on wyzwalany sprzętowo przez timer – nie korzystaj z przerwania ADC. Jeśli jest wyzwalany sprzętowo przez timer – używaj tylko jednego z przerwań – timera albo ADC.


I.9. Unikaj pętli zdarzeń ...

Unikaj niepustych pętli głównych, o ile jest to możliwe. Nie stosuj pętli zdarzeń przy programowaniu procesorów z wielopoziomowym systemem przerwań. Nie jest dobrze używać pętli głównej do czegokolwiek poza usypianiem procesora.


I.10. ... a przynajmniej zbyt długich

Jeśli Twoja pętla główna urosła do pięciuset linii - pomyśl o użyciu systemu operacyjnego.





II. Zapis programu w języku C


II.1. Deklaracje funkcji 

Używaj pełnej listy argumentów, a w funkcjach bezargumentowych – słowa kluczowego void.


II.2. Zapis stałych

Stałe używane w programie zapisuj w takiej postaci, w jakiej pierwszy raz o nich pomyślałeś.
Zapisuj:
  • znaki jako znaki, a nie ich numery w tabeli ASCII,
  • maski bitowe – jako liczby binarne, szesnastkowe lub ósemkowe,
  • liczniki i podzielniki – jako liczby dziesiętne.

II.3. Typy o jawnych rozmiarach

Jeśli dana ma mieć określoną długość, używaj typów o jawnie określonych długościach, zdefiniowanych w standardowym pliku nagłówkowym stdint.h, np. uint8_t, int8_t, uint32_t, itp..


II.4. Rozmiary danych

Dla stałych i zmiennych statycznych (czyli takich, których czas życia jest równy czasowi życia programu) używaj najkrótszych typów gwarantujących poprawne działanie programu.

Dla lokalnych zmiennych roboczych procedur używaj w miarę możliwości typów o rozmiarze odpowiadającym szerokości rejestrów procesora, np. programując procesory ARM używaj typów 32-bitowych, a dla AVR8-bitowych.


II.5. Atrybut static w deklaracjach zewnętrznych 

Stosuj atrybut static dla wszystkich danych i funkcji na poziomie zewnętrznym, za wyjątkiem publicznych, czyli tych, które są używane w innych modułach Umożliwia to kompilatorowi skuteczniejszą optymalizację kodu oraz lepszą detekcję błędów w programie.


II.6. Dane lokalne funkcji

Zmienne używane wyłącznie wewnątrz jednej funkcji deklaruj wewnątrz tej funkcji. Jeżeli zmienna ma zachowywać wartość pomiędzy wywołaniami, należy ją zadeklarować z atrybutem static.


II.7. Stałe statyczne

Używaj atrybutu const dla wszystkich stałych, a stałe definiowane wewnątrz procedur deklaruj jako static const.


II.8. Volatile

Używaj atrybutu volatile dla wszystkich zmiennych statycznych, które są używane na więcej niż jednym poziomie priorytetowym oraz dla takich, które są współużywane przez procedury mogące podlegać wywłaszczaniu (czyli np. przez dwa procesy w systemie operacyjnym). Nie używaj atrybutu volatile dla zmiennych deklarowanych wewnątrz funkcji.


II.9. switch() oraz if()

Nie używaj instrukcji switch(), gdy poszczególne sekcje case różnią się tylko wartościami danych, a nie wykonywanymi operacjami.



II.10. Ostrzeżenia kompilatora

Pisz program tak, by kompilator nie generował ostrzeżeń. Zawsze czytaj ostrzeżenia i usuwaj ich przyczyny, a nawet jeśli uważasz, że zapis jest poprawny, a kompilator nadgorliwy (a tak naprawdę jest w przypadku nowych wersji kompilatorów).


To moje zasady, a Wasze?


Autor: BlueDraco
Redakcja: Dondu

32 komentarze:

  1. Ja także polecam stosowanie się do punktu I.2. Naprawdę warto. Najczęściej mam o jedno przerwanie oparte co najmniej eliminowanie drgań styków, zegar RTC, i multipleksowanie LED (jeśli taki mam projekt).

    OdpowiedzUsuń
  2. Faktem jest, że warto co się da opierać o przerwania. Wtedy pętla główna jest króciutka. Ale oczywiście bez przesady, tak jak piszesz i od wszystkiego są wyjątki i czasami także mam w niej sporo kodu.

    Ja dopisałbym jeszcze tutaj o tym, by dzielić większe projektu na pliki zawierające wydzielone tematycznie funkcje. Wtedy jest łatwiej operować i przerabiać kod.

    I na koniec uwaga. Wydaje mi się, że całkiem początkujący, mogą nie zrozumieć co chcesz im przekazać. A chyba dla nich są te zasady. Przydały by się rozwinięcia tematyczne większości punktów.

    OdpowiedzUsuń
  3. Zasada czystego kodu. Staraj się minimalizować ilość komentarzy - kod ewoluuje, komentarze niestety nie i często są czystą abstrakcją. To kod ma być komentarzem sam w sobie. W komentarzach wystarczy tylko zawrzeć zarys działania danej sekcji kodu.

    OdpowiedzUsuń
    Odpowiedzi
    1. Co do tego, że kod powinien być czytelny - i w miarę możliwości "samoobjaśniający się" - to pełna zgoda, natomiast co do zalecenia(?) "minimalizowania komentarzy", to... dawno nie czytałem czegoś tak w oczywisty sposób głupiego.

      Usuń
    2. To jest Twoje zdanie, masz do niego pełne prawo. Twórz kod taki jaki Ci najbardziej odpowiada. Ja napisałem dlaczego tak uważam, ale nikt nie przeczyta Twojego uzasadnienia, bo go wcale nie napisałeś!

      Usuń
    3. Zasada wskazana przez Deucalion jest powszechnie stosowana wśród programistów. Ja robię wyjątek jedynie dla komentarzy programów prezentowanych na blogu, gdzie szczegółowe komentarze mają służyć początkującym.

      Usuń
  4. Dlaczego switch nie powinno się stosować do przypadku, gdy tylko podstawiane różne dane w case?

    OdpowiedzUsuń
  5. Popieram anonimowy komentarz wyżej... Dla początkujących są troche niejasne te zasady.. Przydałyby sie jakieś rozwinięcia.

    OdpowiedzUsuń
  6. Powolutku będę rozwijać - w miarę pojawiania się wątpliwości. Co do switch - nie ma sensu rozmnażać kodu, gdy wystarcza stablicowanie danych i jeden kawałek kodu - dane indeksujemy tym, co byłoby wartością sterującą switch(). Jak już napisałem - rozwinięcia pojawią się w przyszłości.

    OdpowiedzUsuń
  7. połowy punktów nie rozumie

    OdpowiedzUsuń
  8. BlueDraco więcej więcej!

    Zapowiada się ciekawy cykl artykułów...

    OdpowiedzUsuń
  9. Każdy punkt mógłby być rozszerzony o: "
    [...]
    czyli zamiast:
    kawałek
    złego
    kodu
    powinniśmy zrobić:
    kawałek
    dobrego
    kodu
    bo w przeciwnym razie grozi to ...
    [...]"
    bo inaczej czytelnik może pomyśleć "łatwo powiedzieć!" albo przynajmniej nie zrozumieć w czym problem.

    OdpowiedzUsuń
  10. Niech Anonim z godziny 21.50 raczy napisać, których punktów nie rozumie, a postaramy się jakoś temu zaradzić. :)

    OdpowiedzUsuń
  11. Zapowiada się naprawdę ciekawie.

    Pytanie co do punktu 1.6 - jeżeli przerwanie jednocześnie wybudza mikrokontroler i ustawia tylko jedną flagę w obsłudze przerwania to jest OK?

    Pytanie do punktu 1.8 - Tutaj naprawdę się zgubiłem. Jeżeli wyzwalam ADC z kodu np. w "free running mode" to dlaczego miałbym nie używać przerwania od ADC ?
    Przecież można wejść w tryb "noise reduction mode" to jak z niego wyjść jak nie przez przerwanie?

    Pozdrawiam
    MB

    OdpowiedzUsuń
  12. Dobra robota, bardzo przydatne dla początkujących. A powiedz mi, o jakie systemy operacyjne chodzi w pkt. I.10? Nie wiedziałem, że istnieją takowe na mikrokontrolery.

    OdpowiedzUsuń
  13. Jest całe mnóstwo systemów operacyjnych dla uC, zajmujących po 1..8 KB pamięci, np. FreeRTOS. Niektórzy używają ich nawet do migania diodami.
    W przygotowaniu są rozwinięcia i wyjaśnienia kilku punktów - proszę o cierpliwość

    OdpowiedzUsuń
  14. Ciekawi mnie 1.7 to wyłączanie przerwania na czas zmiany rejestru danego portu? Z jakiego powodu? BTw: ile trwa zmiana (powiedzmy że instrukcja PORTC |= (1<<PC3)) ? 2 takty? (nie ironizuje, broń boże, tylko pytam)

    OdpowiedzUsuń
  15. Co do 1.7 - przykład będzie wkrótce. To nieistotne, ile czasu to zajmuje; istotne jest ile do tego trzeba instrukcji procesora. W większości mikrokontrolerów nie da się tego zrobić jedną instrukcją (da się na MSP430, 51, a w AVR na przestrzeni IO, ale nie za zwykłych zmiennych). Przy trzech instrukcjach wystarczy, że przerwanie złośliwie "wstrzeli się" pomiędzy odczyt i zapis - i już jest problem. Mam do tego przykład na STM32F0, w którym widać to gołym okiem.

    OdpowiedzUsuń
  16. Tylko dla uzupełnienia wypowiedzi kolegi BlueDraco, istotnie w zwykłych AVRach z wyjątkiem SBI/CBI (po warunkiem jednakże, że zmieniamy tylko jeden bit) nie są to operacje atomowe. Jednak już na XMEGA możemy posłużyć się dla zmiany dowolnej liczby bitów rejestru IO rejestrami CLR (kasowanie), SET (ustawianie) i TGL (zmiana na przeciwny), co jest już wykonywane atomowo. Podobnie do operacji atomowej zmiany zawartości pamięci mamy operacje LAC, LAS i LAT, jednak co warto zaznaczyć język c w żaden sposób nie wspiera i nie daje narzedzi gwarantujących wygenerowanie atomowego kodu.

    OdpowiedzUsuń
  17. Kolega BlueDraco strzelił z grubej rury. Szkoda, że nie pamięta, iż są tu także bardzo początkujący i młodzi programiści. Ci zwłaszcza potrzebują dobrych wzorców bo właśnie nasiąkają. Może dodać poddział pt. "Smoczkowe zasady" :)

    OdpowiedzUsuń
  18. Pytając się o czas zmiany stanu portu właśnie miałem na myśli ilość taktów. Tak czy inaczej dziękuję za odpowiedź. Pozostaje czekać na rozwinięcie wątku, bo jakoś dziwnie to widzę, żeby blokować przerwania przy każdej zmianie stanów.

    @up Nie wiem co tu kolega miał na myśli, generalnie te zasadny są dosyć "zdroworozsądkowe", i chyba nie są cięzkie. Choć jestem bardzo bardzo początkujący i dużo nie wiem. Na przykład jeśli w trakcie oczekiwania _delay wpadnie nam przerwanie - to jak się zachowa uC? opóźnienie pętli głównej będzie trwało delay + czas przerwania? Czyli uC może będzie wiedział ile trwało przerwanie i będzie opóźnienie trwało = (delay do momentu przerwania) + (reszta delay, lub jesli przerwanie było dłuższe od pozostałego delay)

    CHociaż w sumie można to sprawdzić samemu dioda+ w programie + delay w przerwaniu ;)

    OdpowiedzUsuń
  19. Także z niecierpliwością czekam na dalsze rozwinięcia punktów, choć część wydaje się oczywista, ale pewnie dla mnie, a dla kogoś innego może nie. Pozdrawiam!

    OdpowiedzUsuń
  20. Witam,
    myślę, że większość z tych punktów ma jakieś uzasadnienie, jednak ja - nie zajmując się programowaniem uC zawodowo chciałbym wiedzieć, dlaczego czegoś nie stosować, albo coś stosować - bo na przykład "takie" stosowanie powoduje, że kod programu jest większy o 500 B, a "inne", że mniejszy o 100 B. Czy "takie" powoduje, że maksymalna częstotliwość przełączania wynosi 2 MHz, a "takie" aż 3 MHz.
    A dla początkujących byłoby to na pewno bardziej obrazowe, niż podanie zasad, bez ich sensowności.
    To tak jakby zakazać dziecku grzebania w gniazdku "bo tak i już" :).

    OdpowiedzUsuń
  21. Kr0nos - popieram. Zasady musza mieć swoje uzasadnienie.
    Poza tym z tego byłby materiał na cykl artykułów, i to nie dla początkujących, bo zagadnienia w nim omówione stoją w połowie drogi do własnego systemu operacyjnego.

    OdpowiedzUsuń
  22. Mogę prosić o rozwinięcieI.8. ADC i przerwania? Bo w sumie używałem i przerwań ADC i timera a timer w swym przerwaniu uruchamiał przetwarzanie adc...

    OdpowiedzUsuń
  23. Witam
    Nie rozumiem pkt 1.6- wg mnie po to są przerwania aby reagować na zdarzenia, ustawić flagę i wyjść a w głównym wątku zająć się obsługą flagi np timer, ustwiam "tykanie" na co 1ms a w głównym wątku odliczam kolejne dziesiątki i robię co trzeba.

    OdpowiedzUsuń
  24. Dwa lata niedługo miną - a "rozwijanie" dalej w toku... ;)

    Zgadzam się z wieloma głosami, że zamieszczanie takich "przykazań" BEZ UZASADNIENIA I SZCZEGÓŁOWEGO WYJAŚNIENIA mija się z celem.

    OdpowiedzUsuń
  25. Szybciej linux chodzi na Atmega8 ( http://dmitry.gr/index.php?r=05.Projects&proj=07.%20Linux%20on%208bit ) niż trwa to rozwijanie tych zasad.
    Pytasz o "Nasze" zasady?
    Moja zasada jest taka: Jak mówię, że coś zrobię, to to robię :P

    OdpowiedzUsuń
  26. Odnośnie pkt. 1.6 - Nie ma sensu angażować przerwania w celu ustawienia flagi. Sam uC dla każdego przerwania dysponuje taką flagą. Także zamiast monitorować globalną flagę, należy monitorować odpowiedni bit odpowiedniego rejestru. Logicznie wychodzi na to samo, a mocy oszczędzamy nieporównanie więcej. Samo wejście do funkcji obsługi przerwania i jej opuszczenie trwa nieporównanie dłużej. uC musi przecież odłożyć na stosie wszystkie używane rejestry, wykonać kod funkcji obsługi, odtworzyć stan rejestru ze stosu i to wszystko przy zablokowanych innych przerwania, gdzie możemy przegapić inne, ważniejsze zdarzenia.

    OdpowiedzUsuń
  27. Odnośnie pkt. 1.6 - Nie ma sensu angażować przerwania w celu ustawienia flagi. Sam uC dla każdego przerwania dysponuje taką flagą. Także zamiast monitorować globalną flagę, należy monitorować odpowiedni bit odpowiedniego rejestru. Logicznie wychodzi na to samo, a mocy oszczędzamy nieporównanie więcej. Samo wejście do funkcji obsługi przerwania i jej opuszczenie trwa nieporównanie dłużej. uC musi przecież odłożyć na stosie wszystkie używane rejestry, wykonać kod funkcji obsługi, odtworzyć stan rejestru ze stosu i to wszystko przy zablokowanych innych przerwania, gdzie możemy przegapić inne, ważniejsze zdarzenia.

    OdpowiedzUsuń
  28. Do "Daniel Kowszun" kto Ci powiedział że procesor musi długo siedzieć w przerwaniu :) popatrz na ten kod to jest incrementacja zmiennej 16 bit w przerwaniu, da się szybko? da się :)

    ISR(TIMER0_OVF_vect, ISR_NAKED)
    {__asm__ __volatile__
    ("in r2,0x3f \n\t"
    "inc r3 \n\t" //U8_T0L
    "brbc 1, dal \n\t"
    "inc r4 \n\t" //U8_T0H
    "dal: \n\t"
    "out 0x3f,r2 \n\t"
    "reti \n\t" ::);
    }

    OdpowiedzUsuń
  29. Tu kolego napisałeś kod, ale nie określiłeś ile taktów zajmie procesorowi jego realizacja... bywa, że długi kod ma śmiesznie krótką realizację i odwrotnie.

    OdpowiedzUsuń