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.
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).
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.
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:
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 AVR – 8-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
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 AVR – 8-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
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ń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.
OdpowiedzUsuń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.
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ń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ń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ń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ńDlaczego switch nie powinno się stosować do przypadku, gdy tylko podstawiane różne dane w case?
OdpowiedzUsuńPopieram anonimowy komentarz wyżej... Dla początkujących są troche niejasne te zasady.. Przydałyby sie jakieś rozwinięcia.
OdpowiedzUsuń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ńpołowy punktów nie rozumie
OdpowiedzUsuńBlueDraco więcej więcej!
OdpowiedzUsuńZapowiada się ciekawy cykl artykułów...
Każdy punkt mógłby być rozszerzony o: "
OdpowiedzUsuń[...]
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.
Niech Anonim z godziny 21.50 raczy napisać, których punktów nie rozumie, a postaramy się jakoś temu zaradzić. :)
OdpowiedzUsuńZapowiada się naprawdę ciekawie.
OdpowiedzUsuń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
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ń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.
OdpowiedzUsuńW przygotowaniu są rozwinięcia i wyjaśnienia kilku punktów - proszę o cierpliwość
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ń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ń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ń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ń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.
OdpowiedzUsuń@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 ;)
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ńWitam,
OdpowiedzUsuń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ż" :).
Kr0nos - popieram. Zasady musza mieć swoje uzasadnienie.
OdpowiedzUsuń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.
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ńWitam
OdpowiedzUsuń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.
Dwa lata niedługo miną - a "rozwijanie" dalej w toku... ;)
OdpowiedzUsuńZgadzam się z wieloma głosami, że zamieszczanie takich "przykazań" BEZ UZASADNIENIA I SZCZEGÓŁOWEGO WYJAŚNIENIA mija się z celem.
Szybciej linux chodzi na Atmega8 ( http://dmitry.gr/index.php?r=05.Projects&proj=07.%20Linux%20on%208bit ) niż trwa to rozwijanie tych zasad.
OdpowiedzUsuńPytasz o "Nasze" zasady?
Moja zasada jest taka: Jak mówię, że coś zrobię, to to robię :P
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ń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ń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ę :)
OdpowiedzUsuń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" ::);
}
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ń