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
constto protect read-only data and interfaces; - use
volatilefor memory-mapped registers and ISR-shared objects; - explain what
volatiledoes 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.
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
volatileto "fix" timing bugs. - Forgetting
volatileon a hardware register definition. - Assuming
constmeans the value is stored in Flash on every target. - Using
volatileinstead of atomic operations or critical sections. - Casting away
constto modify read-only data. - Applying
volatileto 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
- cppreference: const Type Qualifier
- cppreference: volatile Type Qualifier
- SEI CERT C: DCL and CON Rules