Dev Log
Tutorials Ashley Golland

Understanding Chip Register Maps: Datasheet to Firmware

A guide to register maps, bitfield definitions, access modes (RW/RO/WO/W1C), reset values, and their translation into C structs and accessor macros in generated drivers.

Annotated chip register map table from a datasheet

A peripheral register can determine how an MCU behaves, so reading its reference manual accurately is a central embedded firmware skill. This guide explains register map structure, the terminology found in major MCU datasheets, and how Embedd turns that information into C code.

Experienced embedded developers may know most of this already. For newer learners and software engineers joining hardware projects, it provides the basis for understanding what Embedd generates and the reasons behind its structure.

Address structure: base and offset

Each MCU peripheral has a base address in the memory map. For example, STM32F4 USART1 begins at 0x40011000, while USART2 begins at 0x40004400. The reference manual's memory map lists these peripheral base addresses.

A register's location is its offset from the peripheral base. USART_SR uses 0x00, USART_DR uses 0x04, and USART_BRR uses 0x08. Thus, USART1's baud rate register is at 0x40011000 + 0x08 = 0x40011008.

C represents this with a volatile pointer dereference:

#define USART1_BASE   0x40011000UL
#define USART_BRR_OFF 0x08UL

#define USART1_BRR    (*(volatile uint32_t *)(USART1_BASE + USART_BRR_OFF))

The volatile qualifier is essential. Without it, the compiler can retain a register value in a CPU register and miss later hardware changes. Embedd-generated peripheral accesses all use volatile.

Register width

Most MCU peripheral registers are 32 bits wide, though some older or lower-power peripherals use 8 or 16 bits. The reference manual specifies each width. An 8-bit access to a 32-bit register, using uint8_t, may not have the expected result: transaction width depends on the APB or AHB peripheral bus and the chip's supported bus width. Generated drivers follow the datasheet's register width.

Bitfield definitions

A 32-bit register contains fields, each made from one or more bits that control or report part of a peripheral's state. Every field has a name, a bit position identifying its least significant bit, and a width defining its span.

In STM32F4 USART_CR1, the M field begins at bit 12 and selects word length: 0 means 8 data bits, while 1 means 9. Other fields include PCE at bit 10, PS at bit 9, and TE at bit 3.

To change M while preserving the other fields, use read-modify-write:

/* Set 9-bit word length: M=1 */
uint32_t cr1 = USART1_CR1;
cr1 |= (1UL << 12);   /* set bit 12 */
USART1_CR1 = cr1;

To clear it:

cr1 &= ~(1UL << 12);  /* clear bit 12 */

For multi-bit fields, combine the mask and shift:

#define USART_CR1_M_POS  12U
#define USART_CR1_M_MASK (0x1UL << USART_CR1_M_POS)

/* Set field value */
cr1 = (cr1 & ~USART_CR1_M_MASK) | ((value & 0x1UL) << USART_CR1_M_POS);

For each field, Embedd creates position and mask macros using PERIPH_REG_FIELD_POS and PERIPH_REG_FIELD_MASK.

Access modes: RW, RO, WO, W1C

Each register field has an access mode describing the hardware's permitted use:

RW (Read-Write): Firmware can read the current value and write a replacement. Configuration fields are usually RW, including USART_CR1 word length M and SPI_CR1 clock polarity CPOL.

RO (Read-Only): Hardware supplies the value and firmware only reads it. Status fields are commonly RO. On most ARM peripherals, writes are ignored, including writes to USART_SR RXNE and SPI_SR BSY.

WO (Write-Only): A write sets or triggers an action, while a read returns an undefined or zero value. These fields are less common and occur in some command and trigger registers.

W1C (Write 1 to Clear): Hardware sets the bit, and firmware clears it by writing 1 to that position. Writing 0 does nothing. Interrupt flag registers almost universally use W1C. A common error is read-modify-write: read the register, OR in other values, then write it back. If hardware sets the W1C bit between those operations, the write can clear an unintended flag. Instead, write the required bit position directly, without reading first.

The register table's "Access" column identifies each mode. Embedd uses it to select the write method: direct writes for WO, read-modify-write for RW, and targeted W1C writes for interrupt flags.

Reset values

A reset value is what hardware places in a register field after power-on reset or system reset. The register table documents it in the "Reset value" column or in footnotes.

Reset values serve two purposes. They establish the peripheral's initial behavior, which init code must account for. They also let generated init code preserve fields it does not configure instead of replacing them with zero and unintentionally changing behavior.

At the start of the generated init function, a comment block lists each register it accesses and the expected state before and after initialization. This provides a direct way to review its reset-state assumptions.

C structs versus mask macros

Register fields are commonly represented in C in two ways. One is a C bitfield struct:

typedef struct {
    uint32_t PE   : 1;
    uint32_t STOP : 2;
    uint32_t M    : 1;
    /* ... */
} USART_CR1_t;

The other is a set of mask macros, as in the examples above.

C bitfield structs are easier to read but less portable. The C standard leaves bit ordering inside such structs unspecified, so behavior can differ between GCC ARM, IAR, and Keil. They can also complicate atomic register writes because accessing one field creates a separate read-modify-write cycle that an interrupt can preempt.

Embedd uses mask macros as the main interface and can also provide a typedef'd struct for readability. Generated function bodies use the accessor macros, while the struct helps developers inspect field names in a debugger watch window. This does not make masks universally superior. The choice favors portability and correctness during interrupt preemption over shorter syntax.

Putting it together

When a datasheet is uploaded, Embedd's parser reads its register tables and builds a model containing every register's offset, fields, bit positions, widths, access modes, and reset values. Code generation then creates the mask macros, an init function for selected RW values, status readers for RO fields, and interrupt-flag clear functions using the proper W1C pattern.

The generated C is straightforward. Its value lies not in novel algorithm design, but in applying datasheet information consistently so the code matches the silicon. Understanding register maps makes that process manageable; without that understanding, it is easy to make errors.

Create your first driver

Upload a chip datasheet for C99 peripheral drivers in under two minutes.

Get started free