What Is a Pointer in Embedded C?
A pointer is a variable that stores the memory address of another object.
Consider a simple C program:
int value = 10;
int *ptr;
ptr = &value;
Here, value is an integer variable and ptr is a pointer to an integer.
The address operator & obtains the memory address of value:
ptr = &value;
The pointer now contains the address where value is stored.
The * operator is used to dereference the pointer:
*ptr = 20;
This changes the value stored at the memory location pointed to by ptr.
The basic relationship can be represented as:
Variable
|
| & address operator
v
Memory Address
|
| stored in
v
Pointer
|
| * dereference
v
Value at that address
In conventional C programming, pointers are frequently used for arrays, structures, dynamic memory, function arguments, and data structures.
In embedded systems, pointers have an additional and extremely important application: accessing hardware registers.

Why Are Pointers Important in Embedded Systems?
A microcontroller contains different types of memory and hardware resources. These may include Flash memory, SRAM, peripheral registers, and system-control registers.
Many peripherals are assigned specific addresses within the microcontroller’s address space.
A simplified example could look like this:
0x40020000 GPIO Control Register
0x40020004 GPIO Status Register
0x40020008 GPIO Output Register
These addresses are examples only. Actual addresses depend on the microcontroller.
A pointer can be used to represent one of these addresses:
volatile unsigned int *gpio_output =
(volatile unsigned int *)0x40020008U;The pointer now represents the address of the GPIO output register.
A write operation can then be performed using:
*gpio_output = 0x00000001U;
The CPU performs a memory write to that address, and the associated peripheral hardware responds according to the register definition.
This is the foundation of register-level programming.
Understanding Memory Addresses in a Microcontroller
To understand pointers and hardware registers, it is necessary to understand the concept of a memory address.
A microcontroller uses an address space to identify different memory locations and hardware resources.
A simplified address map might look like:
Higher Addresses
+---------------------------+
| System Control Registers |
+---------------------------+
| Peripheral Registers |
+---------------------------+
| SRAM |
+---------------------------+
| Flash |
+---------------------------+
Lower Addresses
The exact organization depends on the processor architecture and microcontroller.
A memory address identifies a particular location within this address space.
A C pointer provides a way for software to store such an address.
For example:
uint32_t *ptr;
declares a pointer that can store the address of a uint32_t object.
For hardware access:
volatile uint32_t *reg =
(volatile uint32_t *)0x40020000U;the numerical address is converted into a pointer.
The pointer can then be dereferenced to perform a register access.
What Is Memory-Mapped I/O?
Memory-mapped I/O is a technique in which hardware peripheral registers are assigned addresses within the processor’s address space.
The CPU can access these registers using memory read and write operations.
For example, assume a hypothetical peripheral register exists at:
0x40020000
A pointer can be created for that address:
volatile uint32_t *REG =
(volatile uint32_t *)0x40020000U;Reading the register:
uint32_t value = *REG;
Writing to the register:
*REG = 0x00000001U;
The important point is that REG is not the register itself. It is a pointer containing the address used to access the register.
The relationship can be visualized as:
C Pointer
|
v
Register Address
|
v
Memory-Mapped Register
|
v
Peripheral HardwareThis is why memory mapped registers in Embedded C and pointers are closely related.
Pointer to a Microcontroller Register
A pointer to a microcontroller register generally involves three important elements:
- Register address
- Correct data type
- volatile qualifier
For example:
volatile uint32_t *reg =
(volatile uint32_t *)0x40020000U;Each part has a purpose.
Register Address
0x40020000U
represents the address assigned to the hardware register.
The actual address must come from the microcontroller’s official reference manual or device header.
Pointer Type
uint32_t *
indicates that the pointer refers to a 32-bit unsigned value.
The access width must match the register definition and the requirements of the target device.
Volatile Qualifier
volatile
indicates that the value may change independently of normal program execution and that accesses to it have observable effects.
Therefore:
volatile uint32_t *reg =
(volatile uint32_t *)0x40020000U;creates a pointer suitable for accessing a 32-bit memory-mapped hardware register.
Understanding the Address Operator &
The address operator & returns the memory address of an object.
For example:
uint32_t data = 100;
uint32_t *ptr = &data;
The expression:
&data
means “address of data.”
The pointer stores that address.
For example, conceptually:
data
|
| address
v
0x20000000
|
| stored in
v
ptr
The actual address will vary depending on the microcontroller and memory layout.
The address operator is fundamental when learning pointers because it explains how a C variable is associated with a particular memory location.
When working with hardware registers, however, programmers normally start with a predefined register address supplied by the microcontroller documentation rather than obtaining the address using &.
Dereferencing a Pointer
Dereferencing means accessing the object or memory location represented by a pointer.
Consider:
uint32_t value = 25;
uint32_t *ptr = &value;
*ptr = 50;
The statement:
*ptr = 50;
changes the value stored at the memory location pointed to by ptr.
The same principle applies to a memory-mapped hardware register.
volatile uint32_t *reg =
(volatile uint32_t *)0x40020000U;
*reg = 0x00000001U;
Here, *reg represents an access to the memory location at 0x40020000.
If that address corresponds to a hardware register, the write operation affects the associated peripheral.
This is one of the most important concepts behind accessing registers using pointers in C.
Why Is the volatile Keyword Important?
Hardware registers behave differently from ordinary program variables.
Consider a normal variable:
uint32_t counter;
The compiler generally understands how the program changes counter.
A peripheral register can behave differently because hardware may change its value without the CPU explicitly executing an instruction that modifies a normal C variable.
For example, a UART status register may change when data is received.
Therefore, hardware registers are commonly accessed through volatile objects or pointers.
volatile uint32_t *status_reg =
(volatile uint32_t *)0x40010000U;The volatile qualifier tells the compiler that accesses to this memory location are significant and should not be treated like ordinary memory accesses that can freely be removed or combined by optimization.
For example:
while ((*status_reg & 0x01U) == 0U)
{
/* Wait for hardware status */
}The program needs to repeatedly read the register because its value may be updated by hardware.
Without appropriate volatile qualification, compiler optimization can potentially result in behavior that does not match the programmer’s intended hardware accesses.
Register Access Using Pointers
A simple register-level program might look like this:
#include
#define GPIO_REG_ADDR 0x40020000U
volatile uint32_t *gpio_reg =
(volatile uint32_t *)GPIO_REG_ADDR;
int main(void)
{
*gpio_reg = 0x00000001U;
while (1)
{
}
}The important statement is:
*gpio_reg = 0x00000001U;
The program is writing the value 1 to the memory address represented by gpio_reg.
If that address corresponds to a GPIO register, the GPIO peripheral interprets the write according to the register specification.
This illustrates the relationship between:
- Embedded C pointers
- Memory addresses
- Hardware registers
- Memory-mapped I/O
- Peripheral control
Bit Manipulation with Hardware Registers
Microcontroller registers usually contain multiple control and status bits.
For example, consider an imaginary 8-bit register:
Bit: 7 6 5 4 3 2 1 0
-----------------
0 0 0 0 0 0 0 0
^
Bit 0Suppose bit 0 enables a peripheral.
Instead of replacing the entire register, the program can set only bit 0:
*reg |= (1U << 0);
To clear bit 0:
*reg &= ~(1U << 0);
To toggle bit 0:
*reg ^= (1U << 0);
To check bit 0:
if (*reg & (1U << 0))
{
/* Bit 0 is set */
}This combines pointers, bit manipulation, and bitwise operators.
The pointer provides access to the register, while the bitwise operation determines which register bits are modified or tested.
Understanding Bit Masking
Bit masking is commonly used when working with hardware registers.
Suppose bit 3 needs to be modified.
The mask can be created using:
uint32_t mask = (1U << 3);
The resulting mask is:
00001000
To set bit 3:
*reg |= mask;
To clear bit 3:
*reg &= ~mask;
To check bit 3:
if (*reg & mask)
{
/* Bit 3 is set */
}Bit masking is important because a register often contains several independent fields.
Changing the entire register when only one bit needs modification can unintentionally affect other settings.
GPIO Register Access Using Pointers
GPIO is a useful example for understanding hardware register access.
A GPIO peripheral may have registers for:
- Pin direction
- Input state
- Output state
- Pull-up or pull-down configuration
- Alternate-function selection
Consider a simplified GPIO register map:
GPIO_DIR → 0x40020000
GPIO_OUT → 0x40020004
GPIO_IN → 0x40020008
These addresses are only examples.
The corresponding pointers could be defined as:
#define GPIO_DIR_ADDR 0x40020000U
#define GPIO_OUT_ADDR 0x40020004U
volatile uint32_t *gpio_dir =
(volatile uint32_t *)GPIO_DIR_ADDR;
volatile uint32_t *gpio_out =
(volatile uint32_t *)GPIO_OUT_ADDR;Suppose GPIO pin 5 needs to be configured as an output:
*gpio_dir |= (1U << 5);
To set the output:
*gpio_out |= (1U << 5);
To clear the output:
*gpio_out &= ~(1U << 5);
The exact register names, addresses, and bit positions depend on the microcontroller.
This example demonstrates how GPIO register access using pointers combines pointer operations with bit manipulation.
Read and Write Registers
Pointers can be used for both reading and writing hardware registers.
Reading a Register
uint32_t status = *status_reg;
The CPU reads the current register value.
Writing a Register
*control_reg = 0x05U;
The CPU writes a value to the register.
Read-Modify-Write
A common operation is:
*control_reg |= (1U << 2);
Conceptually, this involves:
Read current register value
↓
Create bit mask
↓
Set required bit
↓
Write modified valueHowever, read-modify-write should not automatically be used for every register.
Some hardware registers contain fields with special behaviors, such as write-one-to-clear bits, read-to-clear status bits, write-only fields, or reserved bits.
The microcontroller reference manual should always be checked before modifying a hardware register.
ARM Cortex-M Registers and Pointers
ARM Cortex-M microcontrollers make extensive use of memory-mapped registers.
The Cortex-M architecture provides memory-mapped system and processor resources, while individual microcontroller vendors define the addresses and layouts of their own peripheral registers.
In professional development, programmers usually do not manually write every register address.
Instead, CMSIS and vendor device headers commonly provide structures and definitions representing peripheral registers.
A simplified example could be:
typedef struct
{
volatile uint32_t CTRL;
volatile uint32_t STATUS;
volatile uint32_t DATA;
} UART_TypeDef;A peripheral base address could then be represented as:
#define UART_BASE 0x40010000U
#define UART ((UART_TypeDef *)UART_BASE)
Register access becomes:
UART->CTRL = 0x01U;
Although this syntax looks different from manually dereferencing a pointer, the underlying concept is still based on a pointer to a memory-mapped peripheral structure.
Understanding this makes vendor-specific register definitions much easier to understand.
STM32 Registers and Pointer-Based Access
STM32 microcontrollers use memory-mapped peripheral registers extensively.
In STM32 development, device headers commonly provide structures and definitions that allow code such as:
GPIOA->ODR |= (1U << 5);
The programmer does not normally need to manually write the raw register address.
Conceptually, however, GPIOA represents a peripheral located at a defined base address, while ODR represents a register at a defined offset.
The underlying idea can be simplified as:
Peripheral Base Address
↓
Peripheral Structure
↓
Register Offset
↓
Specific Hardware RegisterThis abstraction makes register-level programming easier to read and maintain.
Understanding pointers is therefore useful when learning STM32 registers because it explains what is happening underneath the device header definitions.

UART Registers and Pointers
UART peripherals normally contain several registers associated with:
- Control
- Status
- Transmit data
- Receive data
- Baud-rate configuration
Consider a simplified UART register map:
UART_STATUS → 0x40010000
UART_DATA → 0x40010004
Pointers could be defined as:
#define UART_STATUS_ADDR 0x40010000U
#define UART_DATA_ADDR 0x40010004U
volatile uint32_t *uart_status =
(volatile uint32_t *)UART_STATUS_ADDR;
volatile uint32_t *uart_data =
(volatile uint32_t *)UART_DATA_ADDR;A program could check a status bit before transmitting data:
if (*uart_status & (1U << 7))
{
*uart_data = 'A';
}Bit 7 is only an example. The actual bit position and register address must be obtained from the target microcontroller documentation.
The important concept is that the pointer provides access to the register address, while the register definition determines the meaning of each bit.
Timer Registers and Pointers
Timers are also controlled through hardware registers.
A simplified timer peripheral structure could be:
typedef struct
{
volatile uint32_t CONTROL;
volatile uint32_t STATUS;
volatile uint32_t COUNTER;
volatile uint32_t PRESCALER;
} TIMER_TypeDef;Suppose the timer base address is:
#define TIMER_BASE 0x40030000U
The peripheral can be represented as:
#define TIMER ((TIMER_TypeDef *)TIMER_BASE)
The control register can then be accessed as:
TIMER->CONTROL = 0x01U;
The counter could be read as:
uint32_t count = TIMER->COUNTER;
This is a cleaner approach than manually calculating every register address.
The structure-based technique is widely used in professional embedded development because it gives the code a logical representation of the peripheral register map.
Pointer Arithmetic in Embedded C
Pointer arithmetic is another important concept in C.
For a pointer to a uint32_t:
uint32_t *ptr;
incrementing the pointer:
ptr++;
normally advances it by the size of uint32_t.
If uint32_t is four bytes, an example would be:
Before: 0x40020000
After: 0x40020004
This can appear useful when registers are arranged sequentially.
For example:
uint32_t *ptr =
(uint32_t *)0x40020000U;
ptr++;However, programmers should not assume that every following address represents another valid register.
Peripheral register maps may contain:
- Reserved addresses
- Different register widths
- Gaps between registers
- Special-purpose registers
The register layout should always be taken from the microcontroller’s documentation or official device header.
Hardware Register Access in C: Common Mistakes
Using an Incorrect Register Address
A pointer is only useful when it points to the correct address.
volatile uint32_t *reg =
(volatile uint32_t *)0x12345678U;If this is not a valid register address for the target device, accessing it may cause unexpected behavior or a processor fault.
Always verify the address using the official device documentation.
Using an Incorrect Data Width
A register may be defined as 8-bit, 16-bit, or 32-bit, depending on the hardware.
For example:
volatile uint8_t *reg;
and:
volatile uint32_t *reg;
represent different access widths.
The appropriate type must be selected according to the register specification.
Forgetting volatile
Hardware registers can change because of peripheral activity.
Using an inappropriate non-volatile access can allow compiler optimizations to interfere with the intended register accesses.
Overwriting an Entire Register
Consider:
*reg = 0xFFFFFFFFU;
This changes every writable bit represented by the access.
If only one control bit needs to change, bit masking is often more appropriate:
*reg |= (1U << 5);
However, even bitwise read-modify-write operations need to be used carefully when registers have special hardware semantics.
Modifying Reserved Bits
Many registers contain reserved bits.
Writing arbitrary values to reserved fields can produce unintended behavior.
The device reference manual should be consulted before writing values to hardware registers.
Pointers vs Register Macros
A hardware register can also be represented using a macro.
For example:
#define GPIO_REG (*(volatile uint32_t *)0x40020000U)
The register can then be accessed as:
GPIO_REG |= (1U << 5);
Another approach is to explicitly define a pointer:
volatile uint32_t *gpio_reg =
(volatile uint32_t *)0x40020000U;Then:
*gpio_reg |= (1U << 5);
Both approaches ultimately rely on the same concept: accessing a specific memory address using a correctly typed volatile memory access.
For larger peripherals, a structure-based representation is usually more readable:
typedef struct
{
volatile uint32_t CTRL;
volatile uint32_t STATUS;
volatile uint32_t DATA;
} PERIPHERAL_TypeDef;This approach makes a peripheral’s register organization easier to understand.
Register-Level Programming vs Hardware Abstraction Layers
Modern embedded projects often use abstraction layers such as vendor HALs, drivers, or middleware.
For example, an application may call:
HAL_GPIO_WritePin(...);
instead of directly manipulating a GPIO register.
This does not mean that pointers and registers are no longer important.
The lower-level implementation still needs to communicate with the hardware.
Understanding low level embedded programming helps engineers:
- Debug peripheral problems
- Understand vendor libraries
- Read device headers
- Develop device drivers
- Optimize critical code
- Work with unfamiliar microcontrollers
- Interpret reference manuals
- Diagnose register configuration problems
A high-level API improves maintainability and portability, while register-level knowledge provides a deeper understanding of how the hardware actually operates.
Practical Example: Setting a GPIO Output Bit
The following example demonstrates the complete concept using a hypothetical GPIO register:
#include
#define GPIO_OUT_ADDR 0x40020000U
volatile uint32_t *gpio_out =
(volatile uint32_t *)GPIO_OUT_ADDR;
void gpio_set_pin5(void)
{
*gpio_out |= (1U << 5);
}
void gpio_clear_pin5(void)
{
*gpio_out &= ~(1U << 5);
}
int main(void)
{
gpio_set_pin5();
while (1)
{
}
}The statement:
*gpio_out |= (1U << 5);
can be broken down into several operations.
First, gpio_out contains the register address.
Second, *gpio_out accesses the register.
Third:
(1U << 5)
creates a mask for bit 5.
Finally, the OR operation sets bit 5 while preserving the other bits represented by the register value.
The overall process is:
C Pointer
↓
GPIO Register Address
↓
Dereference Pointer
↓
Read Register
↓
Apply Bit Mask
↓
Write Register
↓
GPIO HardwareThis is a practical example of how register access using pointers works in Embedded C.
Why Register-Level Knowledge Matters for Embedded Engineers
Understanding hardware registers is not only useful for writing low-level code. It also helps engineers understand what happens when a peripheral is configured through a library or development framework.
For example, if a GPIO API configures a pin as an output, somewhere underneath the abstraction, the microcontroller must configure one or more hardware registers.
Similarly, configuring a UART requires registers for baud rate, control, status, and data handling.
A timer requires configuration registers, counter registers, prescaler settings, and status information.
Learning pointers and memory-mapped registers therefore provides a foundation for understanding how these peripherals actually operate.
Key Difference Between a Normal Pointer and a Hardware Register Pointer
A normal C pointer may look like:
uint32_t value = 10;
uint32_t *ptr = &value;
The pointer refers to an ordinary object in program memory.
A hardware register pointer may look like:
volatile uint32_t *reg =
(volatile uint32_t *)0x40020000U;The pointer refers to a memory address associated with hardware.
The syntax is similar, but the purpose and behavior are different.
| Normal C Pointer | Hardware Register Pointer |
| Points to program data | Represents a hardware register address |
| Usually refers to RAM or another object | Refers to memory-mapped peripheral space |
| Used for data manipulation | Used for hardware control or status |
| volatile is not normally required | volatile is commonly required |
| Value is normally changed by software | Hardware may change the value |
The key idea is that a hardware register pointer is still a C pointer, but the address represented by the pointer has a hardware-defined meaning.
Best Practices for Using Pointers with Microcontroller Registers
When working with hardware registers in Embedded C, follow these practices:
Use Official Register Definitions
Do not guess register addresses or bit positions. Use the microcontroller’s reference manual, datasheet, or official device header.
Use Appropriate Data Types
Match the pointer and access width to the register requirements.
Use volatile Correctly
Hardware-controlled registers generally require volatile-qualified access so that necessary memory operations remain observable to the compiler.
Prefer Bit Masks for Individual Fields
Avoid replacing an entire register when only one bit or field needs modification, unless the register documentation specifically requires a full-register write.
Understand Register Semantics
Check whether a register is read-only, write-only, read-to-clear, write-one-to-clear, or has reserved fields before accessing it.
Avoid Hard-Coded Addresses Throughout the Application
Centralize register definitions or use the vendor’s device headers and CMSIS definitions.
Understand the Abstraction You Are Using
Even when using a HAL or driver library, understanding the underlying registers makes debugging much easier.
Conclusion
Pointers are a fundamental part of Embedded C because they provide a direct mechanism for working with memory addresses.
In microcontroller programming, this becomes especially powerful because peripherals are commonly controlled through memory-mapped hardware registers. A pointer can represent the address of a register, and dereferencing that pointer allows software to read or write the associated hardware resource.
The basic relationship is:
Pointer
↓
Memory Address
↓
Hardware Register
↓
Bit or Register Field
↓
Peripheral Hardware
Concepts such as the address operator, pointer dereferencing, volatile keyword, memory-mapped I/O, bitwise operators, bit masking, and register manipulation form the foundation of register-level programming.
Once these concepts are understood, technologies such as ARM Cortex-M and STM32 device headers become easier to understand because their peripheral definitions are built around the same fundamental relationship between addresses, pointers, structures, and hardware registers.
For embedded engineers, learning pointers is therefore not simply about understanding a C language feature. It is about understanding how software communicates with the hardware at the memory level.
