In embedded development, software interacts directly with hardware. This typically means reading from and writing to memory-mapped registers, dynamically selecting hardware-specific behaviors, and manipulating individual bits inside registers without disturbing others.
This article introduces three fundamental C techniques that every embedded developer must master.
🧱 Operating Hardware Registers #
Embedded peripherals expose control and status registers at fixed memory addresses. Accessing these registers in C requires casting the address to a pointer and dereferencing it.
Single Register Access #
A common macro definition for a hardware register looks like this:
#define GSTATUS1 (*(volatile unsigned int *)0x560000B0)
Why This Works #
-
0x560000B0The physical memory address of the hardware register. -
unsigned int *Casts the address to a pointer to a 32-bit register. -
volatilePrevents the compiler from optimizing accesses. Every read or write must hit the actual hardware, because the value may change asynchronously (interrupts, DMA, peripherals). -
Dereference (
*) Converts the pointer into an lvalue, allowing direct read/write access.
Usage example:
GSTATUS1 = 0x1; // write register
if (GSTATUS1 & 0x1) { // read register
// ...
}
Register Access via Structures #
For peripherals with many related registers, structures provide a cleaner and safer abstraction.
typedef struct {
volatile unsigned int NFCONF;
// ...
volatile unsigned int NFSTAT;
// ...
} S3C2410_NAND;
#define NAND_BASE 0x4e000000
static S3C2410_NAND *s3c2410nand = (S3C2410_NAND *)NAND_BASE;
Accessing a register becomes intuitive:
if (s3c2410nand->NFSTAT & 0x01) {
// NAND ready
}
Benefits
- Clear register grouping
- Fewer magic addresses
- Easier maintenance and portability
🧠 Operating Function Pointers #
Function pointers store the address of a function, enabling indirect calls and runtime behavior selection. They are heavily used in drivers, HAL layers, and portable firmware.
Basic Function Pointer Usage #
int max(int a, int b) { return (a > b) ? a : b; }
int (*func)(int, int);
int main(void)
{
func = max;
int result = func(3, 5);
}
Advanced Use: Portable NAND Flash Driver #
Function pointers allow the same driver logic to work across multiple chip variants.
typedef struct {
void (*nand_reset)(void);
void (*wait_idle)(void);
unsigned char (*read_data)(void);
} t_nand_chip;
static t_nand_chip nand_chip;
Chip-specific implementations:
static void s3c2410_nand_reset(void);
static void s3c2440_nand_reset(void);
Runtime selection:
void nand_init(void)
{
if ((GSTATUS1 == 0x32410000) || (GSTATUS1 == 0x32410002)) {
nand_chip.nand_reset = s3c2410_nand_reset;
} else {
nand_chip.nand_reset = s3c2440_nand_reset;
}
nand_chip.nand_reset();
}
Why This Matters #
- Abstraction: Upper layers call
nand_chip.read_data()without caring about the hardware. - Portability: Supporting a new SoC only requires adding new assignments.
- Clean architecture: Hardware differences are isolated in one place.
🧩 Operating Register Bits (Bit Manipulation) #
Hardware control often requires modifying one bit without changing others. C bitwise operators make this safe and efficient.
Clearing a Bit (Set to 0) #
Clear bit 3 of GPFCON:
GPFCON &= ~(0x1 << 3);
Explanation
0x1 << 3creates a mask:00001000~inverts it:11110111&=clears only bit 3
Setting a Bit (Set to 1) #
GPFCON |= (0x1 << 3);
Explanation
|=forces bit 3 to 1- All other bits remain unchanged
Why This Is Critical #
- Prevents accidental register corruption
- Required for configuring GPIO, clocks, interrupts, and peripherals
- Essential for real-time and safety-critical systems
📝 Summary #
These three techniques form the foundation of embedded C programming:
- Register access: Direct, precise hardware control
- Function pointers: Portability and runtime flexibility
- Bit manipulation: Safe and deterministic register updates
Mastering them enables you to write efficient, portable, and maintainable embedded software, whether you are building bootloaders, drivers, RTOS components, or bare-metal firmware.