Pointer Casting to Absolute Addresses in Embedded C
In embedded systems programming, casting integer literals directly to
pointer types is a standard practice used to access specific physical
hardware registers. This article explains why this technique is
ubiquitous in embedded C, exploring how microcontrollers utilize
memory-mapped input/output (MMIO), the absence of virtual memory layers,
the vital role of the volatile qualifier, and how direct
address casting enables zero-overhead peripheral control.
Memory-Mapped I/O (MMIO) Architecture
Microcontrollers (such as ARM Cortex-M, AVR, or RISC-V) typically use memory-mapped input/output. In this architecture, hardware peripherals—such as timers, GPIO ports, ADCs, and communication interfaces (UART, SPI, I2C)—are mapped directly into the processor's unified memory address space.
Hardware engineers assign fixed physical addresses to the
configuration and data registers of these peripherals, which are
detailed in the microcontroller's datasheet and reference manual.
Because these registers share the same address bus as system RAM and
Flash memory, the CPU communicates with hardware using the same read and
write instructions (LDR and STR in ARM
assembly) that it uses for standard variables. Casting an absolute
integer to a pointer allows the C compiler to emit instructions
targeting these precise hardware addresses.
Absence of Virtual Memory and Abstraction Layers
In desktop or server application development, an Operating System (OS) and a Memory Management Unit (MMU) isolate processes through virtual memory. A raw pointer address in user space does not correspond to a physical address, making arbitrary pointer casting dangerous and often illegal (causing segmentation faults).
Embedded systems, by contrast, frequently run "bare-metal" without an operating system or with a lightweight Real-Time Operating System (RTOS) that lacks an MMU. The processor operates directly in the physical address space. Because there is no OS driver API to mediate hardware interaction, the firmware developer is responsible for directly manipulating hardware state at the physical address level.
The Mechanism of Direct Memory Casting
To access a register, an integer representing the physical address is cast to a pointer of appropriate width (e.g., 8-bit, 16-bit, or 32-bit), and then dereferenced.
#define GPIOA_ODR ((volatile uint32_t *)0x40020014)
void set_pin_high(void) {
*GPIOA_ODR |= (1 << 5); // Sets pin 5 high
}In this pattern:
0x40020014is the absolute hardware address defined by the silicon vendor.(volatile uint32_t *)tells the compiler that the value at this memory location is an unsigned 32-bit word.- The
volatilequalifier is mandatory. It informs the compiler's optimizer that the value of this memory location can change outside the compiler's control (e.g., by hardware state changes) and that writes must not be optimized away or reordered. Withoutvolatile, an optimizing compiler might cache the read in a CPU register or omit sequential writes.
Struct Overlays for Peripheral Blocks
While casting individual addresses is common, firmware architectures frequently group contiguous peripheral registers into C structures. Developers cast a base memory address to a pointer to that structure type:
typedef struct {
volatile uint32_t MODER; // Offset 0x00
volatile uint32_t OTYPER; // Offset 0x04
volatile uint32_t OSPEEDR; // Offset 0x08
volatile uint32_t PUPDR; // Offset 0x0C
volatile uint32_t IDR; // Offset 0x10
volatile uint32_t ODR; // Offset 0x14
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef *)0x40020000)
void configure_gpio(void) {
GPIOA->MODER |= (1 << 10);
GPIOA->ODR |= (1 << 5);
}This structural mapping relies on standard C struct layout rules (with padding to match hardware alignment), maintaining readable code while generating optimal machine code that references registers via base-plus-offset addressing modes.
Performance and Zero Overhead
Embedded systems often operate with strict constraints regarding execution time, power consumption, and code size. Abstracting hardware access through function calls or software drivers introduces execution overhead and increases stack usage. Casting to absolute pointer addresses compiles down to the absolute minimum machine instructions required to interact with hardware, providing deterministic execution speed and immediate response times critical for low-level device drivers and interrupt service routines.