Wykorzystanie lotności we wbudowanym rozwoju C.


44

Czytałem niektóre artykuły i odpowiedzi Stack Exchange na temat używania volatilesłowa kluczowego, aby uniemożliwić kompilatorowi stosowanie optymalizacji obiektów, które mogą się zmieniać w sposób, który nie może być określony przez kompilator.

Jeśli czytam z ADC (nazwijmy zmienną adcValue) i deklaruję tę zmienną jako globalną, czy powinienem volatilew tym przypadku użyć słowa kluczowego ?

  1. Bez użycia volatilesłowa kluczowego

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. Za pomocą volatilesłowa kluczowego

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

Zadaję to pytanie, ponieważ podczas debugowania nie widzę żadnej różnicy między obydwoma podejściami, chociaż najlepsze praktyki mówią, że w moim przypadku (zmienna globalna, która zmienia się bezpośrednio ze sprzętu), użycie volatilejest obowiązkowe.


1
Wiele środowisk debugowania (z pewnością gcc) nie stosuje optymalizacji. Kompilacja produkcyjna zwykle będzie (w zależności od twoich wyborów). Może to prowadzić do „interesujących” różnic między kompilacjami. Patrzenie na mapę wyjściową linkera jest pouczające.
Peter Smith

22
„w moim przypadku (zmienna globalna, która zmienia się bezpośrednio ze sprzętu)” - Twoja zmienna globalna nie jest zmieniana przez sprzęt, ale tylko przez kod C, o czym kompilator jest świadomy. - Rejestr sprzętu, w którym ADC podaje wyniki, musi być jednak zmienny, ponieważ kompilator nie może wiedzieć, czy / kiedy zmieni się jego wartość (zmienia się, jeśli / kiedy sprzęt ADC zakończy konwersję.)
JimmyB

2
Czy porównałeś asembler wygenerowany przez obie wersje? To powinno pokazać ci, co dzieje się pod maską
Mawg

3
@stark: BIOS? Na mikrokontrolerze? Odwzorowane w pamięci miejsca we / wy nie będą buforowane (jeśli architektura ma nawet bufor danych w pierwszej kolejności, co nie jest zapewnione) dzięki spójności między regułami buforowania i mapą pamięci. Ale lotne nie ma nic wspólnego z pamięcią podręczną kontrolera pamięci.
Ben Voigt

1
@Davislor Standard językowy nie musi nic więcej mówić w ogóle. Odczyt do obiektu lotnego wykona rzeczywiste ładowanie (nawet jeśli kompilator ostatnio to zrobił i zwykle wiedziałby, jaka jest wartość), a zapis do takiego obiektu wykonałby prawdziwy zapis (nawet gdyby ta sama wartość została odczytana z obiektu ). Więc w if(x==1) x=1;zapisie może być zoptymalizowany dla nietrwałych xi nie może być zoptymalizowany, jeśli xjest niestabilny. OTOH, jeśli potrzebne są specjalne instrukcje, aby uzyskać dostęp do urządzeń zewnętrznych, to do ciebie należy ich dodanie (np. Jeśli trzeba zapisać zakres pamięci).
ciekawy,

Odpowiedzi:


87

Definicja volatile

volatileinformuje kompilator, że wartość zmiennej może ulec zmianie bez wiedzy kompilatora. Dlatego kompilator nie może założyć, że wartość się nie zmieniła tylko dlatego, że program C wydaje się jej nie zmieniać.

Z drugiej strony oznacza to, że wartość zmiennej może być wymagana (odczyt) w innym miejscu, o którym kompilator nie wie, dlatego musi upewnić się, że każde przypisanie do zmiennej jest faktycznie wykonywane jako operacja zapisu.

Przypadków użycia

volatile jest wymagane, gdy

  • reprezentujący rejestry sprzętowe (lub I / O zamapowane w pamięci) jako zmienne - nawet jeśli rejestr nigdy nie zostanie odczytany, kompilator nie może po prostu pominąć operacji zapisu myśląc „Głupi programista. Próbuje zapisać wartość w zmiennej, którą on / ona” nigdy nie przeczyta. Nie zauważy nawet, jeśli pominiemy pisanie. ” I odwrotnie, nawet jeśli program nigdy nie zapisuje wartości zmiennej, jej wartość może nadal zostać zmieniona sprzętowo.
  • współdzielenie zmiennych między kontekstami wykonania (np. ISR / główny program) (patrz odpowiedź @ kkramo)

Efekty volatile

Kiedy deklarowana jest zmienna, volatilekompilator musi upewnić się, że każde przypisanie do niej w kodzie programu jest odzwierciedlone w rzeczywistej operacji zapisu, oraz że każdy odczyt w kodzie programu odczytuje wartość z (mmapped) pamięci.

W przypadku zmiennych nieulotnych kompilator zakłada, że ​​wie, czy / kiedy zmienia się wartość zmiennej, i może optymalizować kod na różne sposoby.

Po pierwsze, kompilator może zmniejszyć liczbę odczytów / zapisów w pamięci, zachowując wartość w rejestrach procesora.

Przykład:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

Tutaj kompilator prawdopodobnie nawet nie przydzieli pamięci RAM dla resultzmiennej i nigdy nie będzie przechowywać wartości pośrednich nigdzie poza rejestrem procesora.

Jeśli resultbył niestabilny, każde wystąpienie resultkodu C wymagałoby od kompilatora dostępu do pamięci RAM (lub portu I / O), co prowadziłoby do obniżenia wydajności.

Po drugie, kompilator może zmienić kolejność operacji na nielotnych zmiennych w celu zwiększenia wydajności i / lub rozmiaru kodu. Prosty przykład:

int a = 99;
int b = 1;
int c = 99;

można ponownie zamówić

int a = 99;
int c = 99;
int b = 1;

co może zapisać instrukcję asemblera, ponieważ wartość 99nie musi być ładowana dwukrotnie.

Jeśli a, bi cbyły niestabilne kompilator musiałby emitują instrukcji, które przypisać wartości w takiej kolejności, jak podano w programie.

Inny klasyczny przykład jest taki:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

Gdyby w tym przypadku signaltak nie było volatile, kompilator „pomyślałby”, że while( signal == 0 )może to być nieskończona pętla (ponieważ signalnigdy nie zostanie zmieniona przez kod wewnątrz pętli ) i może wygenerować odpowiednik

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

Rozważne postępowanie z volatilewartościami

Jak wspomniano powyżej, volatilezmienna może wprowadzić karę wydajności, gdy jest uzyskiwana częściej niż jest to faktycznie wymagane. Aby złagodzić ten problem, można „nieulotną” wartość przez przypisanie do zmiennej nieulotnej, takiej jak

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

Może to być szczególnie korzystne w ISR, gdzie chcesz być jak najszybciej nie dostępu do tego samego sprzętu lub urządzeń pamięci kilka razy kiedy pan wie, że nie jest potrzebna, ponieważ wartość ta nie ulegnie zmianie w czasie, gdy Izrael jest uruchomiony. Jest to powszechne, gdy ISR jest „producentem” wartości zmiennej, jak sysTickCountw powyższym przykładzie. W AVR byłoby szczególnie bolesne, gdyby funkcja miała doSysTick()dostęp do tych samych czterech bajtów w pamięci (cztery instrukcje = 8 cykli procesora na dostęp sysTickCount) pięć lub sześć razy zamiast tylko dwa razy, ponieważ programista wie, że wartość nie będzie zostać zmieniony z innego kodu podczas jego / jej doSysTick()uruchomienia.

Dzięki tej sztuczce robisz dokładnie to samo, co kompilator robi dla zmiennych nieulotnych, tj. Odczytujesz je z pamięci tylko wtedy, gdy jest to konieczne, przechowujesz wartość w rejestrze przez pewien czas i zapisujesz w pamięci tylko wtedy, gdy trzeba ; ale tym razem, ty lepiej niż kompilator wiedzieć, jeśli / kiedy odczytu / zapisu musi się zdarzyć, więc zwalnia kompilator od tego zadania optymalizacji i zrobić to samemu.

Ograniczenia volatile

Dostęp bezatomowy

volatilenie nie zapewniają dostęp do zmiennych atomowej multi-word. W takich przypadkach oprócz korzystania należy zapewnić wzajemne wykluczenie w inny sposób volatile. W AVR możesz korzystać ATOMIC_BLOCKz <util/atomic.h>lub z prostych cli(); ... sei();połączeń. Odpowiednie makra działają również jako bariera pamięci, co jest ważne, jeśli chodzi o kolejność dostępu:

Realizacja zamówienia

volatilenakłada ścisłe polecenie wykonania tylko w odniesieniu do innych zmiennych zmiennych. Oznacza to na przykład, że

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

gwarantuje się najpierw przypisanie 1 do, ia następnie przypisanie 2 do j. Nie ma jednak gwarancji, że azostaną one przypisane pomiędzy; kompilator może wykonać to przypisanie przed fragmentem kodu lub po nim, w zasadzie w dowolnym momencie do pierwszego (widocznego) odczytu a.

Gdyby nie bariera pamięci wyżej wspomnianych makr, kompilator byłby w stanie dokonać translacji

uint32_t x;

cli();
x = volatileVar;
sei();

do

x = volatileVar;
cli();
sei();

lub

cli();
sei();
x = volatileVar;

(Ze względu na kompletność muszę powiedzieć, że bariery pamięci, takie jak te sugerowane przez makra sei / cli, mogą faktycznie uniemożliwić użycie volatile, jeśli wszystkie dostępy zostaną uzupełnione tymi barierami.)


7
Dobra dyskusja na temat braku ulotności dla występu :)
awjlogan

3
Zawsze lubię wspominać o definicji substancji lotnych w ISO / IEC 9899: 1999 6.7.3 (6): An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Więcej osób powinno ją przeczytać.
Jeroen3

3
Warto wspomnieć, że cli/ seijest zbyt ciężkim rozwiązaniem, jeśli Twoim jedynym celem jest osiągnięcie bariery pamięci, a nie zapobieganie przerwom. Te makra generują rzeczywiste cli/ seiinstrukcje i dodatkowo pamięć clobber, i to właśnie takie clobbering powoduje powstanie bariery. Aby mieć tylko barierę pamięci bez wyłączania przerwań, możesz zdefiniować własne makro z ciałem podobnym __asm__ __volatile__("":::"memory")(tj. Pusty kod asemblera z clobberem pamięci).
Ruslan

3
@NicHartley nr C17 5.1.2.3 §6 określa obserwowalne zachowanie : „Dostępy do obiektów lotnych są oceniane ściśle według zasad maszyny abstrakcyjnej”. Standard C tak naprawdę nie wyjaśnia, gdzie ogólnie potrzebne są bariery pamięci. Na końcu wyrażenia, które używa volatile, jest punkt sekwencji, a wszystko po nim musi być „zsekwencjonowane po”. Oznacza to, że wyrażenie jest swoistą barierą pamięci. Dostawcy kompilatorów postanowili rozpowszechniać wszelkiego rodzaju mity, aby na programistach spoczywać odpowiedzialność za bariery pamięci, ale to narusza zasady „abstrakcyjnej maszyny”.
Lundin,

2
@JimmyB Lokalna zmienna może być przydatna dla kodu takiego jak volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka,

13

Zmienne słowo kluczowe informuje kompilator, że dostęp do zmiennej ma zauważalny efekt. Oznacza to, że za każdym razem, gdy kod źródłowy używa zmiennej, kompilator MUSI utworzyć dostęp do zmiennej. Czy to dostęp do odczytu lub zapisu.

Skutkuje to tym, że każda zmiana zmiennej poza normalnym przepływem kodu będzie również obserwowana przez kod. Np. Jeśli program obsługi przerwań zmienia wartość. Lub jeśli zmienna jest w rzeczywistości jakimś rejestrem sprzętowym, który zmienia się sam.

Ta wielka korzyść jest również jej wadą. Każdy pojedynczy dostęp do zmiennej przechodzi przez zmienną, a wartość nigdy nie jest przechowywana w rejestrze, co zapewnia szybszy dostęp przez dowolny czas. Oznacza to, że zmienna zmienna będzie wolna. Wielkość wolniej. Dlatego używaj lotnych tylko wtedy, gdy jest to rzeczywiście konieczne.

W twoim przypadku, o ile pokazałeś kod, zmienna globalna jest zmieniana tylko wtedy, gdy sam ją zaktualizujesz adcValue = readADC();. Kompilator wie, kiedy to się dzieje, i nigdy nie będzie przechowywał wartości adcValue w rejestrze nad czymś, co może wywołać readFromADC()funkcję. Lub jakiejkolwiek funkcji, o której nie wie. Lub cokolwiek, co zmanipuluje wskaźniki, które mogą wskazywać adcValuei tak dalej. Naprawdę nie ma potrzeby niestabilności, ponieważ zmienna nigdy nie zmienia się w nieprzewidywalny sposób.


6
Zgadzam się z tą odpowiedzią, ale „wolniejsze wielkości” brzmią zbyt strasznie.
kkrambo

6
Dostęp do rejestru procesora można uzyskać w mniej niż cyklu procesora na nowoczesnych superskalarnych procesorach. Z drugiej strony dostęp do rzeczywistej pamięci niebuforowanej (pamiętaj, że zmieniłby to jakiś sprzęt zewnętrzny, więc nie wolno buforować procesora) może mieścić się w zakresie 100-300 cykli procesora. Tak, wielkości. Nie będzie tak źle na AVR lub podobnym mikrokontrolerze, ale pytanie nie określa sprzętu.
Goswin von Brederlow

7
W systemach wbudowanych (mikrokontroler) kara za dostęp do pamięci RAM jest często znacznie mniejsza. AVR, na przykład, zajmuje tylko dwa cykle procesora dla odczytu z lub zapisu do pamięci RAM (ruch rejestru-rejestru zajmuje jeden cykl), więc oszczędności związane z utrzymywaniem rzeczy w rejestrach zbliżają się (ale nigdy nie osiągają) maks. 2 cykle zegara na dostęp. - Oczywiście, względnie mówiąc, zapisanie wartości z rejestru X do pamięci RAM, a następnie natychmiastowe ponowne załadowanie tej wartości do rejestru X w celu dalszych obliczeń zajmie 2x2 = 4 zamiast 0 cykli (przy zachowaniu wartości w X), a zatem jest nieskończone wolniej :)
JimmyB

1
Jest „o wiele wolniejsze” w kontekście „zapisu do konkretnej zmiennej lub czytania z niej”, tak. Jednak w kontekście kompletnego programu, który prawdopodobnie znacznie więcej niż tylko odczytuje / zapisuje do jednej zmiennej w kółko, nie, nie bardzo. W takim przypadku ogólna różnica jest prawdopodobnie „mała lub nieistotna”. Podczas dokonywania stwierdzeń dotyczących wydajności należy zachować ostrożność, aby wyjaśnić, czy stwierdzenie dotyczy jednego konkretnego działania lub programu jako całości. Spowolnienie rzadko używanych operacji o współczynnik ~ 300x prawie nigdy nie jest wielkim problemem.
aroth

1
Masz na myśli to ostatnie zdanie? Oznaczało to znacznie więcej w sensie „przedwczesna optymalizacja jest źródłem wszelkiego zła”. Oczywiście nie powinieneś używać volatilewszystkiego tylko dlatego , że , ale nie powinieneś również unikać tego w przypadkach, w których uważasz, że jest to słusznie wymagane ze względu na zapobiegawcze obawy o wydajność.
aroth

9

Głównym zastosowaniem lotnego słowa kluczowego we wbudowanych aplikacjach C jest oznaczenie zmiennej globalnej zapisywanej w module obsługi przerwań. W tym przypadku z pewnością nie jest to opcjonalne.

Bez tego kompilator nie może udowodnić, że wartość jest zawsze zapisywana po inicjalizacji, ponieważ nie może udowodnić, że wywoływana jest funkcja obsługi przerwań. Dlatego uważa, że ​​może zoptymalizować zmienną nieistniejącą.


2
Z pewnością istnieją inne praktyczne zastosowania, ale imho jest to najczęściej.
vicatcu

1
Jeśli wartość jest odczytywana tylko w ISR (i zmieniana z main ()), potencjalnie musisz również użyć zmiennej, aby zagwarantować dostęp ATOMIC dla zmiennych wielobajtowych.
Rev1.0

15
@ Rev1.0 Nie, substancja lotna nie gwarantuje aromatyczności. Obawy te należy rozwiązać osobno.
Chris Stratton

1
W opublikowanym kodzie nie ma odczytu ze sprzętu ani przerw. Przyjmujesz rzeczy z pytania, którego nie ma. Naprawdę nie można na nie odpowiedzieć w obecnej formie.
Lundin,

3
„zaznacz zmienną globalną zapisaną w module obsługi przerwań” nope. Ma oznaczać zmienną; globalny lub inny; że może to zostać zmienione przez coś poza zrozumieniem kompilatorów. Przerwanie nie jest wymagane. Może to być pamięć współdzielona lub ktoś wkładający sondę do pamięci (ta ostatnia
opcja

9

Istnieją dwa przypadki, w których należy użyć volatilew systemach wbudowanych.

  • Podczas odczytu z rejestru sprzętu.

    Oznacza to, że sam rejestr odwzorowany w pamięci, część sprzętowych urządzeń peryferyjnych wewnątrz MCU. Prawdopodobnie będzie miał jakąś tajemniczą nazwę, taką jak „ADC0DR”. Rejestr ten musi być zdefiniowany w kodzie C, albo poprzez mapę rejestrów dostarczoną przez dostawcę narzędzia, albo przez ciebie. Aby zrobić to sam, zrobiłbyś (zakładając rejestr 16-bitowy):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    gdzie 0x1234 to adres, na którym MCU zamapował rejestr. Ponieważ volatilejest już częścią powyższego makra, każdy dostęp do niego będzie miał zmienną kwalifikację. Więc ten kod jest w porządku:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Podczas udostępniania zmiennej między ISR a powiązanym kodem przy użyciu wyniku ISR.

    Jeśli masz coś takiego:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }

    Następnie kompilator może pomyśleć: „adc_data ma zawsze wartość 0, ponieważ nie jest nigdzie aktualizowane. I ta funkcja ADC0_interrupt () nigdy nie jest wywoływana, więc zmiennej nie można zmienić”. Kompilator zwykle nie zdaje sobie sprawy, że przerwania wywoływane są przez sprzęt, a nie przez oprogramowanie. Tak więc kompilator usuwa i usuwa kod, if(adc_data > 0){ do_stuff(adc_data); }ponieważ uważa, że ​​nigdy nie będzie to prawdą, powodując bardzo dziwny i trudny do debugowania błąd.

    Deklarując adc_data volatile, kompilator nie może przyjmować takich założeń i nie może optymalizować dostępu do zmiennej.


Ważne notatki:

  • ISR zawsze deklaruje się w sterowniku sprzętowym. W takim przypadku ADC ISR powinien znajdować się w sterowniku ADC. Nikt inny oprócz kierowcy nie powinien komunikować się z ISR - wszystko inne to programowanie spaghetti.

  • Podczas pisania C wszelka komunikacja między ISR a programem działającym w tle musi być chroniona przed warunkami wyścigowymi. Zawsze za każdym razem bez wyjątków. Rozmiar magistrali danych MCU nie ma znaczenia, ponieważ nawet jeśli wykonasz pojedynczą 8-bitową kopię w C, język nie może zagwarantować atomowości operacji. Nie, chyba że użyjesz funkcji C11 _Atomic. Jeśli ta funkcja nie jest dostępna, musisz użyć jakiegoś semafora lub wyłączyć przerwanie podczas odczytu itp. Inline asembler to kolejna opcja. volatilenie gwarantuje atomowości.

    To, co może się zdarzyć, to: -
    Załaduj wartość ze stosu do rejestru -
    Wystąpienie przerwania - Zastosuj
    wartość z rejestru

    I wtedy nie ma znaczenia, czy część „użyj wartości” jest sama w sobie instrukcją. Niestety, znaczna część wszystkich programistów systemów wbudowanych jest tego nieświadoma, co prawdopodobnie czyni go najczęstszym błędem systemów wbudowanych w historii. Zawsze przerywany, trudny do sprowokowania, trudny do znalezienia.


Przykład poprawnie napisanego sterownika ADC wyglądałby tak (zakładając, że C11 _Atomicjest niedostępny):

adc.h

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

adc.c

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • Ten kod zakłada, że ​​samo przerwanie nie może być przerwane. W takich systemach prosty boolean może działać jako semafor i nie musi być atomowy, ponieważ nie ma żadnej szkody, jeśli przerwanie nastąpi przed ustawieniem boolean. Wadą powyższej uproszczonej metody jest to, że odrzuci ona odczyty ADC, gdy wystąpią warunki wyścigu, używając poprzedniej wartości. Można tego również uniknąć, ale kod staje się bardziej złożony.

  • Tutaj volatilechroni przed błędami optymalizacji. Nie ma to nic wspólnego z danymi pochodzącymi z rejestru sprzętowego, tylko że dane są udostępniane ISR.

  • staticchroni przed programowaniem spaghetti i zanieczyszczeniem przestrzeni nazw, ustawiając zmienną lokalnie dla kierowcy. (Jest to dobre w aplikacjach jedno-rdzeniowych, jednowątkowych, ale nie w aplikacjach wielowątkowych.)


Trudny do debugowania jest względny, jeśli kod zostanie usunięty, zauważysz, że Twój cenny kod zniknął - to dość odważne stwierdzenie, że coś jest nie tak. Ale zgadzam się, że mogą być bardzo dziwne i trudne do debugowania efekty.
Arsenał

@Arsenal Jeśli masz ładny debugger, który łączy asembler z literą C, i znasz przynajmniej trochę asm, to tak, może być łatwo zauważyć. Ale w przypadku większego złożonego kodu duża część asm generowanego maszynowo nie jest prosta. Lub jeśli nie znasz asm. Lub jeśli twój debugger jest gówniany i nie wyświetla asm (cougheclipsecough).
Lundin

Możliwe, że jestem wtedy trochę rozpieszczony przy użyciu debuggerów Lauterbach. Jeśli spróbujesz ustawić punkt przerwania w kodzie, który został zoptymalizowany, ustawi go w innym miejscu i wiesz, że coś się tam dzieje.
Arsenal

@Arsenal Tak, rodzaj mieszanego C / asm, który można uzyskać w Lauterbach, nie jest w żadnym wypadku standardem. Większość debuggerów wyświetla asm w oddzielnym oknie, jeśli w ogóle.
Lundin

semaphorezdecydowanie powinien być volatile! W rzeczywistości, to najbardziej podstawowy przypadek użycia wich wymaga : coś sygnału z jednego kontekstu wykonania do drugiego. - W twoim przykładzie kompilator może po prostu pominąć, ponieważ „widzi”, że jego wartość nigdy nie jest czytana, zanim zostanie zastąpiona przez . volatilesemaphore = true;semaphore = false;
JimmyB

5

We fragmentach kodu przedstawionych w pytaniu nie ma jeszcze powodu, aby używać zmiennej. Nie ma znaczenia, że ​​wartość adcValuepochodzi z ADC. A adcValuebycie globalnym powinno wzbudzać podejrzenia co do adcValueniestabilności, ale nie jest to sam w sobie powód.

Bycie globalnym jest wskazówką, ponieważ otwiera możliwość, do której adcValuemożna uzyskać dostęp z więcej niż jednego kontekstu programu. Kontekst programu obejmuje moduł obsługi przerwań i zadanie RTOS. Jeśli zmienna globalna zostanie zmieniona o jeden kontekst, wówczas inne konteksty programu nie mogą założyć, że znają wartość z poprzedniego dostępu. Każdy kontekst musi ponownie odczytać wartość zmiennej za każdym razem, gdy z niej korzysta, ponieważ wartość mogła zostać zmieniona w innym kontekście programu. Kontekst programu nie jest świadomy, kiedy nastąpi przerwanie lub zmiana zadania, dlatego należy założyć, że wszelkie zmienne globalne używane przez wiele kontekstów mogą zmieniać się między dostępami do zmiennej z powodu możliwego przełączenia kontekstu. Do tego służy zmienna deklaracja. Mówi kompilatorowi, że ta zmienna może się zmieniać poza kontekstem, więc czytaj ją przy każdym dostępie i nie zakładaj, że znasz już wartość.

Jeśli zmienna jest odwzorowana w pamięci na adres sprzętowy, to zmiany dokonane przez sprzęt są faktycznie innym kontekstem poza kontekstem twojego programu. Odwzorowanie pamięci jest również wskazówką. Na przykład, jeśli twoja readADC()funkcja uzyskuje dostęp do wartości odwzorowanej w pamięci, aby uzyskać wartość ADC, to zmienna odwzorowana w pamięci powinna być prawdopodobnie niestabilna.

Wracając do pytania, jeśli kod zawiera coś więcej i adcValuedostęp do niego zapewnia inny kod działający w innym kontekście, to tak, adcValuepowinien być zmienny.


4

„Zmienna globalna, która zmienia się bezpośrednio ze sprzętu”

To, że wartość pochodzi z jakiegoś sprzętowego rejestru ADC, nie oznacza, że ​​jest on „bezpośrednio” zmieniany przez sprzęt.

W twoim przykładzie po prostu wywołujesz readADC (), która zwraca pewną wartość rejestru ADC. Jest to w porządku w odniesieniu do kompilatora, wiedząc, że adcValue ma w tym momencie nową wartość.

Byłoby inaczej, gdybyś używał procedury przerwania ADC do przypisania nowej wartości, która jest wywoływana, gdy nowa wartość ADC jest gotowa. W takim przypadku kompilator nie miałby pojęcia, kiedy wywoływany jest odpowiedni ISR ​​i może zdecydować, że adcValue nie będzie dostępny w ten sposób. W tym miejscu pomogłaby niestabilność.


1
Ponieważ twój kod nigdy nie „wywołuje” funkcji ISR, Kompilator widzi, że zmienna jest aktualizowana tylko w funkcji, której nikt nie wywołuje. Kompilator to optymalizuje.
Swanand

1
Zależy od reszty kodu, jeśli adcValue nie jest nigdzie odczytywany (jak tylko odczyt przez debugger) lub jeśli jest odczytywany tylko raz w jednym miejscu, kompilator prawdopodobnie go zoptymalizuje.
Damien

2
@Damien: Zawsze „zależy”, ale chciałem odpowiedzieć na rzeczywiste pytanie „Czy w tym przypadku należy użyć słowa kluczowego niestabilnego?” tak krótko, jak to możliwe.
Rev1.0

4

Zachowanie volatileargumentu zależy w dużej mierze od kodu, kompilatora i wykonanej optymalizacji.

Istnieją dwa przypadki użycia, w których osobiście korzystam volatile:

  • Jeśli istnieje zmienna, na którą chcę spojrzeć za pomocą debuggera, ale kompilator ją zoptymalizował (oznacza, że ​​ją usunął, ponieważ okazało się, że ta zmienna nie jest konieczna), dodanie volatilezmusi kompilator do jej zachowania, a zatem można zobaczyć podczas debugowania.

  • Jeśli zmienna może zmienić się „poza kodem”, zazwyczaj, jeśli masz do niej dostęp sprzętowy lub mapujesz zmienną bezpośrednio na adres.

Wbudowane są też czasem pewne błędy w kompilatorach, które optymalizują, które faktycznie nie działają, a czasem volatilemogą rozwiązać problemy.

Biorąc pod uwagę, że twoja zmienna jest zadeklarowana globalnie, prawdopodobnie nie zostanie zoptymalizowana, dopóki zmienna jest używana w kodzie, przynajmniej zapisywana i odczytywana.

Przykład:

void test()
{
    int a = 1;
    printf("%i", a);
}

W takim przypadku zmienna prawdopodobnie zostanie zoptymalizowana do printf („% i”, 1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

nie będzie zoptymalizowany

Inny:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

W takim przypadku kompilator może zoptymalizować (jeśli zoptymalizujesz pod kątem prędkości), a tym samym odrzucić zmienną

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

W twoim przypadku „może to zależeć” od reszty kodu, sposobu adcValueużycia w innym miejscu oraz używanych wersji / ustawień optymalizacji kompilatora.

Czasami posiadanie kodu, który działa bez optymalizacji, może być denerwujące, ale ulega awarii po zoptymalizowaniu.

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

Można to zoptymalizować do printf („% i”, readADC ());

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

Prawdopodobnie nie zostaną one zoptymalizowane, ale nigdy nie wiadomo „jak dobry jest kompilator” i mogą ulec zmianie wraz z parametrami kompilatora. Zwykle kompilatory z dobrą optymalizacją są licencjonowane.


1
Na przykład a = 1; b = a; i c = b; kompilator może pomyśleć chwilę, aib są bezużyteczne, po prostu ustawmy bezpośrednio 1 na c. Oczywiście nie zrobisz tego w kodzie, ale kompilator jest lepszy niż je znajdziesz, również jeśli spróbujesz od razu napisać zoptymalizowany kod, byłoby to nieczytelne.
Damien

2
Prawidłowy kod z poprawnym kompilatorem nie zepsuje się przy włączonych optymalizacjach. Poprawność kompilatora jest trochę problemem, ale przynajmniej w przypadku IAR nie spotkałem się z sytuacją, w której optymalizacja prowadzi do uszkodzenia kodu, w którym nie powinna.
Arsenał

5
Wiele przypadków, w których optymalizacja psuje kod, ma miejsce również wtedy, gdy wkraczasz na terytorium UB ..
pipe

2
Tak, efektem ubocznym lotności jest to, że może ona pomóc w debugowaniu. Ale to nie jest wystarczający powód, aby używać lotnych. Prawdopodobnie powinieneś wyłączyć optymalizacje, jeśli twoim celem jest łatwe debugowanie. Ta odpowiedź nawet nie wspomina o przerwaniach.
kkrambo

2
Dodanie argumentu debugującego volatilezmusza kompilator do przechowywania zmiennej w pamięci RAM i do aktualizacji tej pamięci RAM, gdy tylko wartość zostanie przypisana do zmiennej. Przez większość czasu kompilator nie „usuwa” zmiennych, ponieważ zwykle nie piszemy przypisań bez efektu, ale może zdecydować o zachowaniu zmiennej w jakimś rejestrze procesora i może później lub nigdy nie zapisać wartości tego rejestru do pamięci RAM. Debugery często nie potrafią zlokalizować rejestru procesora, w którym przechowywana jest zmienna, a zatem nie mogą pokazać jej wartości.
JimmyB

1

Wiele technicznych wyjaśnień, ale chcę skoncentrować się na praktycznym zastosowaniu.

Słowo volatilekluczowe zmusza kompilator do odczytu lub zapisu wartości zmiennej z pamięci za każdym razem, gdy jest używana. Zwykle kompilator będzie próbował zoptymalizować, ale nie robić niepotrzebnych odczytów i zapisów, np. Utrzymując wartość w rejestrze procesora, a nie uzyskując dostęp do pamięci za każdym razem.

Ma to dwa główne zastosowania w kodzie osadzonym. Po pierwsze służy do rejestrów sprzętowych. Rejestry sprzętowe mogą się zmieniać, np. Rejestr wyników ADC może być zapisywany przez urządzenie peryferyjne ADC. Rejestry sprzętowe mogą także wykonywać akcje po uzyskaniu dostępu. Typowym przykładem jest rejestr danych UART, który często usuwa flagi przerwań podczas odczytu.

Kompilator zwykle próbuje zoptymalizować powtarzane odczyty i zapisy rejestru przy założeniu, że wartość nigdy się nie zmieni, więc nie ma potrzeby dalszego dostępu do niej, ale volatilesłowo kluczowe zmusi ją do wykonania operacji odczytu za każdym razem.

Drugim powszechnym zastosowaniem są zmienne używane zarówno przez kod przerwań, jak i bez przerwania. Przerwania nie są wywoływane bezpośrednio, więc kompilator nie może określić, kiedy będą wykonywane, i dlatego zakłada, że ​​żadne dostępy w przerwaniu nigdy się nie zdarzają. Ponieważ volatilesłowo kluczowe wymusza na kompilatorze dostęp do zmiennej za każdym razem, założenie to jest usuwane.

Należy zauważyć, że volatilesłowo kluczowe nie jest kompletnym rozwiązaniem tych problemów i należy dołożyć starań, aby ich uniknąć. Na przykład w systemie 8-bitowym zmienna 16-bitowa wymaga dwóch dostępów do pamięci do odczytu lub zapisu, a zatem nawet jeśli kompilator jest zmuszony do uzyskania dostępu, następuje sekwencyjnie, a sprzęt może działać przy pierwszym dostępie lub przerwa między nimi.


0

W przypadku braku volatilekwalifikatora wartość obiektu może być przechowywana w więcej niż jednym miejscu podczas niektórych części kodu. Rozważmy na przykład, biorąc pod uwagę coś takiego:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

Na początku C kompilator przetworzyłby instrukcję

foo++;

poprzez kroki:

load foo into a register
increment that register
store that register back to foo

Bardziej wyrafinowane kompilatory rozpoznają jednak, że jeśli wartość „foo” jest przechowywana w rejestrze podczas pętli, trzeba będzie ją załadować tylko raz przed pętlą i zapisać raz później. Jednak podczas pętli będzie to oznaczać, że wartość „foo” jest przechowywana w dwóch miejscach - w pamięci globalnej i w rejestrze. Nie będzie to stanowić problemu, jeśli kompilator będzie widział wszystkie sposoby dostępu do „foo” w pętli, ale może powodować problemy, jeśli wartość „foo” jest dostępna w jakimś mechanizmie, o którym kompilator nie wie ( takich jak moduł obsługi przerwań).

Być może autorzy Standardu mogliby dodać nowy kwalifikator, który wyraźnie zachęciłby kompilator do dokonania takich optymalizacji i powiedziałby, że staromodna semantyka miałaby zastosowanie w przypadku jej braku, ale przypadki, w których optymalizacje są użyteczne, znacznie przewyższają liczbę te, w których byłoby to problematyczne, więc Standard zamiast tego pozwala kompilatorom założyć, że takie optymalizacje są bezpieczne przy braku dowodów, że tak nie jest. Celem tego volatilesłowa kluczowego jest dostarczenie takich dowodów.

Kilka sytuacji spornych między niektórymi autorami kompilatorów i programistami występuje w sytuacjach takich jak:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

Historycznie większość kompilatorów albo dopuszczałaby możliwość, że zapisanie miejsca w volatilepamięci mogłoby wywołać dowolne skutki uboczne i uniknąć buforowania jakichkolwiek wartości w rejestrach w takim sklepie, albo powstrzymałyby się od buforowania wartości w rejestrach między wywołaniami funkcji, które są nie kwalifikuje się jako „wbudowany”, a zatem zapisuje 0x1234 output_buffer[0], ustawia dane wyjściowe, poczekaj na zakończenie, a następnie napisz 0x2345 output_buffer[0]i kontynuuj od tego momentu . Standard nie wymaga implementacji, aby traktować akt przechowywania adresu output_bufferdovolatile-kwalifikował wskaźnik jako znak, że coś może się z tym wydarzyć, co oznacza, że ​​kompilator nie rozumie, ponieważ autorzy sądzili, że kompilator pisarzy kompilatorów przeznaczonych dla różnych platform i celów rozpozna, gdy to zrobi, będzie służyć tym celom na tych platformach bez konieczności mówienia. W związku z tym niektóre „sprytne” kompilatory, takie jak gcc i clang, przyjmą, że nawet jeśli adres output_bufferjest zapisany w zmiennym wskaźniku między dwoma sklepami output_buffer[0], nie ma powodu zakładać, że cokolwiek może obchodzić wartość przechowywana w tym obiekcie w ten czas.

Ponadto, podczas gdy wskaźniki, które są rzutowane bezpośrednio z liczb całkowitych, rzadko są używane do celów innych niż manipulowanie rzeczami w sposób, którego kompilatory raczej nie zrozumieją, Standard ponownie nie wymaga kompilatorów do traktowania takich dostępów jak volatile. W konsekwencji pierwszy zapis do *((unsigned short*)0xC0001234)może zostać pominięty przez „sprytne” kompilatory, takie jak gcc i clang, ponieważ opiekunowie takich kompilatorów wolą twierdzić, że kod, który nie kwalifikuje takich rzeczy jako volatile„zepsuty”, niż uznają, że przydatność takiego kodu jest przydatna . Wiele plików nagłówkowych dostarczonych przez dostawcę pomija volatilekwalifikatory, a kompilator zgodny z plikami nagłówkowymi dostarczonymi przez dostawcę jest bardziej przydatny niż ten, który nie jest.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.