In this short lecture, we will talk about the qualifier volatile.
A compiler can translate a C program into a binary executable.
During the translation, the compiler can make aggressive performance optimization.
For example, compiler can cache a variable in a register to reduce the number of memory
accesses.
Compiler can also reorder computations, or eliminate useless and redundant computations.
However, sometimes things don't go as planned.
Optimization can break a program that was working well.
Let me show you a short demo.
In this C program, the SysTick interrupt handler only performs two simple tasks:
first, set the variable flag to 1,
and then turn on the green LED.
The main() function first initializes GPIO and SysTick interrupt,
then waits until the flag is set,
and finally turns on the red LED.
Let's first use the default compiler setting,
which applies the minimal amount of optimization.
Compile and build the program,
download the program,
click the reset button to run it.
As you can see, both the red and green LEDs light up.
The program works exactly as we expected.
However, let's see what happens if we let the compiler performs some optimization.
There are three levels of optimization: level 1 - restricted optimization, level 2 - high
optimization, and level 3 - maximum optimization.
Let's choose level 3.
Compile the program,
download it,
click reset and run.
Surprisingly, only the green LED lights up.
The program has failed to turn on the red LED.
Let's find out what was going on.
Here is the source code.
The variable flag is defined as global,
and it is initialized to 0.
The SysTick interrupt handler sets the flag to 1, and turns on the green LED.
After GPIO and SysTick initialization, the main() function waits until the flag is set,
and then turns on the red LED.
Under the default compiler setting, the program works exactly as we expected.
Here is one example translation of this while() loop.
The assembly code repeatedly reads the value of the variable flag from the memory, and
exits the while loop if the flag is not zero any more.
However, if we increase the level of compiler optimization, the program will be broken,
and fail to turn on the red LED.
Here is an example assembly translation with compiler optimization.
The compiler assumes that the flag value does not change, because the C program doesn't
seem to have changed it.
Pay attention, the compiler has no knowledge that the SysTick interrupt handler will be executed.
The compiler treats interrupt handlers as normal functions that are never called.
Therefore, the assembly code assumes that the flag value does not change.
It only reads the flag value once from the memory, and stores it into the register r1,
and then repeatedly use the value of register r1 without reading the memory again.
The flag variable was initialized to zero, and thus the load instruction loads 0 into
register r1.
Accordingly, the BEQ branch instruction is repeatedly taken, and it essentially becomes
a dead loop.
Although the interrupt handler has successfully updated the flag value in the memory and turned
on the green LED, the main() function is blocked by the dead loop.
As result, the red LED remains off.
How to prevent the compiler from making such mistakes?
The solution is very simple.
Declare the flag variable as volatile.
When a variable or a memory location is marked as volatile, the C compiler ensures that the
compiled program will always read or write to the memory location every time it is referenced
in the C code.
As shown in the assembly program, the flag variable is always read from the memory in
the loop.
Let me give you another example.
Suppose the variable x is declared as volatile.
In this C statement, we add x four times.
The volatile qualifier tells the compiler that the value of x may change at any time.
Accordingly, the compiler should not apply any optimization to remove reads or writes
to the variable x.
Therefore, the compiler should not optimize the original C statement as: y equals to 4*x.
In embedded systems, a memory value may be changed anytime by hardware, such as the input
data register, as well as the status register of various peripherals.
A memory value may also be changed anytime by software, such as interrupt handlers, or
another thread if the processor runs with operating systems or real-time operating systems (RTOS).
The qualifier volatile is designed for those variables that may be changed anytime by hardware
or software that runs concurrently.
In embedded systems, there are three common usages of volatile.
First, we should use volatile on accessing memory-mapped I/O registers.
For example, we should declare the input date register of GPIO as a volatile unsigned integer.
As external digital input signal may change at any time, the volatile qualifier ensures
that software always read the latest value of the input data register (IDR), whenever
IDR is referenced in the source code.
When an interrupt handler or a thread reads or writes to a global variable, this global
variable should be declared as volatile.
For example, the SysTick handler modifies the global variable counter.
As result, the counter should be declared as volatile.
In summary, volatile forces the compiler to generate binary executable that performs actual
memory reads and writes, instead of caching values in registers.
Thank you for watching this lecture.
Please visit the book website for more examples.
See you next time.