Saltar al contenido

Uso de volatile en el desarrollo de C integrado

Basta ya de indagar en internet ya que has llegado al sitio exacto, tenemos la solución que deseas pero sin liarte.

Solución:

Una definición de volatile

volatile le 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 algún otro lugar que el compilador no conozca, 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

  • representar registros de hardware (o E / S mapeadas en memoria) como variables, incluso si el registro nunca se leerá, el compilador no debe simplemente omitir la operación de escritura pensando “Programador estúpido. Intenta almacenar un valor en una variable que él / ella nunca volverá a leer. Ni siquiera se dará cuenta si omitimos la escritura “. Por el contrario, incluso si el programa nunca escribe un valor en la variable, el hardware aún puede cambiar su valor.
  • 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 volatile el compilador debe asegurarse de que cada asignación a él 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 asume que sabe si / cuándo 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 el result variable, y nunca almacenará los valores intermedios en ningún otro lugar que no sea en un registro de la CPU.

Si result era volátil, cada aparición de result en el código C requeriría que el compilador realizara un acceso a la RAM (o un puerto de E / S), lo que conduciría a un rendimiento más bajo.

En segundo lugar, el compilador puede reordenar las operaciones sobre 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 para

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

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

Si a, b y c fueran volátiles, el compilador tendría que emitir instrucciones que asignan los valores en el orden exacto en 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, signal no eran volatile, el compilador ‘pensaría’ que while( signal == 0 ) puede ser un bucle infinito (porque signal nunca será cambiado por 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.
    
  

Manejo considerado de volatile valores

Como se indicó anteriormente, un volatile La variable puede introducir una penalización de rendimiento cuando se accede a ella con más frecuencia de la necesaria. Para mitigar este problema, puede "anular la volatilidad" del valor asignándolo 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 ISR donde desea ser lo más rápido posible sin acceder al mismo hardware o memoria varias veces cuando usted sepa que no es necesario porque el valor no cambiará mientras su ISR esté funcionando. Esto es común cuando el ISR es el 'productor' de valores para la variable, como el sysTickCount en el ejemplo anterior. En un AVR sería especialmente doloroso tener la función doSysTick() acceder a los mismos cuatro bytes en la memoria (cuatro instrucciones = 8 ciclos de CPU por acceso a sysTickCount) cinco o seis veces en lugar de solo dos, porque el programador sabe que el valor no se cambiará de algún otro código mientras su doSysTick() carreras.

Con este truco, esencialmente hace exactamente lo mismo que hace el compilador para las variables no volátiles, es decir, leerlas de la memoria solo cuando sea necesario, mantener el valor en un registro durante algún tiempo y volver a escribir en la memoria solo cuando sea necesario. ; Pero esta vez, usted saber mejor que el compilador si / cuando lee / escribe debe suceda, por lo que libera al compilador de esta tarea de optimización y lo hace usted mismo.

Limitaciones de volatile

Acceso no atómico

volatile lo hace no proporcionar acceso atómico a variables de varias palabras. Para esos casos, deberá proporcionar la exclusión mutua por otros medios, además a usar volatile. En el AVR, puede usar ATOMIC_BLOCK de o simple cli(); ... sei(); llamadas. Las respectivas macros 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

volatile impone un orden de ejecución estricto 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;

está garantizado para primero asignar 1 a i y luego asignar 2 a j. Sin embargo lo és no garantizado que a será 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();

para

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

o

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

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

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. 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 manejador de interrupciones cambia el valor. O si la variable es en realidad algún registro de hardware que cambia por sí solo.

Este gran beneficio también es su desventaja. Cada acceso a la variable pasa por la variable y el valor nunca se mantiene en un registro para un acceso más rápido durante cualquier período 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 se cambia cuando la actualiza usted mismo por adcValue = readADC();. El compilador sabe cuándo sucede esto y nunca mantendrá el valor de adcValue en un registro en algo que pueda llamar al readFromADC() función. O cualquier función que no conozca. O cualquier cosa que manipule punteros que puedan apuntar a adcValue y tal. Realmente no hay necesidad de volátiles ya que la variable nunca cambia de manera impredecible.

Existen dos casos en los que debe utilizar volatile en sistemas embebidos.

  • Al leer de un registro de hardware.

    Eso significa, el registro mapeado en memoria en sí mismo, 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 algún mapa de registro entregado por el proveedor de la herramienta o por usted mismo. Para hacerlo usted mismo, lo haría (asumiendo un registro de 16 bits):

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

    donde 0x1234 es la dirección donde la MCU ha mapeado el registro. Ya que volatile ya es parte de lo anterior macro, cualquier acceso a él será calificado para volátiles. 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 piensa que nunca puede ser true, provocando un error muy extraño y difícil de depurar.

    Declarando adc_datavolatile, 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 controlador debe comunicarse con el ISR; todo lo demás es programación espagueti.

  • Al escribir C, toda la comunicación entre un ISR y el programa en segundo plano debe estar protegido contra las condiciones de carrera. Siempre, cada vez, sin excepciones. El tamaño del bus de datos MCU no importa, porque incluso si hace una sola copia de 8 bits en C, el idioma no puede garantizar la atomicidad de las operaciones. No, a menos que use la función C11 _Atomic. Si esta característica 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. volatile no garantiza la atomicidad.

    Lo que puede pasar es esto:
    -Cargar valor de la pila al registro
    -Ocurre una interrupción
    -Utilizar valor del registro

    Y luego no importa si la parte del "valor de uso" es una sola instrucción en sí misma. Lamentablemente, una parte importante de todos los programadores de sistemas integrados no se dan cuenta de esto, lo que probablemente lo convierte en el error de sistemas integrados más común de todos los tiempos. 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 _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 asume que una interrupción no se puede interrumpir 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 ocurran las 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í volatile protege 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.

  • static protege 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 los de varios subprocesos).

Ten en cuenta mostrar este enunciado si te fue de ayuda.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *