Skip to content
Menu
Klein Embedded
  • About
Klein Embedded
December 29, 2021August 28, 2022

The ‘volatile’ qualifier

When programming embedded systems in C, you will most likely have stumbled upon the volatile keyword in a variable declaration, like this:

volatile int my_variable;

But what does this actually mean – and when should you use it?

What is a volatile variable?

When looking up the word ‘volatile’ on Merriam-Webster, one definition reads:

volatile, adjective

characterized by or subject to rapid or unexpected change

Merriam-Webster

In programming, a variable is considered volatile if its value may change at any time – even if there doesn’t seem to be any code that would cause this change. When adding the volatile qualifier to a variable, we’re basically telling the compiler: “I know this variable seems useless, but please do not optimize it out of my program.” If a variable is not declared volatile (and compiler optimization is enabled) the compiler may choose to leave out unused pieces of code in the final binary output, in order to reduce the size and improve the execution speed of the program.

But how can the value of a variable just change out of the blue? Well, this can happen if the variable is changed:

  1. by hardware (e.g. a memory-mapped peripheral register)
  2. within an interrupt service routine
  3. in a separate thread

Let’s take a look at each of these cases with some examples.

Case 1: Hardware

If you look at the register definition header file for your microcontroller of choice, you will notice that all peripheral registers are declared volatile. Here’s an example from the header file for the TM4C123GH6PM from Texas Instruments:

#define GPIO_PORTA_DATA_R       (*((volatile unsigned long *)0x400043FC))

The same goes for the STM32H743 from STMicroelectronics. Here __IO is a macro that expands to volatile:

typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO port bit set/reset,               Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;

What would happen if these were not declared volatile?

Imagine you are doing a bunch of reads from the input data register for a GPIO peripheral. The compiler will notice that your program never writes a new value to the register, and therefore will assume that the value never changes. If this is correct, it makes sense for the compiler to simply read the value once and then omit all subsequent reads and assume the value to be unchanged.

In the opposite case, i.e. when writing to the output data register, the compiler will see that the program never uses the register value. The compiler may choose to omit the write instruction entirely, since it seemingly has no impact on the program behavior.

When using the volatile qualifier in the declaration of e.g. a pointer-variable pointing to a memory-mapped peripheral register, we’re telling the compiler not to optimize out any read or write instructions to this register.

Case 2: Interrupt service routine (ISR)

Let’s say we want to execute some code once every millisecond. We’ll set up a hardware timer to trigger an interrupt at the desired interval. Since we want to keep our ISR short, we will simply increment a global tick variable and let our main loop handle the actual function-calling. The ISR will look like this:

unsigned int tick = 0;

void timer_isr()
{
  tick++;
}

Now, in our main loop, we will execute some code if tick has been incremented:

void main()
{
  while (1)
  {
    if (tick)
    {
      --tick;
      /* Do something */
    }
  }
}

The compiler will see that tick is initialized to 0 and is only incremented in timer_isr() which is never explicitly invoked. Since tick will always be 0, the compiler will happily remove the entire if statement and rid you of this “unused code”.

Declaring the tick variable as volatile will solve the problem:

volatile unsigned int tick = 0;

Case 3: Separate thread

When using an RTOS to organize your firmware into separate tasks, one way to communicate between these tasks, is by using global variables (although this is not recommended).

Let’s say we have two tasks: a data_acquisition_task() that gets data from an ADC, and a data_processing_task() that does something with the raw data. The former may signal to the latter that new data is ready, by setting a boolean data_ready variable:

bool data_ready = false;
int buffer[BUFFER_SIZE];

void data_acquisition_task()
{
  while (1)
  {
    /* Get data from the ADC and put it in a shared buffer */
    get_data_from_adc(buffer);

    /* Signal that new data is ready */
    data_ready = true;
  }
}

When the data_processing_task receives the signal, it does some processing to the data:

extern bool data_ready;
extern int buffer[BUFFER_SIZE];

void data_processing_task()
{
  while (1)
  {
    if (data_ready)
    {
      data_ready = false;
      process_data(buffer);
    }  
  }
}

Function pointers to these tasks are given to the RTOS scheduler during initialization, and it is then the responsibility of the RTOS to call these functions appropriately during run-time. Thus, at compile-time the compiler does not know if these functions will actually be called. Since they are not called explicitly anywhere in the program, the compiler may assume that data_ready is never read and therefore optimize out the write instruction, or that it is never written and therefore assuming that the value is always false – neither of which will result in correct program behavior.

Again, adding the volatile qualifier to the variable declaration solves the issue.

Conclusion

Hopefully, this provided some insight into the necessity of the volatile qualifier in embedded software – perhaps it may even save you from a frustrating debugging experience.

1 thought on “The ‘volatile’ qualifier”

  1. Pingback: STM32 without CubeIDE (Part 1): The bare necessities - Klein Embedded

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Subscribe to the newsletter

Get notified by email when a new blog post is published.

Check your inbox or spam folder to confirm your subscription.

Recent Posts

  • STM32 without CubeIDE (Part 4): CMake, FPU and STM32 libraries
  • Adding right-click context menu items in Windows 10
  • CI/CD with Jenkins and Docker
  • STM32 without CubeIDE (Part 3): The C Standard Library and printf()
  • Understanding the (Embedded) Linux boot process

Recent Comments

  1. Kristian Klein-Wengel on STM32 without CubeIDE (Part 2): CMSIS, make and clock configuration
  2. Nora on STM32 without CubeIDE (Part 2): CMSIS, make and clock configuration
  3. Kristian Klein-Wengel on STM32 without CubeIDE (Part 3): The C Standard Library and printf()
  4. Milos on STM32 without CubeIDE (Part 3): The C Standard Library and printf()
  5. otann on STM32 without CubeIDE (Part 1): The bare necessities

Archives

  • October 2025
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • May 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021

Categories

  • C++
  • DevOps
  • DSP
  • Electronics
  • Embedded C
  • Embedded Linux
  • Firmware
  • Project
  • Python
  • Software Design
  • Testing
  • Tutorial
  • Uncategorized
©2025 Klein Embedded