He estado leyendo algunos artículos y las respuestas de Stack Exchange sobre el uso de la volatile
palabra 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 volatile
en este caso?
Sin usar
volatile
palabra clave// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
Usando la
volatile
palabra 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 volatile
es obligatorio.
microcontroller
c
embedded
Pryda
fuente
fuente
if(x==1) x=1;
la escritura se puede optimizar para un no volátilx
y no se puede optimizar six
es 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).Respuestas:
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 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 cuandoEfectos de
volatile
Cuando se declara una variable,
volatile
el 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:
Aquí, el compilador probablemente ni siquiera asignará RAM para la
result
variable, y nunca almacenará los valores intermedios en ningún lugar que no sea en un registro de CPU.Si
result
fuera volátil, cada apariciónresult
en 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:
podría ser reordenado a
que puede guardar una instrucción de ensamblador porque el valor
99
no tendrá que cargarse dos veces.Si
a
,b
yc
fuera 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í:
Si, en este caso,
signal
no fuera asívolatile
, el compilador 'pensaría' quewhile( signal == 0 )
puede ser un bucle infinito (porquesignal
nunca será cambiado por el código dentro del bucle ) y podría generar el equivalente deConsiderado manejo de
volatile
valoresComo se indicó anteriormente, una
volatile
variable 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, comoEsto 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
sysTickCount
en el ejemplo anterior. En un AVR, sería especialmente doloroso que la funcióndoSysTick()
acceda a los mismos cuatro bytes en la memoria (cuatro instrucciones = 8 ciclos de CPU por accesosysTickCount
) 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 sedoSysTick()
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
volatile
no 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 usovolatile
. En el AVR, puede usarATOMIC_BLOCK
desde<util/atomic.h>
ocli(); ... 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
volatile
impone un estricto orden de ejecución solo con respecto a otras variables volátiles. Esto significa que, por ejemplose garantiza que primero asigne 1 a
i
y luego asigne 2 aj
. Sin embargo, se no garantiza quea
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) dea
.Si no fuera por la barrera de memoria de las macros mencionadas anteriormente, el compilador podría traducir
a
o
(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).fuente
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.cli
/sei
es una solución demasiado pesada si su único objetivo es lograr una barrera de memoria, no evitar interrupciones. Estas macros generan instruccionescli
/ reales ysei
, 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).volatile
hay 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".volatile data_t data = {0}; set_mmio(&data); while (!data.ready);
.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 lareadFromADC()
función. O cualquier función que no conozca. O cualquier cosa que manipule punteros que puedan apuntaradcValue
y tal. Realmente no hay necesidad de volátiles ya que la variable nunca cambia de manera impredecible.fuente
volatile
en 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.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.
fuente
Existen dos casos en los que debe usar
volatile
en 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):
donde 0x1234 es la dirección donde la MCU ha asignado el registro. Como
volatile
ya es parte de la macro anterior, cualquier acceso a ella será calificado de forma volátil. Entonces este código está bien:Al compartir una variable entre un ISR y el código relacionado utilizando el resultado del ISR.
Si tienes algo como esto:
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.volatile
No 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
_Atomic
no esté disponible):adc.h
adc.c
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í
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 las de múltiples subprocesos).fuente
semaphore
definitivamente debería servolatile
! De hecho, es el caso de uso más básico que requierevolatile
: Señalar algo de un contexto de ejecución a otro. - En su ejemplo, el compilador podría omitirsemaphore = true;
porque 've' que su valor nunca se lee antes de que se sobrescribasemaphore = false;
.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
adcValue
provenga de un ADC. YadcValue
ser global debería hacerte sospechar siadcValue
debería ser volátil, pero no es una razón en sí misma.Ser global es una pista porque abre la posibilidad de que
adcValue
se 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
adcValue
se accede a él por otro código que se ejecuta en un contexto diferente, entonces sí,adcValue
debería ser volátil.fuente
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.
fuente
El comportamiento del
volatile
argumento 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
volatile
obligará 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
volatile
puede 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:
En este caso, la variable probablemente se optimizará para imprimirf ("% i", 1);
no será optimizado
Otro:
En este caso, el compilador podría optimizar (si optimiza la velocidad) y, por lo tanto, descartar la variable
Para su caso de uso, "podría depender" del resto de su código, cómo
adcValue
se 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.
Esto podría optimizarse para printf ("% i", readADC ());
-
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.
fuente
volatile
obliga 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.Muchas explicaciones técnicas, pero quiero concentrarme en la aplicación práctica.
La
volatile
palabra 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
volatile
palabra 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
volatile
palabra clave obliga al compilador a acceder a la variable cada vez, esta suposición se elimina.Es importante tener en cuenta que la
volatile
palabra 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.fuente
En ausencia de un
volatile
calificador, 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:En los primeros días de C, un compilador habría procesado la declaración
a través de los pasos:
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
volatile
palabra clave es proporcionar dicha evidencia.Un par de puntos de discusión entre algunos compiladores escritores y programadores ocurre con situaciones como:
Históricamente, la mayoría de los compiladores permitirían la posibilidad de que escribir una
volatile
ubicació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 enoutput_buffer[0]
, configurar las cosas para generar los datos, esperar a que se complete, luego escribir 0x2345output_buffer[0]
y continuar desde allí. El Estándar no requiere implementaciones para tratar el acto de almacenar la direcciónoutput_buffer
envolatile
- 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 deoutput_buffer
esté escrita en un puntero calificado volátil entre las dos tiendasoutput_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 comovolatile
"roto" que reconocer que la compatibilidad con dicho código es útil . Muchos archivos de encabezado suministrados por el proveedor omiten losvolatile
calificadores, 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.fuente