Autor: Dondu
ARTYKUŁ W TRAKCIE MODYFIKACJI
CZYTASZ NA WŁASNE RYZYKO
:-)
Początkujący programiści C zapominają często, że w niektórych przypadkach zmienne globalne muszą być poprzedzone magicznym: VOLATILE
Zaletą kompilatora języka C wykorzystującego różne poziomy optymalizacji kodu jest znaczne zmniejszenie zajmowanej przez program pamięci.
Ale ponieważ nie ma nic za darmo, stąd w niektórych przypadkach program pokazany poniżej nie będzie działał prawidłowo. Dlaczego?
nonor
Nie mam już bladego pojęcia co się dzieje, bo spodziewałbym się, że program albo działa, albo nie działa...
Nie mam już bladego pojęcia co się dzieje, bo spodziewałbym się, że program albo działa, albo nie działa...
folkien
Ktoś wie w czym jest haczyk?
Ktoś wie w czym jest haczyk?
Przykład:
#include <avr/io.h> //podstawowa biblioteka AVRów #include <avr/interrupt.h> //--- Definicje zmiennych globalnych ----------------------------------- unsigned char flaga; //przykładowa zmienna //--- Obsługa przerwania z INT0 ----------------------------------- ISR(INT0_vect){ //wystąpiło przerwanie z INT0 //... jakiś kod flaga = 1; //ustaw flagę //... jakiś kod } //--- Program główny --------------------------------------------- int main(void){ // tutaj ustaw przerwanie INT0 dla swojego procesora na którym działasz // ... DDRA = 0x01; //ustaw pin z podłączoną diodą jako wyjście sei(); //włącz przerwania while(1){ //pętla główna if(flaga){ //gdy flaga zostanie ustawiona PORTA = 0x01; //zapal diodę LED } } }
Ponieważ kompilator optymalizując kod nie wie, czy i kiedy przerwanie z INT0 zostanie wykonane, stąd zmienna flaga może nie zostać prawidłowo obsłużona.
Przykład
Przyglądnijmy się kodowi wynikowemu kompilatora, czyli instrukcjom assemblera wygenerowanych przez prosty program dla dwóch przypadków zmiennej globalnej flaga:
- bez modyfikatora volatile
- z moidyfikatorem volatile
Naszym programem bazowym będzie bardzo prosty program:
#include <avr/io.h> char flaga = 0; //definicja zmiennej globalnej int main(void){ //główna funkcja programu PORTB = flaga; //zapisz zmienną do portu B PORTC = flaga; //zapisz zmienną do portu C PORTD = flaga; //zapisz zmienną do portu D }
Programy zostały skompilowane kompilatorem GCC ver. 4.3.3 dla mikrokontrolera ATmega8.
Przypadek bez modyfikatora volatile
Dla wersji powyższej, czyli bez modyfikatora volatile kompilator przygotował następujący wynikowy kod assemblera:
char flaga = 0; //definicja zmiennej globalnej int main(void){ //główna funkcja programu PORTB = flaga; //zapisz zmienną do portu B 48: 80 91 60 00 lds r24, 0x0060 4c: 88 bb out 0x18, r24 ; 24 PORTC = flaga; //zapisz zmienną do portu C 4e: 85 bb out 0x15, r24 ; 21 PORTD = flaga; //zapisz zmienną do portu D 50: 82 bb out 0x12, r24 ; 18 }
Ponieważ możesz nie znać assemblera stąd możesz posłużyć się ..... AVR INSTRUCTIO SET
ale byś tego nie musiał robić wytłumaczę po kolei instrukcje assemblera.
Zacznijmy od tego, że kompilator dla przechowywania naszej zmiennej globalnej flaga:
char flaga = 0; //definicja zmiennej globalnej
wyznaczył miejsce w pamięci SRAM pod adresem: 0x0060 (pierwszy bajt pamięci SRAM) ponieważ jest to pierwsza zmienna w naszym programie:
Nie musiał jej przypisywać wartości zero ponieważ domyślnie po resecie wartości komórek w pamięci SRAM są równe zero.
W pierwszej instrukcji assemblera mikrokontroler odczyta za pomocą rozkazu LDS (ang. Load Direct from Data Space) zawartość pamięci spod adresu 0x0060, w której jest przechowywana nasza zmienna flaga:
48: 80 91 60 00 lds r24, 0x0060
Daną tę zapisał sobie do rejestru R24 (rejestry na których bezpośrednio operuje rdzeń mikrokontrolera, czyli CPU). Następnie mikrokontroler wykonuje trzy kolejne rozkazy OUT
4c: 88 bb out 0x18, r24 ; 24 4e: 85 bb out 0x15, r24 ; 21 50: 82 bb out 0x12, r24 ; 18
zapisując kolejno do portów:
Wniosek 1
Kompilator dla zmiennej bez modyfikatora volatile dokonał odczytu zmiennej do rejestru R24 tylko raz, i następnie korzystał już z rejestru do wykonania rozkazów zapisu do portów PORTB, PORTC oraz PORTD.
Kompilator dla zmiennej bez modyfikatora volatile dokonał odczytu zmiennej do rejestru R24 tylko raz, i następnie korzystał już z rejestru do wykonania rozkazów zapisu do portów PORTB, PORTC oraz PORTD.
Zastanówmy się co by się stało, gdyby program zawierał funkcje obsługi przerwania, które modyfikowałoby zmienną flaga tak jak okazałem na początku tego artykułu. Mogłoby to wyglądać na przykład tak:
char flaga = 0; //definicja zmiennej globalnej int main(void){ //główna funkcja programu 48: 80 91 60 00 lds r24, 0x0060 4c: 88 bb out 0x18, r24 ; 24 4e: 85 bb out 0x15, r24 ; 21 //tutaj nastąpiło przerwanie i wykonanie funkcji przerwania w którym zmienna //flaga zostałaby zmodyfikowana np. ustawiona na 1 50: 82 bb out 0x12, r24 ; 18 }
Jak widzisz program nie uwzględni nowej wartości zmiennej flaga, ponieważ jest ona zapisywana w pamięci SRAM pod adresem 0x0060. Powyższy program przed ostatnim rozkazem zapisu do PORTD nie odczytuje ponownie pamięci SRAM, tylko kontynuuje zapis wartości z rejestru R24.
Wniosek 2
Program nie zauważył, że zmienna flaga została zmieniona poprzez przerwanie i nadal posługuje się starą wartością zmiennej flaga!
Program nie zauważył, że zmienna flaga została zmieniona poprzez przerwanie i nadal posługuje się starą wartością zmiennej flaga!
Dlatego w takich przypadkach można wymusić, aby za każdym użyciem zmiennej flaga była ona odczytywana i/lub zapisywana do pamięci. Służy do tego właśnie volatile dodawana na początku definicji zmiennej globalnej.
Czyli linia nr 07 powinna wyglądać tak:
volatile unsigned char flaga;
nonor
Faktycznie - zupełnie zapomniałem o volatile ...
Faktycznie - zupełnie zapomniałem o volatile ...
Krzysiu6699
Problem rozwiązany - brakowało volatile przy zmiennej ...
Problem rozwiązany - brakowało volatile przy zmiennej ...
folkien
Szok! Taka mała rzecz, a ja straciłem nad tym 1h.
Szok! Taka mała rzecz, a ja straciłem nad tym 1h.
Rady TYLKO dla początkujących:
- stosuj volatile do wszystkich zmiennych globalnych,
- stosuj volatile zawsze dla zmiennych używanych w przerwaniach i poza nimi.
Należy jednak pamiętać, że w częściach programu, w których zależy nam na szybkości zmienne globalne z użyciem volatile powodują wydłużenie kody wynikowego, przez co wolniejsze jego działanie.
Zobacz: Kurs języka C z kompilatorem CManiak online.
Więcej dowiesz się z tych książek: Książki dla Ciebie
Witam mam takie pytanie.
OdpowiedzUsuńPisząc program w języku C na Atmegę tworzę zmienną powiedzmy 'a' i zapisuję do niej wartość. Czy w mikrokontrolerze ta zmienna jest rozumiana jako rejestr uniwersalny?
Pozdrawiam
nie
OdpowiedzUsuń- stosuj volatile do wszystkich zmiennych globalnych
OdpowiedzUsuń- stosuj zawsze gdy zmienna globalna używana jest w przerwaniach
A czy drugi warunek nie jest zawarty już w pierwszym? Bo skoro do wszystkich, to i do tych używanych w przerwaniach i tych nie używanych?
Słuszna uwaga :)
OdpowiedzUsuńUsunąłem drugi punkt. Dziękuję.
"Stąd zmienna flaga może nie zostać prawidłowo obsłużona."
OdpowiedzUsuńTzn? Jak może objawić się nieprawidłowe obsłużenie zmiennej?
Tak, jak opisano w artykule - program "nie zauważy" zmiany, używając wartości zmiennej pobranej gdzieś na początku pętli, zamiast tej, którą przerwanie nadało jej gdzieś w trakcie działania programu.
UsuńCzy artykuł wciąż jest modyfikowany?
OdpowiedzUsuńDondu, jak zawsze Twoje tłumaczenia są bardzo proste ale jednocześnie wnikliwe i pokazujące sedno sprawy. Dzięki wielkie :-)
OdpowiedzUsuńCieszę się, że się przydał. :)
UsuńZamówiłem książkę "Język C dla mikrokontrolerów AVR" żeby mieć całe kompendium pod ręką, jednak na Waszą stronę dalej będę zaglądał.
UsuńAutor książki (Tomasz Francuz) publikuje na blogu artykuły uzupełniające wiedzę w jego książkach. Wszystkie jego publikacje można znaleźć w spisie treści w menu "Strefy autorów książek", a bezpośredni link to: Strefa Tomasza Francuza
Usuń