Using volatile in embedded C development

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 when

  • representing hardware registers (or memory-mapped I/O) as variables - even if the register will never be read, the compiler must not just skip the write operation thinking "Stupid programmer. Tries to store a value in a variable which he/she will never ever read back. He/she won't even notice if we omit the write." Conversly, even if the program never writes a value to the variable, its value may still be changed by hardware.
  • sharing variables between execution contexts (e.g. ISRs/main program) (see kkramo's answer)

Effects 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:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

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 of result 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:

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

could be re-ordered to

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

which may save an assembler instruction because the value 99 won't have to be loaded twice.

If a, b and c 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:

volatile uint8_t signal;

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

If, in this case, signal were not volatile, the compiler would 'think' that while( signal == 0 ) may be an infinite loop (because signal will never be changed by code inside the loop) and might generate the equivalent of

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

Considerate handling of volatile values

As 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, like

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
}

This 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 function doSysTick() access the same four bytes in memory (four instructions = 8 CPU cycles per access to sysTickCount) 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/her doSysTick() 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 using volatile. On the AVR, you can use ATOMIC_BLOCK from <util/atomic.h> or simple cli(); ... 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 example

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

is guaranteed to first assign 1 to i and then assign 2 to j. However, it is not guaranteed that a 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 of a.

If it weren't for the memory barrier of the above mentioned macros, the compiler would be allowed to translate

uint32_t x;

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

to

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

or

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

(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.)


The volatile keyword tells the compiler that access to the variable has an observable effect. That means every time your source code uses the variable the compiler MUST create an access to the variable. Be that a read or write access.

The effect of this is that any change to the variable outside the normal code flow will also be observed by the code. E.g. if an interrupt handler changes the value. Or if the variable is actually some hardware register that changes on it's own.

This great benefit is also its downside. Every single access to the variable goes through the variable and the value is never held in a register for faster access for any amount of time. That means a volatile variable will be slow. Magnitudes slower. So only use volatile where it is actually necessary.

In your case, as far as you shown code, the global variable is only changed when you update it yourself by adcValue = readADC();. The compiler knows when this happens and will never hold the value of adcValue in a register across something that may call the readFromADC() function. Or any function it doesn't know about. Or anything that will manipulate pointers that might point to adcValue and such. There really is no need for volatile as the variable never changes in unpredictable ways.


There exist two cases where you must use volatile in embedded systems.

  • When reading from a hardware register.

    That means, the memory-mapped register itself, part of hardware peripherals inside the MCU. It will likely have some cryptic name like "ADC0DR". This register must be defined in C code, either through some register map delivered by the tool vendor, or by yourself. To do it yourself, you'd do (assuming 16 bit register):

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

    where 0x1234 is the address where the MCU has mapped the register. Since volatile is already part of the above macro, any access to it will be volatile-qualified. So this code is fine:

    uint16_t adc_data;
    adc_data = ADC0DR;
    
  • When sharing a variable between an ISR and the related code using the result of the ISR.

    If you have something like this:

    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;
    }
    

    Then the compiler might think: "adc_data is always 0 because it isn't updated anywhere. And that ADC0_interrupt() function is never called, so the variable can't be changed". The compiler usually doesn't realize that interrupts are called by hardware, not by software. So the compiler goes and removes the code if(adc_data > 0){ do_stuff(adc_data); } since it thinks it can never be true, causing a very strange and hard-to-debug bug.

    By declaring adc_data volatile, the compiler is not allowed to make any such assumptions and it is not allowed to optimize away the access to the variable.


Important notes:

  • An ISR shall always be declared inside the hardware driver. In this case, the ADC ISR should be inside the ADC driver. None else but the driver should communicate with the ISR - everything else is spaghetti programming.

  • When writing C, all communication between an ISR and the background program must be protected against race conditions. Always, every time, no exceptions. The size of the MCU data bus does not matter, because even if you do a single 8 bit copy in C, the language cannot guarantee atomicity of operations. Not unless you use the C11 feature _Atomic. If this feature isn't available, you must use some manner of semaphore or disable the interrupt during read etc. Inline assembler is another option. volatile does not guarantee atomicity.

    What can happen is this:
    -Load value from stack into register
    -Interrupt occurs
    -Use value from register

    And then it doesn't matter if the "use value" part is a single instruction in itself. Sadly, a significant portion of all embedded systems programmers are oblivious to this, probably making it the most common embedded systems bug ever. Always intermittent, hard to provoke, hard to find.


An example of a correctly written ADC driver would look like this (assuming C11 _Atomic isn't available):

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;
  }
}
  • This code is assuming that an interrupt cannot be interrupted in itself. On such systems, a simple boolean can act as semaphore, and it need not be atomic, as there is no harm if the interrupt occurs before the boolean is set. The down-side of the above simplified method is that it will discard ADC reads when race conditions occur, using the previous value instead. This can be avoided too, but then the code turns more complex.

  • Here volatile protects against optimization bugs. It has nothing to do with the data originating from a hardware register, only that the data is shared with an ISR.

  • static protects against spaghetti programming and namespace pollution, by making the variable local to the driver. (This is fine in single-core, single-thread applications, but not in multi-threaded ones.)