Spotkałem się już z bardzo wieloma próbami wytłumaczenia początkującym, czym jest? i jak działa specyfikator volatile w języku C. Przyznam, że niektórzy tłumaczą to arcy-pokrętnie lub w sposób tak uproszczony, że niewiele z tego wynika. Postaram się tutaj przedstawić wyjaśnienie przygotowane przeze mnie oraz podać przykłady kodów źródłowych żeby to omówić. Uważam, że szczególnie w aspekcie mikrokontrolerów tak w ogóle, jest o wiele łatwiej to wyjaśnić niż na przykładach programowania na komputerach PC. Spotkałem na swojej drodze, wiele osób, które pomimo, tego że programują w C lub C++ na PC to dopiero po tak szczegółowych wyjaśnieniach, przyznały, iż w końcu, po kilku latach dotarło do nich tak na prawdę w czym rzecz.
Przejdźmy zatem do konkretów. Żeby to dobrze przedstawić i udokumentować, przygotowałem króciutki kod programu w C dla AVR. Program ten nie ma żadnego sensu tak na prawdę, jednak nie chcę tak jak inni podchodzić do tego czysto teoretycznie. Poznajmy zatem założenia tegoż "programu"
- tworzymy zmienną globalną o nazwie licznik, która będzie inkrementowana (zwiększana) w przerwaniu
- w głównej funkcji main definiujemy oraz inicjalizujemy dwie zmienne pomocnicze b oraz c
- tworzymy pętlę nieskończoną while(1) { }
- cyklicznie zwiększamy zmienną b za pomocą prostego działania b = c + licznik
- za pomocą warunku IF sprawdzamy czy zmienna b jest już większa niż 200
- jeśli tak? to zerujemy zmienną i wystawiamy na PORTA wartość 0 oraz ustawiamy wartość zmiennej licznik na 2 i wysyłamy ją na PORTB
- tworzymy procedurę przerwania INT0 w którym dokonujemy tylko jednej operacji - zwiększamy za każdym razem gdy wystąpi wartość zmiennej licznik o jeden.
Tak jak wspominałem wyżej, program pozbawiony jest jakiegoś specjalnego sensu - jednak chciałem tak go ułożyć aby po pierwsze, w wyniku optymalizacji -Os żadne jego fragmenty nie zostały usunięte. Po drugie żeby można było prześledzić pewne charakterystyczne miejsca kodu w skompilowanej wersji do asemblera, co za chwilę pokażę. Przypominam jeszcze raz - ta wersja kodu zawiera specyfikator volatile przed definicją zmiennej licznik.
Zauważ proszę, że mamy tutaj do czynienia z klasycznym przypadkiem, gdzie wszystkie uproszczone wyjaśnienia podają informację, że:
ZAWSZE GDY KORZYSTAMY Z JAKIEJŚ ZMIENNEJ GLOBALNEJ ZARÓWNO W ZWYKŁYCH FUNKCJACH (W TYM MAIN()) ORAZ W PROCEDURACH OBSŁUGI PRZERWAŃ, NALEŻY BEZWZGLĘDNIE OPATRYWAĆ DEFINICJĘ TAKIEJ ZMIENNEJ SPECYFIKATOREM VOLATILE !
I bardzo dobrze. To na pewno warto sobie zapamiętać ale warto także spojrzeć teraz na kod w asemblerze, żeby zobaczyć co się dzieje z taką zmienną volatile. Proszę bardzo poniżej kod po kompilacji:
Przedstawiłem czerwonym kolorem opisy tego co dzieje się ze zmienną licznik opatrzoną specyfikatorem volatile. Zauważ, że w kodzie asemblera pętla while rozpoczyna się za każdym razem od wczytania wartości komórki pamięci RAM, która przechowuje wartość naszej zmiennej licznik. Podobnie rzecz się ma w procedurze obsługi przerwania, zawartość komórki czyli naszej zmiennej licznik wczytywana jest do rejestru procesora o nazwie R24, bo w asemblerze tylko na rejestrach można dokonywać operacji dodawania bądź odejmowania i innych. Za pomocą rozkazu asemblera subi , nasza zmienna jest inkrementowana czyli zwiększana za każdym razem gdy wystąpi przerwanie. (oczywiście w programie głównym ze względu na przejrzystość pominąłem oczywiście inicjalizację tego przerwania).
Jakie można wysnuć wnioski z tego co widzimy? Hmm pomyślisz zapewne - jakież tu wnioski wysuwać - wszystko dzieje się tak jak należy. Czyli zarówno w przerwaniu jak i w funkcji main() za każdym razem gdy mamy dostęp do zmiennej licznik, to w asemblerze wykonywane jest odpowiednio załadowanie wartości tej zmiennej do jakiegoś rejestru (tutaj akurat R24), a następnie jeśli wartość się zmieniła w yniku jakichś obliczeń to rejestr znowu jest zapisywany do komórki pamięci przechowującej naszą zmienną. Mamy zatem wciąż jej aktualną wartość niezależnie czy działania na niej wykonywane są w przerwaniu czy też w jakiejkolwiek funkcji.
Dobrze w takim razie spójrzmy co się stanie gdy pozbędziemy się specyfikatora volatile ;). Najpierw kod programu w C. Praktycznie taki sam poza tym, że znika volatile.
Spójrzmy zatem co się stało w kodzie asemblera po kompilacji i dlaczego będziemy mieli tutaj poważne problemy bez tego specyfikatora.
main() 0000007c
Czy widzisz co się stało niebezpiecznego ??? Zmienna licznik, z komórki pamięci RAM o adresie 0x0060 została załadowana TYLKO RAZ do rejestru tym razem R18, na początku asemblerowej funkcji main(), jeszcze przed nieskończoną pętlą while(1) { } !!!!!!!
pokażę to w jeszcze bardziej widoczny sposób poniżej:
Zatem teraz pętla while w trakcie działania operuje już tylko i wyłącznie na wartości w rejestrze R18 nie "zaglądając" ponownie za każdym razem przy starcie pętli jak to było wyżej, gdy użyliśmy volatile. Co więcej, wartość rejestru R18 jest zmieniana w tejże pętli - widzisz linijkę: ldi r18, 0x02 !!!! Ale czy ta zmiana wartości została wpisana pod adres pamięci RAM gdzie znajduje się nasza zmienna licznik ???? O nie !!!
Co więcej, w przerwaniu zmienna licznik jest sobie prawidłowo zwiększana o jeden, ale czy nasza pętla główna ma tego świadomość ????? Oj nie !!! .... przecież wartość licznika została wczytana do rejestru R18
tylko raz i to przed rozpoczęciem się pętli głównej.
DLATEGO BEZ SPECYFIKATORA VOLATILE WYSTĄPIĄ BŁĘDY W KAŻDYM PROGRAMIE Z WŁĄCZONĄ OPCJĄ OPTYMALIZACJI GDY ZMIENNA BĘDZIE WYKORZYSTYWANA ZARÓWNO W FUNKCJACH PROGRAMU JAK I PRZERWANIACH.
Zapamiętaj to sobie na całe życie ;)
Być może zadajesz sobie teraz pytanie - "No dobrze, wiem dlaczego tak się dzieje w takich przypadkach, ale po jakiego grzyba kompilator dokonuje takich cudactw ???"
To nie żadne cudactwa albo co gorsza błędy kompilatora - jak zwykło pisać wiele osób na różnych forach. To bardzo ale to bardzo pozytywna cecha języka C. Na czym ona polega?
Otóż jak zauważyłeś, włączenie optymalizacji, spowodowało przede wszystkim skrócenie programu o kilka bajtów !!! Masz pierwszą zaletę ;) ale jest jeszcze druga, ważniejsza. Jak sobie porównasz obydwa kody to zobaczysz, że w pierwszym gdzie użyliśmy volatile wykonywanych było więcej instrukcji asemblera, które wykonywane są w dwóch cyklach zegara procesora. To właśnie te, gdzie następował odczyt lub zapis do pamięci RAM naszej wartości. W drugiej wersji takich instrukcji nie zauważamy prawda ? Dzięki czemu poza zmniejszeniem objętości programu zyskujemy jeszcze na szybkości jego wykonywania !!!!
Teraz już chyba widzisz, że to czy użyjemy specyfikatora volatile czy nie - ma wpływ nie tylko na prawidłowe działanie programu w takim przypadku ale także na to, że możemy pozbyć się optymalizacji dostępu do wybranych zmiennych jeśli jest taka konieczność. Jeśli jej nie ma - to wyobraź sobie ile dobrego dla nas programistów czyni kompilator, który domyślnie przy włączonej maksymalnej optymalizacji przyśpiesza jak tylko może wykonywanie najróżniejszych fragmentów programu.
Żeby to ostatecznie unaocznić proponuję zobaczyć na jeszcze prostszy program w C. Tak prosty, że niektórzy się dziwią z jakiego powodu kompilator po dokonaniu kompilacji w ogóle pomija ten kod i w asemblerze go nie widać.
I teraz najciekawsze, ponieważ przy włączonej optymalizacji, funkcja main w asemblerze wygląda tak ;) :
Czyli proszę, kod pętli for() zniknął. Dlaczego? bo optymalizator uznał, że kod ten nie ma wpływu na nic ważnego w programie co miałoby wpływ na jego dalsze działania. Pozbył się więc go jak śmiecia. Dlatego, niektórzy początkujący dochodzą do wniosku, że to jednak jakiś błąd kompilatora i wyłączają bezmyślnie optymalizację -Os. Ponieważ gdy jest wyłączona to kod asemblerowy tej funkcji już wygląda pokaźnie, co oznacza, że kompilator posłusznie skompilował wszystkie wymysły i niepotrzebne rzeczy napisane przez początkującego.
Ale UWAGA !!! nie zrozum mnie źle, wcale to nie oznacza, że nigdy nie należy korzystać z wyłączania optymalizacji czyli z wyboru poziomu -O0 !!! Czasem bywa to konieczne, ale nie będę już tego omawiał z uwagi na to, że to całkiem inny temat.
Za to podpowiem ci, że wcale nie trzeba w tym konkretnym przypadku wyłączać optymalizacji aby uzyskać ten sam efekt. Wystarczy bowiem, że zmienne i oraz a opatrzysz specyfikatorem volatile i uzyskasz po kompilacji dokładnie ten sam efekt. Czyli kod asemblerowy funkcji main() będzie wyglądał podobnie do powyższego. Jak widzisz, na takim przykładzie pokazałem , że nasz specyfikator volatile miewa jeszcze ważne znaczenie nie tylko w tych przypadkach jakie są opisywane popularnie na wielu blogach czy forach.
Tym razem jednak możesz zadać spokojnie pytanie, o jakiś konkretny przykład konieczności takiego odmiennego użycia specyfikatora volatile. Proszę bardzo, mogę przywołać przykład, w którym zechcemy użyć jako argumentu jakiejś tam funkcji nazwy dowolnego portu aby funkcja mogła wykonać na nim stosowne operacje. Jak zapewne wiesz, rejestry portów znajdują się tak na prawdę w przestrzeni adresowej pamięci RAM ale nie w obszarze gdzie umieszczane są zmienne lecz wcześniej. Z tego punktu widzenia logicznym wydaje się fakt, że odwoływanie się więc do portów jako do komórek pamięci RAM nie może w żadnym wypadku podlegać procesom optymalizacji. Zresztą już sam kompilator zaopatruje definicje portów specyfikatorem volatile, aby w przyszłości programista poprzez wyłączenie optymalizacji nie narobił sam sobie szkód w tym zakresie. Zatem definicja takiej funkcji musi wyglądać jak poniżej:
Czyli przy włączonej optymalizacji -Os możemy spokojnie przekazać wskaźnik do komórki/rejestru portu ale zmuszeni wręcz jesteśmy opatrzyć ów argument specyfikatorem volatile !!! Gdybyśmy wyrzucili volatile i spróbowali skompilować kod, natychmiast otrzymamy ostrzeżenie:
Naturalnie, znowu wyłączenie całkowicie optymalizacji także pozwoli nam skompilować taki kod bez volatile, ale teraz już chyba wszystko rozumiesz doskonale. Mam taką nadzieję. Jeśli tak, to postaraj się ocenić jakoś ten post dodając swój komentarz pod spodem.
Sporo innych i ciekawych rzeczy w tym zastosowań praktycznych znajdziesz jeszcze w książce "Mikrokontrolery AVR Język C Podstawy programowania". Jest tam m.in bardzo ciekawa funkcja o nazwie Superdebounce, do absolutnie nieblokującej obsługi klawiszy. Wykorzystuje ona właśnie volatile w taki sposób.
Nigdy nie myślałem o volatile w takim kontekscie i czesto przyprawiało mnie o mdłosci wachlowanie ustawieniami optymalizacji - zaś sie człowiek czegos nowego nauczył. Ciekawe czy sie da przenieśc te metodę na C dla STM32 zaraz zerkne
OdpowiedzUsuńNo a mnie akurat volatile zainteresowało znowu dlatego mocniej, bo też nie raz, na elektrodzie albo na innych blogach, widziałem tak pokrętne tłumaczenia tego zagadnienia, że lepiej byłoby gdyby ci doradcy w ogóle nic nie pisali zamiast mieszać innym, a szczególnie początkującym w głowach....
OdpowiedzUsuńNo muszę przyznać, że w końcu w pełni zrozumiałem znaczenie volatile :)
OdpowiedzUsuńAle... widzę w tekście kilka błędów :) Na pierwszym i drugim listingu kodu C używasz zmiennej a, która nie jest zadeklarowana :) I na trzecim listingu kodu w tym języku używasz volatile przy deklaracji zmiennych, a otrzymujesz kod asemblera zoptymalizowany! I jeszcze później w tekście napisałeś, że nie trzeba wyłączać optymalizacji, wystarczy umieścić volatile! Popraw te byczki jak najszybciej :D
Jakie byczki. Owszem rzeczywiście wcięło mi definicję zmiennej a. Jednak ona jest tylko w roli FANTOMA, i raczej przy omawianiu takiego zagadnienia to chyba każdy sobie dopisze we własnej wyobraźni jej definicję, co zdaje się że także kolega zrobił, skoro zauważył jej brak a pomimo to zrozumiał volatile.
OdpowiedzUsuńZa to nie rozumiem końcówki wypowiedzi. Bo reszta się zgadza co do tego że wystarczy volatile i nie trzeba wyłączać optymalizacji. Ja pokazałem tylko przykład i raczej zachęcam do wykonania własnych prób. Bo te kody mają tylko pokazać samą ideę jak to można w prosty sposób sprawdzać.
Reasumując - chciałbym poprawić i dodać te definicje zmiennej a jeśli tak kolegę drażni ich brak. Ale coś mi się skopało na tym bloggerze z SyntaxHighlighter i teraz w ogóle nie mogę opublikować poprawek. Musiałbym cały post na nowo pisać i przypominać sobie jak od samego początku tego syntaxhighlightera skonfigurować. A nie mam na to przynajmniej teraz siły i czasu - tym bardziej, że merytorycznie przez takie drobnostki artykuł nie traci na wartości.
Witam,
OdpowiedzUsuńchcę zadeklarować strukturę z kilkoma zmiennymi, która będzie użuwana i w przerwaniach i w main().
np:
struct xyz_float_t
{
volatile float x;
volatile float y;
volatile float z;
};
to z tego co rozumiem argumenty mają mieć specyfikator volatile w deklaracji, a coz samą deklaracją struktury czy przednią też należy umieścić specyfikator volatile?
np:
volatile struct xyz_float_t Buff_float;
pozdrawiam
W takich wypadkach polecam zawsze skorzystanie z utworzenia nowego typu, za pomocą dyrektywy typedef
OdpowiedzUsuńczyli np:
typedef struct {
volatile float x;
volatile float y;
volatile float z;
} _XYZ_FLOAT_T;
a potem co za problem zdefiniować już całą strukturę ze specyfikatorem volatile o tk:
volatile _XYZ_FLOAT_T xyz_float_t
i proszę bardzo masz już xyz_float_t w postaci volatile w całości. ;)
No dobra, a jeśli nie chcę tworzyć nowego typu to można deklarować w ten sposób
OdpowiedzUsuńvolatile struct xyz_float_t Buff_float; ?
I w zasadzie chodziło mi o to w pytaniu czy powinno się tak robić czy wystarczą tylko skaryfikatory volatile przy definicji elementów struktury?
pozdrawiam
Nie wiem zabardzo o co ci chodzi? Wytłumacz mi jaki jest sens dawania specyfikatora przy każdej zmiennej wewnątrz struktury jeśli to samo wygodniej i szybciej i dodatkowo bardziej uniwersalnie uzyskasz za pomocą stworzenia typu i zdefiniowania nowej zmiennej tego typu ze specyfikatorem volatile. No czym to się różni? Bo jak na razie to tylko słyszę, że ty nie chcesz tworzyć nowego typu. Może to wynika z tego, że nie widzisz zalet takiego postępowania czy jak ? nie rozumiem
OdpowiedzUsuńNo właśnie chodzi mi o to, że nie wiem czy muszę dodawać specyfikator przed definicją każdej zmiennej w strukturze.
OdpowiedzUsuńCzy wystarczy po prostu dodać przed deklaracją danej struktury (bez korzystania z tworzenia nowego typu) volatile?
Czyli tak jak już napisałem:
volatile struct xyz_float_t Buff_float; ?
i czy wtedy każdy składnik struktury będzie mógł być używany w przerwaniach i main?
pewnie, że wystarczy ale powtarzam ci, że tak się zwykle nie robi. I nie dlatego że nie będzie działać, bo musi działać w końcu to jest najnormalniejsza definicja zmiennej/struktury i volatile zadziała tak samo. Zrozum, stworzenie typu daje ci większą swobodę i możesz nawet powołać do życia szybko jeszcze inną taką strukturę. Coś się tak uparł ? Ale rób jak chcesz ;) Na siłę nikomu nie uda się nic wytłumaczyć .... Kiedyś zrozumiesz.
OdpowiedzUsuńW końcu
Dobra jeszcze raz dla upewnienia się.
OdpowiedzUsuńDefiniuję strukturę:
struct xyz_float_t
{
float x;
float y;
float z;
};
Deklaruje strukturę wcześniej zdefiniowaną:
Jako, że chcę używać jej i w przerwaniach i w main() dodaję volatile.
volatile struct xyz_float_t Buff_float;
I teraz mogę jej składniki używać i w przerwaniach i w main.
Ty proponujesz natomiast by w definicji stworzyć od razu nowy typ;
typedef struct {
float x;
float y;
float z;
} _XYZ_FLOAT_T;
Deklaracja struktury:
volatile _XYZ_FLOAT_T Buff_float;
Powyższe struktury będą działać poprawnie i w przerwaniach i w main? Różnica natomiast będzie to, że mam nowy typ i nie dodaję za każdym razem struct?
Bardzo dobrze to zrozumiałeś i dokładnie będzie tak jak napisałeś. Dokładnie ;)
OdpowiedzUsuńDziękuję za pomoc:)
OdpowiedzUsuńChyba przejdę na typdef być może skoro większość osób z tego korzysta jest to wygodniejsze:)
pozdrawiam
w listingu nr 3 w języku C jest błąd. Zmienne i oraz a są opatrzone volatile, a kod asemblerowy ich nie uwzględnia. O błedzie świadczy też fakt, że później jest fragment
OdpowiedzUsuń"Za to podpowiem ci, że wcale nie trzeba w tym konkretnym przypadku wyłączać optymalizacji aby uzyskać ten sam efekt. Wystarczy bowiem, że zmienne i oraz a opatrzysz specyfikatorem volatile i uzyskasz po kompilacji dokładnie ten sam efekt."
A te zmienne są już przecież opatrzone tym specyfikatorem :)
Panie Anonimowy, proponuję przeczytać jeszcze raz i na spokojnie to wtedy lepiej ci się rozjaśni o czym piszę, dlaczego tak a nie inaczej i dlaczego nie ma błędu tam gdzie próbujesz się go dopatrzeć. W tym cytacie wszystko się zgadza co do joty. Tylko jak pisałem, trzeba uważniej a może drugi albo trzeci raz to przeczytać.
OdpowiedzUsuńOwszem, cytat jest w porządku. Ale panuje nieścisłość w stosunku do tego co było wcześniej w artykule. I tak, jest tam błąd. Pozwól, że ja ci to rozjaśnię:
OdpowiedzUsuńcytat:
#include ;
int main(void) {
volatile uint8_t i, a;
for(i=0; i<10; i++) a++;
}
I teraz najciekawsze, ponieważ przy włączonej optymalizacji, funkcja main w asemblerze wygląda tak ;) :
0000006c :
6c: 80 e0 ldi r24, 0x00 ; 0
6e: 90 e0 ldi r25, 0x00 ; 0
70: 08 95 ret
koniec cytatu.
Otóż kod wcale nie będzie tak wyglądał. Będzie jeśli nie usuniemy tamto volatile przed zmiennymi a,i. Tylko na to chciałem zwrócić uwagę. Pozdrawiam
Poprawka: na końcu ma być "Będzie jeśli usuniemy tamto volatile przed zmiennymi a,i"
OdpowiedzUsuńNo dobrze można to uznać za nieścisłość - ale po prostu to co jest w kodzie opisałem słowami. Bo inaczej to po raz kolejny musiałbym podać jeszcze raz taki sam kod tyle że bez tego volatile przed tymi zmiennymi. Za to dalej piszę:
OdpowiedzUsuń"Wystarczy bowiem, że zmienne i oraz a opatrzysz specyfikatorem volatile i uzyskasz po kompilacji dokładnie ten sam efekt."
i jak dotąd raczej każdy to rozumiał. Chociaż może i jak teraz patrzę i czytam - to być może masz rację, że warto było podać ten pierwszy przykład bez volatile a później tylko o tym wspomnieć, żeby je dodać.
Tak czy inaczej cieszę się, że to zrozumiałeś.
Właśnie o to mi chodziło co napisałeś. I oczywiście zrozumiałem. Bardzo ciekawy artykuł, dla mnie nowością była ta kwestia optymalizacji. Odwalasz kawał dobrej roboty. Pozdrawiam :)
OdpowiedzUsuńNo dziękuję, wprawdzie mam zawsze spore kłopoty z tym poprawianiem kodów programów na blogu bo to wymaga przypominania sobie obsługi syntaxhighligtera, który też czasem przestaje mi działać - to jednak udało mi się i w takim razie zdjąłem to volatile z miejsca o którym pisałeś.
OdpowiedzUsuńWitam,
OdpowiedzUsuńNa prawdę świetne wytłumaczenie. Mam tylko jedno pytanie gwoli uściślenia rozmowy z Anonimowym.
mirekk36 napisał
W takich wypadkach polecam zawsze skorzystanie z utworzenia nowego typu, za pomocą dyrektywy typedef
czyli np:
typedef struct {
volatile float x;
volatile float y;
volatile float z;
} _XYZ_FLOAT_T;
a potem co za problem zdefiniować już całą strukturę ze specyfikatorem volatile o tk:
volatile _XYZ_FLOAT_T xyz_float_t
a później przyznałeś rację anonimowemu w jego tekście:
typedef struct {
float x;
float y;
float z;
} _XYZ_FLOAT_T;
Deklaracja struktury:
volatile _XYZ_FLOAT_T Buff_float;
stąd moje pytanie, czy zmienne wewnątrz struktury muszą także być volatile? czy wystarczy tak jak w tym co Anonimowy napisał, że zdefiniowany typ wewnątrz ma zmienne bez volatile a przy deklaracji dodaje się volatile
Eeeeh za dużo tu tych anonimowych i widzę że teraz to problem do którego kto się odniósł. ;)
OdpowiedzUsuńJeszcze raz powiem, lepiej i wygodniej użyć typedef i zrobić całą strukturę jako volatile. Przecież przyznałem mu rację w tym podsumowaniu, które napisał dokładnie tak samo jak ja. Nie potrzeba i bez sensu jest wręcz wstawianie przy każdej zmiennej w strukturze specyfikatora volatile. Dlatego Panowie - ostatecznie jeszcze raz podaję dobry przepis i korzystajcie z niego:
typedef struct {
float x;
float y;
float z;
} _XYZ_FLOAT_T;
Deklaracja struktury:
volatile _XYZ_FLOAT_T Buff_float;
i tu przyznałem rację, bo to były moje wcześniejsze słowa. Widzisz tu gdzieś volatile w polach struktury ? Nie. Volatile poprzedza tylko już dalej definicję zmiennej.
Proponuję jednak takie pytania zadawać na forum, bo na blogu strasznie ciężko się na nie odpowiada, tym bardziej jak pyta stu anonimowych ;)
właśnie o to doprecyzowanie mi chodziło.. bo w komentarzu z Nov 26, 2011 05:23 AM umieściłeś przy zmiennych w strukturze volatile i stąd się wzięła moja niepewność. Niemniej jednak jeszcze raz wielkie dzięki za wytłumaczenie i za świetny artykuł. W razie kolejnych pytań odezwę się na forum.
UsuńPozdrawiam Hubal
Dla mnie świetnym, zwięzłym wytłumaczeniem kwalifikatora typu volatile jest ten z książki S. Prata "język C"
OdpowiedzUsuńPiszę tam, że volatile używamy przy zmiennych ulotnych, które są zmieniane, nie przez sam program, ale przez czynniki zewnętrzne, a przez to, że kompilator może optymalizować występowanie w wielu miejscach zmiennej, przy braku volatile buforuje ją w jednym miejscu by skorzystać z niej w innym, ale z bufora właśnie, a nie samej wartości zmiennej w danym momencie. Volatile powoduje, że zawsze wartość jest brana z zmiennej bez względu na to ile razy występuje ona w programie.
Tak to jest bardzo dobre wytłumaczenie - i to niejako podstawa - którą zawsze podaję w podobny sposób - ale niestety prawda jest taka, że rzadko do kogo to dociera. Nie każdy tak samo dobrze to odbiera. Poza tym sporo osób interesuje się tym jak to się dziej od spodu. Ja przyznam że mnie osobiście nigdy absolutnie nie wystarczało jakieś określenie o ulotności - bo gdyby się na tym miało skończyć to bym się poczuł jak podczas oglądania filmu fantasy.
UsuńZdecydowanie wolę zobaczyć jak to się przekłada na asembler - bo tu na bardzo prostych przykładach widać to jak na dłoni.... i to przekonuje ostatecznie co to jest volatile ;)
Czy w takim kodzie zmienna zmien powinna być volatile?
OdpowiedzUsuńchar zmien = 0;
int main(void)
{
inicjuj();
while (1)
{
if (/* cos sie stalo */)
zmien = 1;
}
}
ISR (jakis timer)
{
if (zmien)
{
zmienia wyjscia
zmien = 0;
}
}
Ja powiem tak - tzn przypomnę taką PODSTAWOWĄ i uproszczoną ale ZAWSZE aktualną zasadę.
UsuńZAWSZE ale to ZAWSZE - jeśli korzystamy ze zmiennej zarówno w przerwaniu jaki i w dowolnej funkcji programu - TRZEBA użyć specyfikatora volatile.
Więc jak by kolega sobie teraz sam odpowiedział ? Podpowiadam - tak w takim kodzie też trzeba zgodnie z tą regułką, którą przypomniałem wyżej OK ? ;)
A konkretnie co się stanie w tym przypadku, jeśli ta zmienna nie będzie "volatile"?
OdpowiedzUsuńNo ale zobacz - opisałem TO dokładnie w artykule wyżej a ty pytasz co się stanie. Żeby to zrozumieć od środka - jeśli cię to interesuje to trzeba spojrzeć na te kody asemblerowe - czyli trzeba znać chociaż troszkę.
UsuńW uproszczeniu - pętla gówna będzie działała tak jakby na innej zmiennej a przerwanie na innej i nie będą się poprawnie wykonywały twoje IF'y itd itp
No ale przerwanie zgłasza się od czasu do czasu, więc nie może trzymać zmiennej w rejestrach procesora wtedy, kiedy się nie wykonuje, czyli procedura przerwania musi przeczytać właściwą zmienną, nie?
OdpowiedzUsuńA, i jeszcze nie napisałem, że main woła inne procedury.
W przerwaniu to się najczęściej nie ma co martwić tam prawie zawsze nastąpi dostęp do komórki RAM, ale w main w pętli głównej będzie twoja "zmienna" wrzucona do jakiegoś REJESTRU procesora (musisz spróbować przeanalizować to na górze) ... i będzie widoczne TYLKO to co w tym rejestrze a nie w komórce pamięci.
UsuńNo ale jak main woła inną procedurę, to nie może zostawić danej w rejestrze, nie?
OdpowiedzUsuńA co ma wspólnego jedno z drugim ? No będzie jeszcze gorzej, w mainie wywołasz sobie jakąś funkcję to teraz w zależności od jej konstrukcji (jeśli bez pętli while) to oczywiście odczyta dane z RAM, ale jeśli też będzie tam jakaś pętla to podobnie - dana do rejestru. Chociaż tak na prawdę - przy wywołaniu innej funkcji być może kompilator zrzuci wartość w main z rejestru do RAM aby funkcja mogła odczytać - tyle że zauważ - zaczynamy rozmawiać CO BY BYŁO GDYBY? Po co ? Chcesz wiedzieć jak działa volatile i jakie są zasady postępowania z nim ? czy chcesz próbować szukać dróg obejścia.
UsuńVolatile de facto powoduje ominięcie optymalizacji dla zmiennej, więc to nie oznacza że bez volatile zawsze będzie ona tak samo w kodzie optymalizowana niezależnie od warunków i wielu innych wywołań, procedur itp - toż to się zmienia jak w kalejdoskopie po każdej kompilacji zmienionego programu. I co ? teraz będziemy po kolei rozważać 1000000 przypadków "a co by było gdyby kod był napisany tak? albo inaczej?" - jeśli ciebie to interesuje to NIC Nie stoi na przeszkodzie abyś sobie sprawdzał jak będzie reagował kompilator.
Po kompilacji masz do dyspozycji plik z rozszerzeniem *.lss czyli kod asemblerowy - więc prześledź sobie co w każdym z interesujących cię przypadków będzie się działo. Ja podałem w artykule wyżej najbardziej dobitny przypadek, który ładnie trafia do świadomości i pokazuje co się może stać - masz to na obrazkach z kodem w asemblerze i strzałkami co stanie się ze zmienną po optymalizacji ....
więc bez asemblera - rozważania na ten temat nie mając celu - co będzie gdy to? co będzie gdy tamto?
Chodzi o to, że wiadomo, że main zapisze zmienną do pamięci, a przerwanie musi ją odczytać z pamięci.
OdpowiedzUsuńWłaśnie chcę wiedzieć, jak działa volatile i kiedy NIE trzeba tego stosować. I tu chyba nie trzeba, nie?
Nie piszesz też nic o używaniu volatile, kiedy ze zmiennej korzystają dwa przerwania.
No i dawaj znowu to samo piszesz - toż pokazałem i mówię ci że jeśli nie masz volatile to zmienna w main może nigdy nie być aktualizowana w RAM - wszystko zależy od kontekstu kodu. Kurczę - bo cały czas wymijająco mi tu piszesz - oglądałeś ten rysunek wyżej w asemblerze ????????????? z tą strzałką fioletową i pomarańczową ??????? nawet bez znajomości asemblera opisałem do czego może dojść. Czy do ciebie nie dociera że MOŻE do tego dojść ale nie zawsze MUSI. Czyli co będziesz z uporem godnym podziwu szukał kiedy nie musi ??? sorki ale to chore podejście. Masz WYRAŹNĄ I PROSTĄ zasadę:
UsuńJEŚLI ZMIENNĄ WYKORZYSTUJEMY I W PRZERWANIU I W PROGRAMIE TO OPATRUJEMY JĄ SPECYFIKATOREM VOLATILE.
albo się do tego jako początkujący i nie znający asemblera stosujesz albo nie. Twoja sprawa. A jak chcesz wiedzieć dokładniej - to jednak może umówmy się tak - czytaj to co napisałem wyżej i zadawaj pytania do tego czego nie rozumiesz to będę rozjaśniał aż zaskoczysz całość. A nie takie wyrwane wciąż z kontekstu pytania i na końcu powtarzasz je od nowa :(
Jeśli masz zmienną która ma być używana w dwóch przerwaniach a w pętli głównej i w programie (innych funkcjach NIE) to teoretycznie nie musisz stosować volatile, ale ja nie przewidzę wszystkich konstrukcji przerwań i ich konfiguracji np z parametrem ISR_NOBLOCK - więc - warto użyć volatile.
Mam pytanko, porównując bascoma do c, czy bascom ze swoim "dim zmienna as byte", to właśnie analogiczne volatile?
OdpowiedzUsuńCzyli kazda operacja na zmiennych to odczyt z RAMu do rejestru, operacja i zapis rejestru do RAMU, nawet w obrębie tej samej funkcji?
Po pierwsze Bascoma i C można porównywać tylko hmmm może w kilku i to jakichś najprostszych aspektach. Przy czym wcale nie wnikam tu w ocenę który lepszy a który gorszy. Jeśli chodzi o volatile to POD ŻADNYM pozorem nie da się tego porównać do żadnego mechanizmu w Bascomie bo też jest CAŁKOWICIE inna idea samej kompilacji ... Więc proponuję - nie iść nawet tu w porównania bo zabrniemy w totalnie ślepy zaułek.
UsuńJeśli chodzi o drugie - to zarówno w C jak i Bascomie i tak całość jest tłumaczona do asemblera a tam na dole często już wiele operacji wykonywanych na rejestrach .... ale przyznam że nawet w C nie wnika się w to za bardzo bo nie potrzeba.
Tak więc polecam odejść od porównań bo to często nawet przeszkadza np w nauce języka C jeśli próbujemy przejść z Bascoma. Można to zaobserwować na naszym forum w niektórych dyskusjach .... Zwykle i tak na końcu ktoś sam dochodzi że czym dalej w las - ucząc się C - to trzeba zrezygnować z porównań - a uczyć się po kolei C. To polecam.
Nie, nie chodzi mi o porównanie obu kompilatorów, tylko bardziej o informacje, co wyprawia bascom ze zmiennymi i jak to wpływa na szybkość kodu.
OdpowiedzUsuńAkurat w powyzszym przypadku zapytałem o to, czy dzialajac na zmiennych bascom tak kompiluje program, że procesor wykonuje operacje w ten sam lub podobny sposób, co C ze swoim volatile.
arc.
Trzeba zrozumieć, że kompilacja w Bascomie jest prosta i jednoprzebiegowa w odróżnieniu od wielu etapów kompilacji w C. Oczywiście to że jedno-przebiegowa to nie chcę przez to oceniać czy gorsza czy lepsza, nie. Chodzi raczej o to, że twórca Bascoma podszedł do tego całkowicie inaczej i dlatego nie ma tu takich samych mechanizmów jak w C ani nawet wiele nie jest podobnych.
UsuńCo Bascom wprawia ze zmiennymi ? ;) tego się nawet nie da porównać - no chyba że napiszesz sobie prosty kod do migania diodą LED i porównasz żmudnie obydwa programy w asemblerze po kompilacji. Ale co da ci taki przykład ? NIC :( ....
Bascom kompiluje że tak powiem całe duże fragmenty gotowych własnych bibliotek wcześniej przygotowanych wprost w asemblerze właśnie. Nie wiem czy wiesz ale biblioteki Bascoma nie są pisane w Bascomie a w asemblerze ;) .... sam pisałem takie to wiem ....
Reasumując - w Bascomie nie dowiesz się co się dzieje w środku bo to tzw zamknięte czarne skrzynki procedur gotowych napisanych w asemblerze
zaś w C jestem w stanie wiele ci pokazać i omówić co po kolei z czym się dzieje - i tym różnią się te języki programowania. Każdy ma jednak swoje wady i zalety ;)
Witam
OdpowiedzUsuńBardzo dziękuję za ten wpis, sprostował kilka niejasnych dla mnie kwestii.
Mam jednak pytanie co do ilości zmiennych opatrzonych specyfikatorem volatile, czy jest jakiś górny limit tworzenia zmiennych globalnych obsługiwanych zarówno w main() jak i w ISR'ach?
np.
volatile char a[12];
volatile uint8_t licznik=10;
volatile char tekst[100];
i jeszcze mnóstwo innych zmiennych?
Z tego co zauważyłem taka zmienna nie zwiększa w pamięci RAM swojej objętości z np. 1 bajta do 2, natomiast ilość rejestrów w procesorze jest ograniczona prawda?
Tytułem uzupełnienia ;)
UsuńNie ma żadnego ograniczenia i nie ma obaw że zabraknie rejestrów, bo przecież nie wszystkie zmienne są NARAZ w jednym momencie używane i tu może sobie kompilator optymalizować "do woli" ... ale...
jest jedno małe ale ...
Absolutnie NIE ZALECA się tak na zapas opatrywania WSZYSTKICH zmiennych globalnych specyfikatorem volatile ;) PO CO ? toż to spowalnia procesy - uwierz mi że optymalizacja jest potrzebna. Mówię tu o tych które - wydaje mi się że chcesz "na ZAPAS" dawać jako volatile ...
jest prosta ZASADA, jeśli zmiennej używasz TYLKO w przerwaniu - nie dawaj volatile
Jeśli zmiennej używasz tylko w funkcjach a nigdy w przerwaniu to też NIGDY nie dawaj volatile
OK ... ? rozumiesz
a teraz druga sprawa - zmiennych tablicowych w ogóle nie daje się jako volatile bo do nich dostęp jest i tak indeksowany.
Jak zauważyłeś volatile nie wpływa na zwiększenie zajętości RAM - bo niby dlaczego ... ale za to na pewno może wpływać i wpływa na zwiększenie zajętości FLASH i to drugi powód dlaczego nie warto z tym przesadzać.
Generalnie wszędzie tam gdzie chcesz powiedzieć kompilatorowi ... TE , Panie kompilator nie optymalizuj mi tej zmiennej bo jest użyta w przwerwaniu i funkcji albo bo po prostu mam inny ważny powód to wtedy dajesz volatile
mam nadzieję, że dobre uzupełnienie ;)
Bardzo dobre uzupełnienie, zaraz wyrzucę volatile z tablic i przetestuję program i zobaczę czy rzeczywiście nie ma potrzeby.
OdpowiedzUsuńNa zapas nigdy nie wpisywałem, tam gdzie to konieczne, czyli w przypadku gdy zmienne były używane w ISR'ach i main();.
A co gdy stworzę własną funkcję funkcja() tam używam zmienną i wywołuję funkcję w programie głównym? to też część głównej funkcji więc chyba nie ma potrzeby ale wolałem zapytać.
Uzupełnienie jak zwykle wyczerpujące i dobre :)
Dziękuję za pomoc :)
Nie ma podziału na własne funkcje czy nie własne funkcje ... wszystkie są twoje ;) ... jest tylko podział
Usuńfunkcje
i
przerwania
i tylko wtedy dajemy volatile gdy zmienna ma działać w obydwu
Ten post dał mi więcej informacji niż dwie godziny na wykładzie o systemach wbudowanych - gratulacje dla autora!!!
OdpowiedzUsuńWitam. Tam się chyba rozkrzaczyło coś z formatowaniem kodu (drugi kod assemblera, zmienna bez volatile). Pozdrawiam.
OdpowiedzUsuńeeeeeh co chwilę to formatowanie pada :( ... ale dzięki za info postaram się znowu poprawić
UsuńBardzo dobry i merytoryczny blog Panie Mirku. Pełen profesjonalizm.
OdpowiedzUsuń