Utilisation de volatiles dans le développement C intégré

44

J'ai lu des articles et des réponses de Stack Exchange sur l'utilisation du volatilemot - 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é volatiledans ce cas?

  1. Sans utiliser de volatilemot clé

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. En utilisant le volatilemot 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 volatileest alors obligatoire.

Pryda
la source
1
Un certain nombre d'environnements de débogage (certainement gcc) n'appliquent aucune optimisation. Une construction de production sera normalement (en fonction de vos choix). Cela peut conduire à des différences «intéressantes» entre les builds. Regarder la carte de sortie de l'éditeur de liens est informatif.
Peter Smith
22
"dans mon cas (variable globale qui change directement à partir du matériel)" - Votre variable globale n'est pas modifiée par le matériel mais uniquement par votre code C dont le compilateur a connaissance. - Le registre matériel dans lequel le CAN fournit ses résultats doit cependant être volatile, car le compilateur ne peut pas savoir si / quand sa valeur changera (il change si / quand le matériel du CAN termine une conversion.)
JimmyB
2
Avez-vous comparé l'assembleur généré par les deux versions? Cela devrait vous montrer ce qui se passe sous le capot
Mawg
3
@ stark: BIOS? Sur un microcontrôleur? L'espace d'E / S mappé en mémoire ne pourra pas être mis en cache (si l'architecture dispose même d'un cache de données en premier lieu, ce qui n'est pas assuré) par la cohérence de la conception entre les règles de mise en cache et la mappe de mémoire. Mais volatile n’a rien à voir avec le cache du contrôleur de mémoire.
Ben Voigt
1
@Davislor La norme de langage n'a pas besoin de dire plus en général. Une lecture sur un objet volatil effectuera une charge réelle (même si le compilateur en a récemment fait une et sait habituellement quelle est la valeur) et une écriture dans un tel objet effectuera un stockage réel (même si la même valeur était lue à partir de l'objet ). Donc, dans if(x==1) x=1;l'écriture peut être optimisé loin pour un non volatile xet ne peut pas être optimisé s'il xest 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).
Curiousguy

Réponses:

87

Une définition de volatile

volatileindique 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, volatilele 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 resultvariable et ne stockera jamais les valeurs intermédiaires ailleurs que dans un registre de la CPU.

Si elle resultétait volatile, chaque occurrence du resultcode 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 99ne devra pas être chargée deux fois.

Si a, bet 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 signaln'était pas le cas volatile, le compilateur «penserait» que cela while( signal == 0 )pourrait être une boucle infinie (car signalelle 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 volatilevaleurs

Comme indiqué ci-dessus, une volatilevariable 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 sysTickCountdans 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

volatilene 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_BLOCKdé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

volatileimpose 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 à iet ensuite assigner à 2 j. Cependant, il n'est pas garanti que ace 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.)

JimmyB
la source
7
Bonne discussion sur la non-volatilité pour la performance :)
awjlogan
3
J'aime toujours mentionner la définition de volatile dans 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. Plus de gens devraient la lire.
Jeroen3
3
Il est peut-être intéressant de mentionner que cli/ seiest 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 instructions cli/ réelles sei, 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).
Ruslan
3
@NicHartley No. C17 5.1.2.3 Le §6 définit le comportement observable : "Les accès aux objets volatils sont évalués strictement selon les règles de la machine abstraite." La norme C ne précise pas vraiment où les barrières de mémoire sont nécessaires. À la fin d'une expression qui utilise volatileil 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".
Lundin
2
@JimmyB Local volatile peut être utile pour un code comme volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka
13

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 la readFromADC()fonction. Ou toute fonction qu'il ignore. Ou tout ce qui va manipuler des pointeurs qui pourraient pointer vers adcValue, etc. Il n’est vraiment pas nécessaire de recourir à la volatilité, car la variable ne change jamais de façon imprévisible.

Goswin von Brederlow
la source
6
Je suis d’accord avec cette réponse mais "magnitudes plus lentes" semble trop grave.
kkrambo
6
Il est possible d'accéder à un registre de CPU en moins d'un cycle de processeur sur les CPU superscalaires modernes. D'autre part, un accès à la mémoire non mise en cache réelle (rappelez-vous que certains matériels externes le modifieraient, de sorte que les caches de processeur ne soient pas autorisés) peut être compris entre 100 et 300 cycles de processeur. Donc, oui, les grandeurs. Ce ne sera pas si grave sur un AVR ou un micro-contrôleur similaire, mais la question ne spécifie pas le matériel.
Goswin von Brederlow
7
Dans les systèmes embarqués (microcontrôleurs), l’accès à la RAM est souvent beaucoup moins pénalisant. Les AVR, par exemple, ne prennent que deux cycles de processeur pour une lecture ou une écriture dans la RAM (un mouvement de registre à registre prend un cycle), de sorte que les économies de conservation des éléments dans les registres s'approchent (mais n'atteignent jamais réellement) max. 2 cycles d'horloge par accès. - Bien entendu, enregistrer une valeur du registre X dans la RAM, puis recharger immédiatement cette valeur dans le registre X pour des calculs ultérieurs, prendra 2x2 = 4 au lieu de 0 cycles (en ne conservant que la valeur dans X), et est donc infini plus lent :)
JimmyB
1
C'est «plus lent» dans le contexte «d'écrire ou de lire une variable particulière», oui. Cependant, dans le contexte d'un programme complet, il est probable que beaucoup plus que de lire / écrire dans une variable encore et encore, non, pas vraiment. Dans ce cas, la différence globale est probablement «faible à négligeable». Lorsque vous faites des affirmations sur la performance, veillez à préciser si l’affirmation a trait à une opération en particulier ou à un programme dans son ensemble. Ralentir un facteur rarement utilisé de 300 fois n’est presque jamais une grosse affaire.
aroth
1
Tu veux dire, cette dernière phrase? Cela signifiait beaucoup plus dans le sens de "l'optimisation prématurée est la racine de tout le mal". Évidemment, vous ne devriez pas utiliser volatilesur 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.
aroth
9

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.

vicatcu
la source
2
Il existe certes d’autres applications pratiques, mais à mon avis, c’est la plus courante.
vicatcu
1
Si la valeur est uniquement lue dans un ISR (et modifiée depuis main ()), vous devez éventuellement également utiliser la méthode volatile pour garantir l'accès ATOMIC aux variables multi-octets.
Rev1.0
15
@ Rev1.0 Non, volatile ne garantit pas l' aromicité. Cette préoccupation doit être traitée séparément.
Chris Stratton
1
Il n'y a pas de lecture de matériel ni d'interruption dans le code affiché. Vous supposez des choses de la question qui ne sont pas là. On ne peut pas vraiment y répondre dans sa forme actuelle.
Lundin
3
"marque une variable globale qui est écrite dans un gestionnaire d'interruption" nope. C'est pour marquer une variable; global ou non; que cela puisse être changé par quelque chose en dehors de la compréhension des compilateurs. Interruption non requise. Il peut s'agir d'une mémoire partagée ou d'une personne collant une sonde dans la mémoire (cette dernière n'est pas recommandée pour des activités plus modernes que 40 ans)
UKMonkey
9

Il existe deux cas où vous devez utiliser volatiledes 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):

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

    où 0x1234 est l'adresse où la MCU a mappé le registre. Puisque volatilefait déjà partie de la macro ci-dessus, tout accès à celle-ci sera qualifié de manière volatile. Donc, ce code est bon:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • 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:

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

    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. volatilene 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 _Atomicn’est pas disponible):

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;
  }
}
  • 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 volatileprotè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.

  • staticprotè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.)

Lundin
la source
Difficile à déboguer, c’est relatif, si le code est supprimé, vous remarquerez que votre code précieux a disparu - c’est une déclaration assez audacieuse selon laquelle quelque chose ne va pas. Mais je suis d’accord, il peut y avoir des effets très étranges et difficiles à déboguer.
Arsenal
@Arsenal Si vous avez un bon débogueur qui insère un assembleur dans le C et que vous en connaissez au moins un peu, alors il peut être facile à repérer. Mais pour un code complexe plus volumineux, il n’est pas facile de passer à travers une grande quantité d’asm générée par machine. Ou si vous ne connaissez pas asm. Ou si votre débogueur est de la merde et ne montre pas asm (cougheclipsecough).
Lundin
Peut-être que je suis un peu gâté en utilisant les débogueurs Lauterbach alors. Si vous essayez de définir un point d'arrêt dans le code optimisé, il sera placé ailleurs et vous savez qu'il se passe quelque chose.
Arsenal
@Arsenal Eh oui, le genre de C / as mixte que vous pouvez obtenir à Lauterbach n’est en aucun cas standard. La plupart des débogueurs affichent l'asm dans une fenêtre séparée, voire pas du tout.
Lundin
semaphoredevrait certainement être volatile! En fait, c’est le cas d’utilisation le plus fondamental qui appelle volatile: Signaler un élément d’un contexte d’exécution à un autre. - Dans votre exemple, le compilateur peut simplement omettre semaphore = true;car il "voit" que sa valeur n'est jamais lue avant d'être écrasée par semaphore = false;.
JimmyB
5

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 adcValuevienne d'un ADC. Et adcValueêtre mondial devrait vous faire douter de la adcValuevolatilité, mais ce n'est pas une raison en soi.

Être global est un indice, car il ouvre la possibilité d' adcValueaccé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 adcValueaccédé par un autre code qui fonctionne dans un contexte différent, alors, oui, il adcValuedevrait être volatil.

Kkrambo
la source
4

"Variable globale qui change directement du matériel"

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.

Rev1.0
la source
1
Comme votre code ne "appelle" jamais la fonction ISR, Compiler voit que la variable n'est mise à jour que dans une fonction que personne n'appelle. Donc, le compilateur l’optimise.
Swanand
1
Cela dépend du reste du code, si adcValue n'est pas lu n'importe où (par exemple, uniquement à travers le débogueur), ou s'il n'est lu qu'une fois à un endroit, le compilateur l'optimisera probablement.
Damien
2
@Damien: Cela "dépend" toujours, mais je voulais répondre à la question "Devrais-je utiliser le mot clé volatile dans ce cas?" aussi court que possible.
Rev1.0
4

Le comportement de l' volatileargument 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 volatileforcerait 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 volatilepeut 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:

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

Dans ce cas, la variable sera probablement optimisée pour printf ("% i", 1);

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

ne sera pas optimisé

Un autre:

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

Dans ce cas, le compilateur peut optimiser par (si vous optimisez pour la vitesse) et donc ignorer la variable

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

Dans votre cas d'utilisation, "cela peut dépendre" du reste de votre code, de la manière dont il adcValueest 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é.

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

Cela peut être optimisé pour 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
}

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.

Damien
la source
1
Par exemple a = 1; b = a; et c = b; le compilateur pourrait penser attendre une minute, a et b sont inutiles, ajoutons simplement 1 à c directement. Bien sûr, vous ne ferez pas cela dans votre code, mais le compilateur est meilleur que vous pour les trouver, même si vous essayez d'écrire immédiatement du code optimisé, il serait illisible.
Damien
2
Un code correct avec un compilateur correct ne rompra pas avec les optimisations activées. L’exactitude du compilateur pose un problème, mais au moins avec IAR, je n’ai pas rencontré de situation dans laquelle l’optimisation conduirait à casser du code là où il ne devrait pas.
Arsenal
5
Dans de nombreux cas où l'optimisation casse le code, c'est aussi lorsque vous vous aventurez sur le territoire UB .
pipe du
2
Oui, l'un des effets secondaires de volatile est qu'il peut aider au débogage. Mais ce n'est pas une bonne raison d'utiliser volatile. Vous devriez probablement désactiver les optimisations si votre objectif est de déboguer facilement. Cette réponse ne mentionne même pas les interruptions.
kkrambo
2
L'ajout à l'argument de débogage volatileoblige 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.
JimmyB
1

Beaucoup d'explications techniques mais je veux me concentrer sur l'application pratique.

Le volatilemot-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 volatilemot - 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 volatilemot - clé oblige le compilateur à accéder à la variable à chaque fois, cette hypothèse est supprimée.

Il est important de noter que le volatilemot 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.

utilisateur
la source
0

En l'absence de volatilequalificatif, 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:

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

Dans les premiers jours de C, un compilateur aurait traité l'instruction

foo++;

via les marches:

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

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 volatilemot-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:

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

Historiquement, la plupart des compilateurs permettaient soit d'écrire un volatileemplacement 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 dans output_buffer[0], configurez les choses pour sortir les données, attendez qu’elles s’achèvent, puis écrivez 0x2345 dans output_buffer[0], et continuez à partir de là. La norme n’exige pas que les implémentations traitent le fait de stocker l’adresse de output_bufferdans unevolatilepointeur 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 de output_bufferest écrite sur un pointeur qualifié volatile entre les deux magasins output_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 comme volatile"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 les volatilequalificateurs, et un compilateur compatible avec les fichiers d'en-tête fournis par le fournisseur est plus utile qu'un autre.

supercat
la source