Use of the volatile keyword in C

Incorrect, or non-use of the volatile keyword in C code is a frequent source of errors in real time, embedded software.

The basic idea of volatile is that it tells the compiler that a variable marked as volatile may be changed ‘at any time’, and by events external to the function / file being compiled.

This has particular relevance when compiler optimization is in use.

Example 1 – Hardware device registers.

One of the most notable examples where volatile is needed is when accessing hardware device registers that are memory mapped. In the simple example below, a part of a fictional DMA controller ‘driver’ is shown.

Assume that the DMA controller has a register set including a command reg, status reg, a source address, a destination address, and a count. To effect a DMA transfer, the programmer must load the source, destination, and count registers, then write a 1 to the cmd register to tell the controller to perform the transfer. When the transfer is complete, the device will set a value of 1 into the status register.

Here is a bit of code illustrating the issue:

/* Structure representing DMA device register set */
struct {
    unsigned long cmd;
    unsigned long status;
    unsigned long source_address;
    unsigned long destination_address;
    unsigned long count;
} volatile dma_dev_regs_t;


/* Hardware base address of register set */
#define DEVICE_BASE_ADDRESS 0x7f004000


/*
     Function to do a dma.
	
     Parameters:
     source        starting address of data to move
     dest          starting address of destination
     count         byte count of data to move

*/
void do_dma(unsigned long source, unsigned long dest, int count) {

    dma_dev_regs_t *pRegs = DEVICE_BASE_ADDRESS;

    pRegs->status = 0;

    pRegs->source_address = source;
    pRegs->destination_address = dest;
    pRegs->count = count;

    pRegs->cmd = 1;

    while ( pRegs->status == 0 ) {}
}

In this simple example, if the volatile keyword were left out, and the code were compiled without optimization, the function may work correctly. In particular, the while ( pRegs->status == 0 ) {} statement would do as expected – continuously poll the status register, reading the physical register at location 0x7f004004 until the register contains a non-zero value.

On the other hand, if the code were compiled with optimization enabled, the code generated by the compiler would most likely not poll the status register. The compiler knows that the line pRegs->status = 0 set the status value to 0, and it ‘knows’ that pRegs->status was not set again in the routine. Therefore, the compiler would determine that it is not necessary to re-read the status register in the while loop. It is likely the result would be an endless loop as in ‘while(1) {}’

The volatile keyword alerts the compiler to the fact that it must include the read of the status register in the while loop, since the value may be changing external to this routine.

Example 2 – Interrupt service routine

A similar example can occur when an interrupt service routine (ISR) is in the picture.

Using the fictional DMA controller from the above example, now assume that the DMA controller generates an interrupt when the DMA is done.

/* Structure representing DMA device register set */
typedef struct {
    unsigned long cmd;
    unsigned long status;
    unsigned long source_address;
    unsigned long destication_address;
    unsigned long count;
} volatile dma_dev_regs_t;

/* Hardware base address of register set */
#define DEVICE_BASE_ADDRESS 0x7f004000

/* global flag indicates dma is done */
volatile int dma_done_flag;

/*
     DMA controller Interrupt service routine, is invoked
     when DMA controller completes a DMA operation.
*/
void dma_isr() {

    dma_done_flag = 1;
}

/*
     Function to do a dma.
	
     Parameters:
     source        starting address of data to move
     dest          starting address of destination
     count         byte count of data to move

*/
void do_dma(unsigned long source, unsigned long dest, int count) {

    dma_done_flag = 0;

    dma_dev_regs_t *pRegs = DEVICE_BASE_ADDRESS;

    pRegs->source_address = source;
    pRegs->destination_address = dest;
    pRegs->count = count;

    pRegs->cmd = 1;

    while ( dma_done_flag == 0 ) {}
}

As with the first example, if the volatile keyword were left out for dma_done_flag, the compiler optimizer would remove the read of the dma_done_flag in the while loop. The difference here is that instead of a hardware register, we have a memory location that is changing due to the interrupt service routine which is triggered by the action of the hardware.

Example 3 – Multiple Threads

A similar situation can occur in a multi-tasking or multi threaded environment. This is illustrated with the following simple example:

/* Global counter variable */
volatile int counter;

/* Task enable flags */
volatile int task_1_enable = 0
volatile int task_2_enable = 0

/* 
     Task 2 waits for counter before performing 
      other actions
*/
void task_2 (void ) {

    while (task_2_enable == 0 ) {}

    while (counter < 100) {}

    /* do what task 2 needs to do */
      ...
}     


/* 
     Task 1 increments a counter in a loop
*/
void task_1 ( void ) {

    while (task_1_enable == 0 ) {}

    task_2_enable = 1;

    for ( int i = 0; i < =100; i++) {

        counter++;

        sleep(10)
    }
}


/*
    main routine
*/
int main() {

    counter = 0; /* init global counter */

    task_1_enable = 1; /* start task 1 */
}

In this example, the three global variables counter, task_1_enable, and task_2_enable all need to be marked as volatile, or the compiler optimizer will prevent the program from running as expected.

Please note that the above code snippets are meant to illustrate the point being made, and are very simplified for that purpose. Often, the situations illustrated above may be present in a complex piece of software, and it may not be obvious that that is the case. Careful examination of the code while giving thought to each of the above scenarios may reveal a problem.

A key indicator that there may be a problem with the use of the volatile keyword is that the software will work correctly when compiler optimization is disabled, but fail when optimization is enabled.

However, it is also important to limit the use of the volatile keyword to the situations where it is actually needed. Using volatile on variables that do not really need it will result in performance degradation because the optimizer will be restricted from doing the best job possible when optimizing the code.

Please contact us for help with your embedded systems projects!

Comments are closed