The "Holy Bible" for embedded engineers
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.
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];
}
volatile
for memory-mapped registers and ISR-shared flags.memcpy
.restrict
only when you can prove non-aliasing.sizeof
in caller vs callee; confirm pointer vs array.uint8_t*
and read via uint32_t*
; compare -O0 vs -O2.const
legal/illegal?volatile
?Embedded_C/Type_Qualifiers.md
Embedded_C/Memory_Mapped_IO.md
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.
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.
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
Indirect Access:
Type Safety:
Memory Management:
Hardware Access:
Performance Benefits:
System Control:
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];
Use Pointers When:
Avoid Pointers When:
Address Space:
Memory Hierarchy:
Memory Hierarchy:
┌─────────────────────────────────────────────────────────────┐
│ CPU Registers │
│ (Fastest, Smallest) │
├─────────────────────────────────────────────────────────────┤
│ Cache Memory │
│ (Fast, Small) │
├─────────────────────────────────────────────────────────────┤
│ Main Memory (RAM) │
│ (Slower, Larger) │
├─────────────────────────────────────────────────────────────┤
│ Flash Memory │
│ (Slowest, Largest) │
└─────────────────────────────────────────────────────────────┘
Physical Addresses:
Virtual Addresses:
Memory-Mapped Addresses:
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 │
└─────────────────┴─────────────┴─────────────────┘
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 Pointer Concepts:
Function Pointer Types:
Void Pointer Characteristics:
Void Pointer Uses:
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:
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:
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:
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
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
}
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 are generic pointers that can point to any data type. They provide flexibility for generic programming but require careful type casting.
Generic Nature:
Type Safety:
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 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.
Callback Mechanisms:
Function Pointer Types:
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 involves using pointers to directly manipulate hardware registers. This is essential for embedded systems where software must control hardware peripherals.
Memory-Mapped Registers:
Register Types:
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 treats hardware peripherals as memory locations. Reading from or writing to specific memory addresses controls hardware behavior, enabling software to interact with hardware peripherals.
Address Space:
Peripheral Types:
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
}
#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;
}
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
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;
}
}
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;
}
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;
Next Steps: Explore Memory Management to understand memory allocation strategies, or dive into Type Qualifiers for advanced C language features.