Using Volatile keyword in embedded code

Volatile is probably the least documented keyword in most tutorials and books. This is the primary cause of most misuses and bugs related to it. If you already are programming microcontrollers, you probably know that volatile is always used on global variables that are accessed from interrupt service routines. Otherwise, code won’t work.

volatile

After few requests, I decided to drop few lines about volatile keywords. This keyword is commonly used to tag memory type. We hear “volatile memory” and “non-volatile memory” when discussing computer hardware. As a quick reminder – “non-volatile memory” is a type of memory that stores its contents even when power is off. Such type of memory is EEPROM, Flash, and FRAM. This is easy from a hardware perspective. But what volatile keyword means in C or C++ code? This is an indicator (called qualifier) to the compiler that tells that this variable may be changed during program flow even if it doesn’t look like it be. This means that compiler must treat this value seriously and keep optimizer away from it.

Declaring volatile variable

As you already may know, a volatile variable is declared in a simple sentences:

volatile uint8_t variable;

This is also same:

uint8_t volatile variable;

If you need to use pointers to volatile data then use following declaration:

volatile uint8_t * variable;

Or:

uint8_t volatile * variable;

There are more ways of using a volatile variable, like a volatile pointer to volatile data. But this is sporadic case so that we won’t go into that.

It doesn’t have to be a volatile variable. If you need, you can declare structures and unions as volatile. Or, in other cases, declare specific members of the structure. It all depends on your program design.

Where to use volatile variables?

Ideally, you may think, why not use volatile everywhere if it is safe. Matter of fact is that your code would work fine with all volatiles, but it would be inefficient with lots of overhead. Hopefully, you noticed the situation when you write a code and compile it without optimization “-O0”. It works fine without any volatiles. This means that code is compiled with all overheads. It takes much more space and may be executed much slower. But once you include some level of optimization, code stops working, and as a beginner, you may start wondering what a problem – code looks fine as there is no logical errors and so on. Your interrupts don’t do what they are supposed to do; you get into infinite loops, etc. This happens because compiler optimizer does some initial code analyze and finds that variable is once initialized and doesn’t seem to be changed. Optimizer isn’t that smart to predict interrupt behavior or other event like RTOS tasks. So compiler optimizes the variable out, leaving it with predefined value, and may not check its contents even if you would expect it to.

In that case, I am declaring a such variable as volatile rings the bell to the optimizer, not to optimize it. Let’s give a quick example of the problem.

Case 1:

#include <avr/io.h>
uint8_t i;
int main(void)
{
    //while loop
    while(1)
    {
            for(i=2; i<255; i++)
            {
                //empty
            }
    }
}

 

In the following program, we simply use a for loop where we use the global variable i as the counter. For this case, I’ve chosen a compiled program without optimization (-O0).

If we open list file (.lss) of compiled program you can see that while(1) loop does what it has to:

98: 82 e0 ldi r24, 0x02 ; 2

9a: 80 93 00 01 sts 0x0100, r24

9e: 05 c0 rjmp .+10 ; 0xaa <main+0x1a>

a0: 80 91 00 01 lds r24, 0x0100

a4: 8f 5f subi r24, 0xFF ; 255

a6: 80 93 00 01 sts 0x0100, r24

aa: 80 91 00 01 lds r24, 0x0100

ae: 8f 3f cpi r24, 0xFF ; 255

b0: b9 f7 brne .-18 ; 0xa0 <main+0x10>

It loads counter value (address 0x0100) and compares it with 255. So non-optimized code works fine but it takes much code space. Unoptimized code is only recommended for debugging purposes.

Case 2:

Now let’s leave same program but simply change optimization to “-Os” and compile. Now our program shouldn’t work. To understand the reason, let’s open same list file and look at for loop:

90: ff cf rjmp .-2 ; 0x90 <main>

No surprise – the code is completely optimized out. Optimizer assumes that for running loop is useless as the variable i isn’t used anywhere else. So it took loop code part ultimately out.

Case 3:

And now, let’s make i variable volatile.

volatile uint8_ti;

Leave optimization “-Os” and compile. After this open list file to see how it looks:

00000090 <main>:

90: 92 e0 ldi r25, 0x02 ; 2

92: 90 93 00 01 sts 0x0100, r25

96: 05 c0 rjmp .+10 ; 0xa2 <main+0x12>

98: 80 91 00 01 lds r24, 0x0100

9c: 8f 5f subi r24, 0xFF ; 255

9e: 80 93 00 01 sts 0x0100, r24

a2: 80 91 00 01 lds r24, 0x0100

a6: 8f 3f cpi r24, 0xFF ; 255

a8: a1 f3 breq .-24 ; 0x92 <main+0x2>

aa: f6 cf rjmp .-20 ; 0x98 <main+0x8>

Now we can see that for loop is up and running.

This simple code demonstrates the benefit of using volatile variables. The same rule applies when the variable is accessed from interrupt service routine (ISR). So, as a rule of thumb – always make variables volatile if they are modified from ISR. Generally speaking, we can make a small list of where volatile is required:

  • global variables modified from ISR;
  • variables for empty delay loops – instead, use better solution;
  • global variables used in multitask applications – RTOS;
  • Peripheral registers variables.

As for peripheral registers, this is somewhat what you usually use but not notice. Let’s open atmega328p header file from avr toolchain. You can find that, for instance, PORTD is defined as:

#define PORTB _SFR_IO8(0x05)

Let’s dig deeper and find how SFR_IO8() is defined. For this open sfr_defs.h header file and locate:

#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

where

#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

so, in the end, we get that PORTB is defined as:

#define PORTB (*(volatile uint8_t)*)(0x18 + 0x20)

So compiler treats all register labels as volatile that point to a specific location in SRAM. This way, the compiler is forced to write or read specific RAM locations in any case.

In the end, if you start thinking of making all variables volatile and be safe – don’t. This is not the way out of problems. First of all, your program becomes less efficient as the optimizer is left outside. As you know optimizers are pretty good these days – so give them some work. It is better to understand why you need a volatile instead of putting it blindly.

9 Comments:

  1. ОГРОМНОЕ СПАСИБО ! )))
    many thanks !!! )))

  2. Thank you for a clear explanation about volatile variables. I’m int the learning curve from PIC to AVR, and your explanations ae very helpfull! Keep the good working.

  3. Hi,
    I have one question about volatile for operations with I/O Ports. I don´t know if must be variable for reading PINA, or set PORTA volatile, or not. For example:

    uint8_t x;
    x = PINA;
    PORTB = x;

    or must be used:

    volatile uint8_t x;
    x = PINA;
    PORTB = x;

    Is some difference between optimization Os, O1 for this cases?
    Thank you for your reply

  4. Lets run these codes to see:

    uint8_t x;
    x = PINC;
    PORTB = x;

    same results for both optimizations
    -Os and -O1

    in r24, 0x06
    sts 0x0100, r24
    out 0x05, r24

    and with volatile:

    volatile uint8_t x;
    x = PINC;
    PORTB = x;

    same results for both optimizations
    -Os and -O1

    in r24, 0x06
    sts 0x0100, r24
    lds r24, 0x0100
    out 0x05, r24

    As you can see without volatile value is read to register r24 which then is stored to SRAM address 0x0100, but when setting PORTB the value is taken directly from register r24.
    If volatile is used, then reading from pin operation is same, but when writing tp port, the value is read from sram to register and then port value is set. Optimization level doesn’t influence the resulting compilation.
    So if you code doesn’t use fancy code which would access registers (eg. during interrupts), then you don’t need volatile, but if you feel, that register value can be modified by other routine just after PINC is read, then before writing to port seems logical to read this value from memory before out.In this case you should use volatile.
    It is hard to advice withotu seeing whole picture what your code does. Just ecperiment ant try to figure out by inspecting generated listing file (.lss).

  5. Thank you for your reply.
    I didn’t know, if is rule, that must be variable volatile, which will work with PORTx (read or write). From your answer I understand, that both versions are correct for using with c language without direct access to registers using assembler.

  6. Both are correct, but version with volatile is not optimal as it adds overhead. If variable is not accessed from ISR or not accessed from RTOS task, don’t use volatile.

  7. I always used volatile just only for variables, which was changed in ISR, but when I read your posts and I saw fourth point when is used volatile “peripheral registers variables”, so I thought that is for variables which works with I/O Ports.
    Thanks

  8. i want more explanation…

Comments are closed