Occasionally, you might encounter problems when compiling code at the higher optimization levels -O2 and -O3. For example, you might get stuck in a loop when polling hardware, or multi-threaded code might exhibit strange behavior. In such cases it is likely that you need to declare some of your variables as volatile.
Declaring a variable as volatile tells the compiler that the variable can be modified at any time externally to the implementation, for example, by the operating system or by hardware. Because the value of a volatile-qualified variable can change at any time, the physical address of the variable in memory must always be accessed whenever the variable is referenced in code. This means the compiler cannot perform optimizations on the variable, for example, caching it in a local register to avoid memory accesses.
In contrast, when a variable is not declared as volatile, the compiler can assume its value cannot be modified outside the implementation. Therefore the compiler can perform optimizations on the variable.
The use of the volatile keyword is illustrated in the two sample routines of Table 4.5, both of which loop reading a buffer until a status flag buffer_full is set to true. Both routines assume that the state of buffer_full can change asynchronously with program flow.
The first routine shows a naive implementation of the loop. Notice that the variable buffer_full is not qualified as volatile in this implementation. In contrast, the second routine shows the same loop where buffer_full is correctly qualified as volatile in the implementation.
Table 4.5. C code for nonvolatile and volatile buffer loops
| Nonvolatile version of buffer loop | Volatile version of buffer loop |
|---|
int buffer_full;
int read_stream(void)
{
int count = 0;
while (!buffer_full)
{
count++;
}
return count;
}
|
volatile int buffer_full;
int read_stream(void)
{
int count = 0;
while (!buffer_full)
{
count++;
}
return count;
}
|
Table 4.6 shows the corresponding disassembly of the machine code produced by the compiler for each of the sample implementations of Table 4.1, where the C code for each implementation has been compiled using the option -O2.
Table 4.6. Disassembly for nonvolatile and volatile buffer loop
| Nonvolatile version of buffer loop | Volatile version of buffer loop |
|---|
read_stream PROC
LDR r1, |L1.28|
MOV r0, #0
LDR r1, [r1, #0]
|L1.12|
CMP r1, #0
ADDEQ r0, r0, #1
BEQ |L1.12| ; infinite loop
BX lr
ENDP
|L1.28|
DCD ||.data||
AREA ||.data||, DATA, ALIGN=2
buffer_full
DCD 0x00000000
|
read_stream PROC
LDR r1, |L1.28|
MOV r0, #0
|L1.8|
LDR r2, [r1, #0]; ; buffer_full
CMP r2, #0
ADDEQ r0, r0, #1
BEQ |L1.8|
BX lr
ENDP
|L1.28|
DCD ||.data||
AREA ||.data||, DATA, ALIGN=2
buffer_full
DCD 0x00000000
|
In the disassembly of the nonvolatile version of the buffer loop in Table 4.6, the statement LDR r0, [r0, #0] loads the value of buffer_full into register r0 outside the loop labeled |L1.8|. Because buffer_full is not declared as volatile, the compiler assumes that its value cannot be modified outside the program. Having already read the value of buffer_full into r0, the compiler omits reloading the variable when optimizations are enabled, because its value cannot change. The result is the infinite loop labeled |L1.8|.
In contrast, in the disassembly of the volatile version of the buffer loop, the compiler assumes the value of buffer_full can change outside the program and performs no optimizations. Consequently, the value of buffer_full is loaded into register r0 inside the loop labeled |L1.4|. As a result, the loop |L1.4| is implemented correctly in assembly code.
To avoid optimization problems caused by changes to program state external to the implementation, you must declare variables as volatile whenever their values can change unexpectedly in ways unknown to the implementation. In practice, you must declare a variable as volatile whenever you are:
accessing memory mapped peripherals
sharing global variables between multiple threads
accessing global variables in an interrupt routine.