I am struggling with the correct use of the volatile
keyword when implementing a circular buffer. The buffer is written in an ISR and read in the main program. I am running a bare-metal microcontroller, so no multithreading.
My current implementation looks something like this:
volatile char circ_buf[n] = {0};
volatile size_t head_index = 0;
volatile size_t tail_index = 0;
void buffer_input(const char c)
{
if((head_index+ 1) % n != tail_index) {
//Only if buffer is not full
circ_buf[head_index] = c;
head_index = (head_index + 1) % n;
}
}
char buffer_read(void)
{
if (tail_index != head_index)
{
char c = circ_buf[tail_index];
tail_index = (tail_index + 1) % n;
return c;
}
//Lets assume 0 will never be content of the buffer for this example
return 0;
}
buffer_input is called from an ISR, buffer_read from the main program.
This implementation works, but I have read somewhere that making the buffer array itself volatile is not needed, because the content is only accessed via the volatile indices. Is that true? The content is accessed both in the interrupt and main program, so in my understanding the buffer should be volatile too?
This implementation works, but I have read somewhere that making the buffer array itself volatile is not needed, because the content is only accessed via the volatile indices. Is that true?
No, because if circ_buf
is not qualified with volatile
, then the C standard does not require a program to reread an element of the buffer when that element is used for a second or subsequent time, regardless of whether the indices are volatile or not.
On the other hand, yes, but dangerously so. The reason for the rules around volatile
is to require a C implementation to reread things that may change (and similarly to write things that must be made visible to the hardware, but that is not a concern when we are only considering the buffer_read
routine) and not perform optimizations that can omit the rereads. We can reason that, due to the way the buffer is used and how many elements it contains (if it is sufficiently large), there are no reasonable optimizations a compiler could make that would allow it to cache buffer elements. This is a hazardous way to proceed and would not be used in normal software engineering, but I include it to make the answer complete.
If x
is volatile, then int t0 = x, t1 = x;
is required to read x
twice. If x
were not volatile, a compiler would easily optimize this to read x
once. But given how buffer_read
steps through the buffer, it is not feasible for the compiler to read, say, buffer[0]
once and hold onto a copy to be reused after the reads step through the entire buffer and return to the beginning. The compiler must reread buffer[0]
because it does not have a practical way to avoid it. So, in practice, if you omit volatile
from circ_buf
, you will not see the program malfunction from this.
Nonetheless, it is correct for to have volatile
. Without it, the meaning of the C source code as specified by the C standard is not the desired meaning for the program, that the latest value of circ_buf[tail_index]
as updated by the ISR be read from memory.