Embedded Systems And The Volatile Keyword
Many C/C++ software engineers could go their entire life without ever needing to use the volatile
keyword. However in embedded systems, knowing how the volatile
keyword works and how to use it is a vital skill.
The volatile
keyword is a qualifier you apply to a variable, e.g. volatile uint8_t my_var;
. It tells the compiler that the value stored at this address may change at any time, even if the compiler cannot see a code path that would cause it to change. This forces the compiler to perform a read/write from memory every time the variable is used in the code, rather than the compiler storing the variable locally in a register or optimizing the read/write out completely!
You can place the keyword volatile
before or after the type when declaring a variable (const
behaves the same way). The basic syntax for defining a volatile
variable is:
In the embedded world, it is common to want to use pointers that point to volatile memory, i.e. a pointer to an 8-bit memory mapped register for controlling a UART. Pointers to volatile data are defined with:
volatile
is used for:
- Variables which are read/written to from interrupts and also accessed outside the interrupt. If you forgot the
volatile
the compiler cannot set the code path that calls the interrupt (because interrupts are called asynchronously) and thus may assume nothing else touches the variable and cache the value in a register (or optimize out the read/write completely). - Memory addresses which point to hardware registers in microcontrollers (memory mapped registers). These hardware registers are liable to be changed at any time by the hardware (e.g. a bit in a UART peripheral register which indicates when there are received bytes available for reading), so the compiler cannot assume it’s only changed when the code writes to it.
What volatile
should not be used for:
- Guaranteeing thread safe access of a shared variable between two threads.
volatile
provides some, but not all of the guarantees required for thread safe access. Other threading objects such as memory barriers, mutexes e.t.c are required, but these also provide the guarantees thatvolatile
does, sovolatile
is not needed in this use case.
The Use Of volatile With Buffers
It is also common to write incoming data to buffers (e.g. ring buffers) from interrupts in embedded firmware, and then read the data back out of the buffer outside of the interrupt in the main application routine (or flip the read/write for outgoing data). volatile
can be used when declaring the array of bytes in the buffer.
For example, to declare a 100-byte buffer which contains volatile data:
This forces the compiler to generate code in the main app routine that goes out and reads the values in the buffer. If you did not use volatile
, the compiler may infer that because the interrupt function that writes to the buffer never gets called (remember: the compiler can’t see the interrupt being called), so it never has to read the values from the buffer because it will always stay as the initialized values. This can result in you reading garbage data from your buffer!
Below is an example in C++ of a volatile buffer than is written to from an ISR (interrupt service routine) and read from a main application thread (outside of the interrupt). Because this is on a web-based system, I didn’t have access to true interrupts, so I simulated one with a separate thread instead. If you remove the volatile
keyword, you should get undefined behaviour. In the case when I ran it, I got the same output as when declared volatile
, which I think is because I couldn’t get the compiler to optimize out the read.
(run this code at https://replit.com/@gbmhunter/volatile-buffer-experiment)
memcpy() And volatile
memcpy()
cannot operate on volatile
data. This can be an annoyance if you are trying to copy data to or from a buffer (array of bytes, i.e. char *
) which is declared volatile
. However, you can easily make your own version of memcpy()
which does work with volatile
. Below is a example of a memcpy()
equivalent that works with a destination buffer that is volatile
:
Casting Away volatile
In C++, you can use const_cast
to cast away volatile
(confusingly, it’s called const_cast
, but it actually casts away both const
and volatile
).
C++ Member Functions Declared volatile
TODO: Add info here.