Loading header...

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:

  • value is an 8-bit object;
  • &value is its address;
  • ptr stores that address;
  • *ptr means "the object located at that address."

Declaration Reading

Read from the variable name outward:

  • uint8_t *p -> p is a pointer to uint8_t
  • const uint8_t *p -> p points to read-only uint8_t
  • uint8_t *const p -> p itself cannot be changed after initialization
  • const uint8_t *const p -> both the pointer and the pointed-to bytes are read-only through p

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 0x40020014 as a pointer to volatile 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 volatile for 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 volatile on 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.

Further Reading

Mind Map

mindmap root((Pointers)) Core idea Typed address value Address with ampersand Dereference with star Object lifetime matters Embedded uses Registers Buffers DMA descriptors Callback context Flash tables Key rules Arithmetic by object size Arrays decay in expressions Null means no object Volatile for hardware effects Const for read only data Calculations p plus 1 adds sizeof type buf plus n reaches element n Register address from datasheet Practical checks Initialize before use Validate nullable inputs Check alignment Match DMA lifetime Review casts Common mistakes Null dereference Dead local pointer Missing volatile Past array bounds Cast hides bug