Tuesday, August 18

Embed with Elliot: The Volatile Keyword

Last time on Embed with Elliot we covered the static keyword, which you can use while declaring a variable or function to increase the duration of the variable without enlarging the scope as you would with a global variable. This piqued the curiosity of a couple of our readers, and we thought we’d run over another (sometimes misunderstood) variable declaration option, namely the volatile keyword.

On its face, volatile is very simple. You use it to tell the compiler that the declared variable can change without notice, and this changes the way that the compiler optimizes with respect to this variable. In big-computer programming, you almost never end up using volatile in C. But in the embedded world, we end up using volatile in one trivial and two very important circumstances, so it’s worth taking a look.

Optimization and the Mind of the Compiler

volatile warns the optimizer that a variable can change without notice, so we need to know why that would ever matter. And for that, we’ll take a quick glimpse into the logic of (one type of) compiler optimization.

Since memory space is presumed to be scarce, a compiler’s going to want to make sure you’re not wasting it. One quick and easy way to do so is to check all the variables, and all the functions that call them, and see if they get changed or if they’re just constants in disguise. Simply put, that means that when the compiler sees something silly like this:

int a = 5;
int b = 0;

b = a + 2;
printf("%d\n", b);

the compiler’s going to notice that you never use a anywhere in your code except that one place that you’ve set it equal to five, and that although you initialized b as zero but then all you do with it is add two to a and stored it in b. The compiler is going to see through all your foolery, and will compile the equivalent of the following code:

printf("%d\n", 7);

That’s optimization! It got rid of two variables that never really varied anyway, and freed up two bytes of RAM. But the takehome lesson for us here is that we’ll have to watch out for cases when we give the compiler the false impression that nothing’s changing when it actually is. And that’s the essential use of volatile.

And before we leave optimization, the “opposite” of volatile is not static as one might linguistically expect, but rather const which tells the compiler not only that the variable shouldn’t change, but also to throw a compiler error if you try to assign to it anywhere else in your code. Indeed, if you want to define a file-scoped, persistant variable that can change without warning (and this is common), you’d want static volatile.

Delay Loops

Maybe the first time a beginner would want to use volatile is in writing a delay routine. The simplest delay routine just has the CPU busy itself with counting up to some really big number. That is, you’ll try something like this:

int i;
for (i=0; i<65000; ++i){
        ; // just wait
}

and expect it to slow the computer down, but it might not.

If you have optimization off, or if it’s set to a low-enough level, it’ll actually work. But with any real optimization on, the compiler will notice that you never actually use the variable i for anything, and it’ll optimize your code to the following:


That won’t serve very well as a delay function.

The solution is to declare i as volatile. The compiler now pretends that it doesn’t know when i can change, or when it’s going to be used by other functions, and it runs through the full for loop dutifully, just in case.

volatile int i;
for (i=0; i<65000; ++i){
        ; // just wait, because something _could_ happen to i here
}

(Note that this code works just as well to demonstrate the volatile qualifier on your desktop, but you won’t notice the delay unless you count up to a very large number. We tried out 65,000,000 instead, and it pauses for a tick on our desktop machine.)

That was the trivial example.  The next two are real bugs that catch real-life beginning embedded programmers all the time.

Interrupt Service Routines (ISRs)

ISRs are by their nature invoked only outside of the normal program flow, and this naturally confuses the compiler. Indeed, ISRs look to the compiler like functions that are never called, and in AVR-GCC need to be marked with the special “used” attribute so that they don’t get thrown away entirely. So you can guess that things get ugly when you want to modify or use a variable from within a function that the compiler doesn’t know is even going to be used.

The good news is that you simply declare the variable as a volatile global variable (outside of the scope of main and the ISR in question) and everything works. The solution is super simple, but if you don’t do it, no errors will be thrown and your ISR simply will appear not to be working because the compiler will replace the variable with a constant.

Here’s what the right way looks like in a simple example:

uint16_t counter=0;

ISR(timer_interrupt_vector){
        ++counter;
}

int main(void){
        printf("\n", counter);
}

If counter weren’t marked volatile, you can see how it looks like a constant within the context of main, right? Now you won’t fall for this common beginner pitfall.

Memory-mapped Hardware Registers

The second non-trivial use of volatile in embedded programming is in the declarations of memory-mapped hardware; a microcontroller interacts with the world by reading and writing voltages on its various pins, and the pin states are usually made available to your code as specially mapped locations in memory. When the pins are configured as input, for instance, your code can read a bit from a specific memory location to test if the pin is at a logic high or low voltage level. So far, so good.

But while you see pins on which the external voltages can change without notice, the compiler sees normal-looking memory locations, unless it’s taken care of in your code. In general, this is taken care of by the manufacturer’s libraries, so you’ve been able to pretend that nothing’s afoot. But as long as we’re looking into the volatile qualifier, let’s dig quickly into the chip-specific codebases.

For instance, in the pin definition files that get included with any AVR project, you can dig through a chain of definitions to get down to a volatile definition for the PIN registers which are used for reading input:

#define PINB    _SFR_IO8 (0x03)
// and
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
// and finally
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

Phswew! That is, PINB turns into a volatile pointer to an eight-bit integer, where the specified memory address is built up as an IO address plus an offset. But the point here is that this pointer to this memory location is explicitly declared as volatile and that the compiler knows that it’s changeable.

The same goes for ARM chips. In the ARM-standard CMSIS port definitions, the GPIO memory locations are contained inside a bigger struct, GPIO_TypeDef, that takes care of the relative memory offsets, but if you look into those type definitions (here from ST’s implementation), you find:

// in stm32f4xx.h
 __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
// and in core_cm7.h
/* IO definitions (access restrictions to peripheral registers) */
#define     __IO    volatile             /*!< Defines 'read / write' permissions              */

Tadaa! As promised. And it really had to be so, because otherwise the compiler would look at every instance where the data registers are called and notice that they never change.

Wrapup

So that’s it. volatile simply convinces the compiler not to optimize over a variable that’s apparently constant. But until you’ve been bitten by the resulting bugs, you might not have even known just exactly what types of things the compiler is able to optimize away, and thus which variables you need to flag as volatile. We’ve hit the most common pitfalls, but if we’ve missed any situations where you’d use volatile, please let us know in the comments below.


Filed under: Hackaday Columns, Microcontrollers

No comments:

Post a Comment