HomeAbout UsContact Us

Understanding const and volatile Pointer Types in Embedded C

By Jithin Tom
Published in Embedded C/C++
June 29, 2026
3 min read
Understanding const and volatile Pointer Types in Embedded C

Table Of Contents

01
The Four Combinations
02
Reading Declarations Right-to-Left
03
Why This Matters in Embedded Systems
04
Common Mistakes
05
A Visual Summary
06
Practical Pattern: Peripheral Descriptor
07
Summary
08
Related Reading
09
References
10
Frequently Asked Questions

In embedded C, pointer declarations with const and volatile qualifiers are among the most misunderstood constructs. Misreading a single qualifier can lead to code that silently corrupts hardware state, elides critical reads, or triggers undefined behavior. This article breaks down the four canonical combinations and shows exactly when each one belongs in firmware.

The Four Combinations

Understanding the effect of const starts with seeing how its position relative to * changes the meaning. Here are the four fundamental patterns:

int *ptr_a; /* pointer to int */
const int *ptr_b; /* pointer to const int */
int *const ptr_c; /* const pointer to int */
const int *const ptr_d; /* const pointer to const int */

The rule is simple: const applies to whatever is immediately to its left. If there is nothing to its left (it appears first), it applies to the base type. The * is the boundary — qualifiers left of it apply to the pointee, qualifiers right of it apply to the pointer itself.

Reading Declarations Right-to-Left

For pointer declarations involving const and volatile, a reliable technique is to read starting from the identifier, going strictly right-to-left:

const int *const p
p -> p is a
const -> constant
* -> pointer to a
const int -> constant integer

So p is a constant pointer to a constant integer. Neither the address nor the value can change after initialization.

Why This Matters in Embedded Systems

Hardware peripherals expose memory-mapped registers at fixed addresses. A status register that updates on every ADC conversion must be read fresh each time — the compiler must not cache its value. A transmit-data register must be written with the exact value the firmware intends — the compiler must not optimize away a “redundant” write.

/* Status register: hardware updates it, firmware reads it */
volatile const uint32_t * const STATUS = (volatile const uint32_t *)0x40001000;
/* Data register: firmware writes it, hardware consumes it */
volatile uint32_t * const DATA = (volatile uint32_t *)0x40001004;

Here STATUS is a constant pointer to a volatile constant uint32_t. The address is fixed (it is a hardware register), the data is volatile (hardware changes it asynchronously), and it is also const from the firmware’s perspective (we only read it). DATA is a constant pointer to a volatile uint32_t — the address is fixed, and the data is volatile to ensure the compiler emits every write (otherwise, the compiler could optimize away a “redundant” store to the same address).

Common Mistakes

Mistake 1: Dropping volatile on a status register

/* WRONG: compiler may read STATUS once and reuse the value */
const uint32_t * const STATUS = (const uint32_t *)0x40001000;
/* RIGHT: every read hits the actual register */
volatile const uint32_t * const STATUS = (volatile const uint32_t *)0x40001000;

Without volatile, the compiler sees no side effects from reading STATUS and may hoist the read out of a loop or eliminate it entirely.

Mistake 2: Confusing pointer-to-const with const-pointer

const int *ptr = &sensor_value;
*ptr = 42; /* COMPILE ERROR: cannot modify const data */
ptr = &other; /* OK: pointer itself is not const */

Many firmware bugs arise from assuming const int *ptr makes the pointer immutable. It does not — it makes the data immutable through that pointer.

Mistake 3: Casting away volatile

volatile uint32_t *hw_reg = (volatile uint32_t *)0x40002000;
uint32_t *alias = (uint32_t *)hw_reg; /* DANGEROUS: volatile discarded */

Casting away volatile may compile without errors, but it is dangerous. In fact, if the underlying memory is truly volatile (like a hardware register), accessing it through a non-volatile pointer invokes undefined behavior according to the C standard. The compiler will optimize reads from alias as if it were normal RAM, defeating the entire purpose of memory-mapped access.

A Visual Summary

+------------------------------------------------------------+
| Pointer Qualifier Map |
+------------------------------------------------------------+
| |
| const int *ptr --> data is read-only |
| int *const ptr --> pointer address is fixed |
| const int *const ptr --> both are fixed |
| volatile int *ptr --> data may change externally |
| int *volatile ptr --> pointer itself may change |
| (rare — e.g., ISR callbacks) |
| |
+------------------------------------------------------------+

Practical Pattern: Peripheral Descriptor

A clean and standard way to model a peripheral in embedded C is by mapping a struct of volatile members to a fixed memory address using a macro:

typedef struct {
volatile const uint32_t status; /* read-only, hardware-updated */
volatile uint32_t data; /* write-only, hardware-consumed */
volatile uint32_t control; /* read-write, firmware-configured */
} UartPeripheral;
/* UART0 acts as a constant pointer to a peripheral struct */
#define UART0 ((UartPeripheral *)0x40003000)

Here, the struct members appropriately use volatile (and const for read-only registers) to enforce correct memory access semantics.

Expert Tip: You might wonder why we don’t use #define UART0 ((UartPeripheral const)0x40003000) to explicitly make the macro a constant pointer. In the C standard, the result of a cast is an rvalue, which is inherently unassignable. Adding const to a cast result type is therefore redundant, and compilers like GCC will flag it with a -Wignored-qualifiers warning. The simple pointer cast shown above is the strictly correct, standard approach.*

Summary

  • const left of * → data is read-only through this pointer.
  • const right of * → pointer address is fixed after init.
  • volatile → compiler must access memory every time; never optimize away.
  • Hardware registers almost always need volatile. Read-only registers add const on the data side. Fixed addresses add const on the pointer side.
  • Never cast away volatile — it silently reintroduces the optimization bugs you were trying to prevent.
  • Understanding the volatile Keyword in Embedded C
  • Memory-Mapped IO and Peripheral Register Access in Embedded C
  • Type Punning and Strict Aliasing in Embedded C

References

  1. ISO/IEC 9899:2018, Section 6.7.3 — Type qualifiers. The authoritative specification of const, volatile, and restrict semantics.
  2. Barr Group, “How to Use C’s volatile Keyword,” https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword
  3. Jack Ganssle, “The Firmware Handbook” — Chapter on memory-mapped I/O and the proper use of volatile.
  4. Texas Instruments, “TMS320C28x Optimizing C/C++ Compiler User’s Guide” — Section on volatile and memory-mapped register access.
  5. ARM, “Cortex-M Technical Reference Manual” — Memory model and device memory attributes.

Frequently Asked Questions

What does 'const int *ptr' mean in C?

It declares a pointer to a constant integer — the data pointed to cannot be modified through the pointer, but the pointer itself can be reassigned to point elsewhere.

What does 'int *const ptr' mean in C?

It declares a constant pointer to an integer — the pointer address cannot change after initialization, but the data it points to can be modified.

Why is 'volatile' important for pointers to hardware registers?

volatile tells the compiler that the pointed-to value may change at any time (e.g., by hardware), preventing the compiler from optimizing away reads or caching stale values in registers.

What does 'volatile int *const ptr' mean?

It is a constant pointer to a volatile integer — the address is fixed (cannot be reassigned) and the compiler must read the value from memory every time it is accessed.

How do you read complex C pointer declarations with qualifiers?

Read strictly right-to-left starting from the variable name: 'const int *const p' reads as 'p is a constant pointer to an integer that is constant'.

Tags

embedded-cconstvolatilepointersqualifiers

Share


Previous Article
Unit Testing Embedded Firmware: A Practical Guide
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Stack Usage Analysis and Optimization in Embedded C
Stack Usage Analysis and Optimization in Embedded C
July 12, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media