Utilizzo volatile nello sviluppo C incorporato


44

Ho letto alcuni articoli e risposte a Stack Exchange sull'uso della volatileparola chiave per impedire al compilatore di applicare eventuali ottimizzazioni su oggetti che possono cambiare in modi che non possono essere determinati dal compilatore.

Se sto leggendo da un ADC (chiamiamo la variabile adcValue) e sto dichiarando questa variabile come globale, dovrei usare la parola chiave volatilein questo caso?

  1. Senza usare la volatileparola chiave

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. Usando la volatileparola chiave

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

Sto ponendo questa domanda perché, durante il debug, non riesco a vedere alcuna differenza tra entrambi gli approcci sebbene le migliori pratiche affermino che nel mio caso (una variabile globale che cambia direttamente dall'hardware), l'utilizzo volatileè obbligatorio.


1
Un certo numero di ambienti di debug (sicuramente gcc) non applica ottimizzazioni. Una build di produzione normalmente (a seconda delle tue scelte). Questo può portare a differenze "interessanti" tra le build. Guardare la mappa di output del linker è informativo.
Peter Smith,

22
"nel mio caso (variabile globale che cambia direttamente dall'hardware)" - La tua variabile globale non viene modificata dall'hardware ma solo dal tuo codice C, di cui il compilatore è a conoscenza. - Il registro hardware in cui ADC fornisce i suoi risultati, tuttavia, deve essere volatile, poiché il compilatore non può sapere se / quando cambierà il suo valore (cambia se / quando l'hardware ADC termina una conversione).
JimmyB

2
Hai confrontato l'assemblatore generato da entrambe le versioni? Questo dovrebbe mostrarti cosa sta succedendo sotto il cofano
Mawg

3
@stark: BIOS? Su un microcontrollore? Lo spazio I / O mappato in memoria non sarà memorizzabile nella cache (se l'architettura ha anche una cache di dati, in primo luogo, cosa non garantita) dalla coerenza del progetto tra le regole di memorizzazione nella cache e la mappa di memoria. Ma volatile non ha nulla a che fare con la cache del controller di memoria.
Ben Voigt,

1
@Davislor Lo standard linguistico non ha bisogno di aggiungere altro in generale. Una lettura su un oggetto volatile eseguirà un carico reale (anche se il compilatore ne ha fatto di recente uno e saprebbe di solito quale sia il valore) e una scrittura su tale oggetto eseguirà un archivio reale (anche se lo stesso valore fosse letto dall'oggetto ). Quindi nella if(x==1) x=1;scrittura può essere ottimizzato per un valore non volatile xe non può essere ottimizzato se xè volatile. OTOH se sono necessarie istruzioni speciali per accedere a dispositivi esterni, spetta a te aggiungerle (ad esempio se è necessario scrivere un intervallo di memoria).
curiousguy,

Risposte:


87

Una definizione di volatile

volatiledice 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, volatileil 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 resultvariabile e non memorizzerà mai i valori intermedi da nessuna parte ma in un registro della CPU.

Se resultera volatile, ogni occorrenza di resultnel 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 99non dovrà essere caricato due volte.

Se a, be se cfossero 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, signalnon lo fosse volatile, il compilatore "penserebbe" che while( signal == 0 )potrebbe essere un ciclo infinito (perché signalnon 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 volatilevalori

Come indicato sopra, una volatilevariabile 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 sysTickCountnell'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

volatilenon 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_BLOCKda <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

volatileimpone 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 ie quindi assegnare 2 a j. Tuttavia, è non è garantito che averrà 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.)


7
Buona discussione di non volatilità per la performance :)
awjlogan

3
Mi piace sempre menzionare la definizione di volatile in 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. più persone dovrebbero leggerlo.
Jeroen3,

3
Vale la pena ricordare che cli/ seiè una soluzione troppo pesante se il tuo unico obiettivo è quello di raggiungere una barriera di memoria, non prevenire interruzioni. Queste macro generano istruzioni cli/ effettive seie inoltre intasano la memoria, ed è questa ostruzione che provoca la barriera. Per avere solo una barriera di memoria senza disabilitare gli interrupt è possibile definire la propria macro con il corpo simile __asm__ __volatile__("":::"memory")(ovvero codice assembly vuoto con clobber di memoria).
Ruslan,

3
@NicHartley No. C17 5.1.2.3 §6 definisce il comportamento osservabile : "Gli accessi agli oggetti volatili sono valutati rigorosamente secondo le regole della macchina astratta". Lo standard C non è molto chiaro su dove siano necessarie nel complesso le barriere di memoria. Alla fine di un'espressione che usa volatilec'è un punto sequenza, e tutto ciò che segue deve essere "sequenziato dopo". Significa che l'espressione è una sorta di barriera di memoria. I venditori di compilatori hanno scelto di diffondere tutti i tipi di miti per mettere la responsabilità delle barriere della memoria sul programmatore, ma ciò viola le regole della "macchina astratta".
Lundin,

2
@JimmyB Volatile locale forse utile per codice come volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka,

13

La parola chiave volatile indica al compilatore che l'accesso alla variabile ha un effetto osservabile. Ciò significa che ogni volta che il codice sorgente utilizza la variabile, il compilatore DEVE creare un accesso alla variabile. Sia un accesso in lettura o scrittura.

L'effetto di ciò è che qualsiasi modifica alla variabile al di fuori del normale flusso di codice verrà osservata anche dal codice. Ad esempio, se un gestore di interrupt modifica il valore. O se la variabile è in realtà un registro hardware che cambia da solo.

Questo grande vantaggio è anche il suo aspetto negativo. Ogni singolo accesso alla variabile passa attraverso la variabile e il valore non viene mai tenuto in un registro per un accesso più veloce per un certo periodo di tempo. Ciò significa che una variabile volatile sarà lenta. Magnitudini più lente. Quindi usa volatile solo dove è effettivamente necessario.

Nel tuo caso, per quanto hai mostrato il codice, la variabile globale viene modificata solo quando la aggiorni tu stesso adcValue = readADC();. Il compilatore sa quando ciò accade e non manterrà mai il valore di adcValue in un registro attraverso qualcosa che può chiamare la readFromADC()funzione. O qualsiasi funzione di cui non è a conoscenza. O qualsiasi cosa che manipoli i puntatori che potrebbero puntare adcValuee così via. Non c'è davvero bisogno di volatile poiché la variabile non cambia mai in modo imprevedibile.


6
Concordo con questa risposta, ma "le magnitudini più lente" sembrano troppo terribili.
kkrambo,

6
È possibile accedere a un registro CPU in meno di un ciclo CPU su moderne CPU superscalari. D'altro canto, un accesso alla memoria effettiva non memorizzata nella cache (ricordate che alcuni componenti hardware esterni lo cambierebbero, quindi non sono consentite cache della CPU) può essere compreso tra 100 e 300 cicli della CPU. Quindi sì, magnitudo. Non sarà così male su un AVR o un micro controller simile ma la domanda non specifica l'hardware.
Goswin von Brederlow,

7
Nei sistemi embedded (microcontrollore), la penalità per l'accesso alla RAM è spesso molto inferiore. Gli AVR, ad esempio, richiedono solo due cicli CPU per una lettura o scrittura nella RAM (una mossa registro-registro richiede un ciclo), quindi i risparmi di mantenere le cose nei registri si avvicinano (ma non raggiungono mai effettivamente) max. 2 cicli di clock per accesso. - Naturalmente, relativamente parlando, salvare un valore dal registro X alla RAM, quindi ricaricare immediatamente quel valore nel registro X per ulteriori calcoli richiederà 2x2 = 4 anziché 0 cicli (quando si mantiene semplicemente il valore in X), e quindi è infinitamente più lento :)
JimmyB,

1
È "magnitudes slow" nel contesto di "scrivere o leggere da una particolare variabile", sì. Tuttavia, nel contesto di un programma completo che probabilmente fa molto più che leggere / scrivere su una variabile più e più volte, no, non proprio. In tal caso, la differenza complessiva è probabilmente "da piccola a trascurabile". Si dovrebbe prestare attenzione, quando si fanno affermazioni sulle prestazioni, per chiarire se l'affermazione si riferisce a una particolare operazione o a un programma nel suo insieme. Rallentare un'operazione usata raramente di un fattore di ~ 300x non è quasi mai un grosso problema.
aroth

1
Intendi l'ultima frase? Ciò significa molto di più nel senso di "l'ottimizzazione prematura è la radice di tutti i mali". Ovviamente non dovresti usare volatiletutto solo perché , ma non dovresti evitarlo nei casi in cui pensi che sia legittimamente richiesto a causa delle preoccupazioni preventive sulle prestazioni.
aroth

9

L'uso principale della parola chiave volatile nelle applicazioni C incorporate è di contrassegnare una variabile globale che viene scritta in un gestore di interrupt. Non è certamente facoltativo in questo caso.

Senza di esso, il compilatore non può dimostrare che il valore viene mai scritto dopo l'inizializzazione, perché non può mai essere chiamato il gestore di interrupt. Pertanto pensa di poter ottimizzare la variabile fuori dall'esistenza.


2
Sicuramente esistono altri usi pratici, ma questo è il più comune.
Vicatcu,

1
Se il valore viene letto solo in un ISR (e modificato da main ()), è necessario utilizzare anche volatile per garantire l'accesso ATOMIC per le variabili multibyte.
Rev1.0

15
@ Rev1.0 No, volatile non garantisce l'aromicità. Tale preoccupazione deve essere affrontata separatamente.
Chris Stratton,

1
Non ci sono letture dall'hardware né interruzioni nel codice pubblicato. Assumi cose dalla domanda che non ci sono. Non si può veramente rispondere nella sua forma attuale.
Lundin,

3
"Contrassegna una variabile globale scritta in un gestore di interrupt" no. È per contrassegnare una variabile; globale o altro; che potrebbe essere cambiato da qualcosa al di fuori della comprensione dei compilatori. Interruzione non richiesta. Potrebbe essere memoria condivisa o qualcuno che attacca una sonda nella memoria (quest'ultima non è consigliata per qualcosa di più moderno di 40 anni)
UKMonkey

9

Esistono due casi in cui è necessario utilizzare volatilenei sistemi incorporati.

  • Durante la lettura da un registro hardware.

    Ciò significa che il registro mappato nella memoria stesso fa parte delle periferiche hardware all'interno dell'MCU. Probabilmente avrà un nome criptico come "ADC0DR". Questo registro deve essere definito in codice C, tramite una mappa dei registri fornita dal fornitore dello strumento o dall'utente stesso. Per farlo da solo, lo faresti (assumendo un registro a 16 bit):

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

    dove 0x1234 è l'indirizzo in cui l'MCU ha mappato il registro. Poiché volatilefa già parte della macro sopra, qualsiasi accesso ad essa sarà qualificato volatile. Quindi questo codice va bene:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Quando si condivide una variabile tra un ISR e il codice correlato utilizzando il risultato dell'ISR.

    Se hai qualcosa del genere:

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

    Quindi il compilatore potrebbe pensare: "adc_data è sempre 0 perché non viene aggiornato da nessuna parte. E quella funzione ADC0_interrupt () non viene mai chiamata, quindi la variabile non può essere modificata". Il compilatore di solito non si rende conto che gli interrupt sono chiamati dall'hardware, non dal software. Quindi il compilatore va e rimuove il codice if(adc_data > 0){ do_stuff(adc_data); }poiché pensa che non possa mai essere vero, causando un bug molto strano e difficile da debug.

    Dichiarando adc_data volatile, al compilatore non è consentito fare tali assunzioni e non è consentito ottimizzare l'accesso alla variabile.


Note importanti:

  • Un ISR deve essere sempre dichiarato all'interno del driver hardware. In questo caso, l'ISR ADC dovrebbe trovarsi all'interno del driver ADC. Nient'altro che il conducente dovrebbe comunicare con l'ISR - tutto il resto è la programmazione degli spaghetti.

  • Quando si scrive C, tutte le comunicazioni tra un ISR e il programma in background devono essere protette dalle condizioni di gara. Sempre , ogni volta, nessuna eccezione. Le dimensioni del bus dati MCU non contano, perché anche se si esegue una singola copia a 8 bit in C, il linguaggio non può garantire l'atomicità delle operazioni. A meno che non si usi la funzione C11 _Atomic. Se questa funzione non è disponibile, è necessario utilizzare una sorta di semaforo o disabilitare l'interrupt durante la lettura, ecc. L'assemblatore in linea è un'altra opzione. volatilenon garantisce l'atomicità.

    Ciò che può accadere è questo:
    -Carica il valore dallo stack nel registro
    -Interrupt si verifica -Usa il
    valore dal registro

    E quindi non importa se la parte "valore d'uso" è una singola istruzione in sé. Purtroppo, una parte significativa di tutti i programmatori di sistemi embedded è ignara di ciò, probabilmente rendendolo il bug di sistemi embedded più comune di sempre. Sempre intermittente, difficile da provocare, difficile da trovare.


Un esempio di un driver ADC correttamente scritto sarebbe simile a questo (supponendo che C11 _Atomicnon sia disponibile):

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;
  }
}
  • Questo codice presuppone che un interrupt non possa essere interrotto in sé. Su tali sistemi, un semplice booleano può fungere da semaforo e non è necessario che sia atomico, poiché non vi è alcun danno se l'interrupt si verifica prima che il booleano sia impostato. Il lato negativo del metodo semplificato sopra è che scarterà le letture ADC quando si verificano le condizioni di gara, usando invece il valore precedente. Anche questo può essere evitato, ma il codice diventa più complesso.

  • Qui volatileprotegge dai bug di ottimizzazione. Non ha nulla a che fare con i dati provenienti da un registro hardware, solo che i dati sono condivisi con un ISR.

  • staticprotegge dalla programmazione degli spaghetti e dall'inquinamento dello spazio dei nomi, rendendo la variabile locale per il conducente. (Questo va bene nelle applicazioni single-core, single-thread, ma non in quelle multi-thread.)


Difficile eseguire il debug è relativo, se il codice viene rimosso, noterai che il tuo codice stimato è andato - è un'affermazione piuttosto audace che qualcosa non va. Ma sono d'accordo, ci possono essere effetti molto strani e difficili da eseguire il debug.
Arsenale,

@Arsenal Se hai un buon debugger che allinea l'assemblatore con la C e conosci almeno un po 'di asm, allora sì, può essere facile da individuare. Ma per un codice complesso più grande, un grosso pezzo di asm generato dalla macchina non è banale da attraversare. O se non conosci asm. O se il tuo debugger è una schifezza e non mostra asm (tosse acuta).
Lundin,

Potrei essere un po 'viziato usando i debugger di Lauterbach allora. Se provi a impostare un punto di interruzione nel codice che è stato ottimizzato, lo imposterà in un posto diverso e sai che qualcosa sta succedendo lì.
Arsenale,

@Arsenal Sì, il tipo di C / asm misto che puoi ottenere a Lauterbach non è affatto standard. La maggior parte dei debugger visualizza l'asm in una finestra separata, se non del tutto.
Lundin,

semaphoredovrebbe assolutamente essere volatile! In realtà, è il caso d'uso più basilare che richiede volatile: segnalare qualcosa da un contesto di esecuzione a un altro. - Nel tuo esempio, il compilatore potrebbe semplicemente omettere semaphore = true;perché "vede" che il suo valore non viene mai letto prima che venga sovrascritto semaphore = false;.
JimmyB,

5

Negli snippet di codice presentati nella domanda, non vi è ancora un motivo per usare volatile. È irrilevante che il valore di adcValueprovenga da un ADC. Ed adcValueessere globale dovrebbe farti sospettare se adcValuedebba essere volatile ma non è una ragione in sé.

Essere globali è un indizio perché apre la possibilità a cui è adcValuepossibile accedere da più di un contesto di programma. Un contesto di programma include un gestore di interrupt e un'attività RTOS. Se la variabile globale viene modificata da un contesto, gli altri contesti del programma non possono presumere di conoscere il valore di un accesso precedente. Ogni contesto deve rileggere il valore della variabile ogni volta che lo usano perché il valore potrebbe essere stato modificato in un diverso contesto del programma. Un contesto di programma non è consapevole quando si verifica un interrupt o un cambio di attività, quindi deve presumere che qualsiasi variabile globale utilizzata da più contesti possa cambiare tra gli accessi della variabile a causa di un possibile cambio di contesto. Ecco a cosa serve la dichiarazione volatile. Indica al compilatore che questa variabile può cambiare al di fuori del proprio contesto, quindi leggete ogni accesso e non date per scontato che conoscete già il valore.

Se la variabile è mappata in memoria a un indirizzo hardware, le modifiche apportate dall'hardware sono effettivamente un altro contesto al di fuori del contesto del programma. Quindi la mappatura della memoria è anche un indizio. Ad esempio, se la tua readADC()funzione accede a un valore mappato in memoria per ottenere il valore ADC, quella variabile mappata in memoria dovrebbe probabilmente essere volatile.

Quindi, tornando alla tua domanda, se c'è altro nel tuo codice e vi adcValuesi accede da un altro codice che viene eseguito in un contesto diverso, allora sì, adcValuedovrebbe essere volatile.


4

"Variabile globale che cambia direttamente dall'hardware"

Solo perché il valore proviene da un registro ADC hardware, non significa che sia "direttamente" modificato dall'hardware.

Nel tuo esempio, devi semplicemente chiamare readADC (), che restituisce un valore del registro ADC. Questo va bene rispetto al compilatore, sapendo che adcValue viene assegnato un nuovo valore in quel punto.

Sarebbe diverso se si stesse utilizzando una routine di interruzione ADC per assegnare il nuovo valore, che viene chiamato quando è pronto un nuovo valore ADC. In tal caso, il compilatore non avrebbe idea di quando verrà chiamato l'ISR corrispondente e potrebbe decidere che adcValue non sarà accessibile in questo modo. Questo è dove volatile sarebbe d'aiuto.


1
Poiché il codice non "chiama" mai la funzione ISR, il compilatore vede che la variabile viene aggiornata solo in una funzione che nessuno chiama. Quindi il compilatore lo ottimizza.
Swanand,

1
Dipende dal resto del codice, se adcValue non viene letto da nessuna parte (come leggere solo il debugger) o se viene letto solo una volta in un punto, il compilatore probabilmente lo ottimizzerà.
Damien,

2
@Damien: "Dipende sempre", ma miravo a rispondere alla domanda "Dovrei usare la parola chiave volatile in questo caso?" il più corto possibile.
Rev1.0

4

Il comportamento volatiledell'argomento dipende in gran parte dal codice, dal compilatore e dall'ottimizzazione effettuata.

Esistono due casi d'uso in cui utilizzo personalmente volatile:

  • Se c'è una variabile che voglio guardare con il debugger, ma il compilatore l'ha ottimizzata (significa che l'ha eliminata perché ha scoperto che non è necessario avere questa variabile), l'aggiunta volatileforzerà il compilatore a mantenerla e quindi può essere visto su debug.

  • Se la variabile potrebbe cambiare "fuori dal codice", in genere se si dispone di hardware che vi accede o se si associa la variabile direttamente a un indirizzo.

In embedded ci sono anche alcuni bug nei compilatori, che fanno l'ottimizzazione che in realtà non funziona e talvolta volatilepossono risolvere i problemi.

Dato che la tua variabile è dichiarata a livello globale, probabilmente non sarà ottimizzata, fintanto che la variabile viene utilizzata sul codice, almeno scritta e letta.

Esempio:

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

In questo caso, la variabile sarà probabilmente ottimizzata per printf ("% i", 1);

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

non sarà ottimizzato

Un altro:

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

In questo caso, il compilatore potrebbe ottimizzare (se ottimizzi per la velocità) e quindi scartando la variabile

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

Per il tuo caso d'uso, "potrebbe dipendere" dal resto del codice, da come adcValueviene utilizzato altrove e dalle impostazioni di versione / ottimizzazione del compilatore che utilizzi.

A volte può essere fastidioso avere un codice che funziona senza ottimizzazione, ma si interrompe una volta ottimizzato.

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

Questo potrebbe essere ottimizzato per 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
}

Questi probabilmente non saranno ottimizzati, ma non si sa mai "quanto è buono il compilatore" e potrebbe cambiare con i parametri del compilatore. Di solito i compilatori con una buona ottimizzazione sono autorizzati.


1
Ad esempio a = 1; b = a; e c = b; il compilatore potrebbe pensare di aspettare un minuto, aeb sono inutili, mettiamo solo 1 a c direttamente. Ovviamente non lo farai nel tuo codice, ma il compilatore è meglio di te nel trovarli, anche se provi a scrivere subito un codice ottimizzato sarebbe illeggibile.
Damien,

2
Un codice corretto con un compilatore corretto non si interromperà con le ottimizzazioni attivate. La correttezza del compilatore è un po 'un problema, ma almeno con IAR non ho riscontrato una situazione in cui l'ottimizzazione porta alla rottura del codice dove non dovrebbe.
Arsenale,

5
Molti casi in cui l'ottimizzazione infrange il codice è quando ci si avventura anche nel territorio UB ..
pipe

2
Sì, un effetto collaterale di volatile è che può aiutare il debug. Ma questo non è un buon motivo per usare volatile. Probabilmente dovresti disattivare le ottimizzazioni se il tuo obiettivo è il debug semplice. Questa risposta non menziona nemmeno gli interrupt.
kkrambo,

2
Aggiungendo all'argomento di debug, volatileimpone al compilatore di memorizzare una variabile nella RAM e di aggiornarla non appena viene assegnato un valore alla variabile. Il più delle volte, il compilatore non "elimina" le variabili, perché di solito non scriviamo assegnazioni senza effetto, ma potrebbe decidere di mantenere la variabile in un registro della CPU e in seguito o mai scrivere il valore di quel registro nella RAM. I debugger spesso non riescono a individuare il registro della CPU in cui si trova la variabile e quindi non possono mostrare il suo valore.
JimmyB,

1

Molte spiegazioni tecniche, ma voglio concentrarmi sull'applicazione pratica.

La volatileparola chiave forza il compilatore a leggere o scrivere il valore della variabile dalla memoria ogni volta che viene utilizzato. Normalmente il compilatore tenterà di ottimizzare ma non di effettuare letture e scritture non necessarie, ad esempio mantenendo il valore in un registro CPU anziché accedere alla memoria ogni volta.

Questo ha due usi principali nel codice incorporato. Innanzitutto viene utilizzato per i registri hardware. I registri hardware possono cambiare, ad es. Un registro dei risultati ADC può essere scritto dalla periferica ADC. I registri hardware possono anche eseguire azioni quando vi si accede. Un esempio comune è il registro dati di un UART, che spesso cancella i flag di interruzione durante la lettura.

Il compilatore normalmente tenterebbe di ottimizzare le letture e le scritture ripetute del registro presupponendo che il valore non cambierà mai, quindi non è necessario continuare ad accedervi, ma la volatileparola chiave lo costringerà a eseguire un'operazione di lettura ogni volta.

Il secondo uso comune è per le variabili utilizzate sia dal codice di interrupt che da quello non-interrupt. Gli interrupt non vengono chiamati direttamente, quindi il compilatore non è in grado di determinare quando verranno eseguiti e quindi presuppone che gli accessi all'interno dell'interrupt non avvengano mai. Poiché la volatileparola chiave impone al compilatore di accedere alla variabile ogni volta, questo presupposto viene rimosso.

È importante notare che la volatileparola chiave non è una soluzione completa a questi problemi e occorre fare attenzione ad evitarli. Ad esempio, su un sistema a 8 bit una variabile a 16 bit richiede due accessi alla memoria per leggere o scrivere, e quindi anche se il compilatore è costretto a effettuare tali accessi, si verificano in sequenza, ed è possibile che l'hardware agisca al primo accesso o si verifica un'interruzione tra i due.


0

In assenza di un volatilequalificatore, il valore di un oggetto può essere memorizzato in più di una posizione durante determinate parti del codice. Considera, ad esempio, dato qualcosa come:

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

All'inizio di C, un compilatore avrebbe elaborato la dichiarazione

foo++;

tramite i passaggi:

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

Compilatori più sofisticati, tuttavia, riconosceranno che se il valore di "pippo" viene mantenuto in un registro durante il ciclo, dovrà essere caricato solo una volta prima del ciclo e memorizzato una volta dopo. Durante il ciclo, tuttavia, ciò significa che il valore di "pippo" viene mantenuto in due punti: all'interno della memoria globale e all'interno del registro. Questo non sarà un problema se il compilatore può vedere tutti i modi in cui è possibile accedere a "pippo" all'interno del ciclo, ma può causare problemi se si accede al valore di "pippo" in un meccanismo di cui il compilatore non è a conoscenza ( come un gestore di interrupt).

Potrebbe essere stato possibile per gli autori dello Standard aggiungere un nuovo qualificatore che inviterebbe esplicitamente il compilatore a fare tali ottimizzazioni e dire che la semantica vecchio stile si applicherebbe in sua assenza, ma i casi in cui le ottimizzazioni sono utili sono notevolmente più numerosi quelli in cui sarebbe problematico, quindi lo Standard consente invece ai compilatori di supporre che tali ottimizzazioni siano sicure in assenza di prove che non lo siano. Lo scopo della volatileparola chiave è fornire tali prove.

Un paio di contese tra alcuni scrittori di compilatori e programmatori si verifica in situazioni come:

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

Storicamente, la maggior parte dei compilatori consentirebbe la possibilità che la scrittura di una volatileposizione di archiviazione possa innescare effetti collaterali arbitrari ed eviterebbe di memorizzare nella cache qualsiasi valore nei registri in tale archivio, oppure si asterrà dal memorizzare nella cache i valori nei registri attraverso chiamate a funzioni che sono non qualificato "inline", e quindi scriverebbe 0x1234 su output_buffer[0], imposterà le cose per produrre i dati, aspetterebbe che si completino, quindi scrivere 0x2345 su output_buffer[0]e continuare da lì. Lo standard non richiede implementazioni per trattare l'atto di memorizzare l'indirizzo di output_bufferavolatile-indicatore qualificato come segno che qualcosa potrebbe accadergli tramite significa che il compilatore non capisce, tuttavia, perché gli autori pensavano che gli autori di compilatori pensati per compilatori destinati a varie piattaforme e scopi avrebbero riconosciuto che quando lo facevano avrebbero servito quegli scopi su quelle piattaforme senza essere detto. Di conseguenza, alcuni compilatori "intelligenti" come gcc e clang presumeranno che anche se l'indirizzo di output_bufferè scritto su un puntatore qualificato volatile tra i due negozi output_buffer[0], non c'è motivo di ritenere che qualcosa possa interessare al valore contenuto in quell'oggetto in quella volta.

Inoltre, mentre i puntatori che vengono lanciati direttamente da numeri interi vengono raramente utilizzati per scopi diversi dalla manipolazione di cose in modi che i compilatori non sono in grado di comprendere, lo Standard non richiede nuovamente ai compilatori di trattare tali accessi come volatile. Di conseguenza, la prima scrittura su *((unsigned short*)0xC0001234)può essere omessa da compilatori "intelligenti" come gcc e clang, perché i manutentori di tali compilatori preferirebbero affermare che il codice che trascura di qualificare cose come volatile"rotte" piuttosto che riconoscere che la compatibilità con tale codice è utile . Molti file di intestazione forniti dal fornitore omettono i volatilequalificatori e un compilatore compatibile con i file di intestazione forniti dal fornitore è più utile di uno che non lo è.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.