Eine Definition von volatile
volatile
teilt 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, volatile
muss 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 result
Variable zuweisen und die Zwischenwerte niemals irgendwo anders als in einem CPU-Register speichern.
Wenn result
flüchtig, würde jedes Auftreten result
im 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 99
nicht zweimal geladen werden muss.
Wenn a
, b
und c
flü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 signal
war nicht volatile
, würde der Compiler ‚denken‘ , dass while( signal == 0 )
eine unendliche Schleife sein kann (weil signal
nie 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 volatile
Werten
Wie oben erwähnt, kann eine volatile
Variable 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 sysTickCount
im 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
volatile
bietet 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_BLOCK
aus <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
volatile
legt 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 i
und dann 2 zuweisen j
. Es kann jedoch nicht garantiert werden, dass a
die 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.)