In this lesson, you will learn how to use C struct definitions to create a clean, readable, and type-safe interface for interacting with hardware peripheral registers.
From Magic Numbers to Structured Mapping
Previously, we learned that peripherals are mapped to specific memory addresses. A naive way to control hardware is by creating #define macros for every single register offset. For example:
#define UART_BASE 0x4000C000
#define UART_DR (UART_BASE + 0x00)
#define UART_BRR (UART_BASE + 0x04)
#define UART_CR1 (UART_BASE + 0x0C)
While this works, it is error-prone and visually cluttered. As a project grows, managing hundreds of these macros becomes a nightmare. Instead, we can map the peripheral’s register layout directly onto a C struct.
Defining the Peripheral Structure
Since registers are located at sequential memory offsets, we can define a struct where each member corresponds to a register. The compiler handles the memory layout automatically, placing members in order:
typedef struct {
volatile uint32_t DR; // Offset 0x00
volatile uint32_t BRR; // Offset 0x04
uint32_t RESERVED; // Offset 0x08 (padding)
volatile uint32_t CR1; // Offset 0x0C
} UART_TypeDef;
Note: We use volatile to prevent the compiler from optimizing away memory accesses, as hardware state can change outside the program's control.
Accessing the Hardware
Once the structure is defined, we can cast the base address of our peripheral to a pointer of that struct type. This transforms raw memory manipulation into intuitive object-like access:
#define UART1 ((UART_TypeDef *) 0x4000C000)
// Usage:
UART1->DR = 0x55; // Write data to the register
UART1->CR1 |= 0x01; // Enable the peripheral
This approach is powerful because it groups related registers together, ensures we don't accidentally write to the wrong offset, and makes the code significantly more readable. The structure acts as a "map" that sits directly on top of the hardware's physical memory, allowing the CPU to interact with bits and bytes as if it were simply updating members of a data structure.
Key Takeaway: Using C structs to map peripheral registers provides a structured, type-safe, and highly readable way to interface with hardware, replacing fragmented macros with an organized memory representation.