The volatile Keyword

What volatile actually tells the compiler, why register and ISR-shared variables need it, and what it does not do.

An optimising compiler assumes that a variable only changes when your code changes it. For hardware registers and variables shared with interrupts, that assumption is false — the value changes behind the compiler's back. volatile tells the compiler: every read and every write in the source must really happen, in that order.

Seeing the bug in the compiler's output

Without volatile the compiler legally hoists the read out of the loop: as far as C is concerned nothing in the loop can change the value. The program then hangs the first time the byte has not arrived yet.

wait_plain:                     @ no volatile
    movw  r0, #17408           @ r0 = 0x40004400 (USART2_SR)
    movt  r0, #16384
    ldrb  r0, [r0]             @ read the register ONCE
    lsls  r0, r0, #26          @ test bit 5
    it    mi
    popmi {r7, pc}             @ already set: return
.LBB0_1:
    b     .LBB0_1              @ otherwise spin forever, never reading again

wait_volatile:                  @ with volatile
.LBB1_2:
    ldr   r1, [r0]             @ re-read the register on every pass
    lsls  r1, r1, #26
    bmi   .LBB1_4              @ bit set: leave the loop
    ...                        @ (loop unrolled, every copy re-reads)
    bpl   .LBB1_1

What volatile does NOT do

bool data_ready; // set in the UART ISR, polled in main volatile bool data_ready;

Without volatile, main's loop may read the flag once into a register and never see the ISR's update.

volatile uint32_t count; ... count++; // in main and in an ISR

Disable interrupts around the update, or keep separate counters per context volatile makes each access real but does not make the read-add-write sequence indivisible.

Is const volatile a contradiction?

No. const means this code may not write it; volatile means it may still change by other means. A hardware status or ID register is exactly that.

Does volatile make i++ atomic?

No. It guarantees the load and store happen, not that nothing runs between them. Use a critical section or an atomic instruction.

Name three kinds of objects that need volatile in bare-metal code.

Memory-mapped registers, variables shared with ISRs, and variables changed by DMA or another bus master.