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?
Sin usar
volatilepalabra clave// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }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.
microcontroller
c
embedded
Pryda
fuente
fuente

if(x==1) x=1;la escritura se puede optimizar para un no volátilxy no se puede optimizar sixes 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
volatilevolatilele 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
volatilese requiere cuandoEfectos de
volatileCuando 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:
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ónresulten 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
99no tendrá que cargarse dos veces.Si
a,bycfuera 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,
signalno fuera asívolatile, el compilador 'pensaría' quewhile( signal == 0 )puede ser un bucle infinito (porquesignalnunca será cambiado por el código dentro del bucle ) y podría generar el equivalente deConsiderado manejo de
volatilevaloresComo 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, 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
sysTickCounten 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
volatileAcceso 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 usovolatile. En el AVR, puede usarATOMIC_BLOCKdesde<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
volatileimpone 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
iy luego asigne 2 aj. Sin embargo, se no garantiza queaserá 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/seies 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).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".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 apuntaradcValuey tal. Realmente no hay necesidad de volátiles ya que la variable nunca cambia de manera impredecible.fuente
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.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
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):
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: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_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 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.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í
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).fuente
semaphoredefinitivamente 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
adcValueprovenga de un ADC. YadcValueser global debería hacerte sospechar siadcValuedeberí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.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
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:
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
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.
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
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.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.fuente
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: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
volatilepalabra 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
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 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_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 deoutput_bufferesté 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 losvolatilecalificadores, 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