Volatile Keyword In C

Table of Contents

  1. What is Volatile Variable in C
  2. Volatile in Embedded Systems (Memory-Mapped I/O)
  3. Why volatile is Important?
  4. CPU Registers Interaction
  5. The Role of volatile
  6. Why volatile is Important

1. What Is Volatile Variable In C?

The volatile keyword in C is used to inform the compiler that a variable's value may change at any time, without any action being taken by the code in which it appears. This is crucial when working with hardware registers, memory-mapped I/O, or variables that are modified by an interrupt service routine (ISR) or another thread in a multi-threaded environment.

2. Volatile In Embedded Systems (Memory-Mapped I/O)

Suppose you are working with a microcontroller and you need to read the status of a hardware register that is memory-mapped. The value of this register can change at any time due to external events, such as hardware signals.

#include "stdio.h"

#define STATUS_REGISTER_ADDRESS 0x40001000U
volatile uint32_t *status_register = (volatile uint32_t *)STATUS_REGISTER_ADDRESS;

int main(void) {
    // Wait until a specific bit in the status register is set
    while ((*status_register & 0x01) == 0) {
        // Do nothing, just wait
    }
    
    // Proceed when the bit is set
    // For example, start processing the received data
    // ...

    return 0;
}
  • volatile uint32_t *status_register: The volatile keyword is used here to tell the compiler that the value of *status_register may change at any time, even if the program itself does not modify it. This prevents the compiler from optimizing out the while loop based on an assumption that the value of *status_register does not change.

  • while ((*status_register & 0x01) == 0): The program continuously checks whether the least significant bit of the status register is set. If the volatile keyword were not used, the compiler might assume that *status_register does not change and optimize the loop in a way that prevents it from working as intended.

3. CPU Registers Interaction:

When the *status_register is accessed in the while loop, the CPU will read the value stored at the memory address 0x40001000. This read operation might involve loading the value into a CPU register (e.g., r0 or r1 in ARM assembly) before performing the bitwise AND operation to check if the specific bit is set.

  • Example in Assembly:

ldr r0, = STATUS_REGISTER_ADDRESS  ; Load the address of the status register into r0
ldr r1, [r0]                      ; Load the value at the address (status register) into r1
tst r1, #0x01                     ; Test if the least significant bit is set
beq wait                          ; If the bit is not set, branch to wait

4. The Role Of Volatile:

1. Without the volatile keyword, the compiler might optimize the code by assuming that the value of *status_register does not change within the loop, possibly storing it in a register and reusing the value without re-reading it from memory. This would cause the loop to potentially never exit if the hardware updates the status register but the CPU does not re-read it.


2. By declaring status_register as volatile, you ensure that each iteration of the loop reads the current value of the status register directly from the memory-mapped address, reflecting any changes made by the hardware.

3. Why Volatile Is Important?

Without the volatile keyword, the compiler might optimize the loop to something like this:

if ((*status_register & 0x01) == 0) {
    while (1) {
        // Infinite loop, never checking the register again
    }
}

This would result in a bug where the program never detects changes in the status register, because the compiler assumes the register's value does not change within the loop.

  • Other Common Use Cases for volatile
    1. Interrupt Service Routines (ISR): Variables shared between an ISR and the main program should be declared as volatile.
    2. Multithreading: In a multi-threaded application, shared variables that are accessed by multiple threads should be declared as volatile to ensure the correct value is read from memory each time.
    3. Memory-Mapped I/O: As in the example above, when dealing with hardware registers or peripherals where the data may change due to external events.