J'ai lu des articles et des réponses de Stack Exchange sur l'utilisation du volatile
mot - clé pour empêcher le compilateur d'appliquer des optimisations aux objets susceptibles de changer d'une manière que le compilateur ne peut pas déterminer.
Si je lis un ADC (appelons la variable adcValue
) et que je déclare cette variable comme globale, devrais-je utiliser le mot-clé volatile
dans ce cas?
Sans utiliser de
volatile
mot clé// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
En utilisant le
volatile
mot clé// Includes #include "adcDriver.h" // Global variables volatile uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
Je pose cette question parce que lors du débogage, je ne vois aucune différence entre les deux approches bien que les meilleures pratiques indiquent que dans mon cas (une variable globale qui change directement à partir du matériel), l’utilisation volatile
est alors obligatoire.
microcontroller
c
embedded
Pryda
la source
la source
if(x==1) x=1;
l'écriture peut être optimisé loin pour un non volatilex
et ne peut pas être optimisé s'ilx
est volatil. OTOH si des instructions spéciales sont nécessaires pour accéder aux périphériques externes, il vous appartient de les ajouter (par exemple, si une plage de mémoire doit être laissée en écriture).Réponses:
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 lorsqueLes 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:
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 duresult
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:
pourrait être réordonné à
ce qui peut sauver une instruction assembleur car la valeur
99
ne devra pas être chargée deux fois.Si
a
,b
etc
é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:
Si, dans ce cas, ce
signal
n'était pas le casvolatile
, le compilateur «penserait» que celawhile( signal == 0 )
pourrait être une boucle infinie (carsignal
elle ne sera jamais modifiée par le code à l'intérieur de la boucle ) et pourrait générer l'équivalent deTraitement attentif des
volatile
valeursComme 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 queCela 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 fonctiondoSysTick()
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 / elledoSysTick()
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'utilisationvolatile
. Sur l’AVR, vous pouvez utiliser des appels simples ou auATOMIC_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 exempleest garanti pour premier assign 1 à
i
et ensuite assigner à 2j
. Cependant, il n'est pas garanti quea
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) dea
.S'il n'y avait pas la barrière de mémoire des macros mentionnées ci-dessus, le compilateur serait autorisé à traduire
à
ou
(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.)la source
An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.
Plus de gens devraient la lire.cli
/sei
est une solution trop lourde si votre seul objectif est d’obtenir une barrière de mémoire, et non d’empêcher des interruptions. Ces macros génèrent des instructionscli
/ réellessei
, ainsi que de la mémoire de clobérisation, et c’est ce clobérisation qui entraîne la barrière. Pour avoir uniquement une barrière de mémoire sans désactiver les interruptions, vous pouvez définir votre propre macro avec le corps tel que__asm__ __volatile__("":::"memory")
(par exemple, un code d'assemblage vide avec une mémoire volante).volatile
il y a un point de séquence et tout ce qui suit doit être "séquencé après". Cela signifie que cette expression est une sorte de barrière de mémoire. Les vendeurs de compilateurs ont choisi de répandre toutes sortes de mythes pour imposer au programmeur la responsabilité des barrières de mémoire, mais cela viole les règles de "la machine abstraite".volatile data_t data = {0}; set_mmio(&data); while (!data.ready);
.Le mot clé volatile indique au compilateur que l'accès à la variable a un effet observable. Cela signifie que chaque fois que votre code source utilise la variable, le compilateur DOIT créer un accès à la variable. Que ce soit un accès en lecture ou en écriture.
Ceci a pour effet que toute modification de la variable en dehors du flux de code normal sera également observée par le code. Par exemple, si un gestionnaire d'interruption change la valeur. Ou si la variable est en fait un registre matériel qui change par lui-même.
Ce grand avantage est aussi son inconvénient. Chaque accès à la variable passe par la variable et la valeur n'est jamais conservée dans un registre pour un accès plus rapide, peu importe la durée. Cela signifie qu'une variable volatile sera lente. Magnitudes plus lentes. Donc, utilisez uniquement volatile là où c'est réellement nécessaire.
Dans votre cas, dans la mesure où vous avez affiché du code, la variable globale n'est modifiée que lorsque vous la mettez à jour vous-même
adcValue = readADC();
. Le compilateur sait quand cela se produit et ne conservera jamais la valeur de adcValue dans un registre à travers quelque chose qui peut appeler lareadFromADC()
fonction. Ou toute fonction qu'il ignore. Ou tout ce qui va manipuler des pointeurs qui pourraient pointer versadcValue
, etc. Il n’est vraiment pas nécessaire de recourir à la volatilité, car la variable ne change jamais de façon imprévisible.la source
volatile
sur tout simplement parce que , mais vous ne devriez pas non plus vous en abstenir dans les cas où vous pensez que cela est légitimement demandé en raison d'inquiétudes sur les performances préemptives.L'utilisation principale du mot clé volatile dans les applications C intégrées est de marquer une variable globale écrite dans un gestionnaire d'interruption. Ce n'est certainement pas facultatif dans ce cas.
Sans cela, le compilateur ne peut pas prouver que la valeur est écrite après l'initialisation, car il ne peut pas prouver que le gestionnaire d'interruptions est appelé. Par conséquent, il pense qu'il peut optimiser la variable d'existence.
la source
Il existe deux cas où vous devez utiliser
volatile
des systèmes intégrés.Lors de la lecture d'un registre de matériel.
Cela signifie que le registre mappé en mémoire lui-même fait partie des périphériques matériels de la MCU. Il aura probablement un nom cryptique comme "ADC0DR". Ce registre doit être défini en code C, soit via une carte de registre fournie par le fournisseur de l’outil, soit par vous-même. Pour le faire vous-même, vous feriez (en supposant un registre 16 bits):
où 0x1234 est l'adresse où la MCU a mappé le registre. Puisque
volatile
fait déjà partie de la macro ci-dessus, tout accès à celle-ci sera qualifié de manière volatile. Donc, ce code est bon:Lors du partage d'une variable entre un ISR et le code associé à l'aide du résultat de l'ISR.
Si vous avez quelque chose comme ça:
Ensuite, le compilateur pourrait penser: "adc_data est toujours égal à 0 car il n’est mis à jour nulle part. Et cette fonction ADC0_interrupt () n’est jamais appelée, la variable ne peut donc pas être modifiée". Le compilateur ne réalise généralement pas que les interruptions sont appelées par le matériel, pas par le logiciel. Ainsi, le compilateur supprime le code
if(adc_data > 0){ do_stuff(adc_data); }
car il pense que cela ne peut jamais être vrai, ce qui cause un bug très étrange et difficile à déboguer.En déclarant
adc_data
volatile
, le compilateur n'est pas autorisé à faire de telles hypothèses et il n'est pas permis d'optimiser l'accès à la variable.Notes IMPORTANTES:
Un ISR doit toujours être déclaré dans le pilote du matériel. Dans ce cas, l’ADR ISR doit être à l’intérieur du pilote ADC. Le pilote ne doit communiquer avec l'ISR que pour le reste. Tout le reste est de la programmation spaghetti.
Lors de l'écriture en C, toutes les communications entre un ISR et le programme en arrière-plan doivent être protégées contre les conditions de concurrence. Toujours , à chaque fois, sans exception. La taille du bus de données MCU n'a pas d'importance, car même si vous effectuez une copie à 8 bits unique en C, le langage ne peut pas garantir l'atomicité des opérations. Non, sauf si vous utilisez la fonctionnalité C11
_Atomic
. Si cette fonctionnalité n'est pas disponible, vous devez utiliser une sorte de sémaphore ou désactiver l'interruption pendant la lecture, etc. L'assembleur en ligne est une autre option.volatile
ne garantit pas l'atomicité.Ce qui peut arriver est la suivante:
-charger la valeur de la pile dans le registre
-une interruption survient -utiliser la
valeur du registre
Et puis, peu importe si la partie "valeur d'utilisation" est une instruction unique en soi. Malheureusement, une partie importante de tous les programmeurs de systèmes intégrés n’ignorent pas cela, ce qui en fait probablement le bogue de système intégré le plus répandu à ce jour. Toujours intermittent, difficile à provoquer, difficile à trouver.
Voici un exemple de pilote ADC correctement écrit (en supposant que C11
_Atomic
n’est pas disponible):adc.h
adc.c
Ce code suppose qu'une interruption ne peut être interrompue en soi. Sur de tels systèmes, un simple booléen peut jouer le rôle de sémaphore et ne doit pas nécessairement être atomique, car il n’ya pas de mal à ce que l’interruption se produise avant la définition du booléen. L'inconvénient de la méthode simplifiée ci-dessus est qu'elle supprime les lectures ADC lorsque des conditions de concurrence se produisent, en utilisant plutôt la valeur précédente. Cela peut aussi être évité, mais le code devient alors plus complexe.
Ici
volatile
protège contre les bugs d'optimisation. Cela n'a rien à voir avec les données provenant d'un registre matériel, mais seulement que les données sont partagées avec un ISR.static
protège contre la programmation spaghetti et la pollution de l’espace de noms en rendant la variable locale pour le conducteur. (Cela convient très bien dans les applications mono-cœur, mono-thread, mais pas dans celles multi-threadées.)la source
semaphore
devrait certainement êtrevolatile
! En fait, c’est le cas d’utilisation le plus fondamental qui appellevolatile
: Signaler un élément d’un contexte d’exécution à un autre. - Dans votre exemple, le compilateur peut simplement omettresemaphore = true;
car il "voit" que sa valeur n'est jamais lue avant d'être écrasée parsemaphore = false;
.Dans les extraits de code présentés dans la question, il n'y a pas encore de raison d'utiliser volatile. Peu importe que la valeur de
adcValue
vienne d'un ADC. EtadcValue
être mondial devrait vous faire douter de laadcValue
volatilité, mais ce n'est pas une raison en soi.Être global est un indice, car il ouvre la possibilité d'
adcValue
accéder à plus d'un contexte de programme.. Un contexte de programme comprend un gestionnaire d'interruptions et une tâche RTOS. Si la variable globale est modifiée par un contexte, les autres contextes de programme ne peuvent pas supposer qu'ils connaissent la valeur d'un accès précédent. Chaque contexte doit relire la valeur de la variable chaque fois qu'il l'utilise car la valeur peut avoir été modifiée dans un contexte de programme différent. Un contexte de programme ne sait pas quand une interruption ou un changement de tâche se produit. Il doit donc supposer que toute variable globale utilisée par plusieurs contextes peut changer entre tous les accès à la variable en raison d'un changement de contexte possible. C'est à cela que sert la déclaration volatile. Il indique au compilateur que cette variable peut changer en dehors de votre contexte, lisez-la à chaque accès et ne supposez pas que vous connaissez déjà la valeur.Si la variable est mappée en mémoire sur une adresse matérielle, les modifications apportées par le matériel constituent en réalité un autre contexte en dehors du contexte de votre programme. Donc, la cartographie de la mémoire est aussi un indice. Par exemple, si votre
readADC()
fonction accède à une valeur mappée en mémoire pour obtenir la valeur ADC, cette variable mappée en mémoire devrait probablement être volatile.Donc, pour en revenir à votre question, s'il y a plus dans votre code et s'il est
adcValue
accédé par un autre code qui fonctionne dans un contexte différent, alors, oui, iladcValue
devrait être volatil.la source
Ce n'est pas parce que la valeur provient d'un registre ADC matériel que le matériel le modifie "directement".
Dans votre exemple, vous appelez simplement readADC (), qui renvoie une valeur de registre ADC. Cela convient pour le compilateur, sachant qu'une nouvelle valeur est affectée à adcValue à ce stade.
Ce serait différent si vous utilisiez une routine d'interruption ADC pour attribuer la nouvelle valeur, appelée lorsqu'une nouvelle valeur ADC est prête. Dans ce cas, le compilateur n'aurait aucune idée du moment où l'ISR correspondant est appelé et pourrait décider de ne pas accéder à adcValue de cette manière. C'est là que volatile pourrait aider.
la source
Le comportement de l'
volatile
argument dépend en grande partie de votre code, du compilateur et de l'optimisation effectuée.J'utilise personnellement deux cas d'utilisation
volatile
:S'il y a une variable que je veux examiner avec le débogueur, mais que le compilateur a optimisée (c'est-à-dire qu'elle l'a supprimée car elle a découvert qu'il n'était pas nécessaire de disposer de cette variable), l'ajout
volatile
forcerait le compilateur à la conserver et donc peut être vu sur le débogage.Si la variable peut changer «en dehors du code», généralement si un matériel y accède ou si vous mappez la variable directement à une adresse.
Dans Embedded, les compilateurs présentent parfois de nombreux bugs, une optimisation qui ne fonctionne pas et qui
volatile
peut parfois résoudre les problèmes.Étant donné que votre variable est déclarée globalement, elle ne sera probablement pas optimisée tant que la variable est utilisée sur le code, au moins en écriture et en lecture.
Exemple:
Dans ce cas, la variable sera probablement optimisée pour printf ("% i", 1);
ne sera pas optimisé
Un autre:
Dans ce cas, le compilateur peut optimiser par (si vous optimisez pour la vitesse) et donc ignorer la variable
Dans votre cas d'utilisation, "cela peut dépendre" du reste de votre code, de la manière dont il
adcValue
est utilisé ailleurs et des paramètres de version / optimisation du compilateur que vous utilisez.Parfois, il peut être agaçant d’avoir un code qui fonctionne sans optimisation, mais qui casse une fois optimisé.
Cela peut être optimisé pour printf ("% i", readADC ());
-
Celles-ci ne seront probablement pas optimisées, mais vous ne saurez jamais "quelle est la qualité du compilateur" et cela pourrait changer avec les paramètres du compilateur. Les compilateurs avec une bonne optimisation sont généralement licenciés.
la source
volatile
oblige le compilateur à stocker une variable dans la RAM et à la mettre à jour dès qu'une valeur est affectée à la variable. La plupart du temps, le compilateur ne "supprime" pas les variables, car nous n'écrivons généralement pas d'assignations sans effet, mais il peut décider de conserver la variable dans un registre de la CPU et d'écrire ultérieurement ou jamais la valeur de ce registre dans la RAM. Les débogueurs échouent souvent dans la localisation du registre de la CPU dans lequel la variable est conservée et ne peuvent donc pas afficher sa valeur.Beaucoup d'explications techniques mais je veux me concentrer sur l'application pratique.
Le
volatile
mot-clé oblige le compilateur à lire ou à écrire la valeur de la variable en mémoire à chaque utilisation. Normalement, le compilateur essaiera d’optimiser, mais ne fera pas de lectures et d’écritures inutiles, par exemple en conservant la valeur dans un registre de la CPU plutôt qu’en accédant à la mémoire à chaque fois.Cela a deux utilisations principales dans le code incorporé. Tout d'abord, il est utilisé pour les registres de matériel. Les registres matériels peuvent changer, par exemple un registre de résultat ADC peut être écrit par le périphérique ADC. Les registres de matériel peuvent également effectuer des actions lors de l'accès. Un exemple courant est le registre de données d'un UART, qui efface souvent les indicateurs d'interruption lors de la lecture.
Le compilateur essaiera normalement d’optimiser les lectures et écritures répétées du registre en partant du principe que la valeur ne changera jamais. Il n’est donc pas nécessaire de continuer à y accéder, mais le
volatile
mot - clé l’obligera à effectuer une lecture à chaque fois.La deuxième utilisation courante concerne les variables utilisées par les codes d’interruption et de non-interruption. Les interruptions n'étant pas appelées directement, le compilateur ne peut pas déterminer quand elles vont s'exécuter et suppose donc qu'aucun accès à l'intérieur de l'interruption ne se produit jamais. Comme le
volatile
mot - clé oblige le compilateur à accéder à la variable à chaque fois, cette hypothèse est supprimée.Il est important de noter que le
volatile
mot clé n'est pas une solution complète à ces problèmes et qu'il faut veiller à les éviter. Par exemple, sur un système 8 bits, une variable 16 bits nécessite deux accès mémoire en lecture ou en écriture. Ainsi, même si le compilateur est obligé de faire ces accès de manière séquentielle, il est possible que le matériel agisse sur le premier accès ou une interruption se produit entre les deux.la source
En l'absence de
volatile
qualificatif, la valeur d'un objet peut être stockée à plusieurs endroits au cours de certaines parties du code. Considérons, par exemple, quelque chose comme:Dans les premiers jours de C, un compilateur aurait traité l'instruction
via les marches:
Des compilateurs plus sophistiqués, cependant, reconnaîtront que si la valeur de "foo" est conservée dans un registre pendant la boucle, elle n'aura besoin d'être chargée qu'une fois avant la boucle et stockée une fois après. Pendant la boucle, cependant, cela signifiera que la valeur de "foo" est conservée à deux endroits: dans la mémoire globale et dans le registre. Ce ne sera pas un problème si le compilateur peut voir toutes les manières dont on peut accéder à "foo" dans la boucle, mais peut poser problème si la valeur de "foo" est utilisée dans un mécanisme inconnu du compilateur ( comme un gestionnaire d’interruptions).
Les auteurs de la norme auraient peut-être pu ajouter un nouveau qualificatif invitant explicitement le compilateur à effectuer de telles optimisations et indiquant que la sémantique ancienne s'appliquerait en son absence, mais dans les cas où les optimisations sont beaucoup plus utiles C’est pourquoi la norme permet aux compilateurs de supposer que de telles optimisations sont sûres en l’absence de preuves à l’évidence. Le
volatile
mot-clé a pour but de fournir de telles preuves.Quelques problèmes de conflit entre certains rédacteurs de compilateur et programmeurs se produisent dans des situations telles que:
Historiquement, la plupart des compilateurs permettaient soit d'écrire un
volatile
emplacement de stockage, soit provoquer des effets secondaires arbitraires, et évitaient de mettre en cache les valeurs des registres d'un tel magasin, soit ils s'abstiendraient de mettre en cache des valeurs dans des registres lors d'appels à des fonctions non qualifié "inline", et écrirait donc 0x1234 dansoutput_buffer[0]
, configurez les choses pour sortir les données, attendez qu’elles s’achèvent, puis écrivez 0x2345 dansoutput_buffer[0]
, et continuez à partir de là. La norme n’exige pas que les implémentations traitent le fait de stocker l’adresse deoutput_buffer
dans unevolatile
pointeur qualifié en tant que signe que quelque chose pourrait lui arriver via signifie que le compilateur ne comprend pas, cependant, parce que les auteurs pensaient que le compilateur reconnaîtrait les rédacteurs de compilateurs destinés à diverses plates-formes et à des fins différentes quand cela servirait ces objectifs sur ces plates-formes sans avoir à être dit. En conséquence, certains compilateurs "intelligents" tels que gcc et clang supposeront que, même si l'adresse deoutput_buffer
est écrite sur un pointeur qualifié volatile entre les deux magasinsoutput_buffer[0]
, ce n'est pas une raison pour supposer que rien ne tient à la valeur de cet objet ce temps.En outre, bien que les pointeurs directement générés à partir d’entiers soient rarement utilisés à des fins autres que celle de manipulations que les compilateurs ne comprennent probablement pas, la norme n’oblige pas à nouveau les compilateurs de traiter de tels accès
volatile
. Par conséquent, la première écriture sur*((unsigned short*)0xC0001234)
peut être omise par des compilateurs "intelligents" tels que gcc et clang, car les responsables de ces compilateurs affirment plutôt que le code qui omet de qualifier des choses commevolatile
"cassé" plutôt que de reconnaître que la compatibilité avec un tel code est utile . Un grand nombre de fichiers d'en-tête fournis par le fournisseur omettent lesvolatile
qualificateurs, et un compilateur compatible avec les fichiers d'en-tête fournis par le fournisseur est plus utile qu'un autre.la source