ES: What is Embedded C

Embedded C is a specialized extension of the C programming language designed for microcontrollers and embedded hardware. It enables direct memory-mapped register access, bit manipulation, and hardware-specific control that standard C alone cannot provide.

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

md
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.

md
+---------------------------+
|        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 volatile keyword 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.

c
#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.

c
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.

c
#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:

ExtensionDescription
Fixed-point arithmeticInteger math that avoids floating point on MCUs
Named address spacesMemory regions (flash, RAM, peripheral space)
I/O addressingDirect 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.

c
// 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.

c
// 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

c
// 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

c
// 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

AspectMISRA CCERT C
FocusSafety and reliabilitySecurity and vulnerability prevention
DomainAutomotive, aerospace, medicalBanking, defense, critical infrastructure
AvailabilityPaid standard (MISRA website)Free (SEI wiki)
Primary concernFunctional safetyAttack surface reduction
StandardsISO 26262Common security guidelines

Toolchain for Embedded C

Embedded C development requires tools that differ from desktop C development.

md
[ 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.