czwartek, 14 kwietnia 2011

Problemy C - VOLATILE


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...

folkien
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:

  1. bez modyfikatora volatile
  2. 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.



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!





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 ...

Krzysiu6699
Problem rozwiązany - brakowało volatile przy zmiennej ...

folkien
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.
Takie podejście zmniejszy ilość Twoich problemów w początkowej fazie nauki C. Później gdy już poznasz dokładniej język C, będziesz wiedział kiedy i jakie odstępstwa od tych zasad można lub należy robić.

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

11 komentarzy:

  1. Witam mam takie pytanie.
    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

    OdpowiedzUsuń
  2. - stosuj volatile do wszystkich zmiennych globalnych
    - 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?

    OdpowiedzUsuń
  3. Słuszna uwaga :)
    Usunąłem drugi punkt. Dziękuję.

    OdpowiedzUsuń
  4. "Stąd zmienna flaga może nie zostać prawidłowo obsłużona."
    Tzn? Jak może objawić się nieprawidłowe obsłużenie zmiennej?

    OdpowiedzUsuń
    Odpowiedzi
    1. 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ń
  5. Czy artykuł wciąż jest modyfikowany?

    OdpowiedzUsuń
  6. Dondu, jak zawsze Twoje tłumaczenia są bardzo proste ale jednocześnie wnikliwe i pokazujące sedno sprawy. Dzięki wielkie :-)

    OdpowiedzUsuń
    Odpowiedzi
    1. Cieszę się, że się przydał. :)

      Usuń
    2. 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ń
    3. 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ń