
Introduction
In embedded systems, the microcontroller interacts with hardware such as LEDs, sensors, motors, and displays.
But how does software control hardware?
The answer lies in Memory-Mapped Registers.
Memory-mapped registers allow the CPU to control hardware peripherals using the same instructions used to access memory. This concept is fundamental to professional embedded development, especially on ARM Cortex-M microcontrollers.
What Are Memory-Mapped Registers?
A memory-mapped register is a special hardware register that is assigned a fixed memory address.
- Writing to that address → controls hardware
- Reading from that address → reads hardware status
From the CPU’s point of view:
Registers = Memory locations
This means no special instructions are needed to access peripherals.
Why Memory Mapping Is Used
Uniform access using load/store instructions
Faster and simpler hardware design
Easier compiler support
Scalable for complex microcontrollers
That’s why ARM, RISC-V, and most modern MCUs use memory-mapped I/O.
Basic Memory Map Example
| Address Range | Purpose |
|---|---|
| 0x00000000 – 0x1FFF | Flash (Program Memory) |
| 0x20000000 – 0x2003 | SRAM (Data Memory) |
| 0x40000000 – 0x5FFF | Peripheral Registers |
All peripherals like GPIO, Timer, ADC, UART live in the peripheral address space.
How Software Accesses Registers
In Embedded C, we use pointers to access register addresses.
General Syntax
#define REGISTER_NAME (*(volatile unsigned int*)0xADDRESS)
- volatile → prevents compiler optimization
- Pointer → allows direct memory access
Practical 1: Toggling an LED Using Memory-Mapped Registers
Scenario
An LED is connected to GPIO Port A, Pin 5
Assume Register Addresses
#define GPIOA_MODER (*(volatile unsigned int*)0x40020000)
#define GPIOA_ODR (*(volatile unsigned int*)0x40020014)
Code Example
int main(void)
{
/* Configure PA5 as output */
GPIOA_MODER |= (1 << 10);
while(1)
{
GPIOA_ODR ^= (1 << 5); // Toggle LED
for(volatile int i = 0; i < 100000; i++);
}
}
What’s Happening
- Writing to MODER configures pin direction
- Writing to ODR controls the LED
- CPU talks to hardware via memory addresses
Practical 2: Reading a Switch Input
Assume Input Register
#define GPIOA_IDR (*(volatile unsigned int*)0x40020010)
Code
if(GPIOA_IDR & (1 << 0))
{
// Switch is pressed
}
Explanation
- Reading memory gives real-time pin status
- Hardware updates the register automatically
Role of volatile in Memory-Mapped Registers
Without volatile, the compiler may:
Remove register reads
Cache values
Break hardware interaction
Correct Usage
volatile unsigned int *reg;
This ensures every access reaches the hardware.
Bit-Level Control Using Macros
Set, Clear, Toggle Bits
#define SET_BIT(x,n) (x |= (1 << n))
#define CLEAR_BIT(x,n) (x &= ~(1 << n))
#define TOGGLE_BIT(x,n) (x ^= (1 << n))
Used heavily in register programming.
Practical 3: Delay Using a Timer Register (Conceptual)
#define TIMER_CTRL (*(volatile unsigned int*)0x40001000)
TIMER_CTRL = 0x01; // Start timer
This single line:
- Writes to hardware
- Starts counting
- No function calls involved
Common Mistakes Beginners Make
Forgetting volatile
Using wrong register addresses
Writing magic numbers without comments
Modifying reserved bits
Why Industry Prefers Register-Level Programming
Maximum control
Better debugging
Lower latency
No hidden library behavior
Required for drivers & RTOS work