Registers are small, fixed-width storage locations built directly into a processor or peripheral, typically accessible in a single clock cycle on most architectures, though some bus paths or pipeline states may require additional cycles. They serve two distinct roles in embedded systems: CPU registers hold operands, addresses, and status flags for the ALU and control logic, while peripheral (memory-mapped) registers configure and control on-chip hardware such as UARTs, timers, ADCs, and GPIO ports.
In practice
CPU registers are the fastest storage available on nearly any processor. On an ARM Cortex-M core, the general-purpose register file contains sixteen 32-bit registers (R0–R15), where R13 is the stack pointer, R14 the link register, and R15 the program counter (as defined by the Cortex-M architecture profile). 8-bit architectures such as PIC mid-range or AVR have far fewer registers — the AVR has 32 8-bit general-purpose registers while many PIC cores work primarily through a single working register (W or WREG). The compiler allocates CPU registers automatically during code generation; running out of registers causes spills to the stack, which adds load/store overhead.
Peripheral registers are the primary mechanism for interacting with on-chip hardware. They are typically memory-mapped, meaning the CPU accesses them with ordinary load/store instructions at fixed addresses defined in the device's datasheet or header file. Common register types include control registers (configure a peripheral's operating mode), status registers (report flags like buffer-full or transfer-complete), and data registers (read sensor values or write transmission bytes). Some architectures also use I/O-mapped registers accessed via dedicated instructions (IN/OUT on AVR, for example).
A critical rule when working with peripheral registers is to read the datasheet carefully for read/write semantics. Many status flags are cleared by writing a 1 to them (write-1-to-clear, or W1C), and writing without reading first can inadvertently clear bits you did not intend to modify. Read-modify-write sequences on control registers must also be handled carefully in interrupt-driven code to avoid race conditions.
Peripheral registers are almost always declared volatile in C and C++ when accessed directly, to prevent the compiler from caching their values in CPU registers or reordering accesses. (Code that uses abstraction layers or dedicated accessor functions may handle this differently, but the same volatile semantics must still be ensured at some level.) Omitting volatile is one of the most common sources of hard-to-reproduce bugs on embedded targets, because the compiler legally optimizes away repeated reads of a variable it believes nothing else modifies.
Frequently asked
What is the difference between a CPU register and a peripheral register?
CPU registers are part of the processor's register file and hold temporary values during instruction execution — operands, addresses, the program counter, and status flags. Peripheral registers are fixed memory (or I/O) addresses mapped to hardware blocks on the chip; reading or writing them directly controls or observes the behavior of peripherals like timers,
UARTs, and
GPIO ports. They are unrelated storage mechanisms that happen to share the word 'register'.
Why must peripheral registers be declared volatile?
The compiler does not know that hardware can change a peripheral register's value between two reads, or that writing the register has a side effect beyond storing a value. Without
volatile, the compiler may eliminate a second read (returning a cached value instead), hoist a write out of a loop, or reorder accesses in ways that break peripheral operation. Declaring the register pointer or variable volatile forces the compiler to emit an actual load or store for every access in the source code.
What does 'write-1-to-clear' (W1C) mean, and why does it matter?
Some peripheral status flags use W1C semantics: writing a 1 to a flag bit clears it, while writing a 0 leaves it unchanged. This is common for
interrupt-pending and
DMA-complete flags. The implication is that a naive read-modify-write (reg |= mask) can accidentally clear other pending flags whose bit positions happen to read back as 1. Correct practice is to write only the specific bits you intend to clear, using a targeted write (reg = FLAG_BIT) rather than a read-modify-write.
How does the compiler decide which variables live in CPU registers?
Register allocation is performed automatically by the compiler's back end. Variables accessed frequently and whose address is never taken are strong candidates. When there are more live variables than available registers, the compiler spills the excess to the
stack. The C/C++ keyword 'register' was historically a hint to the compiler, but modern compilers largely ignore it and make their own decisions based on liveness analysis.
What is a bit-field and when is it appropriate for peripheral registers?
A C/C++ bit-field lets you name individual fields within a struct, and some vendor-supplied device headers use them for peripheral register maps. However, the C standard leaves several aspects of bit-field layout implementation-defined (including bit ordering and padding), so compilers can place fields differently. Hand-crafted bitmask macros with explicit shifts (REG |= (1u << BIT_POS)) are more portable and produce predictable assembly. Bit-fields can be convenient for readability when you control the toolchain and verify the generated code, but they require care and are not universally recommended for direct hardware register access.
Differentiators vs similar concepts
The term 'register' in embedded contexts can refer to either a CPU register (part of the processor's internal register file) or a peripheral/memory-mapped register (a hardware control or status location at a fixed address). These are fundamentally different things and should not be confused. Additionally, 'register' sometimes appears in digital logic discussions to mean any D flip-flop based storage element, which is a broader hardware concept that encompasses both of the above but is not specific to software-visible registers.