So far, we’ve been blinking LEDs and setting GPIO directions using high-level functions like digitalWrite() and pinMode(). But under the hood, your microcontroller communicates with its hardware through something magical:

🔧 Memory-Mapped Registers

Today, we’re diving straight into the low-level stuff—the place where software meets silicon.


🧠 What is Memory-Mapped I/O?

In embedded systems, every peripheral (like GPIO, ADC, Timers) is controlled by registers—small memory locations that store configuration and status information.

But here’s the cool part:

These registers live at specific memory addresses, and you can read/write to them just like variables!

This is called memory-mapped I/O.


🧱 Real Example: Setting a GPIO Pin High

Imagine your microcontroller’s GPIO output register lives at address 0x400FF040.

To turn ON a pin (say, Pin 5), you’d write:

#define GPIO_OUT_REG (*((volatile uint32_t*)0x400FF040))
GPIO_OUT_REG |= (1 << 5); // Set Pin 5 High

🔍 Let’s break that down:

  • volatile: Tells the compiler not to optimize away this read/write
  • uint32_t: 32-bit unsigned integer
  • 0x400FF040: The actual address in memory
  • 1 << 5: Sets bit 5 to 1 (HIGH)

Now you’ve directly manipulated hardware—with no Arduino magic or libraries in the middle.


📦 Anatomy of a Peripheral Register

Most peripherals have multiple registers:

  • Control Register (CR) – Enable/disable or configure the feature
  • Status Register (SR) – Report the current state
  • Data Register (DR) – Send or receive data
  • Direction Register (DDR) – Set GPIO as input/output

Each register is mapped to a fixed memory address, documented in the chip’s datasheet.


🧪 A GPIO Example (Bare-Metal)

#define GPIO_DIR_REG   (*((volatile uint32_t*)0x400FF054)) // Direction
#define GPIO_OUT_REG   (*((volatile uint32_t*)0x400FF040)) // Output
void gpio_init() {
  GPIO_DIR_REG |= (1 << 5); // Set Pin 5 as output
}
void led_on() {
  GPIO_OUT_REG |= (1 << 5); // Set Pin 5 high
}
void led_off() {
  GPIO_OUT_REG &= ~(1 << 5); // Clear Pin 5
}

⚠️ Why You Must Use volatile

Without volatile, the compiler might optimize away your hardware interactions—assuming the memory never changes. But hardware can change registers outside the scope of your code (e.g., due to interrupts), so we must prevent optimization.

volatile uint32_t status = *(uint32_t*)STATUS_REG;

🧰 Tools You’ll Use

  • Datasheets: The map of all register addresses
  • Memory Viewer in Debuggers: See actual values in registers
  • Bitwise Operators: |, &, ~ to set/clear bits

💡 Summary

ConceptDescription
Memory-Mapped I/OAccessing hardware via memory addresses
RegisterA fixed memory address to control hardware
volatilePrevents compiler from optimizing access
BitmaskingUsed to modify individual bits in registers

🔍 Up Next (Day 5):

🔧 Bit Manipulation in Embedded C
Learn to set, clear, toggle, and read individual bits like a pro—it’s the secret sauce of embedded programming!

LEAVE A REPLY

Please enter your comment!
Please enter your name here