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:
- by hardware (e.g. a memory-mapped peripheral register)
- within an interrupt service routine
- 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”