Using volatile correctly (C)
volatile tells the compiler that an object can change outside the visible flow of the
program, so every read and write in the source must become a real memory access — none may
be cached in a register, reordered against another volatile access, or optimised away.
Three cases genuinely need it: memory-mapped peripheral registers, objects shared with an
interrupt handler, and variables touched across setjmp/longjmp or a signal handler.
#include <stdint.h>
#include <stdbool.h>
/* Memory-mapped status and data registers. volatile forces a bus access on
every read, so the poll below re-reads the hardware instead of spinning on a
value the compiler hoisted into a register. */
#define UART_SR (*(volatile uint32_t *) 0x40013800u)
#define UART_DR (*(volatile uint32_t *) 0x40013804u)
#define UART_RXNE (1u << 5)
/*******************************************************************************
* Function Name : uartReadByte
* Description : Block until the UART has a byte, then return it.
* Input : None.
* Output : None.
* Return : The received byte.
*******************************************************************************/
uint8_t uartReadByte(void)
{
while ((UART_SR & UART_RXNE) == 0u)
{
/* re-reads UART_SR each iteration */
}
return (uint8_t) UART_DR;
}
/* Set in interrupt context, consumed by the main loop. Without volatile the
compiler may lift the load out of the wait loop and never observe the store
made by the ISR. */
static volatile bool flagTick;
/*******************************************************************************
* Function Name : sysTickHandler
* Description : SysTick interrupt; flags a tick for the main loop.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void sysTickHandler(void)
{
flagTick = true;
}
/*******************************************************************************
* Function Name : waitForTick
* Description : Block until the SysTick ISR signals the next tick.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void waitForTick(void)
{
while (!flagTick)
{
/* wait for the next tick */
}
flagTick = false;
}
What volatile does not guarantee
It is a common mistake to reach for volatile to make code thread- or ISR-safe. It only
orders accesses to a single object and only with respect to other volatile accesses; it
is neither atomic nor a barrier across different objects. The compiler — and on a
weakly-ordered core, the hardware — may still make the flag visible before the payload it
advertises has landed.
static volatile bool flagReady;
static uint8_t ring[64];
/*******************************************************************************
* Function Name : publish
* Description : Copy a payload, then raise the ready flag after a barrier.
* Input : src - source bytes; len - number of bytes.
* Output : None.
* Return : None.
*******************************************************************************/
void publish(const uint8_t *src, uint32_t len)
{
for (uint32_t i = 0; i < len; i++)
{
ring[i] = src[i];
}
/* make the payload visible before the flag */
__asm volatile("dmb ish" ::: "memory");
flagReady = true;
}
Without the barrier a consumer can read flagReady == true while ring[] still holds
stale bytes. volatile keeps each individual access honest; ordering between the data
and the flag needs an explicit barrier — or, for a read-modify-write, a critical section or
a real atomic. See volatile & memory barriers for the
full picture.