Uso de volátiles en el desarrollo de C incrustado

44

He estado leyendo algunos artículos y las respuestas de Stack Exchange sobre el uso de la volatilepalabra clave para evitar que el compilador aplique optimizaciones en los objetos que pueden cambiar de formas que el compilador no puede determinar.

Si estoy leyendo desde un ADC (llamemos a la variable adcValue), y estoy declarando esta variable como global, ¿debería usar la palabra clave volatileen este caso?

  1. Sin usar volatilepalabra clave

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

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

Estoy haciendo esta pregunta porque al depurar, no puedo ver ninguna diferencia entre ambos enfoques, aunque las mejores prácticas dicen que en mi caso (una variable global que cambia directamente desde el hardware), el uso volatilees obligatorio.

Pryda
fuente
1
Varios entornos de depuración (ciertamente gcc) no aplican optimizaciones. Una construcción de producción normalmente lo hará (dependiendo de sus elecciones). Esto puede conducir a diferencias "interesantes" entre las compilaciones. Mirar el mapa de salida del enlazador es informativo.
Peter Smith
22
"en mi caso (variable global que cambia directamente desde el hardware)": su variable global no se cambia por hardware sino solo por su código C, del cual el compilador es consciente. - Sin embargo, el registro de hardware en el que el ADC proporciona sus resultados debe ser volátil, porque el compilador no puede saber si / cuándo cambiará su valor (cambia si / cuando el hardware del ADC finaliza una conversión).
JimmyB
2
¿Comparó el ensamblador generado por ambas versiones? Esto debería mostrar que lo que está sucediendo bajo el capó
MAWG
3
@stark: BIOS? ¿En un microcontrolador? El espacio de E / S mapeado en memoria no será almacenable en caché (si la arquitectura incluso tiene un caché de datos en primer lugar, lo cual no está asegurado) por la consistencia del diseño entre las reglas de almacenamiento en caché y el mapa de memoria. Pero volátil no tiene nada que ver con la memoria caché del controlador de memoria.
Ben Voigt el
1
@Davislor El lenguaje estándar no necesita decir nada más en general. Una lectura en un objeto volátil realizará una carga real (incluso si el compilador lo hizo recientemente y generalmente sabría cuál es el valor) y una escritura en dicho objeto realizaría un almacenamiento real (incluso si el mismo valor se leyera desde el objeto ) Por lo tanto, en if(x==1) x=1;la escritura se puede optimizar para un no volátil xy no se puede optimizar si xes volátil. OTOH, si se necesitan instrucciones especiales para acceder a dispositivos externos, depende de usted agregarlas (por ejemplo, si es necesario escribir un rango de memoria).
curioso

Respuestas:

87

Una definición de volatile

volatilele dice al compilador que el valor de la variable puede cambiar sin que el compilador lo sepa. Por lo tanto, el compilador no puede asumir que el valor no cambió solo porque el programa C parece no haberlo cambiado.

Por otro lado, significa que el valor de la variable puede ser requerido (leído) en otro lugar que el compilador no conoce, por lo tanto, debe asegurarse de que cada asignación a la variable se lleve a cabo realmente como una operación de escritura.

Casos de uso

volatile se requiere cuando

  • representando registros de hardware (o E / S mapeadas en memoria) como variables, incluso si el registro nunca se leerá, el compilador no debe simplemente saltarse la operación de escritura pensando "Programador estúpido. Intenta almacenar un valor en una variable que él / ella nunca volverá a leer. Él / ella no se dará cuenta si omitimos la escritura " Por el contrario, incluso si el programa nunca escribe un valor en la variable, su valor aún puede ser cambiado por el hardware.
  • compartir variables entre contextos de ejecución (por ejemplo, ISR / programa principal) (ver la respuesta de @kkramo)

Efectos de volatile

Cuando se declara una variable, volatileel compilador debe asegurarse de que cada asignación en el código del programa se refleje en una operación de escritura real, y que cada lectura en el código del programa lea el valor de la memoria (mmapped).

Para las variables no volátiles, el compilador supone que sabe si / cuando cambia el valor de la variable y puede optimizar el código de diferentes maneras.

Por un lado, el compilador puede reducir el número de lecturas / escrituras en la memoria, manteniendo el valor en los registros de la CPU.

Ejemplo:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

Aquí, el compilador probablemente ni siquiera asignará RAM para la resultvariable, y nunca almacenará los valores intermedios en ningún lugar que no sea en un registro de CPU.

Si resultfuera volátil, cada aparición resulten el código C requeriría que el compilador realice un acceso a RAM (o un puerto de E / S), lo que lleva a un rendimiento más bajo.

En segundo lugar, el compilador puede reordenar operaciones en variables no volátiles para el rendimiento y / o el tamaño del código. Ejemplo simple:

int a = 99;
int b = 1;
int c = 99;

podría ser reordenado a

int a = 99;
int c = 99;
int b = 1;

que puede guardar una instrucción de ensamblador porque el valor 99no tendrá que cargarse dos veces.

Si a, by cfuera volátil, el compilador tendría que emitir instrucciones que asignen los valores en el orden exacto que se dan en el programa.

El otro ejemplo clásico es así:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

Si, en este caso, signalno fuera así volatile, el compilador 'pensaría' que while( signal == 0 )puede ser un bucle infinito (porque signalnunca será cambiado por el código dentro del bucle ) y podría generar el equivalente de

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

Considerado manejo de volatilevalores

Como se indicó anteriormente, una volatilevariable puede introducir una penalización de rendimiento cuando se accede con más frecuencia de la que realmente se requiere. Para mitigar este problema, puede "desestabilizar" el valor mediante la asignación a una variable no volátil, como

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
}

Esto puede ser especialmente beneficioso en la ISR en el que desea ser tan rápido como sea posible sin el acceso a las mismas tarjetas de memoria o varias veces cuando se sabe que no es necesario porque el valor no cambiará mientras el ISR se está ejecutando. Esto es común cuando el ISR es el 'productor' de valores para la variable, como sysTickCounten el ejemplo anterior. En un AVR, sería especialmente doloroso que la función doSysTick()acceda a los mismos cuatro bytes en la memoria (cuatro instrucciones = 8 ciclos de CPU por acceso sysTickCount) cinco o seis veces en lugar de solo dos veces, porque el programador sabe que el valor no será ser cambiado de algún otro código mientras se doSysTick()ejecuta.

Con este truco, básicamente hace exactamente lo mismo que hace el compilador para las variables no volátiles, es decir, las lee de la memoria solo cuando es necesario, guarda el valor en un registro durante un tiempo y vuelve a escribir en la memoria solo cuando tiene que hacerlo. ; pero esta vez, usted sabe mejor que el compilador si / cuando deben realizarse lecturas / escrituras , por lo que libera al compilador de esta tarea de optimización y lo hace usted mismo.

Limitaciones de volatile

Acceso no atómico

volatileno no proporcionar acceso a las variables atómica de varias palabras. Para esos casos, deberá proporcionar la exclusión mutua por otros medios, además del uso volatile. En el AVR, puede usar ATOMIC_BLOCKdesde <util/atomic.h>o cli(); ... sei();llamadas simples . Las macros respectivas también actúan como una barrera de memoria, lo cual es importante cuando se trata del orden de los accesos:

Orden de ejecución

volatileimpone un estricto orden de ejecución solo con respecto a otras variables volátiles. Esto significa que, por ejemplo

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

se garantiza que primero asigne 1 a iy luego asigne 2 a j. Sin embargo, se no garantiza que aserá asignado en el medio; el compilador puede hacer esa asignación antes o después del fragmento de código, básicamente en cualquier momento hasta la primera lectura (visible) de a.

Si no fuera por la barrera de memoria de las macros mencionadas anteriormente, el compilador podría traducir

uint32_t x;

cli();
x = volatileVar;
sei();

a

x = volatileVar;
cli();
sei();

o

cli();
sei();
x = volatileVar;

(En aras de la exhaustividad, debo decir que las barreras de memoria, como las implicadas por las macros sei / cli, en realidad pueden obviar el uso de volatile, si todos los accesos están entre corchetes con estas barreras).

JimmyB
fuente
77
Buena discusión sobre la no volatilidad para el rendimiento :)
awjlogan
3
Siempre me gusta mencionar la definición de volátil en 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. más personas deberían leerlo.
Jeroen3
3
Vale la pena mencionar que cli/ seies una solución demasiado pesada si su único objetivo es lograr una barrera de memoria, no evitar interrupciones. Estas macros generan instrucciones cli/ reales y sei, adicionalmente, memoria de golpe, y es este golpe lo que genera la barrera. Para tener solo una barrera de memoria sin deshabilitar las interrupciones, puede definir su propia macro con el cuerpo __asm__ __volatile__("":::"memory")(por ejemplo, código de ensamblaje vacío con memoria clobber).
Ruslan
3
@NicHartley No. C17 5.1.2.3 §6 define el comportamiento observable : "Los accesos a objetos volátiles se evalúan estrictamente de acuerdo con las reglas de la máquina abstracta". El estándar C no está realmente claro de dónde se necesitan las barreras de memoria en general. Al final de una expresión que usa volatilehay un punto de secuencia, y todo lo que sigue debe "secuenciarse después". Lo que significa que la expresión es una especie de barrera de memoria. Los vendedores de compiladores optaron por difundir todo tipo de mitos para poner la responsabilidad de las barreras de memoria en el programador, pero eso viola las reglas de "la máquina abstracta".
Lundin
2
@JimmyB El volátil local puede ser útil para código como volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka
13

La palabra clave volátil le dice al compilador que el acceso a la variable tiene un efecto observable. Eso significa que cada vez que su código fuente usa la variable, el compilador DEBE crear un acceso a la variable. Ya sea un acceso de lectura o escritura.

El efecto de esto es que cualquier cambio en la variable fuera del flujo de código normal también será observado por el código. Por ejemplo, si un controlador de interrupciones cambia el valor. O si la variable es en realidad un registro de hardware que cambia por sí solo.

Este gran beneficio es también su desventaja. Cada acceso individual a la variable pasa por la variable y el valor nunca se mantiene en un registro para un acceso más rápido por cualquier cantidad de tiempo. Eso significa que una variable volátil será lenta. Magnitudes más lentas. Por lo tanto, solo use volátiles donde sea realmente necesario.

En su caso, en la medida en que mostró el código, la variable global solo cambia cuando la actualiza usted mismo adcValue = readADC();. El compilador sabe cuándo sucede esto y nunca mantendrá el valor de adcValue en un registro en algo que pueda llamar a la readFromADC()función. O cualquier función que no conozca. O cualquier cosa que manipule punteros que puedan apuntar adcValuey tal. Realmente no hay necesidad de volátiles ya que la variable nunca cambia de manera impredecible.

Goswin von Brederlow
fuente
66
Estoy de acuerdo con esta respuesta, pero "magnitudes más lentas" suena demasiado grave.
kkrambo
66
Se puede acceder a un registro de CPU en menos de un ciclo de CPU en las CPU superescalares modernas. Por otro lado, el acceso a la memoria real no almacenada en caché (recuerde que algún hardware externo cambiaría esto, por lo que no se permiten cachés de CPU) puede estar en el rango de 100-300 ciclos de CPU. Entonces, sí, magnitudes. No será tan malo en un AVR o un microcontrolador similar, pero la pregunta no especifica el hardware.
Goswin von Brederlow
77
En sistemas embebidos (microcontroladores), la penalización por el acceso a la RAM es a menudo mucho menor. Los AVR, por ejemplo, solo toman dos ciclos de CPU para leer o escribir en la RAM (un movimiento de registro-registro toma un ciclo), por lo que los ahorros de mantener las cosas en registros se acercan (pero nunca alcanzan) max. 2 ciclos de reloj por acceso. - Por supuesto, en términos relativos, guardar un valor del registro X en la RAM, luego volver a cargar ese valor inmediatamente en el registro X para otros cálculos tomará 2x2 = 4 en lugar de 0 ciclos (cuando solo se mantiene el valor en X), y por lo tanto es infinitamente más lento :)
JimmyB
1
Es 'magnitudes más lentas' en el contexto de "escribir o leer desde una variable particular", sí. Sin embargo, en el contexto de un programa completo que probablemente haga mucho más que leer / escribir en una variable una y otra vez, no, en realidad no. En ese caso, la diferencia general es probablemente 'pequeña a insignificante'. Se debe tener cuidado, al hacer afirmaciones sobre el rendimiento, para aclarar si la afirmación se relaciona con una operación particular o con un programa en su conjunto. Disminuir la velocidad de una operación poco utilizada por un factor de ~ 300x casi nunca es un gran problema.
aroth
1
¿Te refieres a esa última oración? Eso significa mucho más en el sentido de que "la optimización prematura es la raíz de todo mal". Obviamente, no deberías usarlo volatileen todo solo porque sí , pero tampoco deberías evitarlo en los casos en que creas que es legítimo debido a preocupaciones de rendimiento preventivas.
aroth
9

El uso principal de la palabra clave volátil en aplicaciones C incrustadas es marcar una variable global que se escribe en un controlador de interrupciones. Ciertamente no es opcional en este caso.

Sin él, el compilador no puede probar que el valor se escribe alguna vez después de la inicialización, porque no puede probar que se llame al controlador de interrupciones. Por lo tanto, piensa que puede optimizar la variable fuera de existencia.

vicatcu
fuente
2
Ciertamente existen otros usos prácticos, pero en mi opinión, este es el más común.
vicatcu
1
Si el valor solo se lee en un ISR (y se cambia de main ()), posiblemente también tenga que usar volátil para garantizar el acceso ATÓMICO para las variables de varios bytes.
Rev1.0
15
@ Rev1.0 No, volátil no garantiza la aromicidad. Esa preocupación debe abordarse por separado.
Chris Stratton
1
No hay lectura del hardware ni interrupciones en el código publicado. Estás asumiendo cosas de la pregunta que no están allí. Realmente no se puede responder en su forma actual.
Lundin
3
"marca una variable global que se escribe en un controlador de interrupciones" no. Es para marcar una variable; global o de otra manera; que puede ser cambiado por algo fuera de la comprensión de los compiladores. No se requiere interrupción. Podría ser memoria compartida o alguien que inserte una sonda en la memoria (esta última no se recomienda para nada más moderno que 40 años)
UKMonkey
9

Existen dos casos en los que debe usar volatileen sistemas integrados.

  • Al leer desde un registro de hardware.

    Eso significa que el registro mapeado en memoria es parte de los periféricos de hardware dentro de la MCU. Es probable que tenga un nombre críptico como "ADC0DR". Este registro debe definirse en código C, ya sea a través de un mapa de registro entregado por el proveedor de la herramienta o por usted mismo. Para hacerlo usted mismo, lo haría (suponiendo un registro de 16 bits):

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

    donde 0x1234 es la dirección donde la MCU ha asignado el registro. Como volatileya es parte de la macro anterior, cualquier acceso a ella será calificado de forma volátil. Entonces este código está bien:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Al compartir una variable entre un ISR y el código relacionado utilizando el resultado del ISR.

    Si tienes algo como esto:

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

    Entonces el compilador podría pensar: "adc_data siempre es 0 porque no se actualiza en ninguna parte. Y esa función ADC0_interrupt () nunca se llama, por lo que la variable no se puede cambiar". El compilador generalmente no se da cuenta de que las interrupciones son llamadas por hardware, no por software. Entonces el compilador va y elimina el código, if(adc_data > 0){ do_stuff(adc_data); }ya que cree que nunca puede ser cierto, causando un error muy extraño y difícil de depurar.

    Al declarar adc_data volatile, el compilador no puede hacer tales suposiciones y no puede optimizar el acceso a la variable.


Notas importantes:

  • Siempre se declarará un ISR dentro del controlador de hardware. En este caso, el ADC ISR debe estar dentro del controlador ADC. Nadie más que el conductor debe comunicarse con el ISR; todo lo demás es programación de spaghetti.

  • Al escribir C, toda comunicación entre un ISR y el programa de fondo debe estar protegida contra las condiciones de la carrera. Siempre , siempre , sin excepciones. El tamaño del bus de datos MCU no importa, porque incluso si hace una copia de 8 bits en C, el lenguaje no puede garantizar la atomicidad de las operaciones. No, a menos que use la función C11 _Atomic. Si esta función no está disponible, debe usar algún tipo de semáforo o deshabilitar la interrupción durante la lectura, etc. El ensamblador en línea es otra opción. volatileNo garantiza la atomicidad.

    Lo que puede suceder es esto:
    -Valor de carga de la pila al registro -Se
    produce una interrupción -Utilice el
    valor del registro

    Y luego no importa si la parte de "valor de uso" es una sola instrucción en sí misma. Lamentablemente, una parte importante de todos los programadores de sistemas embebidos son ajenos a esto, lo que probablemente lo convierte en el error de sistemas embebidos más común. Siempre intermitente, difícil de provocar, difícil de encontrar.


Un ejemplo de un controlador ADC escrito correctamente se vería así (suponiendo que C11 _Atomicno esté 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;
  }
}
  • Este código supone que una interrupción no puede ser interrumpida en sí misma. En tales sistemas, un booleano simple puede actuar como semáforo, y no necesita ser atómico, ya que no hay daño si la interrupción ocurre antes de que se establezca el booleano. La desventaja del método simplificado anterior es que descartará las lecturas de ADC cuando se produzcan condiciones de carrera, utilizando en su lugar el valor anterior. Esto también se puede evitar, pero luego el código se vuelve más complejo.

  • Aquí volatileprotege contra errores de optimización. No tiene nada que ver con los datos que se originan en un registro de hardware, solo que los datos se comparten con un ISR.

  • staticprotege contra la programación de espaguetis y la contaminación del espacio de nombres, al hacer que la variable sea local para el controlador. (Esto está bien en aplicaciones de un solo núcleo y un solo hilo, pero no en las de múltiples subprocesos).

Lundin
fuente
Difícil de depurar es relativo, si se elimina el código, notará que su código valioso se ha ido; esa es una declaración bastante audaz de que algo anda mal. Pero estoy de acuerdo, puede haber efectos muy extraños y difíciles de depurar.
Arsenal
@Arsenal Si tiene un buen depurador que alinea al ensamblador con la C, y conoce al menos un poco, entonces sí, puede ser fácil de detectar. Pero para un código complejo más grande, no es trivial pasar una gran cantidad de asm generados por máquina. O si no sabes nada. O si su depurador es una mierda y no muestra asm (cougheclipsecough).
Lundin
Podría ser que estoy un poco mimado al usar depuradores de Lauterbach entonces. Si intenta establecer un punto de interrupción en el código que se optimizó, lo establecerá en un lugar diferente y sabrá que algo está sucediendo allí.
Arsenal
@Arsenal Sí, el tipo de C / asm mixto que puedes obtener en Lauterbach no es estándar. La mayoría de los depuradores muestran el asm en una ventana separada, si es que lo hacen.
Lundin
semaphoredefinitivamente debería ser volatile! De hecho, es el caso de uso más básico que requiere volatile: Señalar algo de un contexto de ejecución a otro. - En su ejemplo, el compilador podría omitir semaphore = true;porque 've' que su valor nunca se lee antes de que se sobrescriba semaphore = false;.
JimmyB
5

En los fragmentos de código presentados en la pregunta, todavía no hay una razón para usar volátil. Es irrelevante que el valor de adcValueprovenga de un ADC. Y adcValueser global debería hacerte sospechar si adcValuedebería ser volátil, pero no es una razón en sí misma.

Ser global es una pista porque abre la posibilidad de que adcValuese pueda acceder desde más de un contexto de programa. Un contexto de programa incluye un controlador de interrupciones y una tarea RTOS. Si un contexto cambia la variable global, los otros contextos del programa no pueden asumir que conocen el valor de un acceso anterior. Cada contexto debe volver a leer el valor de la variable cada vez que lo usa porque el valor puede haber cambiado en un contexto de programa diferente. Un contexto de programa no es consciente cuando se produce una interrupción o cambio de tarea, por lo que debe suponer que cualquier variable global utilizada por múltiples contextos puede cambiar entre los accesos de la variable debido a un posible cambio de contexto. Para eso es la declaración volátil. Le dice al compilador que esta variable puede cambiar fuera de su contexto, así que léalo en cada acceso y no asuma que ya conoce el valor.

Si la variable está asignada en memoria a una dirección de hardware, los cambios realizados por el hardware son efectivamente otro contexto fuera del contexto de su programa. Entonces, el mapeo de memoria también es una pista. Por ejemplo, si su readADC()función accede a un valor mapeado en memoria para obtener el valor ADC, entonces esa variable mapeada en memoria probablemente debería ser volátil.

Entonces, volviendo a su pregunta, si hay más en su código y adcValuese accede a él por otro código que se ejecuta en un contexto diferente, entonces sí, adcValuedebería ser volátil.

kkrambo
fuente
4

"Variable global que cambia directamente desde el hardware"

El hecho de que el valor provenga de algún registro ADC de hardware no significa que el hardware lo cambie "directamente".

En su ejemplo, simplemente llama a readADC (), que devuelve algún valor de registro ADC. Esto está bien con respecto al compilador, sabiendo que a adcValue se le asigna un nuevo valor en ese punto.

Sería diferente si estuviera utilizando una rutina de interrupción de ADC para asignar el nuevo valor, que se llama cuando un nuevo valor de ADC está listo. En ese caso, el compilador no tendría idea de cuándo se llama al ISR correspondiente y puede decidir que no se accederá a adcValue de esta manera. Aquí es donde los volátiles ayudarían.

Rev1.0
fuente
1
Como su código nunca "llama" a la función ISR, el Compilador ve que la variable solo se actualiza en una función que nadie llama. Entonces el compilador lo optimiza.
Swanand
1
Depende del resto del código, si adcValue no se lee en ninguna parte (como solo leer a través del depurador), o si se lee solo una vez en un lugar, el compilador probablemente lo optimizará.
Damien
2
@Damien: Siempre "depende", pero tenía el objetivo de abordar la pregunta real "¿Debería usar la palabra clave volátil en este caso?" tan corto como sea posible.
Rev1.0
4

El comportamiento del volatileargumento depende en gran medida de su código, el compilador y la optimización realizada.

Hay dos casos de uso en los que yo personalmente uso volatile:

  • Si hay una variable que quiero ver con el depurador, pero el compilador la ha optimizado (significa que la ha eliminado porque descubrió que no es necesario tener esta variable), la suma volatileobligará al compilador a mantenerla y, por lo tanto, Se puede ver en la depuración.

  • Si la variable puede cambiar "fuera del código", generalmente si tiene algún hardware accediendo a ella o si asigna la variable directamente a una dirección.

En Embedded también a veces hay bastantes errores en los compiladores, haciendo una optimización que en realidad no funciona, y a veces volatilepuede resolver los problemas.

Dado que su variable se declara globalmente, probablemente no se optimizará, siempre que la variable se esté utilizando en el código, al menos escrita y leída.

Ejemplo:

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

En este caso, la variable probablemente se optimizará para imprimirf ("% i", 1);

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

no será optimizado

Otro:

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

En este caso, el compilador podría optimizar (si optimiza la velocidad) y, por lo tanto, descartar la variable

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

Para su caso de uso, "podría depender" del resto de su código, cómo adcValuese usa en otros lugares y la configuración de versión / optimización del compilador que usa.

A veces puede ser molesto tener un código que funcione sin optimización, pero que se rompa una vez optimizado.

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

Esto podría optimizarse para 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
}

Probablemente no se optimizarán, pero nunca se sabe "qué tan bueno es el compilador" y podría cambiar con los parámetros del compilador. Por lo general, los compiladores con buena optimización tienen licencia.

Damien
fuente
1
Por ejemplo a = 1; b = a; y c = b; el compilador podría pensar esperar un minuto, ayb son inútiles, solo pongamos 1 a c directamente. Por supuesto, no hará eso en su código, pero el compilador es mejor que usted para encontrarlos, también si intenta escribir código optimizado de inmediato sería ilegible.
Damien
2
Un código correcto con un compilador correcto no se romperá con las optimizaciones activadas. La corrección del compilador es un poco problemático, pero al menos con IAR no he encontrado una situación en la que la optimización conduzca a romper el código donde no debería.
Arsenal
55
Una gran cantidad de casos en que se rompe el código de optimización es cuando se está aventurando en UB territorio también ..
tubería
2
Sí, un efecto secundario de volátil es que puede ayudar a la depuración. Pero esa no es una buena razón para usar volátiles. Probablemente debería desactivar las optimizaciones si su objetivo es la depuración fácil. Esta respuesta ni siquiera menciona las interrupciones.
kkrambo
2
Agregar al argumento de depuración, volatileobliga al compilador a almacenar una variable en RAM y actualizar esa RAM tan pronto como se asigne un valor a la variable. La mayoría de las veces, el compilador no 'borra' variables, porque generalmente no escribimos asignaciones sin efecto, pero puede decidir mantener la variable en algún registro de CPU y luego puede o nunca escribir el valor de ese registro en la RAM. Los depuradores a menudo fallan al localizar el registro de la CPU en el que se encuentra la variable y, por lo tanto, no pueden mostrar su valor.
JimmyB
1

Muchas explicaciones técnicas, pero quiero concentrarme en la aplicación práctica.

La volatilepalabra clave obliga al compilador a leer o escribir el valor de la variable de la memoria cada vez que se usa. Normalmente, el compilador intentará optimizar, pero no realizar lecturas y escrituras innecesarias, por ejemplo, manteniendo el valor en un registro de CPU en lugar de acceder a la memoria cada vez.

Esto tiene dos usos principales en el código incrustado. En primer lugar se utiliza para registros de hardware. Los registros de hardware pueden cambiar, por ejemplo, el periférico ADC puede escribir un registro de resultados ADC. Los registros de hardware también pueden realizar acciones cuando se accede a ellos. Un ejemplo común es el registro de datos de un UART, que a menudo borra las banderas de interrupción cuando se lee.

El compilador normalmente trataría de optimizar las lecturas y escrituras repetidas del registro suponiendo que el valor nunca cambiará, por lo que no es necesario seguir accediendo a él, pero la volatilepalabra clave lo obligará a realizar una operación de lectura cada vez.

El segundo uso común es para las variables utilizadas por el código de interrupción y sin interrupción. Las interrupciones no se invocan directamente, por lo que el compilador no puede determinar cuándo se ejecutarán y, por lo tanto, asume que los accesos dentro de la interrupción nunca suceden. Debido a que la volatilepalabra clave obliga al compilador a acceder a la variable cada vez, esta suposición se elimina.

Es importante tener en cuenta que la volatilepalabra clave no es la solución completa a estos problemas, y se debe tener cuidado para evitarlos. Por ejemplo, en un sistema de 8 bits, una variable de 16 bits requiere dos accesos a la memoria para leer o escribir, y por lo tanto, incluso si el compilador se ve obligado a hacer esos accesos, se producen secuencialmente, y es posible que el hardware actúe en el primer acceso o una interrupción para ocurrir entre los dos.

usuario
fuente
0

En ausencia de un volatilecalificador, el valor de un objeto puede almacenarse en más de un lugar durante ciertas partes del código. Considere, por ejemplo, dado algo como:

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

En los primeros días de C, un compilador habría procesado la declaración

foo++;

a través de los pasos:

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

Sin embargo, los compiladores más sofisticados reconocerán que si el valor de "foo" se mantiene en un registro durante el ciclo, solo deberá cargarse una vez antes del ciclo y almacenarse una vez después. Durante el ciclo, sin embargo, eso significará que el valor de "foo" se mantiene en dos lugares: dentro del almacenamiento global y dentro del registro. Esto no será un problema si el compilador puede ver todas las formas en que se puede acceder a "foo" dentro del bucle, pero puede causar problemas si se accede al valor de "foo" en algún mecanismo que el compilador no conoce ( como un manejador de interrupciones).

Podría haber sido posible para los autores de la Norma agregar un nuevo calificador que invitaría explícitamente al compilador a hacer tales optimizaciones, y decir que la semántica anticuada se aplicaría en su ausencia, pero los casos en que las optimizaciones son útiles superan ampliamente en número aquellos donde sería problemático, por lo que el Estándar permite a los compiladores asumir que tales optimizaciones son seguras en ausencia de evidencia de que no lo sean. El propósito de la volatilepalabra clave es proporcionar dicha evidencia.

Un par de puntos de discusión entre algunos compiladores escritores y programadores ocurre con situaciones como:

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

Históricamente, la mayoría de los compiladores permitirían la posibilidad de que escribir una volatileubicación de almacenamiento podría desencadenar efectos secundarios arbitrarios, y evitar el almacenamiento en caché de los valores en los registros de dicha tienda, o de lo contrario se abstendrán de almacenar en caché los valores en los registros a través de llamadas a funciones que son no calificado "en línea" y, por lo tanto, escribiría 0x1234 en output_buffer[0], configurar las cosas para generar los datos, esperar a que se complete, luego escribir 0x2345 output_buffer[0]y continuar desde allí. El Estándar no requiere implementaciones para tratar el acto de almacenar la dirección output_bufferenvolatile- puntero calificado como una señal de que algo le puede pasar a través de significa que el compilador no entiende, sin embargo, porque los autores pensaron que los escritores de compiladores destinados a varias plataformas y propósitos reconocerían cuándo hacerlo serviría para esos propósitos en esas plataformas sin tener que ser contado. En consecuencia, algunos compiladores "inteligentes" como gcc y clang supondrán que, aunque la dirección de output_bufferesté escrita en un puntero calificado volátil entre las dos tiendas output_buffer[0], no hay razón para suponer que algo podría importarle el valor contenido en ese objeto en ese momento.

Además, si bien los punteros que se lanzan directamente desde enteros rara vez se usan para cualquier otro propósito que no sea manipular cosas de una manera que los compiladores probablemente no entiendan, el Estándar nuevamente no requiere que los compiladores traten tales accesos como volatile. En consecuencia, la primera escritura *((unsigned short*)0xC0001234)puede ser omitida por compiladores "inteligentes" como gcc y clang, porque los mantenedores de dichos compiladores preferirían reclamar ese código que no califica las cosas como volatile"roto" que reconocer que la compatibilidad con dicho código es útil . Muchos archivos de encabezado suministrados por el proveedor omiten los volatilecalificadores, y un compilador que es compatible con los archivos de encabezado suministrados por el proveedor es más útil que uno que no lo es.

Super gato
fuente