8.01 Memory Addresses, Dereferencing & Type-Safe Memory-Mapped I/O (MMIO) Peripheral Access
💻 1. Annotated Source Code
#include <iostream> using namespace std; int main() { int myLovelyInt = 150; int* somePtr = &myLovelyInt; double myDouble = 3.14; double* doublePtr = &myDouble; cout << "myLovelyInt is originally: " << myLovelyInt << endl; cout << "pointer holds value: " << somePtr << endl; cout << "pointer dereferenced: " << *somePtr << endl; *somePtr = 200; cout << "myLovelyInt is now: " << myLovelyInt << endl; cout << doublePtr << endl; cout << *doublePtr << endl; return 0; }
📐 2. Architecture & UML Class Model
📚 3. Core C++ Concepts Deep-Dive
1. Pointer Mechanics: Addresses vs Values
A pointer is a variable that stores the physical or virtual memory address of another object. The address-of operator (&) retrieves an object's memory address, while the dereference operator (*) reads or writes the data stored at that address.
2. Pointer Sizing & Architecture
The size of a pointer matches the CPU architecture's address bus width: 4 bytes (32 bits) on 32-bit microcontrollers (e.g. ARM Cortex-M0/M3/M4/M7, ESP32) and 8 bytes (64 bits) on 64-bit systems (x86_64, AArch64).
⚡ 4. Embedded Systems & Hardware Reality
1. Memory-Mapped I/O (MMIO) and the volatile Keyword
In microcontrollers, hardware peripherals (GPIO, Timers, UART, SPI) are mapped directly to specific physical memory addresses in the CPU memory map (e.g., STM32 GPIOA output data register at 0x40020014).
Because peripheral registers can change asynchronously due to external hardware events or clock edges, pointers to MMIO registers must always be qualified with volatile. This prevents the compiler's optimizer from caching register reads in CPU general-purpose registers.
⚠️ MISRA C++:2008 Rule 5-2-7 & Rule 5-2-8
Casting an integer memory address to a pointer is prohibited in general application code, except in low-level hardware abstraction layers (BSP/HAL) accessing hardware registers.
💡 5. Production-Ready Embedded Refactoring
Modern embedded C++ wraps raw MMIO addresses in type-safe, zero-overhead register abstractions:
#include <cstdint> // Type-safe, zero-cost MMIO Register Wrapper template <uintptr_t Address, typename T = uint32_t> struct MmioRegister { static void write(T value) noexcept { *reinterpret_cast<volatile T*>(Address) = value; } static T read() noexcept { return *reinterpret_cast<volatile T*>(Address); } static void set_bit(uint8_t bit) noexcept { *reinterpret_cast<volatile T*>(Address) |= (1UL << bit); } static void clear_bit(uint8_t bit) noexcept { *reinterpret_cast<volatile T*>(Address) &= ~(1UL << bit); } }; // Concrete GPIO Pin Definition (STM32 GPIOA ODR at 0x40020014) using GpioA_ODR = MmioRegister<0x40020014, uint32_t>; void toggle_status_led() noexcept { GpioA_ODR::set_bit(5); // Set Pin 5 HIGH (LED ON) GpioA_ODR::clear_bit(5); // Set Pin 5 LOW (LED OFF) }
📝 Knowledge Verification Quiz
Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.
volatile informs the compiler that the value at the address can change outside the program's control (e.g., by hardware circuitry), preventing the compiler from omitting or reordering reads/writes.
& to a local variable yields its stack memory address.