I have been reading some articles and Stack Exchange answers about using the volatile
keyword to prevent the compiler from applying any optimizations on objects that can change in ways that cannot be determined by the compiler.
If I am reading from an ADC (let's call the variable adcValue
), and I am declaring this variable as global, should I use the keyword volatile
in this case?
-
Without using
volatile
keyword// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
-
Using the
volatile
keyword// Includes #include "adcDriver.h" // Global variables volatile uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
I am asking this question because when debugging, I can see no difference between both approaches although the best practices says that in my case (a global variable that changes directly from the hardware), then using volatile
is mandatory.
Best Answer
A definition of
volatile
volatile
tells the compiler that the variable's value may change without the compiler knowing. Hence the compiler cannot assume the value did not change just because the C program seems not to have changed it.On the other hand, it means that the variable's value may be required (read) somewhere else the compiler does not know about, hence it must make sure that every assignment to the variable is actually carried out as a write operation.
Use cases
volatile
is required whenEffects of
volatile
When a variable is declared
volatile
the compiler must make sure that every assignment to it in program code is reflected in an actual write operation, and that every read in program code reads the value from (mmapped) memory.For non-volatile variables, the compiler assumes it knows if/when the variable's value changes and can optimize code in different ways.
For one, the compiler can reduce the number of reads/writes to memory, by keeping the value in CPU registers.
Example:
Here, the compiler will probably not even allocate RAM for the
result
variable, and will never store the intermediate values anywhere but in a CPU register.If
result
was volatile, every occurrence ofresult
in the C code would require the compiler to perform an access to RAM (or an I/O port), leading to a lower performance.Secondly, the compiler may re-order operations on non-volatile variables for performance and/or code size. Simple example:
could be re-ordered to
which may save an assembler instruction because the value
99
won't have to be loaded twice.If
a
,b
andc
were volatile the compiler would have to emit instructions which assign the values in the exact order as they are given in the program.The other classic example is like this:
If, in this case,
signal
were notvolatile
, the compiler would 'think' thatwhile( signal == 0 )
may be an infinite loop (becausesignal
will never be changed by code inside the loop) and might generate the equivalent ofConsiderate handling of
volatile
valuesAs stated above, a
volatile
variable can introduce a performance penalty when it is accessed more often than actually required. To mitigate this issue, you can "un-volatile" the value by assignment to a non-volatile variable, likeThis may be especially beneficial in ISR's where you want to be as quick as possible not accessing the same hardware or memory multiple times when you know it is not needed because the value will not change while your ISR is running. This is common when the ISR is the 'producer' of values for the variable, like the
sysTickCount
in the above example. On an AVR it would be especially painful to have the functiondoSysTick()
access the same four bytes in memory (four instructions = 8 CPU cycles per access tosysTickCount
) five or six times instead of only twice, because the programmer does know that the value will be not be changed from some other code while his/herdoSysTick()
runs.With this trick, you essentially do the exact same thing the compiler does for non-volatile variables, i.e. read them from memory only when it has to, keep the value in a register for some time and write back to memory only when it has to; but this time, you know better than the compiler if/when reads/writes must happen, so you relieve the compiler from this optimization task and do it yourself.
Limitations of
volatile
Non-atomic access
volatile
does not provide atomic access to multi-word variables. For those cases, you will need to provide mutual exclusion by other means, in addition to usingvolatile
. On the AVR, you can useATOMIC_BLOCK
from<util/atomic.h>
or simplecli(); ... sei();
calls. The respective macros act as a memory barrier too, which is important when it comes to the order of accesses:Execution order
volatile
imposes strict execution order only with respect to other volatile variables. This means that, for exampleis guaranteed to first assign 1 to
i
and then assign 2 toj
. However, it is not guaranteed thata
will be assigned in between; the compiler may do that assignment before or after the code snippet, basically at any time up to the first (visible) read ofa
.If it weren't for the memory barrier of the above mentioned macros, the compiler would be allowed to translate
to
or
(For the sake of completeness I must say that memory barriers, like those implied by the sei/cli macros, may actually obviate the use of
volatile
, if all accesses are bracketed with these barriers.)