Una definizione di volatile
volatile
dice al compilatore che il valore della variabile può cambiare all'insaputa del compilatore. Quindi il compilatore non può presumere che il valore non sia cambiato solo perché il programma C sembra non averlo modificato.
D'altra parte, significa che il valore della variabile può essere richiesto (letto) da qualche altra parte che il compilatore non conosce, quindi deve assicurarsi che ogni assegnazione alla variabile sia effettivamente eseguita come un'operazione di scrittura.
Casi d'uso
volatile
è richiesto quando
- che rappresentano i registri hardware (o I / O mappati in memoria) come variabili - anche se il registro non verrà mai letto, il compilatore non deve semplicemente saltare l'operazione di scrittura pensando "Stupido programmatore. Cerca di memorizzare un valore in una variabile che lui / lei non rileggerà mai. Non noterà nemmeno se omettiamo la scrittura. " Al contrario, anche se il programma non scrive mai un valore nella variabile, il suo valore può comunque essere modificato dall'hardware.
- condividere variabili tra contesti di esecuzione (ad es. ISR / programma principale) (vedere la risposta di @ kkramo)
Effetti di volatile
Quando viene dichiarata una variabile, volatile
il compilatore deve assicurarsi che ogni assegnazione ad essa nel codice del programma si rifletta in un'operazione di scrittura effettiva e che ogni lettura nel codice del programma legga il valore dalla memoria (mmapped).
Per le variabili non volatili, il compilatore presuppone di sapere se / quando il valore della variabile cambia e può ottimizzare il codice in diversi modi.
Per uno, il compilatore può ridurre il numero di letture / scritture in memoria, mantenendo il valore nei registri della CPU.
Esempio:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Qui, il compilatore probabilmente non allocerà nemmeno la RAM per la result
variabile e non memorizzerà mai i valori intermedi da nessuna parte ma in un registro della CPU.
Se result
era volatile, ogni occorrenza di result
nel codice C richiederebbe al compilatore di eseguire un accesso alla RAM (o una porta I / O), portando a prestazioni inferiori.
In secondo luogo, il compilatore può riordinare le operazioni su variabili non volatili per prestazioni e / o dimensioni del codice. Esempio semplice:
int a = 99;
int b = 1;
int c = 99;
potrebbe essere riordinato a
int a = 99;
int c = 99;
int b = 1;
che può salvare un'istruzione assembler perché il valore 99
non dovrà essere caricato due volte.
Se a
, b
e se c
fossero volatili, il compilatore dovrebbe emettere istruzioni che assegnano i valori nell'ordine esatto in cui sono indicati nel programma.
L'altro esempio classico è così:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
Se, in questo caso, signal
non lo fosse volatile
, il compilatore "penserebbe" che while( signal == 0 )
potrebbe essere un ciclo infinito (perché signal
non verrà mai modificato dal codice all'interno del ciclo ) e potrebbe generare l'equivalente di
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
Gestione ponderata dei volatile
valori
Come indicato sopra, una volatile
variabile può introdurre una penalità di prestazione quando vi si accede più spesso di quanto effettivamente richiesto. Per mitigare questo problema, puoi "non volatile" il valore assegnando una variabile non volatile, come
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
}
Ciò può essere particolarmente utile negli ISR in cui si desidera essere il più rapidamente possibile senza accedere allo stesso hardware o memoria più volte quando si sa che non è necessario perché il valore non cambierà mentre l'ISR è in esecuzione. Questo è comune quando l'ISR è il "produttore" di valori per la variabile, come sysTickCount
nell'esempio sopra. Su un AVR sarebbe particolarmente doloroso avere la funzione di doSysTick()
accedere agli stessi quattro byte in memoria (quattro istruzioni = 8 cicli CPU per accesso a sysTickCount
) cinque o sei volte anziché solo due volte, perché il programmatore sa che il valore non sarà essere cambiato da qualche altro codice mentre è in doSysTick()
esecuzione.
Con questo trucco, fai essenzialmente la stessa cosa che fa il compilatore per variabili non volatili, cioè leggili dalla memoria solo quando è necessario, mantieni il valore in un registro per un po 'di tempo e riscrivi in memoria solo quando deve ; ma questa volta, si conosce meglio il compilatore se / quando letture / scritture devono accadere, in modo da alleviare il compilatore da questo compito ottimizzazione e fai da te.
Limitazioni di volatile
Accesso non atomico
volatile
non non fornire l'accesso a variabili atomica composti da più parole. In questi casi, dovrai fornire l'esclusione reciproca con altri mezzi, oltre all'utilizzo volatile
. Sull'AVR, è possibile utilizzare ATOMIC_BLOCK
da <util/atomic.h>
o cli(); ... sei();
chiamate semplici . Le rispettive macro fungono anche da barriera di memoria, il che è importante quando si tratta dell'ordine degli accessi:
Ordine di esecuzione
volatile
impone un ordine di esecuzione rigoroso solo rispetto ad altre variabili volatili. Ciò significa che, per esempio
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
è garantito assegnare prima 1 a i
e quindi assegnare 2 a j
. Tuttavia, è non è garantito che a
verrà assegnato in mezzo; il compilatore può eseguire tale compito prima o dopo lo snippet di codice, praticamente in qualsiasi momento fino alla prima lettura (visibile) di a
.
Se non fosse per la barriera di memoria delle macro sopra menzionate, il compilatore sarebbe autorizzato a tradurre
uint32_t x;
cli();
x = volatileVar;
sei();
per
x = volatileVar;
cli();
sei();
o
cli();
sei();
x = volatileVar;
(Per completezza, devo dire che le barriere di memoria, come quelle implicite dai macro sei / cli, possono effettivamente ovviare all'uso di volatile
, se tutti gli accessi sono racchiusi tra queste barriere.)