Verwendung von Volatile in der Embedded C-Entwicklung


44

Ich habe einige Artikel und Stack Exchange-Antworten zur Verwendung des volatileSchlüsselworts gelesen , um zu verhindern, dass der Compiler Optimierungen auf Objekte anwendet, die sich auf vom Compiler nicht feststellbare Weise ändern können.

Wenn ich aus einem ADC lese (nennen wir die Variable adcValue) und diese Variable als global deklariere, sollte ich volatilein diesem Fall das Schlüsselwort verwenden ?

  1. Ohne volatileSchlüsselwort

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. Verwenden Sie das volatileSchlüsselwort

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

Ich stelle diese Frage, weil ich beim Debuggen keinen Unterschied zwischen beiden Ansätzen feststellen kann, obwohl nach den Best Practices in meinem Fall (eine globale Variable, die sich direkt von der Hardware ändert) die Verwendung volatileobligatorisch ist.


1
Eine Reihe von Debug-Umgebungen (sicherlich gcc) wenden keine Optimierungen an. Normalerweise wird eine Produktionsversion erstellt (abhängig von Ihrer Auswahl). Dies kann zu "interessanten" Unterschieden zwischen Builds führen. Ein Blick auf die Linker-Ausgabekarte ist informativ.
Peter Smith

22
"in meinem Fall (Globale Variable, die sich direkt von der Hardware ändert)" - Ihre globale Variable wird nicht von der Hardware geändert, sondern nur von Ihrem C-Code, der dem Compiler bekannt ist. - Das Hardware-Register, in dem der ADC seine Ergebnisse liefert, muss jedoch flüchtig sein, da der Compiler nicht wissen kann, ob / wann sich sein Wert ändert (es ändert sich, wenn / wann die ADC-Hardware eine Konvertierung beendet.)
JimmyB

2
Haben Sie den Assembler beider Versionen verglichen? Das sollte Ihnen zeigen, was unter der Haube passiert
Mawg

3
@stark: BIOS? Auf einem Mikrocontroller? Speicherzugeordneter E / A-Speicherplatz kann nicht zwischengespeichert werden (wenn die Architektur überhaupt einen Datencache aufweist, was nicht sichergestellt ist), da die Entwurfskonsistenz zwischen den Caching-Regeln und der Speicherzuordnung gewährleistet ist. Volatile hat jedoch nichts mit dem Cache des Speichercontrollers zu tun.
Ben Voigt

1
@Davislor Der Sprachstandard muss im Allgemeinen nichts weiter sagen. Ein Lesevorgang in ein flüchtiges Objekt führt zu einem realen Ladevorgang (selbst wenn der Compiler kürzlich einen durchgeführt hat und normalerweise wissen würde, was der Wert ist), und ein Schreibvorgang in ein solches Objekt führt zu einem realen Speichervorgang (selbst wenn derselbe Wert aus dem Objekt gelesen wurde) ). So kann if(x==1) x=1;beim Schreiben für einen nichtflüchtigen xWert optimiert werden und kann nicht optimiert werden, wenn er xflüchtig ist. OTOH Wenn für den Zugriff auf externe Geräte spezielle Anweisungen erforderlich sind, müssen Sie diese hinzufügen (z. B. wenn ein Speicherbereich durchgeschrieben werden muss).
neugierig

Antworten:


87

Eine Definition von volatile

volatileteilt dem Compiler mit, dass sich der Wert der Variablen ändern kann, ohne dass der Compiler davon erfährt. Daher kann der Compiler nicht davon ausgehen, dass sich der Wert nicht geändert hat, nur weil das C-Programm ihn anscheinend nicht geändert hat.

Auf der anderen Seite bedeutet dies, dass der Wert der Variablen möglicherweise an einer anderen Stelle benötigt (gelesen) wird, über die der Compiler nichts weiß. Daher muss sichergestellt werden, dass jede Zuweisung zu der Variablen tatsächlich als Schreiboperation ausgeführt wird.

Anwendungsfälle

volatile wird benötigt wenn

  • Darstellen von Hardware-Registern (oder speicherabgebildeten E / A) als Variablen - selbst wenn das Register niemals gelesen wird, darf der Compiler nicht einfach den Schreibvorgang überspringen und denkt: "Dummer Programmierer. Versucht, einen Wert in einer Variablen zu speichern, die er / sie wird nie wieder lesen. Er / sie wird es nicht einmal bemerken, wenn wir das Schreiben auslassen. " Umgekehrt, auch wenn das Programm niemals einen Wert in die Variable schreibt, kann sein Wert dennoch von der Hardware geändert werden.
  • Austausch von Variablen zwischen Ausführungskontexten (z. B. ISRs / Hauptprogramm) (siehe Antwort von @ kkramo)

Effekte von volatile

Wenn eine Variable deklariert wird, volatilemuss der Compiler sicherstellen, dass sich jede Zuweisung im Programmcode auf eine tatsächliche Schreiboperation auswirkt und dass jeder eingelesene Programmcode den Wert aus dem (MMAPP-) Speicher liest.

Bei nichtflüchtigen Variablen geht der Compiler davon aus, dass er weiß, ob / wann sich der Wert der Variablen ändert, und dass er den Code auf verschiedene Arten optimieren kann.

Zum einen kann der Compiler die Anzahl der Lese- / Schreibvorgänge in den Speicher reduzieren, indem er den Wert in den CPU-Registern beibehält.

Beispiel:

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

Hier wird der Compiler wahrscheinlich nicht einmal RAM für die resultVariable zuweisen und die Zwischenwerte niemals irgendwo anders als in einem CPU-Register speichern.

Wenn resultflüchtig, würde jedes Auftreten resultim C-Code erfordern, dass der Compiler einen Zugriff auf RAM (oder einen E / A-Port) ausführt, was zu einer geringeren Leistung führt.

Zweitens kann der Compiler Operationen an nichtflüchtigen Variablen hinsichtlich Leistung und / oder Codegröße neu anordnen. Einfaches Beispiel:

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

könnte nachbestellt werden

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

Dies kann eine Assembler-Anweisung speichern, da der Wert 99nicht zweimal geladen werden muss.

Wenn a, bund cflüchtig wäre , würde der Compiler Anweisungen emittieren , welche die Werte in der exakten Reihenfolge vergeben , wie sie im Programm angegeben.

Das andere klassische Beispiel sieht so aus:

volatile uint8_t signal;

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

Wenn in diesem Fall signalwar nicht volatile, würde der Compiler ‚denken‘ , dass while( signal == 0 )eine unendliche Schleife sein kann (weil signalnie von Code geändert wird innerhalb der Schleife ) und könnte die äquivalent erzeugen

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

Rücksichtsvoller Umgang mit volatileWerten

Wie oben erwähnt, kann eine volatileVariable eine Leistungsstrafe nach sich ziehen, wenn auf sie häufiger zugegriffen wird als tatsächlich erforderlich. Um dieses Problem zu beheben, können Sie den Wert "nichtflüchtig" machen, indem Sie ihn einer nichtflüchtigen Variablen zuweisen, z

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
}

Dies kann insbesondere bei ISRs von Vorteil sein, bei denen Sie so schnell wie möglich nicht mehrmals auf dieselbe Hardware oder denselben Speicher zugreifen möchten, wenn Sie wissen, dass dies nicht erforderlich ist, da sich der Wert nicht ändert, während Ihr ISR ausgeführt wird. Dies ist häufig der Fall, wenn der ISR der 'Produzent' von Werten für die Variable ist, wie sysTickCountim obigen Beispiel. Bei einem AVR wäre es besonders schmerzhaft, wenn die Funktion fünf- oder sechsmal statt nur zweimal doSysTick()auf dieselben vier Bytes im Speicher zugreifen würde (vier Anweisungen = 8 CPU-Zyklen pro Zugriff sysTickCount), da der Programmierer weiß, dass dies nicht der Fall ist von einem anderen Code geändert werden, während sein / ihr doSysTick()läuft.

Mit diesem Trick machen Sie genau dasselbe, was der Compiler für nichtflüchtige Variablen macht, dh, Sie lesen sie nur dann aus dem Speicher, wenn sie benötigt werden, behalten den Wert einige Zeit in einem Register und schreiben nur dann in den Speicher zurück, wenn dies erforderlich ist ; Aber dieses Mal wissen Sie besser als der Compiler, ob / wann Lese- / Schreibvorgänge erforderlich sind. Sie entlasten den Compiler von dieser Optimierungsaufgabe und erledigen dies selbst.

Einschränkungen von volatile

Nichtatomarer Zugang

volatilebietet keinen atomaren Zugriff auf Variablen mit mehreren Wörtern. In diesen Fällen müssen Sie zusätzlich zur Verwendung einen gegenseitigen Ausschluss auf andere Weise vorsehen volatile. Auf dem AVR können Sie ATOMIC_BLOCKaus <util/atomic.h>oder einfache cli(); ... sei();Anrufe tätigen. Die jeweiligen Makros fungieren auch als Speicherbarriere, was bei der Reihenfolge der Zugriffe wichtig ist:

Ausführungsreihenfolge

volatilelegt strikte Ausführungsreihenfolge nur in Bezug auf andere flüchtige Variablen fest. Dies bedeutet zum Beispiel, dass

volatile int i;
volatile int j;
int a;

...

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

wird garantiert zuerst 1 zuweisen iund dann 2 zuweisen j. Es kann jedoch nicht garantiert werden, dass adie Zuweisung zwischenzeitlich erfolgt. Der Compiler kann diese Zuweisung vor oder nach dem Code-Snippet vornehmen, und zwar grundsätzlich jederzeit bis zum ersten (sichtbaren) Lesen von a.

Ohne die Speicherbarriere der oben genannten Makros könnte der Compiler übersetzen

uint32_t x;

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

zu

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

oder

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

(Der Vollständigkeit halber muss ich sagen, dass Speicherbarrieren, wie sie in den sei / cli-Makros impliziert sind, die Verwendung von möglicherweise sogar überflüssig machen volatile, wenn alle Zugriffe mit diesen Barrieren eingeklammert sind.)


7
Gute Diskussion über die
Nichtflüchtigkeit

3
Ich erwähne immer gerne die Definition von flüchtig in 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. Mehr Leute sollten es lesen.
Jeroen3,

3
Es kann erwähnenswert sein, dass cli/ seieine zu schwere Lösung ist, wenn Ihr einziges Ziel darin besteht, eine Speicherbarriere zu erreichen, keine Interrupts zu verhindern. Diese Makros generieren aktuelle cli/ seiAnweisungen und zusätzlich den Speicher für Unrat, und es ist dieses Unrat, das zur Barriere führt. Um nur eine Speichersperre zu haben, ohne Interrupts zu deaktivieren, können Sie Ihr eigenes Makro mit dem Body wie __asm__ __volatile__("":::"memory")(dh leerer Assembler-Code mit Memory Clobber) definieren.
Ruslan

3
@NicHartley No. C17 5.1.2.3 §6 definiert das beobachtbare Verhalten : "Zugriffe auf flüchtige Objekte werden streng nach den Regeln der abstrakten Maschine ausgewertet." Der C-Standard ist nicht wirklich klar, wo Speicherbarrieren insgesamt benötigt werden. Am Ende eines Ausdrucks, der verwendet volatilewird, steht ein Sequenzpunkt, und alles, was danach folgt, muss "sequenziert werden". Das heißt, dieser Ausdruck ist eine Art Gedächtnisbarriere. Compiler-Hersteller haben sich entschieden, alle Arten von Mythen zu verbreiten, um dem Programmierer die Verantwortung für Speicherbarrieren aufzuerlegen, aber dies verstößt gegen die Regeln der "abstrakten Maschine".
Lundin

2
@ JimmyB Local volatile kann für Code wie nützlich sein volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka

13

Das Schlüsselwort volatile teilt dem Compiler mit, dass der Zugriff auf die Variable einen beobachtbaren Effekt hat. Das heißt, jedes Mal, wenn Ihr Quellcode die Variable verwendet, MUSS der Compiler einen Zugriff auf die Variable erstellen. Sei das ein Lese- oder Schreibzugriff.

Dies hat zur Folge, dass jede Änderung der Variablen außerhalb des normalen Codeflusses auch vom Code beobachtet wird. ZB wenn ein Interrupt-Handler den Wert ändert. Oder wenn die Variable tatsächlich ein Hardware-Register ist, das sich von selbst ändert.

Dieser große Vorteil ist auch der Nachteil. Jeder einzelne Zugriff auf die Variable durchläuft die Variable, und der Wert wird niemals in einem Register gespeichert, um den Zugriff für einen beliebigen Zeitraum zu beschleunigen. Das bedeutet, dass eine flüchtige Variable langsam ist. Größenordnungen langsamer. Verwenden Sie daher volatile nur dort, wo es tatsächlich erforderlich ist.

In Ihrem Fall wird die globale Variable, soweit Sie den Code angezeigt haben, nur geändert, wenn Sie sie selbst aktualisieren adcValue = readADC();. Der Compiler weiß, wann dies geschieht, und speichert den Wert von adcValue niemals in einem Register über etwas, das die readFromADC()Funktion möglicherweise aufruft . Oder irgendeine Funktion, von der es nichts weiß. Oder irgendetwas, das Zeiger manipuliert, die auf etwas zeigen könnten, adcValueund so weiter. Es ist wirklich keine Volatilität erforderlich, da sich die Variable niemals auf unvorhersehbare Weise ändert.


6
Ich bin mit dieser Antwort einverstanden, aber "Größenordnungen langsamer" klingt zu schrecklich.
kkrambo

6
Auf ein CPU-Register kann auf modernen superskalaren CPUs in weniger als einem CPU-Zyklus zugegriffen werden. Andererseits kann ein Zugriff auf den tatsächlichen nicht zwischengespeicherten Speicher (denken Sie daran, dass dies durch externe Hardware geändert wird, sodass keine CPU-Caches zulässig sind) im Bereich von 100 bis 300 CPU-Zyklen liegen. Also ja, Größen. Wird auf einem AVR oder einem ähnlichen Mikrocontroller nicht so schlecht sein, aber die Frage spezifiziert keine Hardware.
Goswin von Brederlow

7
In eingebetteten (Mikrocontroller-) Systemen ist die Strafe für den RAM-Zugriff oft viel geringer. Zum Beispiel benötigen die AVRs nur zwei CPU-Zyklen für das Lesen oder Schreiben aus dem RAM (ein Register-Register-Vorgang dauert einen Zyklus), so dass die Einsparungen beim Speichern von Dingen in Registern sich dem Maximum nähern (aber dieses tatsächlich nie erreichen). 2 Taktzyklen pro Zugriff. - Relativ gesehen dauert das Speichern eines Wertes von Register X in RAM und das sofortige erneute Laden dieses Wertes in Register X für weitere Berechnungen 2x2 = 4 anstelle von 0 Zyklen (wenn nur der Wert in X beibehalten wird) und ist daher unendlich langsamer :)
JimmyB

1
Es ist "Größenordnungen langsamer" im Zusammenhang mit "Schreiben oder Lesen von einer bestimmten Variablen", ja. Im Kontext eines vollständigen Programms jedoch, das wahrscheinlich wesentlich mehr als das Lesen / Schreiben von einer Variablen immer und immer wieder tut, nein, nicht wirklich. In diesem Fall ist der Gesamtunterschied wahrscheinlich „gering bis vernachlässigbar“. Bei der Aussage über die Leistung sollte sorgfältig geklärt werden, ob sich die Aussage auf eine bestimmte Operation oder auf ein Programm als Ganzes bezieht. Eine selten genutzte Operation um einen Faktor von ~ 300x zu verlangsamen, ist fast nie eine große Sache.
30.

1
Du meinst, diesen letzten Satz? Das heißt viel mehr im Sinne von "vorzeitige Optimierung ist die Wurzel allen Übels". Natürlich sollten Sie nicht volatilealles verwenden, nur weil , aber Sie sollten auch nicht davor zurückschrecken, wenn Sie der Meinung sind, dass dies aufgrund präventiver Leistungsprobleme gerechtfertigt ist.
30.

9

Das flüchtige Schlüsselwort wird hauptsächlich in eingebetteten C-Anwendungen verwendet, um eine globale Variable zu markieren , in die in einem Interrupt-Handler geschrieben wird. Dies ist in diesem Fall sicherlich nicht optional.

Ohne dies kann der Compiler nicht nachweisen, dass der Wert jemals nach der Initialisierung geschrieben wurde, da er nicht nachweisen kann, dass der Interrupt-Handler jemals aufgerufen wurde. Daher denkt es, dass es die Variable aus dem Dasein heraus optimieren kann.


2
Natürlich gibt es auch andere praktische Anwendungen, aber dies ist die häufigste.
Vicatcu

1
Wenn der Wert nur in einem ISR gelesen wird (und von main () geändert wird), müssen Sie möglicherweise auch volatile verwenden, um den ATOMIC-Zugriff für Multi-Byte-Variablen zu gewährleisten.
Rev1.0

15
@ Rev1.0 Nein, flüchtig garantiert keine Aromizität. Dieses Anliegen ist gesondert zu behandeln.
Chris Stratton

1
Es wird weder von der Hardware gelesen noch der Code unterbrochen. Sie nehmen Dinge von der Frage an, die nicht da sind. Es kann in seiner jetzigen Form nicht wirklich beantwortet werden.
Lundin

3
"markiere eine globale Variable, in die in einem Interrupt-Handler geschrieben wird" nope. Es ist eine Variable zu markieren; global oder anderweitig; dass es durch etwas außerhalb des Compiler-Verständnisses geändert werden kann. Interrupt nicht erforderlich. Es könnte Shared Memory oder jemand sein, der eine Sonde in den Speicher
steckt

9

Es gibt zwei Fälle, in denen Sie volatilein eingebetteten Systemen verwenden müssen.

  • Beim Lesen aus einem Hardware-Register.

    Das heißt, das speicherabgebildete Register selbst ist Teil der Hardware-Peripherie innerhalb der MCU. Es wird wahrscheinlich einen kryptischen Namen wie "ADC0DR" haben. Dieses Register muss im C-Code entweder über eine vom Werkzeughersteller bereitgestellte Registerkarte oder von Ihnen selbst definiert werden. Um es selbst zu machen, würden Sie (unter der Annahme eines 16-Bit-Registers) Folgendes tun:

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

    Dabei ist 0x1234 die Adresse, auf die die MCU das Register abgebildet hat. Da dies volatilebereits Teil des oben genannten Makros ist, ist jeder Zugriff auf dieses Makro flüchtig. Also dieser Code ist in Ordnung:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Wenn eine Variable zwischen einem ISR und dem zugehörigen Code unter Verwendung des Ergebnisses des ISR geteilt wird.

    Wenn Sie so etwas haben:

    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;
    }

    Dann könnte der Compiler denken: "adc_data ist immer 0, weil es nirgendwo aktualisiert wird. Und diese ADC0_interrupt () - Funktion wird niemals aufgerufen, so dass die Variable nicht geändert werden kann". Der Compiler merkt normalerweise nicht, dass Interrupts von der Hardware und nicht von der Software aufgerufen werden. Daher entfernt der Compiler den Code, if(adc_data > 0){ do_stuff(adc_data); }da er der Meinung ist, dass er niemals wahr sein kann, was zu einem sehr seltsamen und schwer zu debuggenden Fehler führt.

    Durch die Deklaration adc_data volatiledarf der Compiler keine solchen Annahmen treffen und den Zugriff auf die Variable nicht optimieren.


Wichtige Notizen:

  • Ein ISR muss immer im Hardwaretreiber deklariert werden. In diesem Fall sollte sich der ADC ISR im ADC-Treiber befinden. Niemand anderes als der Fahrer sollte mit dem ISR kommunizieren - alles andere ist Spaghetti-Programmierung.

  • Wenn C schreiben, die gesamte Kommunikation zwischen einem ISR und dem Hintergrundprogramm muss gegen Rennbedingungen geschützt werden. Immer , jedes Mal, keine Ausnahmen. Die Größe des MCU-Datenbusses spielt keine Rolle, denn selbst wenn Sie eine einzelne 8-Bit-Kopie in C erstellen, kann die Sprache keine atomaren Operationen garantieren. Nur wenn Sie die C11-Funktion verwenden _Atomic. Wenn diese Funktion nicht verfügbar ist, müssen Sie eine Art Semaphor verwenden oder den Interrupt beim Lesen usw. deaktivieren. Inline-Assembler ist eine weitere Option. volatilegarantiert keine Atomizität.


    Folgendes kann passieren: - Wert vom Stapel in das Register
    laden. - Es tritt ein
    Interrupt auf. - Wert aus dem Register verwenden

    Und dann spielt es keine Rolle, ob der Teil "use value" eine einzelne Anweisung für sich ist. Leider ist sich ein erheblicher Teil aller Programmierer für eingebettete Systeme dessen bewusst, so dass es wahrscheinlich der häufigste Fehler ist, der jemals bei eingebetteten Systemen aufgetreten ist. Immer wieder sporadisch, schwer zu provozieren, schwer zu finden.


Ein Beispiel für einen korrekt geschriebenen ADC-Treiber sieht folgendermaßen aus (vorausgesetzt, C11 _Atomicist nicht verfügbar):

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;
  }
}
  • Dieser Code geht davon aus, dass ein Interrupt an sich nicht unterbrochen werden kann. Auf solchen Systemen kann ein einfacher Boolescher Wert als Semaphor fungieren und muss nicht atomar sein, da es keinen Schaden gibt, wenn der Interrupt vor dem Setzen des Booleschen Werts auftritt. Der Nachteil der oben beschriebenen vereinfachten Methode ist, dass ADC-Lesevorgänge verworfen werden, wenn Rennbedingungen auftreten, und stattdessen der vorherige Wert verwendet wird. Dies kann auch vermieden werden, aber dann wird der Code komplexer.

  • Hier volatileschützt vor Optimierungsfehlern. Es hat nichts mit den Daten zu tun, die aus einem Hardwareregister stammen, nur dass die Daten mit einem ISR geteilt werden.

  • staticschützt vor Spaghetti-Programmierung und Namespace-Verschmutzung, indem die Variable lokal für den Treiber festgelegt wird. (Dies ist in Single-Core- und Single-Thread-Anwendungen in Ordnung, jedoch nicht in Multi-Thread-Anwendungen.)


Schwer zu debuggen ist relativ, wenn der Code entfernt wird, werden Sie feststellen, dass Ihr geschätzter Code weg ist - das ist eine ziemlich kühne Aussage, dass etwas nicht stimmt. Aber ich stimme zu, es kann sehr seltsame und schwer zu debuggende Effekte geben.
Arsenal

@Arsenal Wenn Sie einen netten Debugger haben, der Assembler mit dem C verbindet, und Sie wissen zumindest ein bisschen asm, dann kann es ja leicht zu erkennen sein. Bei komplexerem Code ist es jedoch nicht trivial, einen großen Teil des maschinengenerierten Asms zu durchlaufen. Oder wenn Sie asm nicht kennen. Oder wenn Ihr Debugger Mist ist und nicht asm zeigt (cougheclipsecough).
Lundin

Könnte sein, dass ich ein bisschen verwöhnt bin, wenn ich dann Lauterbach-Debugger benutze. Wenn Sie versuchen, einen Haltepunkt in Code zu setzen, der optimiert wurde, wird er an einer anderen Stelle gesetzt, und Sie wissen, dass dort etwas vor sich geht.
Arsenal

@Arsenal Ja, die Art von gemischtem C / asm, die man in Lauterbach bekommt, ist keineswegs Standard. Die meisten Debugger zeigen den asm, wenn überhaupt, in einem separaten Fenster an.
Lundin

semaphoresollte auf jeden fall sein volatile! Tatsächlich ist dies der grundlegendste Anwendungsfall, der Folgendes erfordert volatile: Signalisieren Sie etwas von einem Ausführungskontext in einen anderen. - In Ihrem Beispiel könnte der Compiler einfach weglassen, semaphore = true;weil er "sieht", dass sein Wert niemals gelesen wird, bevor er von überschrieben wird semaphore = false;.
JimmyB

5

In den in der Frage vorgestellten Codeausschnitten gibt es noch keinen Grund, volatile zu verwenden. Es ist irrelevant, dass der Wert von adcValueaus einem ADC stammt. Und adcValueglobal zu sein sollte Sie misstrauisch machen, ob adcValuees volatil sein sollte, aber es ist kein Grund für sich.

Global zu sein ist ein Hinweis, weil es die Möglichkeit eröffnet, adcValuevon mehr als einem Programmkontext aus darauf zuzugreifen. Ein Programmkontext enthält einen Interrupt-Handler und eine RTOS-Task. Wenn die globale Variable durch einen Kontext geändert wird, können die anderen Programmkontexte nicht davon ausgehen, dass sie den Wert aus einem vorherigen Zugriff kennen. Jeder Kontext muss den Variablenwert jedes Mal neu lesen, wenn er verwendet wird, da der Wert möglicherweise in einem anderen Programmkontext geändert wurde. Einem Programmkontext ist nicht bekannt, wann ein Interrupt- oder Taskwechsel auftritt. Daher muss davon ausgegangen werden, dass sich globale Variablen, die von mehreren Kontexten verwendet werden, aufgrund eines möglichen Kontextwechsels zwischen Zugriffen auf die Variable ändern können. Dafür ist die volatile-Deklaration gedacht. Es teilt dem Compiler mit, dass sich diese Variable außerhalb Ihres Kontexts ändern kann. Lesen Sie sie daher bei jedem Zugriff und gehen Sie nicht davon aus, dass Sie den Wert bereits kennen.

Wenn die Variable einer Hardwareadresse im Speicher zugeordnet ist, sind die von der Hardware vorgenommenen Änderungen tatsächlich ein anderer Kontext außerhalb des Kontexts Ihres Programms. Memory-Mapped ist also auch ein Hinweis. Wenn Ihre readADC()Funktion beispielsweise auf einen Speicherzuordnungswert zugreift, um den ADC-Wert abzurufen, sollte diese Speicherzuordnungsvariable möglicherweise flüchtig sein.

Zurück zu Ihrer Frage: Wenn Ihr Code mehr enthält und adcValuevon einem anderen Code aufgerufen wird, der in einem anderen Kontext ausgeführt wird, adcValuesollte er volatil sein.


4

"Globale Variable, die sich direkt von der Hardware ändert"

Nur weil der Wert von einem Hardware-ADC-Register kommt, heißt das nicht, dass er von der Hardware "direkt" geändert wird.

In Ihrem Beispiel rufen Sie einfach readADC () auf, was einen ADC-Registerwert zurückgibt. Dies ist in Bezug auf den Compiler in Ordnung, da er weiß, dass adcValue an diesem Punkt einen neuen Wert zugewiesen wird.

Anders wäre es, wenn Sie eine ADC-Interruptroutine verwenden, um den neuen Wert zuzuweisen, der aufgerufen wird, wenn ein neuer ADC-Wert bereit ist. In diesem Fall hat der Compiler keine Ahnung, wann der entsprechende ISR aufgerufen wird, und entscheidet möglicherweise, dass auf adcValue nicht auf diese Weise zugegriffen wird. Hier würde volatile helfen.


1
Da Ihr Code niemals die ISR-Funktion "aufruft", sieht der Compiler, dass die Variable nur in einer Funktion aktualisiert wird, die niemand aufruft. Der Compiler optimiert es also.
Swanand

1
Es hängt vom Rest des Codes ab, ob adcValue nirgendwo gelesen wird (wie nur durch den Debugger) oder ob es nur einmal an einer Stelle gelesen wird, der Compiler wird es wahrscheinlich optimieren.
Damien

2
@Damien: Es kommt immer darauf an, aber ich wollte die eigentliche Frage beantworten: "Soll ich in diesem Fall das Schlüsselwort volatile verwenden?" so kurz wie möglich.
Rev1.0

4

Das Verhalten des volatileArguments hängt weitgehend von Ihrem Code, dem Compiler und der durchgeführten Optimierung ab.

Es gibt zwei Anwendungsfälle, die ich persönlich benutze volatile:

  • Wenn es eine Variable gibt, die ich mit dem Debugger betrachten möchte, die jedoch vom Compiler optimiert wurde (dh, sie wurde gelöscht, weil festgestellt wurde, dass diese Variable nicht erforderlich ist), volatilewird der Compiler durch Hinzufügen gezwungen, sie beizubehalten kann beim Debuggen gesehen werden.

  • Wenn sich die Variable möglicherweise "außerhalb des Codes" ändert, ist dies normalerweise der Fall, wenn Hardware darauf zugreift oder wenn Sie die Variable direkt einer Adresse zuordnen.

In embedded gibt es auch manchmal einige Fehler in den Compilern, die eine Optimierung durchführen, die eigentlich nicht funktioniert, und manchmal volatiledie Probleme lösen können.

Wenn Sie Ihre Variable global deklariert haben, wird sie wahrscheinlich nicht optimiert, solange die Variable im Code verwendet wird, zumindest wenn sie geschrieben und gelesen wird.

Beispiel:

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

In diesem Fall wird die Variable wahrscheinlich für printf ("% i", 1) optimiert.

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

wird nicht optimiert

Noch einer:

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

In diesem Fall optimiert der Compiler möglicherweise (wenn Sie die Geschwindigkeit optimieren) und verwirft die Variable

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

Für Ihren Anwendungsfall hängt "es möglicherweise davon ab", wie der Rest Ihres Codes an adcValueanderer Stelle verwendet wird und welche Compilerversion / Optimierungseinstellungen Sie verwenden.

Manchmal kann es ärgerlich sein, einen Code zu haben, der ohne Optimierung arbeitet, aber einmal optimiert bricht.

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

Dies könnte optimiert werden für 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
}

Diese werden wahrscheinlich nicht optimiert, aber Sie wissen nie, "wie gut der Compiler ist" und können sich mit den Compiler-Parametern ändern. In der Regel werden Compiler mit guter Optimierung lizenziert.


1
Zum Beispiel a = 1; b = a; und c = b; Der Compiler könnte denken, warten Sie eine Minute, a und b sind nutzlos, lassen Sie uns einfach 1 direkt auf c setzen. Natürlich werden Sie das in Ihrem Code nicht tun, aber der Compiler ist besser darin, diese zu finden, auch wenn Sie versuchen, optimierten Code sofort zu schreiben, wäre dies unlesbar.
Damien

2
Ein korrekter Code mit einem korrekten Compiler wird bei aktivierten Optimierungen nicht unterbrochen. Die Korrektheit des Compilers ist ein kleines Problem, aber zumindest bei IAR bin ich nicht auf eine Situation gestoßen, in der die Optimierung dazu führte, dass Code gebrochen wurde, wo dies nicht der Fall sein sollte.
Arsenal

5
Viele Fälle, in denen die Optimierung den Code bricht, sind, wenn Sie sich auch in UB- Territorium wagen .
Pipe

2
Ja, ein Nebeneffekt von Volatile ist, dass es das Debuggen unterstützen kann. Das ist aber kein guter Grund, volatile einzusetzen. Sie sollten Optimierungen wahrscheinlich deaktivieren, wenn einfaches Debuggen Ihr Ziel ist. Diese Antwort erwähnt nicht einmal Interrupts.
kkrambo

2
Durch Hinzufügen zum Debugging-Argument wird volatileder Compiler gezwungen, eine Variable im RAM zu speichern und diesen RAM zu aktualisieren, sobald der Variablen ein Wert zugewiesen wird. Meistens 'löscht' der Compiler keine Variablen, da normalerweise keine Zuweisungen ohne Wirkung geschrieben werden. Es kann jedoch vorkommen, dass die Variable in einem CPU-Register bleibt und später oder nie in den Arbeitsspeicher geschrieben wird. Debugger finden häufig nicht das CPU-Register, in dem sich die Variable befindet, und können daher ihren Wert nicht anzeigen.
JimmyB

1

Viele technische Erklärungen, aber ich möchte mich auf die praktische Anwendung konzentrieren.

Das volatileSchlüsselwort zwingt den Compiler, den Wert der Variablen bei jeder Verwendung aus dem Speicher zu lesen oder zu schreiben. Normalerweise versucht der Compiler zu optimieren, führt jedoch keine unnötigen Lese- und Schreibvorgänge durch, z. B. indem der Wert in einem CPU-Register gespeichert wird, anstatt jedes Mal auf den Speicher zuzugreifen.

Dies hat zwei Hauptverwendungen im eingebetteten Code. Erstens wird es für Hardware-Register verwendet. Hardware-Register können sich ändern, z. B. kann ein ADC-Ergebnisregister vom ADC-Peripheriegerät geschrieben werden. Hardware-Register können auch Aktionen ausführen, wenn auf sie zugegriffen wird. Ein gängiges Beispiel ist das Datenregister eines UART, das beim Lesen häufig Interrupt-Flags löscht.

Der Compiler würde normalerweise versuchen, wiederholte Lese- und Schreibvorgänge des Registers unter der Annahme zu optimieren, dass sich der Wert niemals ändert, sodass nicht ständig darauf zugegriffen werden muss. Das volatileSchlüsselwort erzwingt jedoch, dass jedes Mal eine Leseoperation ausgeführt wird.

Die zweite häufige Verwendung betrifft Variablen, die sowohl vom Interrupt-Code als auch vom Nicht-Interrupt-Code verwendet werden. Interrupts werden nicht direkt aufgerufen, so dass der Compiler nicht bestimmen kann, wann sie ausgeführt werden. Daher wird davon ausgegangen, dass keine Zugriffe innerhalb des Interrupts erfolgen. Da das volatileSchlüsselwort den Compiler zwingt, jedes Mal auf die Variable zuzugreifen, wird diese Annahme entfernt.

Es ist wichtig zu beachten, dass das volatileSchlüsselwort keine vollständige Lösung für diese Probleme darstellt, und es muss sorgfältig vorgegangen werden, um sie zu vermeiden. Beispielsweise erfordert eine 16-Bit-Variable auf einem 8-Bit-System zwei Speicherzugriffe zum Lesen oder Schreiben, und selbst wenn der Compiler gezwungen ist, diese Zugriffe auszuführen, treten sie sequentiell auf, und es ist möglich, dass die Hardware beim ersten Zugriff oder beim ersten Zugriff handelt ein Interrupt zwischen den beiden auftreten.


0

Wenn kein volatileQualifikationsmerkmal vorhanden ist, kann der Wert eines Objekts in bestimmten Teilen des Codes an mehr als einer Stelle gespeichert werden. Betrachten Sie zum Beispiel Folgendes:

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

In den frühen Tagen von C hätte ein Compiler die Anweisung verarbeitet

foo++;

über die schritte:

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

Anspruchsvollere Compiler werden jedoch erkennen, dass, wenn der Wert von "foo" während der Schleife in einem Register gehalten wird, er nur einmal vor der Schleife geladen und danach einmal gespeichert werden muss. Während der Schleife bedeutet dies jedoch, dass der Wert von "foo" an zwei Stellen gespeichert wird - im globalen Speicher und im Register. Dies ist kein Problem, wenn der Compiler alle Arten des Zugriffs auf "foo" in der Schleife sehen kann, aber Probleme verursachen kann, wenn auf den Wert von "foo" in einem Mechanismus zugegriffen wird, über den der Compiler nichts weiß ( wie ein Interrupt-Handler).

Möglicherweise konnten die Autoren des Standards ein neues Qualifikationsmerkmal hinzufügen, das den Compiler explizit auffordert, solche Optimierungen vorzunehmen, und sagen, dass die altmodische Semantik in ihrer Abwesenheit angewendet würde, aber in Fällen, in denen die Optimierungen von großem Nutzen sind, übertrifft die Anzahl Diejenigen, bei denen es problematisch wäre, so dass der Standard den Compilern stattdessen die Annahme erlaubt, dass solche Optimierungen sicher sind, wenn keine Beweise dafür vorliegen, dass dies nicht der Fall ist. Der Zweck des volatileSchlüsselworts ist es, solche Beweise zu liefern.

Ein paar Streitpunkte zwischen einigen Compiler-Autoren und Programmierern treten in folgenden Situationen auf:

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);
}

In der Vergangenheit würden die meisten Compiler entweder die Möglichkeit einräumen, dass das Schreiben eines volatileSpeicherorts willkürliche Nebenwirkungen hervorruft, und das Zwischenspeichern von Werten in Registern in einem solchen Speicher vermeiden, oder sie würden das Zwischenspeichern von Werten in Registern über Aufrufe von Funktionen hinweg unterlassen, die sind nicht als "Inline" qualifiziert und würde daher 0x1234 schreiben output_buffer[0], die Daten ausgeben, warten, bis der Vorgang abgeschlossen ist, dann 0x2345 schreiben output_buffer[0]und fortfahren. Der Standard befasst sich nicht erfordern Implementierungen den Akt der Speicherung der Adresse der behandeln output_bufferin einvolatile-qualifizierter Zeiger als Zeichen dafür, dass etwas mit ihm geschehen könnte, bedeutet jedoch, dass der Compiler dies nicht versteht, da die Autoren der Ansicht waren, dass Compiler, die für verschiedene Plattformen und Zwecke vorgesehen sind, erkennen würden, dass dies diesen Zwecken auf diesen Plattformen dienen würde ohne dass es gesagt werden muss. Folglich gehen einige "clevere" Compiler wie gcc und clang davon aus, dass, obwohl die Adresse von output_bufferin einen flüchtig qualifizierten Zeiger zwischen den beiden Speichern von geschrieben output_buffer[0]wird, dies keinen Grund zur Annahme gibt, dass der Wert, der in diesem Objekt von at enthalten ist, von irgendetwas abhängt diese Zeit.

Während Zeiger, die direkt aus Ganzzahlen erzeugt werden, selten für andere Zwecke verwendet werden, als um Dinge auf eine Weise zu manipulieren, die Compiler wahrscheinlich nicht verstehen, verlangt der Standard wiederum nicht, dass Compiler solche Zugriffe wie behandeln volatile. Folglich kann das erste Schreiben *((unsigned short*)0xC0001234)von "cleveren" Compilern wie gcc und clang weggelassen werden, da die Betreuer solcher Compiler eher behaupten würden, dass Code, der es versäumt, solche Dinge als volatile"kaputt" zu qualifizieren, als zu erkennen, dass die Kompatibilität mit solchem ​​Code nützlich ist . Viele vom Hersteller bereitgestellte Header-Dateien lassen volatileQualifikationsmerkmale aus, und ein Compiler, der mit den vom Hersteller bereitgestellten Header-Dateien kompatibel ist, ist nützlicher als eine andere.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.