Witam serdecznie.
Chciałbym i ja dorzucić swoje "trzy grosze" do tej skarbnicy wiedzy jaką jest to forum i zwrócić uwagę na pewne niebezpieczeństwo związane ze stosowaniem zmiennej typu volatile większej niż 1 bajt na procesorach 8-bitowych.
Najpierw przygotuję poligon doświadczalny, którym będzie w tym wypadku programik z użyciem "timera programowego". Zainstaluję "minę" wykorzystując zmienną, która będzie zadeklarowana jako - volatile uint16_t licz_ms - i na koniec postaram się świadomie na nią nadepnąć aby pokazać, że skutki jej działania mogą się nam nie spodobać.
Ciekawostką może być to, że "zwykły" program może nigdy na taką minę nie natrafić a jeśli już to w zupełnie przypadkowym (z naszego punktu widzenia) momencie i trudno będzie wyłapać przyczynę tego błędnego zachowania. Dodam jeszcze, że aby nastąpiła eksplozja musi jednocześnie wystąpić kilka przesłanek... ale może o tym później.
Na początek poligon czyli pomigamy diodą (ja używam ATmega88, kwarc 11.0592MHz)
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Uruchamiamy program i ... nic specjalnego się nie dzieje. Dioda miga sobie leniwie co ok. 0,5s co widać na poniższym obrazku.
LED_TOG w przerwaniu
Ponieważ w "prawdziwym" programie po osiągnięciu przez licz_ms wartości 0x0000 wykonujemy z reguły coś bardziej skomplikowanego i czasochłonnego niż zmianę stanu diody uwolnijmy od tego przerwanie i wyrzućmy obsługę "migania" ze środka przerwania do pętli głównej i stwórzmy obsługę pseudo zdarzenia.
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Uwaga!!! Właśnie zainstalowaliśmy "minę"!!! Jaką "minę"? Przecież z punktu widzenia programisty nie popełniliśmy żadnego błędu.
Uruchamiamy program i ... wreszcie coś się dzieje (przynajmniej u mnie) . Dioda, która ma zmieniać stan co ok. 0,5s u mnie co trzeci takt skraca sobie pracę o ok. 250ms. co widać na poniższym obrazku.
LED_TOG w pętli głównej
Jest to właśnie efekt działania naszej "miny" ale i my możemy się nazwać szczęśliwcami, że udało się to zobaczyć. Dlaczego?
Dzieje się tak dlatego, że... ale o tym w następnym poście. Żartuję. Jednak żeby nie zanudzić postaram się o bardzo krótką analizę.
Takie błędne przełączanie diody zdarzyć się może jeżeli czas cyklu ustawiany w licz_ms ma wartość większą niż 255 (0x00FF) (tzn. gdy bardziej znaczący bajt licz_ms, który jest - przypomnę - typu uint16_t jest różny od zera). Tak jest w naszym przypadku bo 500 to 0x01F4. Ale to jeszcze za mało. Przerwanie musi nastąpić w czasie badania warunku if (!licz_ms) w pętli głównej, którego kod kompilator zamienił sobie "beztrosko" na następujący widziany z poziomu asemblera:
język asm
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Przerwanie musi trafić dokładnie w momencie pomiędzy pobraniem bajtu do r24 ale przed pobraniem bajtu do r25.
Ale i to nie wystarczy. Dopiero gdy wartość w licz_ms będzie w tym momencie równa 0x0100 spełnione zostaną wszystkie przesłanki do tego aby nasza mina eksplodowała.
Spójrzmy co się stanie. Popatrzmy na kod w asemblerze. Jak wiemy pobranie do analizy słowa 16-bitowego w mikrokontrolerze 8-bitowym musi nastąpić w dwóch cyklach i jak widzimy tak się dzieje. W chwili, o której mówimy w r24 jest już przechowywany mniej znaczący bajt zmiennej licz_ms czyli 0x00. Teraz przychodzi przerwanie, które zatrzymuje wykonywanie pętli głównej, zmniejsza zawartość naszego licz_ms o 1 i zamiast 0x0100 mamy w licz_ms 0x00FF. Pętla główna może teraz dokończyć swoje zadanie i pobiera do r25 bardziej znaczący bajt licz_ms
czyli 0x00 (a nie 0x01 jak było przed przerwaniem)!!! Teraz właśnie eksplodowała nasza mina i samowolnie skróciła czas odliczania naszego timera o 0xFF przerwań (w naszym przypadku o ok. 255ms) bo dla pętli głównej jest spełniony warunek r24=0x00 i r25=0x00 czyli licz_ms = 0x0000 i można przełączyć diodę, i załadować do licz_ms początkową wartość czyli 500 (0x01F4).
Nazwałem nas wcześniej "szczęśliwcami", że możemy zobaczyć efekt eksplozji naszej miny bo widać ile warunków musi być jednocześnie spełnionych aby to nastąpiło.
Na koniec mając już świadomość na jakie niebezpieczeństwo naraził nas kompilator nie blokując przerwań na czas analizy warunku if (!licz_ms) (bo przecież my nie popełniamy błędu z punktu widzenia sztuki programowania) zróbmy to za niego i zobaczmy czy rzeczywiście rozbroimy minę.
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Odpalamy (może niefortunne określenie w tym kontekście) program i.... dioda miga sobie równo co ok. 0,5s co widać na poniższym obrazku
LED_TOG w pętli głównej po poprawce
Jak widać istnieje realne niebezpieczeństwo użycia volatile większej niż 1 bajt na procesorach 8-bitowych (z takim kompilatorem). Im dłuższy program, więcej przerwań, rozgałęzień programu itp. tym mniejsze prawdopodobieństwo, że program natknie się na taką "minę" ale i większy ból głowy z wykryciem tego błędu. Np. po odebraniu jakiś danych przez UART tak się takty procesora ułożą, że "mina" np. zakłóci pracę jakiegoś ważnego "timera programowego" a my będziemy szukali błędu w obsłudze przerwania od UART-a. Jeżeli tych timerów jest kilka może się zacząć naprawdę niezła zabawa doprowadzająca do wniosku, że język C to g... albo, że my jesteśmy programistami do d... No, może mnie trochę poniosło ale mając teraz świadomość "zagrożenia" sami, świadomie będziemy mogli zadecydować czy to w naszym programie jest bez znaczenia, czy od razu rozbroimy tę minę.
Pozdrawiam wszystkich saperów a szczególnie Sapera Wszystkich Saperów - Mirka.