Une définition de volatile
volatile
indique au compilateur que la valeur de la variable peut changer à l'insu du compilateur. Par conséquent, le compilateur ne peut pas supposer que la valeur n'a pas changé simplement parce que le programme C ne semble pas l'avoir changée.
D'un autre côté, cela signifie que la valeur de la variable peut être requise (lue) ailleurs que le compilateur ignore, c'est pourquoi il doit s'assurer que chaque affectation à la variable est réellement effectuée en tant qu'opération d'écriture.
Cas d'utilisation
volatile
est requis lorsque
- représentant des registres matériels (ou des E / S mappées en mémoire) sous forme de variables - même si le registre ne sera jamais lu, le compilateur ne doit pas simplement ignorer l'opération d'écriture en pensant "Programmeur stupide. Essaie de stocker une valeur dans une variable qu'il / elle ne lirons jamais. Il ne remarquera même pas si nous omettons l’écriture. " Inversement, même si le programme n'écrit jamais de valeur dans la variable, sa valeur peut toujours être modifiée par le matériel.
- partage de variables entre les contextes d'exécution (par exemple, ISR / programme principal) (voir la réponse de @ kkramo)
Les effets de volatile
Lorsqu'une variable est déclarée, volatile
le compilateur doit s'assurer que chaque affectation à celle-ci dans le code de programme est reflétée dans une opération d'écriture réelle et que chaque lecture dans le code de programme lit la valeur dans la mémoire (mappée).
Pour les variables non volatiles, le compilateur suppose qu'il sait si / quand la valeur de la variable change et peut optimiser le code de différentes manières.
D'une part, le compilateur peut réduire le nombre de lectures / écritures en mémoire en conservant la valeur dans les registres de la CPU.
Exemple:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Ici, le compilateur n'allouera probablement même pas de RAM pour la result
variable et ne stockera jamais les valeurs intermédiaires ailleurs que dans un registre de la CPU.
Si elle result
était volatile, chaque occurrence du result
code C obligerait le compilateur à effectuer un accès à la RAM (ou à un port d'E / S), entraînant une baisse des performances.
Deuxièmement, le compilateur peut réorganiser les opérations sur des variables non volatiles pour des performances et / ou une taille de code. Exemple simple:
int a = 99;
int b = 1;
int c = 99;
pourrait être réordonné à
int a = 99;
int c = 99;
int b = 1;
ce qui peut sauver une instruction assembleur car la valeur 99
ne devra pas être chargée deux fois.
Si a
, b
et c
étaient volatiles, le compilateur devrait émettre des instructions qui assignent les valeurs dans l’ordre exact telles qu’elles sont données dans le programme.
L’autre exemple classique est le suivant:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
Si, dans ce cas, ce signal
n'était pas le cas volatile
, le compilateur «penserait» que cela while( signal == 0 )
pourrait être une boucle infinie (car signal
elle ne sera jamais modifiée par le code à l'intérieur de la boucle ) et pourrait générer l'équivalent de
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
Traitement attentif des volatile
valeurs
Comme indiqué ci-dessus, une volatile
variable peut entraîner une baisse de performance si elle est utilisée plus souvent que nécessaire. Pour atténuer ce problème, vous pouvez "non-volatile" la valeur en l'attribuant à une variable non volatile, telle que
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
}
Cela peut être particulièrement bénéfique dans les ISR où vous voulez être aussi rapide que possible sans accéder au même matériel ou à la même mémoire plusieurs fois lorsque vous savez que ce n'est pas nécessaire, car la valeur ne changera pas pendant l'exécution de votre ISR. Ceci est courant lorsque l'ISR est le «producteur» de valeurs pour la variable, comme sysTickCount
dans l'exemple ci-dessus. Sur un AVR, il serait particulièrement pénible de laisser la fonction doSysTick()
accéder aux quatre mêmes octets en mémoire (quatre instructions = 8 cycles de traitement par accès à sysTickCount
) cinq ou six fois au lieu de deux fois, car le programmeur sait que la valeur ne sera pas être changé d'un autre code pendant qu'il / elle doSysTick()
court.
Avec cette astuce, vous faites essentiellement la même chose que le compilateur pour les variables non volatiles, c’est-à-dire que vous ne les lisez de la mémoire que quand il le faut, gardez la valeur dans un registre pendant un certain temps et écrivez en mémoire seulement quand il le faut. ; mais cette fois, vous savez mieux que le compilateur si / quand des lectures / écritures doivent avoir lieu. Vous libérez ainsi le compilateur de cette tâche d'optimisation et vous le faites vous-même.
Limites de volatile
Accès non atomique
volatile
ne fournit pas un accès atomique à des variables de plusieurs mots. Pour ces cas, vous devrez prévoir une exclusion mutuelle par d'autres moyens, en plus de l'utilisation volatile
. Sur l’AVR, vous pouvez utiliser des appels simples ou au ATOMIC_BLOCK
départ . Les macros respectives agissent également comme une barrière de mémoire, ce qui est important pour l'ordre des accès:<util/atomic.h>
cli(); ... sei();
Ordre d'exécution
volatile
impose un ordre d'exécution strict uniquement par rapport aux autres variables volatiles. Cela signifie que, par exemple
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
est garanti pour premier assign 1 à i
et ensuite assigner à 2 j
. Cependant, il n'est pas garanti que a
ce soit attribué entre les deux; le compilateur peut effectuer cette affectation avant ou après l'extrait de code, à tout moment jusqu'à la première lecture (visible) de a
.
S'il n'y avait pas la barrière de mémoire des macros mentionnées ci-dessus, le compilateur serait autorisé à traduire
uint32_t x;
cli();
x = volatileVar;
sei();
à
x = volatileVar;
cli();
sei();
ou
cli();
sei();
x = volatileVar;
(Par souci d'exhaustivité, je dois dire que des barrières de mémoire, comme celles impliquées par les macros sei / cli, peuvent en fait empêcher l'utilisation de volatile
, si tous les accès sont placés entre crochets avec ces barrières.)