Loading header...

const, volatile, and Optimization

const and volatile are small keywords with large consequences. They tell the compiler what your program is allowed to change and what the compiler must not assume remains stable. In embedded systems, using them incorrectly can turn a correct circuit into firmware that polls a stale flag forever or writes to memory that should never change.

Learning Objectives

By the end of this lesson, you should be able to:

  • use const to protect read-only data and interfaces;
  • use volatile for memory-mapped registers and ISR-shared objects;
  • explain what volatile does not guarantee;
  • recognize optimization bugs caused by missing qualifiers.

const

const means the object is not modified through that expression.

void uart_send(const uint8_t *data, uint32_t length);

This tells callers that uart_send should not change the bytes.

const is useful for:

  • lookup tables in Flash;
  • configuration objects;
  • function parameters that are read-only;
  • preventing accidental writes.

It also documents ownership:

typedef struct {
    uint32_t baud_rate;
    uint8_t parity;
} uart_config_t;

void uart_init(const uart_config_t *config);

The driver promises not to modify the caller's configuration object. This is useful in code review and lets the compiler catch accidental writes.

const does not always mean "stored in Flash". Placement depends on the target, compiler, linker script, and object lifetime. For large lookup tables, confirm the map file.

volatile

volatile means every access matters and must be performed as written.

#define STATUS_REG (*(volatile uint32_t *)0x40000010u)

while ((STATUS_REG & (1u << 0)) == 0u) {
    /* wait for hardware flag */
}

Without volatile, the compiler might assume STATUS_REG cannot change inside the loop and optimize the read incorrectly.

At optimization level -O2, a non-volatile polling loop can become a single read followed by an infinite branch. The hardware may set the flag, but the program never reads the address again.

Common Volatile Uses

  • memory-mapped peripheral registers;
  • variables written in an interrupt service routine and read in main code;
  • flags changed by DMA or hardware;
  • simple polling loops.

What volatile Does Not Do

volatile does not:

  • make multi-byte access atomic;
  • protect against races;
  • create a memory lock;
  • make code thread-safe;
  • replace interrupt masking or critical sections.
volatile uint32_t tick_count;

uint32_t now = tick_count; /* access is performed, but not necessarily atomic on every CPU */

If tick_count can be updated inside an interrupt while main code reads it, the access width and CPU bus matter. An 8-bit CPU reading a 32-bit counter may need a critical section:

uint32_t systick_get(void)
{
    uint32_t value;

    disable_interrupts();
    value = tick_count;
    enable_interrupts();

    return value;
}

On modern C toolchains, _Atomic or vendor-provided critical-section primitives may be more appropriate for shared state. Use the mechanism supported by the target and coding standard.

const volatile

Some registers are read-only to software but can change in hardware.

#define ADC_RESULT (*(const volatile uint16_t *)0x40012040u)

This says software should not write the object, and each read must still happen.

Common examples include ADC result registers, timer counter registers, and peripheral identification registers.

Pointer Qualification Patterns

The placement of const matters:

Declaration Meaning
const uint8_t *p pointer to read-only bytes
uint8_t * const p constant pointer to writable bytes
const uint8_t * const p constant pointer to read-only bytes
volatile uint32_t *reg pointer to volatile register

For a fixed register address, the macro usually expands to an lvalue rather than a separately stored pointer:

#define TIMER_COUNT (*(const volatile uint32_t *)0x40001024u)

Optimization and Intent

Compilers are allowed to optimize aggressively when the C abstract machine permits it. Qualifiers communicate intent, but the datasheet and concurrency model still matter.

flowchart LR C["C source"] --> OPT["Optimizer assumptions"] HW["Hardware side effects"] --> QUAL["volatile access"] QUAL --> OPT OPT --> BIN["Machine code"]

Optimization is not the enemy. It removes redundant work and can make firmware faster and smaller. The engineering task is to make the C source describe hardware side effects accurately enough that optimization remains legal and useful.

Worked Failure Example

Buggy code:

uint32_t *status = (uint32_t *)0x40000010u;

while ((*status & 1u) == 0u) {
}

The compiler sees no ordinary C write to *status and may reuse the first value. Correct code:

volatile uint32_t *status = (volatile uint32_t *)0x40000010u;

while ((*status & 1u) == 0u) {
}

Now every loop iteration performs a register read.

Common Mistakes

  • Marking every variable volatile to "fix" timing bugs.
  • Forgetting volatile on a hardware register definition.
  • Assuming const means the value is stored in Flash on every target.
  • Using volatile instead of atomic operations or critical sections.
  • Casting away const to modify read-only data.
  • Applying volatile to a pointer variable but not to the object being accessed.

Summary

Use const to express read-only intent. Use volatile when values can change outside the compiler's normal view or when hardware access has side effects. Neither keyword replaces a correct concurrency design, register access policy, or memory model.

Further Reading

Mind Map

mindmap root((const volatile)) const Read-only through expression Protects interfaces Config tables Check linker map for Flash volatile Every access matters Hardware registers ISR shared flags DMA changed memory Both Read-only hardware result ADC result register Timer count register Optimization Nonvolatile polls can cache Qualifiers express side effects Keep useful compiler optimizations Safety rules Volatile is not atomic Use critical sections Do not cast away const Match pointer qualification Common mistakes Volatile everywhere Missing register volatile Const means Flash assumption Race hidden by volatile