HomeAbout UsContact Us

Understanding const and volatile Pointer Types in Embedded C

By Jithin Tom
Published in Embedded C/C++
June 29, 2026
2 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

Every pointer-to-int declaration with these qualifiers falls into one of four patterns. The position of const relative to the * is what changes the meaning.

const int *ptr_a; /* pointer to const int */
int *const ptr_b; /* const pointer to int */
const int *const ptr_c; /* const pointer to const int */
volatile int *ptr_d; /* pointer to volatile 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

A reliable technique is to read the declaration starting from the identifier, going right first (if brackets exist), then left:

const int *const p
p -> p is a name
* -> ...a pointer to...
const int -> ...a constant integer
const -> ...that itself is constant

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), 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, the data is volatile (hardware reads it), and firmware writes to it.

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 is legal C but almost always wrong in embedded code. The compiler will now 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 --> both are fixed |
| volatile int *ptr --> data may change externally |
| int *volatile ptr --> pointer itself may change |
| (rare — e.g., jump tables) |
| |
+-------------------------------------------------------+

Practical Pattern: Peripheral Descriptor

A clean way to model a peripheral is with a struct of qualified pointers:

typedef struct {
volatile const uint32_t * const status; /* read-only, hardware-updated */
volatile uint32_t * const data; /* write-only, hardware-consumed */
volatile uint32_t * const control; /* read-write, firmware-configured */
} UartPeripheral;
static const UartPeripheral UART0 = {
.status = (volatile const uint32_t *)0x40003000,
.data = (volatile uint32_t *)0x40003004,
.control = (volatile uint32_t *)0x40003008,
};

Each member is a constant pointer (the address never changes) with the appropriate volatility and constness for its direction. The struct itself is const and lives in flash — zero RAM cost.

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, “C Qualifiers and Pointer Declarations in Embedded Systems,” 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 from the name outward, right then left: 'const int *const p' is a constant pointer (right of *) to a constant integer (left of *).

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

Volatile vs Memory Barriers — When Volatile Isn't Enough
Volatile vs Memory Barriers — When Volatile Isn't Enough
June 20, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media