Skip to main content Magd Aref

Understanding ARM CMSIS: Memory-Mapped Hardware

Published: 2026-04-16
Reading time: 2 min

Understanding ARM CMSIS: Memory-Mapped Hardware

I’m working through chapter 6 of Gbati’s Bare-Metal Embedded C Programming, which covers the ARM CMSIS standard. The previous chapter taught me to automate the build with a Makefile. This chapter taught me the industry standard approach to hardware definitions in embedded C.

Rewriting Hardware Definitions

I started by rewriting the original blinky program to follow the CMSIS structure pattern. Instead of working with the raw register macros from the hello world post, I defined hardware using structured types that mirror the actual memory layout:

typedef struct {
    volatile uint32_t MODER;    //offset: 0x00
    volatile uint32_t OTYPER;   //offset: 0x04
    volatile uint32_t OSPEEDR;  //offset: 0x08
    volatile uint32_t PUPDR;    //offset: 0x0C
    volatile uint32_t IDR;      //offset: 0x10
    volatile uint32_t ODR;      //offset: 0x14
    volatile uint32_t BSRR;     //offset: 0x18
    volatile uint32_t LCKR;     //offset: 0x1C
    volatile uint32_t AFRL;     //offset: 0x20
    volatile uint32_t AFRH;     //offset: 0x24
} GPIO_TypeDef;

typedef struct {
    volatile uint32_t DUMMY[12];
    volatile uint32_t AHB1ENR;  //offset: 0x30
} RCC_TypeDef;

#define RCC_BASE    0x40023800
#define GPIOA_BASE  0x40020000

#define RCC         ((RCC_TypeDef*)   RCC_BASE)
#define GPIOA       ((GPIO_TypeDef*)GPIOA_BASE)

The Moment of Confusion

When I first flashed this code to my STM32F446RE, I was genuinely confused. We’re defining structs and manipulating their members without ever initializing any values. How did I access GPIOA->MODER when we never created a GPIO_TypeDef instance? Where’s the initialization?

The Breakthrough: Memory-Mapped Hardware

The breakthrough came when I connected this to what I’d already learned about the linker script and memory layout. I stepped back and thought about what the code was actually doing. We’re not creating struct instances. Instead, we’re casting memory addresses to struct pointers, overlaying our struct layout onto hardware registers.

The key insight: memory-mapped hardware exists in contiguous blocks. When I write:

#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)

We’re telling the compiler that starting at address GPIOA_BASE, the memory layout follows our GPIO_TypeDef structure. Each uint32_t member occupies the next 4 bytes in sequence. The hardware designers built the GPIO peripheral this way, and our struct simply mirrors that layout.

Handling Non-Sequential Registers

The RCC_TypeDef structure demonstrates a clever technique for handling cases where we don’t need all registers:

typedef struct {
    volatile uint32_t DUMMY[12];  // Skip first 48 bytes
    volatile uint32_t AHB1ENR;    // The register we actually need
} RCC_TypeDef;

The DUMMY[12] array skips the first 48 bytes (12 × 4 bytes) because we don’t care about those registers for this simple blinky program. We jump straight to AHB1ENR at offset 0x30. This technique lets us define only the registers we need while maintaining the correct memory layout.

Clean Hardware Manipulation

With these definitions in place, the actual hardware manipulation becomes clean and readable:

int main(void)
{
    // Enable clock access to GPIOA
    RCC->AHB1ENR |= GPIOAEN;

    // Set PA5 to general output mode
    GPIOA->MODER |=  (1U<<10);
    GPIOA->MODER &= ~(1U<<11);

    while(1){
        // Toggle PA5 (LED)
        GPIOA->ODR ^= LED_PIN;
        
        // Simple delay
        volatile int i;
        for (i = 0; i < 400000; i++) {
            __asm__ volatile ("nop");
        }
    }
}

Moving to Official CMSIS Headers

The second half of the chapter introduces the official CMSIS headers from STMicrolectronics. Instead of manually defining structs based on reference manual documentation, I can now use the official header files that follow this exact same pattern for all peripherals.

The CMSIS standard uses this technique across all ARM Cortex-M microcontrollers, providing a standardized way to interact with memory-mapped hardware that’s much more readable than the method I had been using previously.