Arduino programming guide series
Understanding the volatile modifier
Variables can be modified at different parts of a program. Often, it is not possible to predict when a particular variable may be accessed for a read or write operation. This is particularly true when a variable is tied to an interrupt service routine. So, how do we ensure that a variable always contains a correct value? This is where the volatile modifier comes in.
A volatile variable is a variable that stores a value that may change at any time from parts of the program that are not related to each other. A volatile variable can be declared in a part of the program, and then be modified out of context.
As such, when the compiler encounters a variable marked as volatile, it does not "optimize" it. It will maintain a single copy of this value in RAM (as opposed to RAM plus CPU a register for faster access).
This way, the programmer sacrifices performance in return for the simplicity of keeping a single copy of a variable, referenced from any part of the program.
Confused?
Let's drill into this.
Volatile variables are tricky to understand because you first need to have a good understanding of how a CPU works, and how a compiler generates the code that is executed inside the CPU.
In a nutshell, the CPU has a component called “ALU”, short for Arithmetic Logic Unit. It is where arithmetic calculations take place. The CPU knows what to do by looking at an instruction.
An instruction is made up of two things: an instruction code, and the arguments.
For example:
MOV AL, 61h
...is composed of the instruction code "MOV" and arguments "AL" and “61h”. It results in the value “61” (in hexadecimal) being loaded (moved) into register “AL”. Arguments can be fixed values or memory locations, or other registers.
These instructions are stored in memory. There are three types of memory that a full CPU has direct access to:
- Random Access Memory (RAM).
- Cache.
- Registers.
RAM is plentiful and cheap, though relatively slow.
Registers are few, very expensive, but very, very fast.
And cache is in the middle: less plentiful compared to RAM, more plentiful compared to register. Much faster than RAM, much slower than registers.
The Atmega328p, for example, has SRAM (static RAM, one of several types of RAM), a few registers, but no cache.
Larger CPUs have all three.
A good compiler will try to generate code that is optimized. The optimized code makes efficient use of the available resources, RAM, cache and registers, and fetch-decode-execute cycles.
In applications where there is only one thing happening at a time, like in our Arduino code that does not use interrupts, variable data can be optimized for speed. The compiler will generate code that stores variable data in high-speed registers in addition to the original copy the RAM.
Because the variables are only updated by code in a single thread of execution (such as, in the loop and other functions in the same thread) there is no question as to the validity of a value stored in the register. The value stored in the register is always correct and up-to-date.
On the Arduino, the code can be executed outside the normal path in the form of the interrupt service routine (also known as "interrupt handler").
You can write interrupt routines to execute based on timer events, or external triggers (like a button being pressed).
If the normal path and the interrupt path access the same variable, then it is possible that this variable can be accessed from both places, at any time. This means that our program can never trust that the value it is reading is correct. Perhaps it has been changed by the interrupt routine?
Consider the following example, and imagine that there is an interrupt service routine that operates on the variable at any time (download from Github):
What is the value of “b” in the serial monitor?
We can’t be sure. It can be either “2” or “7”.
In the loop, we set “a” to “1”. If the interrupt executes before the calculation for “b” happens in the loop, then the value for “a” will be 5 instead of 1. But even if we know that the interrupt has executed is not enough to know what the result of this calculation will be.
We also need to consider that the value for “a” may be stored at different places: RAM, cache (though note the ATMega328p has no cache), or registers.
Which one has the latest correct value? Should we use the copy from the RAM or the copy in the internal register?
I hope this shows that the problem here is uncertainty.
As the designers of a program, we want to remove such uncertainties. We want to write programs that are predictable and deterministic.
The C/C++ language offers the “volatile” modifier for this specific purpose. We can use this modifier to mark a variable as one that can change at any time across different memory accesses, and accross parts of the program that operate at different contexts.
By marking a variable as volatile, we instruct the compiler to not optimize for reads and writes for the variable.
The compiler will ensure that the variable value is always read from RAM.
Without the volatile modifier, the variable value will be copied from the RAM to a local CPU register the first time it is referenced. After that, it will be read from the CPU local register. Only if this variable copy in the register has been replaced by another variable will the CPU re-read it from the RAM.
With all this in mind, let’s update the example code with volatile (download from Github):
The only difference between the first and second sketches is that the "a" variable is now marked as "volatile".
That's it.
During execution, the value for this variable will always be fetched from RAM, ensuring that it is current and not stale.
New to the Arduino?
Arduino Step by Step Getting Started is our most popular course for beginners.
This course is packed with high-quality video, mini-projects, and everything you need to learn Arduino from the ground up. We'll help you get started and at every step with top-notch instruction and our super-helpful course discussion space.
Jump to another article
3. Focus on the type parameter in "println()"
4. "0" or "A0" when used with analogRead()?
5. What is the "_t" in "uint8_t"?
6. Save SRAM with the F() macro
7. What is the gibberish in the Telnet output?
9. Confusing keywords? follow the source code trail
10. The interrupt service routine and volatile variables
11. The problem with delay() and how to fix it
12. How to deal with the millis rollover
13. Can you use delay() inside Interrupt Service Routine?
15. A closer look at line feeds and carriage returns
16. Understanding references and pointers
17. Simple multitasking on the Arduino
19. Concurrency with the Scheduler library on the Arduino Due and Zero
20. Bitshift and bitwise OR operators
21. What is a "static" variable and how to use it
22. Understanding the volatile modifier
Done with the basics? Looking for more advanced topics?
Arduino Step by Step Getting Serious is our comprehensive Arduino course for people ready to go to the next level.
Learn about Wi-Fi, BLE and radio, motors (servo, DC and stepper motors with various controllers), LCD, OLED and TFT screens with buttons and touch interfaces, control large loads like relays and lights, and much much MUCH more.
We publish fresh content each week. Read how-to's on Arduino, ESP32, KiCad, Node-RED, drones and more. Listen to interviews. Learn about new tech with our comprehensive reviews. Get discount offers for our courses and books. Interact with our community. One email per week, no spam; unsubscribe at any time