Pointers and Memory Addresses
Pointers are unavoidable in embedded C because hardware itself is address-based. Peripheral registers live at addresses. Buffers live at addresses. Strings, arrays, vector tables, and DMA descriptors all rely on addresses.
The goal is not to fear pointers. The goal is to use them precisely.
Learning Objectives
By the end of this lesson, you should be able to:
- explain what a pointer stores;
- distinguish pointer value, pointed-to object, and object type;
- use pointers with arrays and registers;
- avoid common invalid-memory and aliasing mistakes.
What a Pointer Is
A pointer is a variable whose value is a memory address.
uint8_t value = 42;
uint8_t *ptr = &value;
Here:
valueis an 8-bit object;&valueis its address;ptrstores that address;*ptrmeans "the object located at that address."
Declaration Reading
Read from the variable name outward:
uint8_t *p->pis a pointer touint8_tconst uint8_t *p->ppoints to read-onlyuint8_tuint8_t *const p->pitself cannot be changed after initializationconst uint8_t *const p-> both the pointer and the pointed-to bytes are read-only throughp
That last form is common for fixed lookup tables or driver configuration objects that should not be reassigned or modified through the interface.
Pointers and Registers
Memory-mapped I/O often looks like:
#define GPIO_OUT (*(volatile uint32_t *)0x40020014u)
That expression says:
- treat address
0x40020014as a pointer tovolatile uint32_t; - dereference it;
- read or write the object at that address.
volatile matters because the register can change outside normal program flow and accesses must not be optimized away.
Prefer device header definitions or a documented register block when possible:
typedef struct {
volatile uint32_t MODER;
volatile uint32_t OTYPER;
volatile uint32_t OSPEEDR;
volatile uint32_t PUPDR;
volatile uint32_t IDR;
volatile uint32_t ODR;
} gpio_regs_t;
#define GPIOA ((gpio_regs_t *)0x40020000u)
The base address must come from the device reference manual, not from example code copied between unrelated MCUs.
Pointer Arithmetic
Pointer arithmetic advances by object size, not byte count.
uint16_t samples[4];
uint16_t *p = samples;
p++;
After p++, the pointer advances by sizeof(uint16_t), not by one byte.
That behavior makes array traversal convenient, but it also means type correctness matters.
Arrays and Decay
An array is not the same thing as a pointer, but in many expressions an array name decays to a pointer to its first element.
uint8_t buf[8];
uint8_t *p = buf;
Here p points to buf[0].
Useful consequence:
for (size_t i = 0; i < 8; i++) {
buf[i] = 0u;
}
and
for (uint8_t *p = buf; p < buf + 8; p++) {
*p = 0u;
}
can express similar work.
Null Pointers
A null pointer points to no valid object:
uint8_t *p = NULL;
It is useful as a sentinel for "not available" or "not initialized." Dereferencing it is a bug.
Aliasing and Shared Mutation
If two pointers refer to the same object, writing through one changes what the other sees.
uint8_t value = 0;
uint8_t *a = &value;
uint8_t *b = &value;
*a = 5;
Now *b is also 5.
This is obvious in small examples and easy to lose track of in larger systems with shared buffers or register blocks.
Pointers and Function Interfaces
Pointers let functions modify caller-owned data:
bool adc_read_mv(uint16_t *out_mv);
This says:
- caller owns the destination object;
- callee writes through the pointer if successful.
Add const when the callee should only read:
uint16_t crc16(const uint8_t *data, size_t len);
Common Embedded Pointer Patterns
- pointer to a peripheral register block;
- pointer to a transmit or receive buffer;
- pointer passed to a driver for DMA;
- callback context pointer;
- table lookup using pointers to constant data in Flash or ROM.
Safety Guidance
- Initialize pointers before use.
- Check nullable pointers at module boundaries.
- Do not cast away type information casually.
- Use
volatilefor true hardware or ISR-shared side effects, not as a general bug fix. - Keep pointer lifetimes aligned with the objects they reference.
- Keep DMA buffers alive and correctly aligned for the whole transfer.
Common Mistakes
- Dereferencing an uninitialized or null pointer.
- Returning a pointer to a dead local variable.
- Forgetting
volatileon memory-mapped register access. - Using pointer arithmetic beyond array bounds.
- Casting incompatible pointer types just to silence warnings.
Summary
Pointers are simply typed addresses, but in embedded systems they connect software directly to hardware and memory layout. When you know what a pointer points to, who owns that object, and how long it remains valid, pointers become a precise tool instead of a mysterious source of bugs.