The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Pointers and Memory Addresses in Embedded Systems

📋 Table of Contents

🎯 Overview

Concept: Addresses, objects, and aliasing

A pointer is just an address; correctness depends on the lifetime and effective type of the object it points to. Hardware access requires volatile; high-performance memory access benefits from no-aliasing assumptions.

Minimal example

extern uint32_t sensor_value;
void update(volatile uint32_t* reg, uint32_t v){ *reg = v; }

// Aliasing pitfall: compiler may assume *a and *b don't alias unless told
void add_buffers(uint16_t* restrict a, const uint16_t* restrict b, size_t n){
  for(size_t i=0;i<n;i++) a[i]+=b[i];
}

Takeaways


🧪 Guided Labs

✅ Check Yourself

Pointers are fundamental to embedded programming, enabling direct memory access, hardware register manipulation, and efficient data structures. Understanding pointers is crucial for low-level programming and hardware interaction.

Key Concepts for Embedded Development

🤔 What are Pointers?

Pointers are variables that store memory addresses. They provide indirect access to data stored in memory, allowing programs to manipulate memory locations directly. In embedded systems, pointers are essential for hardware access, dynamic memory management, and efficient data structures.

Core Concepts

Address and Value:

Memory Organization:

Memory Layout Example:
┌─────────────────────────────────────────────────────────────┐
│                    Memory Addresses                        │
├─────────┬─────────┬─────────┬─────────┬─────────┬───────────┤
│ Address │  0x1000 │  0x1001 │  0x1002 │  0x1003 │  0x1004  │
├─────────┼─────────┼─────────┼─────────┼─────────┼───────────┤
│  Value  │   0x42  │   0x00  │   0x00  │   0x00  │   0x78   │
└─────────┴─────────┴─────────┴─────────┴─────────┴───────────┘

Pointer Example:
int* ptr = 0x1000;  // Pointer stores address 0x1000
int value = *ptr;    // Dereference: get value 0x42 from address 0x1000

Pointer Characteristics

Indirect Access:

Type Safety:

Memory Management:

🎯 Why are Pointers Important?

Embedded System Requirements

Hardware Access:

Performance Benefits:

System Control:

Real-world Applications

Hardware Register Access:

// Access GPIO registers
// Use 'volatile' on memory-mapped registers so reads/writes are not optimized away
volatile uint32_t* const GPIOA_ODR = (volatile uint32_t*)0x40020014;
*GPIOA_ODR |= (1 << 5);  // Set bit 5

Dynamic Data Structures:

// Linked list node
typedef struct node {
    int data;
    struct node* next;
} node_t;

Function Callbacks:

// Event handler system
typedef void (*event_handler_t)(uint32_t event);
event_handler_t handlers[MAX_EVENTS];

When to Use Pointers

Use Pointers When:

Avoid Pointers When:

🧠 Memory Address Concepts

Memory Organization

Address Space:

Memory Hierarchy:

Memory Hierarchy:
┌─────────────────────────────────────────────────────────────┐
│                    CPU Registers                           │
│                  (Fastest, Smallest)                       │
├─────────────────────────────────────────────────────────────┤
│                    Cache Memory                            │
│                   (Fast, Small)                           │
├─────────────────────────────────────────────────────────────┤
│                    Main Memory (RAM)                      │
│                   (Slower, Larger)                        │
├─────────────────────────────────────────────────────────────┤
│                    Flash Memory                            │
│                  (Slowest, Largest)                       │
└─────────────────────────────────────────────────────────────┘

Address Types

Physical Addresses:

Virtual Addresses:

Memory-Mapped Addresses:

Address Alignment

Alignment Requirements:

Alignment Examples:

Alignment Requirements:
┌─────────────────┬─────────────┬─────────────────┐
│   Data Type     │   Size      │   Alignment     │
├─────────────────┼─────────────┼─────────────────┤
│   uint8_t       │   1 byte    │   1 byte        │
│   uint16_t      │   2 bytes   │   2 bytes       │
│   uint32_t      │   4 bytes   │   4 bytes       │
│   uint64_t      │   8 bytes   │   8 bytes       │
└─────────────────┴─────────────┴─────────────────┘

📊 Pointer Types and Uses

Data Pointers

Basic Data Pointers:

Const Pointers:

Examples:

// Pointer to const data
const int* ptr1;           // Can't modify *ptr1
int const* ptr2;           // Same as ptr1

// Const pointer
int* const ptr3;           // Can't modify ptr3

// Const pointer to const data
const int* const ptr4;     // Can't modify ptr4 or *ptr4

Function Pointers

Function Pointer Concepts:

Function Pointer Types:

Void Pointers

Void Pointer Characteristics:

Void Pointer Uses:

🔧 Basic Pointer Operations

Pointer Declaration and Initialization

Declaration Syntax:

// Basic pointer declarations
int* ptr1;                    // Pointer to int
uint8_t* ptr2;               // Pointer to uint8_t
const char* ptr3;            // Pointer to const char
void* ptr4;                  // Void pointer

// Initialization
int value = 42;
int* ptr = &value;           // Address-of operator

// Null pointer
int* null_ptr = NULL;

Initialization Best Practices:

Dereferencing Pointers

Basic Dereferencing:

// Basic dereferencing
int value = 42;
int* ptr = &value;
int retrieved = *ptr;         // Get value: 42

// Modifying through pointer
*ptr = 100;                  // Change value to 100

// Safe dereferencing
if (ptr != NULL) {
    *ptr = 42;
}

Dereferencing Safety:

Pointer to Arrays

Array-Pointer Relationship:

// Array and pointer relationship
uint8_t array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
uint8_t* ptr = array;        // Points to first element

// Accessing elements
uint8_t first = *ptr;        // array[0]
uint8_t second = *(ptr + 1); // array[1]
uint8_t third = ptr[2];      // array[2] (same as *(ptr + 2))

Array Decay:

🔢 Pointer Arithmetic

Basic Arithmetic Operations

Increment and Decrement:

// Pointer arithmetic with different types
uint8_t* byte_ptr = (uint8_t*)0x1000;
uint16_t* word_ptr = (uint16_t*)0x1000;
uint32_t* dword_ptr = (uint32_t*)0x1000;

// Increment operations
byte_ptr++;   // 0x1001 (adds 1)
word_ptr++;   // 0x1002 (adds 2)
dword_ptr++;  // 0x1004 (adds 4)

Addition and Subtraction:

// Addition
uint8_t* ptr = (uint8_t*)0x1000;
ptr = ptr + 5;  // 0x1005

// Subtraction
uint8_t* ptr1 = (uint8_t*)0x1000;
uint8_t* ptr2 = (uint8_t*)0x1008;
ptrdiff_t diff = ptr2 - ptr1;  // 8 bytes difference

Array Traversal

Efficient Array Traversal:

// Traverse array with pointer
uint8_t data[64];
uint8_t* ptr = data;

for (int i = 0; i < 64; i++) {
    *ptr = i;        // Set value
    ptr++;           // Move to next element
}

// Alternative: pointer arithmetic
for (int i = 0; i < 64; i++) {
    *(ptr + i) = i;  // Set value using arithmetic
}

Multi-dimensional Arrays:

// 2D array traversal
uint8_t matrix[4][4];
uint8_t* ptr = &matrix[0][0];

for (int i = 0; i < 16; i++) {
    ptr[i] = i;  // Linear access to 2D array
}

Pointer Comparison

Valid Comparisons:

// Compare pointers to same array
uint8_t array[10];
uint8_t* ptr1 = &array[0];
uint8_t* ptr2 = &array[5];

if (ptr1 < ptr2) {
    printf("ptr1 comes before ptr2\n");
}

// Check for NULL
if (ptr1 != NULL) {
    // Safe to dereference
}

🔄 Void Pointers

What are Void Pointers?

Void pointers are generic pointers that can point to any data type. They provide flexibility for generic programming but require careful type casting.

Void Pointer Characteristics

Generic Nature:

Type Safety:

Void Pointer Implementation

Basic Usage:

// Void pointer declaration
void* generic_ptr;

// Point to different types
int int_value = 42;
float float_value = 3.14f;

generic_ptr = &int_value;
int* int_ptr = (int*)generic_ptr;  // Cast to int pointer

generic_ptr = &float_value;
float* float_ptr = (float*)generic_ptr;  // Cast to float pointer

Generic Functions:

// Generic memory copy function
void* memcpy_generic(void* dest, const void* src, size_t size) {
    uint8_t* d = (uint8_t*)dest;
    const uint8_t* s = (const uint8_t*)src;
    
    for (size_t i = 0; i < size; i++) {
        d[i] = s[i];
    }
    
    return dest;
}

// Usage
int source[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int destination[10];

memcpy_generic(destination, source, sizeof(source));

🔧 Function Pointers

What are Function Pointers?

Function pointers are variables that store addresses of functions. They enable dynamic function selection and callback mechanisms, which are essential for event-driven programming in embedded systems.

Function Pointer Concepts

Callback Mechanisms:

Function Pointer Types:

Function Pointer Implementation

Basic Function Pointers:

// Function pointer type definition
typedef int (*operation_t)(int a, int b);

// Function implementations
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

// Function pointer usage
operation_t operation = add;
int result = operation(5, 3);  // Calls add(5, 3)

Callback Systems:

// Event handler system
typedef void (*event_handler_t)(uint32_t event_id, void* data);

// Event handlers
void led_handler(uint32_t event_id, void* data) {
    if (event_id == LED_TOGGLE) {
        toggle_led();
    }
}

void sensor_handler(uint32_t event_id, void* data) {
    if (event_id == SENSOR_READ) {
        read_sensor();
    }
}

// Event handler registration
event_handler_t handlers[MAX_EVENTS];
handlers[LED_EVENT] = led_handler;
handlers[SENSOR_EVENT] = sensor_handler;

// Event dispatch
void dispatch_event(uint32_t event_id, void* data) {
    if (handlers[event_id] != NULL) {
        handlers[event_id](event_id, data);
    }
}

🔧 Hardware Register Access

What is Hardware Register Access?

Hardware register access involves using pointers to directly manipulate hardware registers. This is essential for embedded systems where software must control hardware peripherals.

Register Access Concepts

Memory-Mapped Registers:

Register Types:

Hardware Register Implementation

Basic Register Access:

// Define register addresses
#define GPIOA_BASE    0x40020000
#define GPIOA_ODR     (GPIOA_BASE + 0x14)
#define GPIOA_IDR     (GPIOA_BASE + 0x10)

// Register pointers
volatile uint32_t* const gpio_odr = (uint32_t*)GPIOA_ODR;
volatile uint32_t* const gpio_idr = (uint32_t*)GPIOA_IDR;

// Read register
uint32_t input_state = *gpio_idr;

// Write register
*gpio_odr |= (1 << 5);  // Set bit 5
*gpio_odr &= ~(1 << 5); // Clear bit 5

Register Bit Manipulation:

// Bit manipulation macros
#define SET_BIT(reg, bit)    ((reg) |= (1 << (bit)))
#define CLEAR_BIT(reg, bit)  ((reg) &= ~(1 << (bit)))
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1 << (bit)))
#define READ_BIT(reg, bit)   (((reg) >> (bit)) & 1)

// Usage
SET_BIT(*gpio_odr, 5);      // Set bit 5
CLEAR_BIT(*gpio_odr, 5);    // Clear bit 5
if (READ_BIT(*gpio_idr, 3)) // Read bit 3

🔧 Memory-Mapped I/O

What is Memory-Mapped I/O?

Memory-mapped I/O treats hardware peripherals as memory locations. Reading from or writing to specific memory addresses controls hardware behavior, enabling software to interact with hardware peripherals.

Memory-Mapped I/O Concepts

Address Space:

Peripheral Types:

Memory-Mapped I/O Implementation

Peripheral Structure:

// UART peripheral structure
typedef struct {
    volatile uint32_t SR;    // Status register
    volatile uint32_t DR;    // Data register
    volatile uint32_t BRR;   // Baud rate register
    volatile uint32_t CR1;   // Control register 1
    volatile uint32_t CR2;   // Control register 2
} uart_t;

// Peripheral instance
uart_t* const uart1 = (uart_t*)0x40011000;

// UART operations
void uart_send_byte(uint8_t byte) {
    // Wait for transmit data register empty
    while (!(*((uint32_t*)&uart1->SR) & 0x80));
    
    // Send byte
    uart1->DR = byte;
}

uint8_t uart_receive_byte(void) {
    // Wait for receive data register not empty
    while (!(*((uint32_t*)&uart1->SR) & 0x20));
    
    // Read byte
    return (uint8_t)uart1->DR;
}

DMA Buffer Access:

// DMA buffer structure
typedef struct {
    uint32_t source_address;
    uint32_t destination_address;
    uint32_t transfer_count;
    uint32_t control;
} dma_channel_t;

// DMA channel instance
dma_channel_t* const dma_ch1 = (dma_channel_t*)0x40020000;

// Configure DMA transfer
void configure_dma_transfer(uint32_t source, uint32_t dest, uint32_t count) {
    dma_ch1->source_address = source;
    dma_ch1->destination_address = dest;
    dma_ch1->transfer_count = count;
    dma_ch1->control = 0x1234;  // Configure control bits
}

🔧 Implementation

Complete Pointer Example

#include <stdint.h>
#include <stdbool.h>

// Hardware register definitions
#define GPIOA_BASE    0x40020000
#define GPIOA_ODR     (GPIOA_BASE + 0x14)
#define GPIOA_IDR     (GPIOA_BASE + 0x10)

// Register pointers
volatile uint32_t* const gpio_odr = (uint32_t*)GPIOA_ODR;
volatile uint32_t* const gpio_idr = (uint32_t*)GPIOA_IDR;

// Function pointer type
typedef void (*led_control_t)(bool state);

// LED control functions
void led_on(bool state) {
    if (state) {
        *gpio_odr |= (1 << 5);  // Set LED pin
    } else {
        *gpio_odr &= ~(1 << 5); // Clear LED pin
    }
}

void led_off(bool state) {
    if (!state) {
        *gpio_odr |= (1 << 5);  // Set LED pin
    } else {
        *gpio_odr &= ~(1 << 5); // Clear LED pin
    }
}

// Button state structure
typedef struct {
    uint8_t current_state;
    uint8_t previous_state;
    uint32_t debounce_time;
} button_state_t;

// Button array
button_state_t buttons[4];

// Function pointer array
led_control_t led_controls[2] = {led_on, led_off};

// Main function
int main(void) {
    // Initialize button states
    for (int i = 0; i < 4; i++) {
        buttons[i].current_state = 0;
        buttons[i].previous_state = 0;
        buttons[i].debounce_time = 0;
    }
    
    // Main loop
    while (1) {
        // Read button states
        uint32_t button_input = *gpio_idr & 0x0F;  // Read lower 4 bits
        
        // Process each button
        for (int i = 0; i < 4; i++) {
            bool button_pressed = (button_input >> i) & 0x01;
            
            if (button_pressed != buttons[i].current_state) {
                // Button state changed
                if (button_pressed) {
                    // Button pressed - toggle LED
                    static bool led_state = false;
                    led_state = !led_state;
                    led_controls[0](led_state);  // Use function pointer
                }
                
                buttons[i].previous_state = buttons[i].current_state;
                buttons[i].current_state = button_pressed;
            }
        }
    }
    
    return 0;
}

⚠️ Common Pitfalls

1. Dangling Pointers

Problem: Using pointers after memory is freed Solution: Set pointers to NULL after freeing

// ❌ Bad: Dangling pointer
uint8_t* ptr = malloc(100);
free(ptr);
*ptr = 42;  // Use-after-free!

// ✅ Good: Null pointer after free
uint8_t* ptr = malloc(100);
free(ptr);
ptr = NULL;  // Prevent use-after-free

2. Null Pointer Dereference

Problem: Dereferencing NULL pointers Solution: Always check for NULL before dereferencing

// ❌ Bad: No NULL check
void bad_function(uint8_t* ptr) {
    *ptr = 42;  // Crash if ptr is NULL
}

// ✅ Good: NULL check
void good_function(uint8_t* ptr) {
    if (ptr != NULL) {
        *ptr = 42;
    }
}

3. Buffer Overflows

Problem: Writing beyond allocated memory Solution: Always check bounds

// ❌ Bad: Buffer overflow
uint8_t buffer[10];
uint8_t* ptr = buffer;
for (int i = 0; i < 20; i++) {
    ptr[i] = 0;  // Buffer overflow!
}

// ✅ Good: Bounds checking
uint8_t buffer[10];
uint8_t* ptr = buffer;
for (int i = 0; i < 10; i++) {
    ptr[i] = 0;
}

4. Type Casting Errors

Problem: Incorrect type casting Solution: Use appropriate types and casting

// ❌ Bad: Incorrect casting
float* float_ptr = (float*)0x1000;
int* int_ptr = (int*)float_ptr;  // May cause alignment issues

// ✅ Good: Proper casting
void* generic_ptr = (void*)0x1000;
float* float_ptr = (float*)generic_ptr;

✅ Best Practices

1. Pointer Safety

2. Memory Management

3. Hardware Access

4. Function Pointers

5. Code Organization

🎯 Interview Questions

Basic Questions

  1. What is a pointer and why is it important in C?
    • Pointer is a variable that stores memory address
    • Enables direct memory access and hardware control
    • Essential for dynamic memory allocation
    • Provides efficient data structure implementation
  2. What is the difference between a pointer and an array?
    • Array is a collection of elements, pointer is an address
    • Arrays decay to pointers to first element
    • Pointers can be modified, array names cannot
    • Arrays have size information, pointers do not
  3. What is a void pointer and when would you use it?
    • Generic pointer that can point to any type
    • Cannot be dereferenced directly
    • Must be cast to specific type before use
    • Useful for generic functions and data structures

Advanced Questions

  1. How would you implement a linked list using pointers?
    • Define node structure with data and next pointer
    • Implement insert, delete, and traversal functions
    • Handle memory allocation and deallocation
    • Consider doubly-linked list for efficiency
  2. How would you use function pointers for event handling?
    • Define function pointer types for event handlers
    • Create array of function pointers
    • Register handlers for different events
    • Implement event dispatch mechanism
  3. How would you access hardware registers using pointers?
    • Define register addresses as constants
    • Create volatile pointer variables
    • Use bit manipulation for register control
    • Follow hardware timing requirements

Implementation Questions

  1. Write a function to reverse a linked list
  2. Implement a callback system using function pointers
  3. Write code to access GPIO registers
  4. Design a generic memory copy function using void pointers

📚 Additional Resources

Books

Online Resources

Tools

Standards


Next Steps: Explore Memory Management to understand memory allocation strategies, or dive into Type Qualifiers for advanced C language features.