52 votos

Utilizando volátiles en el desarrollo integrado de C

He estado leyendo algunos de los artículos y de Intercambio de la Pila respuestas sobre el uso de la volatile palabra clave para evitar que el compilador de la aplicación de alguna de las optimizaciones en los objetos que pueden cambiar en formas que no pueden ser determinados por el compilador.

Si estoy leyendo de un ADC (vamos a llamar a la variable adcValue), y estoy declarando esta variable como global, se debe usar la palabra clave volatile en este caso?

  1. Sin el uso de volatile de palabras clave

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. El uso de la volatile de palabras clave

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

Estoy haciendo esta pregunta porque cuando la depuración, yo no veo ninguna diferencia entre ambos enfoques, aunque las mejores prácticas que se dice que en mi caso (una variable global que los cambios directamente desde el hardware), y luego usando volatile es obligatorio.

96voto

pgs Puntos 2491

Una definición de la volatile

volatile le dice al compilador que el valor de la variable puede cambiar sin que el compilador sepa. Por lo tanto el compilador no puede asumir el valor no cambia sólo porque el programa en C no parece haber cambiado.

Por otro lado, significa que el valor de la variable puede ser requerida (leer) en algún otro lugar que el compilador no sabe nada, por lo tanto debe asegurarse de que cada asignación a la variable que realmente se lleva a cabo como una operación de escritura.

Casos de uso

volatile es necesario cuando

  • en representación de los registros de hardware (o memory-mapped I/O) como variables - incluso si el registro nunca será leída, el compilador no sólo debe omitir la operación de escritura, el pensamiento de "Estúpido programador. Intenta almacenar un valor en una variable a la que él/ella no será nunca jamás vuelva a leer. Él/ella aun no observe si omitimos la escritura." Conversly, incluso si el programa nunca se escribe un valor a la variable, su valor puede ser cambiada por hardware.
  • compartir variables entre los contextos de ejecución (por ejemplo, ISRs/programa principal) (ver @kkramo la respuesta)

Efectos de la volatile

Cuando se declara una variable volatile el compilador debe asegurarse de que toda asignación a en el código del programa se refleja en una operación de escritura, y que cada lectura de código de programa lee el valor de (mmapped) de la memoria.

Para los no-volátil variables, el compilador asume que se conoce a si/cuando el valor de la variable cambia y puede optimizar el código en diferentes maneras.

Para uno, el compilador puede reducir el número de lecturas/escrituras a la memoria, manteniendo el valor de 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 no incluso asignar memoria para el result variable, y nunca va a almacenar los valores intermedios en cualquier lugar, pero en un registro de CPU.

Si result era volátil, cada ocurrencia de result en el código de C requeriría el compilador para realizar un acceso a la memoria RAM (o un puerto de e/S), que conduce a un bajo rendimiento.

En segundo lugar, el compilador puede re-orden de operaciones no volátil variables de rendimiento y/o el tamaño del código. Ejemplo sencillo:

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

podría ser re-orden

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

que puede salvar a un ensamblador de instrucción debido a que el valor de 99 no tiene que ser cargado dos veces.

Si a, b y c fueron volátiles, el compilador tendría que emitir instrucciones que asignar los valores en el mismo orden que están en el programa.

Otro ejemplo clásico es como este:

volatile uint8_t signal;

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

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

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

Considerado el manejo de volatile valores

Como se indicó anteriormente, volatile variable puede introducir una penalización de rendimiento cuando se accede más a menudo de lo que realmente se requiere. Para mitigar este problema, puede que "no volátil" el valor de la asignación a un no-volátil variable, 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 el ISR donde quiero ser tan rápido como sea posible acceder a el mismo hardware o en la memoria varias veces cuando usted sabe que no es necesario porque el valor no va a cambiar mientras el ISR se está ejecutando. Esto es común cuando el ISR es el "productor" de los valores de la variable, al igual que el sysTickCount en el ejemplo anterior. En un AVR sería especialmente doloroso para tener la función doSysTick() de acceso a la misma de cuatro bytes en la memoria (cuatro instrucciones = 8 ciclos de CPU por el acceso a la sysTickCount) cinco o seis veces en lugar de sólo dos veces, porque el programador sabe que el valor no se pueden cambiar de algún otro código, mientras que su/su doSysTick() pistas.

Con este truco, básicamente, de hacer exactamente lo mismo que el compilador hace para no volátil variables, es decir, la lectura de la memoria sólo cuando se tiene que, de mantener el valor en un registro durante algún tiempo y volver a escribir de memoria sólo cuando se tiene; pero, en este momento, usted sabe mejor que el compilador si/cuando lee/escribe debe suceder, por lo que aliviar el compilador de esta tarea de optimización y hacerlo usted mismo.

Limitaciones de volatile

No atómica de acceso

volatile ¿ no proporcionar atómica de acceso a la multi-word variables. Para esos casos, usted tendrá que proporcionar la exclusión mutua por otros medios, además de para el uso de volatile. En el AVR, puede utilizar ATOMIC_BLOCK de <util/atomic.h> o simple cli(); ... sei(); llamadas. Los respectivos macros actuar como una barrera de memoria, que también es importante cuando se trata de la orden de los accesos:

El orden de ejecución de

volatile impone una estricta orden de ejecución sólo 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;

está garantizado el primer asignar 1 i y , a continuación, asignar 2 j. Sin embargo, no se garantiza que a será asignado entre ellos, el compilador puede hacer la tarea antes o después de que el fragmento de código, básicamente, en cualquier momento hasta el primer (visible) lectura de a.

Si no fuera por la memoria de la barrera de los mencionados macros, el compilador sería permitido 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 la memoria de barreras, como las implícitas por el sei/cli macros, en realidad puede obviar el uso de volatile, si todos los accesos están entre corchetes con estas barreras.)

15voto

Neil Foley Puntos 1313

Existe dos casos donde se debe usar volatile en sistemas embebidos.

  • Cuando la lectura de un registro de hardware.

    Eso significa que la memoria asignada a registrarse a sí mismo, una parte de hardware periféricos dentro de la MCU. Es probable que tenga algunos críptico nombre como "ADC0DR". Este registro debe estar definido en el código de C, ya sea a través de algún mapa de registros entregados por la herramienta de proveedor, o por usted mismo. Para hacerlo usted mismo, usted haría (suponiendo 16 bits de registro):

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

    donde 0x1234 es la dirección donde el MCU tiene asignado el registro. Desde volatile ya es parte de la macro anterior, cualquier acceso a la misma será la volatilidad de los calificados. Por lo que este código está bien:

    uint16_t adc_data;
    adc_data = ADC0DR;
    
  • Cuando se comparte una variable entre el ISR y el código relacionado con el uso del resultado de la 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;
    }
    

    A continuación, el compilador podría pensar: "adc_data siempre es 0 porque no se actualiza en cualquier lugar. Y que ADC0_interrupt() función nunca es llamado, por lo que la variable no puede ser cambiado". El compilador por lo general no se dan cuenta de que las interrupciones son llamados por hardware, no de software. Por lo que el compilador va y elimina el código if(adc_data > 0){ do_stuff(adc_data); } , ya que piensa que nunca puede ser verdad, causando una forma muy extraña y difícil de depurar error.

    Declarando adc_data volatile, el compilador no está permitido hacer cualquiera de esas hipótesis y no está permitido optimizar lejos el acceso a la variable.


Notas importantes:

  • Un informe de búsqueda internacional deberá siempre ser declaradas en el controlador de hardware. En este caso, el ADC ISR debe estar dentro de la ADC conductor. Ninguna otra cosa, pero el conductor debe comunicarse con el ISR - todo lo demás es de espagueti de programación.

  • Cuando se escribe C, toda la comunicación entre el ISR y el programa de fondo debe ser protegido contra las condiciones de carrera. Siempre, cada vez, sin excepciones. El tamaño de la MCU bus de datos no importa, porque incluso si usted no hace un solo de 8 bits copia en C, el lenguaje no puede garantizar la atomicidad de las operaciones. No, a menos que usted use el C11 característica _Atomic. Si esta característica no está disponible, se debe utilizar alguna manera de semáforo o deshabilitar la interrupción durante la lectura, etc. Ensamblador en línea es otra opción. volatile no garantiza la atomicidad.

    Lo que puede suceder es este:
    -Valor de la carga de la pila en el registro
    -Interrupción se produce
    -El valor de uso de registro

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


Un ejemplo de un escrito correctamente ADC conductor tendría este aspecto (suponiendo C11 _Atomic no 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 está asumiendo que una interrupción no puede ser interrumpido en sí mismo. En estos sistemas, un simple valor booleano puede actuar como un semáforo, y no tiene que ser atómica, como no hay ningún daño si la interrupción se produce antes de que el booleano es conjunto. El lado negativo de la anterior método simplificado es que se descarte ADC lee cuando las condiciones de carrera se producen, utilizando el valor anterior en su lugar. Esto puede ser evitado, pero, a continuación, el código se vuelve más complejo.

  • Aquí volatile protege contra optimización de errores. No tiene nada que ver con los datos procedentes de un hardware de registro, sólo los que se comparten los datos con un ISR.

  • static protege contra los espaguetis a la programación y el espacio de nombres de la contaminación, por lo que la variable local para el conductor. (Esto está bien en un solo núcleo, de un solo hilo de las aplicaciones, pero no en multi-hilo.)

14voto

La palabra clave volatile le dice al compilador que el acceso a la variable tiene un efecto observable. Eso significa que cada vez que su código fuente se utiliza la variable, el compilador DEBE crear un acceso a la variable. Ser que un acceso de lectura o escritura.

El efecto de esto es que cualquier cambio en la variable fuera del normal flujo de código también será observado por el código. E. g. si un controlador de interrupción cambia el valor. O si la variable es en realidad un poco de hardware registrar que los cambios en su propio.

Esta gran ventaja es también su lado negativo. Cada acceso a la variable pasa a través de la variable y el valor nunca es celebrada en un registro para un acceso más rápido para cualquier cantidad de tiempo. Eso significa que la volatilidad de la variable será lenta. Magnitudes más lento. Así que sólo uso volátil donde es realmente necesario.

En su caso, en la medida que muestra el código, la variable global sólo se cambia cuando se actualice a sí mismo por adcValue = readADC();. El compilador sabe que cuando esto sucede y nunca va a contener el valor de adcValue en un registro a través de algo que puede llamar la readFromADC() función. O cualquier función que no sabe acerca de. O cualquier cosa que la manipulación de punteros que podría apuntar a adcValue y tal. Realmente no hay necesidad para volátil como la variable no cambia nunca de manera impredecible.

10voto

JW. Puntos 145

El principal uso de la palabra clave volátil en aplicaciones embebidas de C es para marcar una variable global que está escrito en un manejador de interrupción. Ciertamente no es opcional en este caso.

Sin él, el compilador no puede probar que el valor se escribe siempre después de la inicialización, ya que no puede probar que nunca se llama al controlador de interrupción. Por lo tanto piensa que puede optimizar la variable fuera de existencia.

4voto

Damien Puntos 136

El comportamiento de la volatile argumento depende en gran medida de su código, el compilador, y la optimización de hecho.

Hay dos casos de uso, donde yo personalmente uso volatile:

  • Si hay una variable quiero ver con el depurador, pero el compilador ha optimizado que (significa que se ha eliminado ya que descubrieron que no es necesario tener esta variable), añadiendo volatile fuerza al compilador a mantener y por lo tanto puede ser visto en la depuración.

  • Si la variable puede cambiar "el código", por lo general, si usted tiene algún componente de hardware de acceso a la misma, o si se puede asignar la variable directamente a una dirección.

En embedded también, a veces, hay algunos errores en los compiladores, haciendo de optimización que en realidad no funciona, y a veces volatile puede resolver los problemas.

Dado que la variable es declarada a nivel mundial, que probablemente no estará optimizado, mientras que la variable que está siendo utilizado en el código, al menos, escrito y leído.

Ejemplo:

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

En este caso, la variable probablemente será optimizado para printf("%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, para optimizar la velocidad) y así 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 el caso de uso, "no puede depender" en el resto de su código, cómo adcValue está siendo utilizado en otros lugares y la versión del compilador / ajustes de optimización de su uso.

A veces puede ser molesto tener un código que funciona con ninguna de optimización, pero se rompe una vez optimizado.

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

Este puede ser optimizado 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
}

Estos probablemente no será optimizado, pero nunca se sabe "qué bueno que el compilador" y podría cambiar con el compilador de parámetros. Generalmente compiladores con una buena optimización de la licencia.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X