Embedded C sits at the boundary between software and hardware. It gives developers the power to write code that directly controls physical devices — toggling GPIO pins, configuring peripherals, and reacting to real-world events — all through clean, structured C syntax extended with hardware-aware concepts.
Learning Embedded C is the essential first step toward writing firmware for any microcontroller platform.
What is Embedded C?
Embedded C is not a separate language. It is C extended with:
- Direct hardware access through memory-mapped registers
- Bit manipulation to configure individual hardware bits
- Volatile qualifiers to prevent compiler optimizations that would break hardware reads/writes
- Fixed-point arithmetic support (ISO/IEC TR 18037)
- Named address spaces for memory regions
- Hardware-specific I/O addressing
Standard C Embedded C
----------- ----------
Runs on OS Runs on bare metal
Uses system calls Uses register access
Hardware is abstracted Hardware is explicit
No timing constraints Real-time requirements
The programmer takes direct responsibility for configuring every hardware register by name or address.
Why Embedded C is Different
On a desktop computer, the operating system manages hardware on your behalf. In an embedded system, there is no OS layer between your code and the hardware.
+---------------------------+
| Your Code |
+---------------------------+
| Hardware Registers | <-- You configure these directly
+---------------------------+
| Microcontroller |
+---------------------------+
This means:
- Timing must be explicit and deterministic
- Memory must be managed manually
- Every peripheral must be initialized before use
- The
volatilekeyword prevents the compiler from caching register values
Key Concepts in Embedded C
Memory-Mapped Registers
Microcontrollers expose hardware peripherals as memory addresses. You read and write these addresses to control hardware behavior.
#define PORTB (*(volatile unsigned char*)0x25) // Output register
#define DDRB (*(volatile unsigned char*)0x24) // Data direction register
int main() {
DDRB |= (1 << 0); // Set PB0 as output
PORTB |= (1 << 0); // Set PB0 HIGH (turn on LED)
while (1);
}
The volatile keyword tells the compiler: do not optimize this away — the hardware can change this value at any time.
Register Structures (ARM Approach)
Modern ARM-based microcontrollers use typedef struct to group peripheral registers into a single named structure mapped to a base address.
typedef struct {
volatile unsigned int MODER; // Mode register (Offset 0x00)
volatile unsigned int OTYPER; // Output type (Offset 0x04)
volatile unsigned int OSPEEDR; // Output speed (Offset 0x08)
volatile unsigned int PUPDR; // Pull-up/Pull-down (Offset 0x0C)
volatile unsigned int IDR; // Input data register (Offset 0x10)
volatile unsigned int ODR; // Output data register (Offset 0x14)
volatile unsigned int BSRR; // Bit set/reset (Offset 0x18)
volatile unsigned int LCKR; // Lock register (Offset 0x1C)
volatile unsigned int AFRL; // Alternate func low (Offset 0x20)
volatile unsigned int AFRH; // Alternate func high (Offset 0x24)
} GPIO_TypeDef;
#define GPIOB ((GPIO_TypeDef *)0x40020400) // Base address of GPIOB
int main() {
GPIOB->MODER |= (1 << 0); // Set PB0 as output
GPIOB->ODR |= (1 << 0); // Set PB0 HIGH
while (1);
}
This approach is clean, readable, and used by nearly every ARM Cortex-M vendor HAL today.
Super Loop Design Pattern
The most fundamental embedded software pattern is the super loop (also called bare-metal loop). There is no scheduler or OS — the program loops forever, reacting to hardware.
#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>
int main(void) {
DDRB |= (1 << DDB5); // Set PORTB5 as output
DDRD &= ~(1 << PD0); // Configure PD0 as input
if (PIND & (1 << PD0)) { // Check if button is pressed
PORTB |= (1 << PB0); // Turn on LED
}
while (1) {
PORTB ^= (1 << PORTB5); // Toggle PORTB5
_delay_ms(1000); // Delay for 1000 milliseconds
}
return 0;
}
This pattern runs forever and directly drives hardware without any OS abstraction.
Embedded C Standards and Extensions
ISO/IEC TR 18037
The official Embedded C extension to standard C includes:
| Extension | Description |
|---|---|
| Fixed-point arithmetic | Integer math that avoids floating point on MCUs |
| Named address spaces | Memory regions (flash, RAM, peripheral space) |
| I/O addressing | Direct port and register access |
Coding Standards for Safety-Critical Embedded C
Two major coding standards govern how Embedded C is written in safety-critical domains.
MISRA C
MISRA (Motor Industry Software Reliability Association) defines a set of coding guidelines for C and C++ to improve safety, security, and portability. It is widely used in:
- Automotive software (ISO 26262 functional safety)
- Aerospace systems
- Medical devices
The most current version is MISRA C:2012 (with amendments).
Rule: Avoid `goto`
Unrestricted jumps make code flow unpredictable and unverifiable.
// Non-compliant MISRA C
void example(int x) {
if (x < 0) {
goto ERROR;
}
return;
ERROR:
printf("Error occurred!\n");
}
// Compliant MISRA C
void example(int x) {
if (x < 0) {
printf("Error occurred!\n");
return;
}
}
Rule: Avoid Dynamic Memory Allocation
malloc and free introduce non-determinism and fragmentation that are unacceptable in safety-critical systems.
// Non-compliant MISRA C
void example() {
int *arr = (int *)malloc(10 * sizeof(int)); // Dynamic allocation is risky
if (arr == NULL) { return; }
free(arr);
}
// Compliant MISRA C
void example() {
int arr[10]; // Use static allocation instead
}
CERT C
CERT C (Secure Coding Standard for C) was developed by Carnegie Mellon University's Software Engineering Institute to prevent security vulnerabilities in C programs.
It focuses on:
- Buffer overflows
- Integer overflows
- Memory corruption
- Null pointer dereferences
CERT C is free and publicly available at the SEI wiki.
Rule: Prevent Buffer Overflow
// Non-compliant CERT C
void unsafeFunction(char *input) {
char buffer[10];
strcpy(buffer, input); // No bounds check — buffer overflow risk
}
// Compliant CERT C
void safeFunction(char *input) {
char buffer[10];
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Ensure null termination
}
Rule: Check for NULL Before Dereferencing
// Non-compliant CERT C and MISRA C
void example() {
int *ptr = NULL;
*ptr = 5; // Dereferencing NULL causes a crash
}
// Compliant CERT C
void example() {
int *ptr = NULL;
if (ptr != NULL) {
*ptr = 5;
}
}
Comparison: MISRA C vs CERT C
| Aspect | MISRA C | CERT C |
|---|---|---|
| Focus | Safety and reliability | Security and vulnerability prevention |
| Domain | Automotive, aerospace, medical | Banking, defense, critical infrastructure |
| Availability | Paid standard (MISRA website) | Free (SEI wiki) |
| Primary concern | Functional safety | Attack surface reduction |
| Standards | ISO 26262 | Common security guidelines |
Toolchain for Embedded C
Embedded C development requires tools that differ from desktop C development.
[ Source Code (.c, .h) ]
|
v
[ Preprocessor ] --> Expands #include, #define
|
v
[ Compiler ] --> Produces object files (.o)
|
v
[ Linker ] --> Combines object files and libraries
|
v
[ Locator ] --> Places code in memory regions via linker script
|
v
[ Binary Image (.elf, .hex, .bin) ]
|
v
[ Flash to Target Hardware ]
Key tools include:
- Cross-compilers:
arm-none-eabi-gcc,avr-gcc - Debuggers: JTAG, SWD, GDB
- IDEs: Keil uVision, STM32CubeIDE, PlatformIO
- Linker scripts: Define memory layout for flash, RAM, and peripherals
Final Thoughts
Embedded C is the lingua franca of microcontroller firmware. It combines the power and portability of C with hardware-level directness that no other widely-adopted language currently provides at the same scale.
Understanding Embedded C means understanding:
- How registers map to hardware
- How volatile prevents dangerous optimizations
- How bit manipulation configures peripherals
- How coding standards make systems safe and secure
Embedded C does not hide the hardware — it reveals it.