SoC Programming in C: Controlling Memory-Mapped I/O Registers

Sunhong··Updated Apr 20, 2026·5 min read·192 views·Translated from Korean
Note

Chapter 9: SoC Programming Using C Overview

The basic approach to programming an embedded SoC in C. Covers peripheral access through memory-mapped I/O, pointer manipulation, the volatile keyword, bit operations, the startup sequence, linker scripts, and more.

Memory-Mapped I/O

On ARM Cortex-M, every peripheral is mapped to a specific memory address. Accessing a register = reading/writing that address.

#define GPIO_ODR (*(volatile uint32_t *)0x40020014)

GPIO_ODR = 0x01;   // set GPIO output
uint32_t val = GPIO_ODR;  // read current state

volatile Keyword

  • Prevents compiler optimization (e.g. it must not skip reading the same address twice)
  • Hardware registers can change from the outside, so they must be volatile
volatile uint32_t *GPIO_IDR = (volatile uint32_t *)0x40020010;
while (!(*GPIO_IDR & 0x1)) ;  // wait until the button is pressed

Without volatile, the compiler optimizes the loop and it waits forever.

Struct-Based Register Access

Large peripherals are defined as structs (CMSIS style):

typedef struct {
    volatile uint32_t MODER;    // 0x00
    volatile uint32_t OTYPER;   // 0x04
    volatile uint32_t OSPEEDR;  // 0x08
    volatile uint32_t PUPDR;    // 0x0C
    volatile uint32_t IDR;      // 0x10
    volatile uint32_t ODR;      // 0x14
    // ...
} GPIO_TypeDef;

#define GPIOA ((GPIO_TypeDef *)0x40020000)

GPIOA->ODR = 0x01;

Bit Operations

Bit Set

GPIOA->ODR |= (1 << 5);    // set bit 5 to 1

Bit Clear

GPIOA->ODR &= ~(1 << 5);   // set bit 5 to 0

Bit Toggle

GPIOA->ODR ^= (1 << 5);    // invert bit 5

Bit Check

if (GPIOA->IDR & (1 << 5)) { /* bit 5 is 1 */ }

Multi-bit Field

Setting a 2-bit field to the value val:

GPIOA->MODER &= ~(0x3 << (pin * 2));    // clear 2 bits
GPIOA->MODER |= (val << (pin * 2));     // set value

Memory-mapped I/O and bit operations

Startup Sequence

The initialization the processor performs on reset:

  1. R13 ← Initial SP (vector table offset 0x00)
  2. PC ← Reset handler (offset 0x04)
  3. Reset handler:
    • Copy the .data section from Flash to RAM
    • Zero-initialize the .bss section
    • Call SystemInit() (clock and PLL setup)
    • Call main()
void Reset_Handler(void) {
    // Copy .data from Flash to RAM
    extern uint32_t _sdata, _edata, _sidata;
    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;
    
    // Zero-init .bss
    extern uint32_t _sbss, _ebss;
    dst = &_sbss;
    while (dst < &_ebss) *dst++ = 0;
    
    SystemInit();
    main();
    while(1);
}

Linker Script

Defines the memory layout:

MEMORY {
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS {
    .isr_vector : {
        KEEP(*(.isr_vector))
    } > FLASH
    
    .text : {
        *(.text*)
        *(.rodata*)
    } > FLASH
    
    .data : AT(_sidata) {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM
    
    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM
}

Startup code and linker script

CMSIS (Cortex Microcontroller Software Interface Standard)

ARM's standard software interface:

  • Core peripheral access (NVIC, SysTick, SCB)
  • Vendor-specific register definitions
  • DSP and NN libraries (optional)

Example:

#include "stm32f10x.h"    // CMSIS device header

int main(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;   // GPIOC clock enable
    GPIOC->CRH = 0x00300000;              // PC13 output
    
    while (1) {
        GPIOC->ODR ^= (1 << 13);          // toggle LED
        for (volatile int i = 0; i < 100000; i++);  // delay
    }
}

Interrupt Handler

CMSIS generates the vector table automatically. Just write the handler and it gets connected:

void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR = ~TIM_SR_UIF;    // clear flag
        GPIOC->ODR ^= (1 << 13);   // toggle LED
    }
}

The linker script places the address of TIM2_IRQHandler at the corresponding offset in the vector table.

Clock Setup

An SoC's clock system is complex (HSI, HSE, PLL, AHB/APB prescalers).

void SystemInit(void) {
    // Enable HSE
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));
    
    // Configure PLL
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));
    
    // Switch SYSCLK to PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
}

CMSIS and clock setup

Debugging Tools

  • printf via UART: Redefine _write() in syscalls.c
  • Semihosting: stdout through the ARM debugger
  • SWD (Serial Wire Debug): Breakpoints, watchpoints, live register view
  • ITM (Instrumentation Trace Macrocell): High-speed trace

Optimization Tips

  • Inline: Make short functions static inline
  • Use volatile only where truly needed (it hinders optimization)
  • Cache alignment: Align DMA buffers to cache lines
  • -O2 / -Os: gcc optimization levels; in embedded, size matters

Debugging and optimization

Summary

Concept Key Point
Memory-mapped I/O Peripheral = address
volatile Prevents compiler optimization
Struct access Abstracts register groups
Bit op Set/Clear/Toggle/Check
Startup Vector table → Reset handler → main
Linker script Defines the FLASH/RAM layout
CMSIS ARM's standard software interface
Interrupt handler C function + linker connection

Programming an SoC in C means abstracting the hardware level with C pointers. Once you master volatile, structs, and bit operations, you can directly control any MCU.

C programming overview