Back to Sensors & Actuators Series

Part 1: Foundations of Embedded Systems

July 14, 2025 Wasil Zafar 45 min read

Discover what embedded systems are, explore core components like microcontrollers and GPIOs, and write your first embedded programs across Arduino, ESP32, and Raspberry Pi platforms.

Table of Contents

  1. What Are Embedded Systems?
  2. Microcontrollers vs Microprocessors
  3. GPIO Fundamentals
  4. Interrupts & Timers
  5. Memory Architecture
  6. Development Platforms
  7. Your First Embedded Programs
  8. Conclusion & Next Steps

What Are Embedded Systems?

An embedded system is a specialized computer designed to perform dedicated functions within a larger mechanical or electrical system. Unlike general-purpose computers, embedded systems are optimized for specific tasks — controlling a washing machine, monitoring engine temperature, or guiding a surgical robot.

Core Definition: An embedded system is a combination of hardware and software designed to perform a specific function, often with real-time computing constraints. It is “embedded” as part of a complete device including hardware, mechanical parts, and firmware.

Embedded Systems Everywhere

Embedded systems are the invisible backbone of modern technology. Consider the sheer scale:

Embedded Systems by the Numbers

Industry Data 2024 Estimates
  • 98% of all processors manufactured go into embedded systems, not PCs or servers
  • A modern car contains 70–150 ECUs (Electronic Control Units)
  • A Boeing 787 runs on 6.5 million lines of embedded code
  • The average smart home has 20+ embedded devices communicating continuously
  • Global embedded systems market exceeds $116 billion annually

Key Characteristics

Embedded systems share several defining traits that distinguish them from general-purpose computing:

Embedded System Characteristics:
  • Task-Specific: Designed for one primary function or a narrow set of functions
  • Resource-Constrained: Limited memory (KB to MB), processing power, and storage
  • Real-Time: Must respond within strict timing deadlines (hard or soft real-time)
  • Low Power: Battery-operated devices may run for years on a coin cell
  • High Reliability: Safety-critical systems must operate continuously without failure
  • Cost-Sensitive: Mass production demands per-unit costs of pennies to dollars

Microcontrollers vs Microprocessors

The heart of any embedded system is its processing unit. Understanding the difference between microcontrollers (MCUs) and microprocessors (MPUs) is fundamental to embedded design.

MCU vs MPU Comparison

Architecture Core Concepts
FeatureMicrocontroller (MCU)Microprocessor (MPU)
IntegrationCPU + RAM + Flash + Peripherals on single chipCPU only; external memory and peripherals
Clock Speed8 MHz – 480 MHz typical1 GHz – 4+ GHz typical
RAM2 KB – 1 MB on-chip256 MB – 16 GB external
PowerMilliwatts to microwattsWatts to tens of watts
OSBare-metal or RTOSLinux, Android, Windows
Cost$0.10 – $15$5 – $100+
Boot TimeMillisecondsSeconds to minutes
ExampleSTM32, ATmega, ESP32Raspberry Pi, BeagleBone

MCU Architecture

A microcontroller integrates everything needed to run a program on a single silicon die. The typical architecture includes:

MCU Internal Components:
  • CPU Core: ARM Cortex-M0/M3/M4/M7, AVR, RISC-V, or PIC
  • Flash Memory: Non-volatile program storage (16 KB – 2 MB)
  • SRAM: Volatile data memory (2 KB – 1 MB)
  • GPIO Pins: General Purpose Input/Output for connecting external hardware
  • ADC/DAC: Analog-to-Digital and Digital-to-Analog converters
  • Timers: Hardware counters for timing, PWM generation, and event counting
  • Comm Peripherals: UART, SPI, I2C, CAN, USB controllers
  • Interrupt Controller: NVIC (Nested Vectored Interrupt Controller) on ARM

MCU Family Overview

ARM Cortex-M Series dominates the 32-bit embedded market:

  • Cortex-M0/M0+: Ultra-low power, simple applications (STM32F0, nRF52)
  • Cortex-M3: General-purpose, balanced performance (STM32F1/F2)
  • Cortex-M4: DSP instructions, FPU for signal processing (STM32F4, nRF5340)
  • Cortex-M7: High-performance, dual-issue pipeline (STM32H7, i.MX RT)
  • Cortex-M33: TrustZone security for IoT (STM32L5, LPC55S69)

Other Architectures:

  • AVR: 8-bit simplicity — ATmega328P (Arduino Uno)
  • Xtensa: ESP32 dual-core with WiFi/BLE built-in
  • RISC-V: Open-source ISA gaining rapid adoption (ESP32-C3, GD32V)

GPIO Fundamentals

General Purpose Input/Output (GPIO) pins are the primary interface between a microcontroller and the physical world. Every sensor reading, LED toggle, and motor command flows through GPIO pins.

Digital I/O

Digital GPIO pins operate in two states: HIGH (logic 1, typically 3.3V or 5V) and LOW (logic 0, 0V). Each pin can be configured as either input or output via register configuration.

// STM32 GPIO Configuration Example (bare-metal)
#include "stm32f4xx.h"

int main(void) {
    // Enable clock for GPIOA peripheral
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // Configure PA5 as output (LED on Nucleo board)
    // MODER register: 2 bits per pin, 01 = output mode
    GPIOA->MODER &= ~(3U << (5 * 2));  // Clear bits
    GPIOA->MODER |=  (1U << (5 * 2));  // Set output mode

    // Configure PA5 as push-pull output
    GPIOA->OTYPER &= ~(1U << 5);

    // Set medium speed
    GPIOA->OSPEEDR |= (1U << (5 * 2));

    // Toggle LED
    while (1) {
        GPIOA->ODR ^= (1U << 5);    // Toggle PA5
        for (volatile int i = 0; i < 100000; i++);  // Simple delay
    }
}

Pull-Up & Pull-Down Resistors

When a GPIO input pin is not connected to a definite HIGH or LOW signal, it “floats” and reads random noise. Pull-up and pull-down resistors solve this by providing a default state.

Pull Resistor Rules:
  • Pull-Up (to VCC): Pin defaults HIGH when switch is open; reads LOW when pressed
  • Pull-Down (to GND): Pin defaults LOW when switch is open; reads HIGH when pressed
  • Internal Pull-Ups: Most MCUs have configurable internal pull resistors (20–50 kΩ)
  • Typical Values: External pull resistors are usually 4.7 kΩ to 10 kΩ
// Enable internal pull-up on STM32 PA0
#include "stm32f4xx.h"

void configure_button(void) {
    // Enable GPIOA clock
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // PA0 as input (MODER = 00)
    GPIOA->MODER &= ~(3U << (0 * 2));

    // Enable internal pull-up (PUPDR = 01)
    GPIOA->PUPDR &= ~(3U << (0 * 2));
    GPIOA->PUPDR |=  (1U << (0 * 2));
}

int read_button(void) {
    // Read PA0 input data register
    return (GPIOA->IDR & (1U << 0)) ? 1 : 0;
}

Interrupts & Timers

Interrupts are the mechanism that allows an MCU to respond to events immediately without wasting CPU cycles polling. When an interrupt fires, the CPU suspends its current task, executes an Interrupt Service Routine (ISR), and returns.

Interrupt Mechanism

Interrupt Processing Flow

ARM Cortex-M NVIC
  1. Event occurs — external pin change, timer overflow, UART byte received
  2. NVIC detects the interrupt request and checks priority
  3. CPU stacks context — pushes R0-R3, R12, LR, PC, xPSR onto the stack
  4. ISR executes — runs the handler function for that interrupt vector
  5. CPU restores context — pops saved registers, resumes main code

Latency: ARM Cortex-M achieves 12-cycle interrupt latency — from event to first ISR instruction.

Interrupt Processing Flow
flowchart TD
    A["🔌 Event Occurs
Pin change / Timer / UART"] --> B["NVIC Detects
Interrupt Request"] B --> C{"Priority Check
Higher than current?"} C -->|Yes| D["CPU Stacks Context
R0-R3, R12, LR, PC, xPSR"] C -->|No| E["Pend Until
Current ISR Completes"] E --> D D --> F["ISR Executes
Handler Function"] F --> G["CPU Restores Context
Pop Saved Registers"] G --> H["Resume Main Code"] style A fill:#3B9797,stroke:#3B9797,color:#fff style H fill:#132440,stroke:#132440,color:#fff style C fill:#fff5f5,stroke:#BF092F,color:#132440 style F fill:#e8f4f4,stroke:#3B9797,color:#132440
// External interrupt on STM32 PA0 (button press)
#include "stm32f4xx.h"

volatile uint8_t button_pressed = 0;

void EXTI0_IRQHandler(void) {
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR |= EXTI_PR_PR0;   // Clear pending bit
        button_pressed = 1;          // Set flag
        GPIOA->ODR ^= (1U << 5);   // Toggle LED
    }
}

void configure_exti(void) {
    // Enable SYSCFG clock for EXTI configuration
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

    // Map EXTI0 to PA0
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;

    // Configure EXTI0 for falling edge trigger
    EXTI->FTSR |= EXTI_FTSR_TR0;
    EXTI->IMR  |= EXTI_IMR_MR0;

    // Enable EXTI0 interrupt in NVIC
    NVIC_EnableIRQ(EXTI0_IRQn);
    NVIC_SetPriority(EXTI0_IRQn, 2);
}

Timer Peripherals

Hardware timers are essential for precise timing, PWM generation, and event counting without CPU intervention.

Timer Applications:
  • Periodic Interrupts: Execute code at exact intervals (1 ms tick, 100 Hz sampling)
  • PWM Generation: Control motor speed, LED brightness, servo positions
  • Input Capture: Measure signal frequency, pulse width, period
  • Output Compare: Generate precise waveforms and trigger events
  • Watchdog Timer: Reset the MCU if software hangs (fault recovery)
// Timer-based 1ms tick on STM32F4 (84 MHz APB1 clock)
#include "stm32f4xx.h"

volatile uint32_t tick_count = 0;

void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;   // Clear update flag
        tick_count++;
    }
}

void timer_init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    TIM2->PSC = 84 - 1;       // Prescaler: 84 MHz / 84 = 1 MHz
    TIM2->ARR = 1000 - 1;     // Auto-reload: 1 MHz / 1000 = 1 kHz (1 ms)
    TIM2->DIER |= TIM_DIER_UIE;  // Enable update interrupt
    TIM2->CR1 |= TIM_CR1_CEN;    // Start timer

    NVIC_EnableIRQ(TIM2_IRQn);
    NVIC_SetPriority(TIM2_IRQn, 1);
}

void delay_ms(uint32_t ms) {
    uint32_t start = tick_count;
    while ((tick_count - start) < ms);
}

Memory Architecture

Flash, SRAM & EEPROM

Embedded systems use a flat memory model with distinct regions for different purposes:

MCU Memory Map

Memory TypePurposeSize (Typical)Speed
FlashProgram code & constants16 KB – 2 MB~24 MHz wait states
SRAMVariables, stack, heap4 KB – 1 MBZero wait state
EEPROMPersistent settings, calibration256 B – 64 KB~5 ms write cycle
Backup SRAMBattery-backed data4 KBZero wait state

Memory-Mapped I/O

In ARM Cortex-M processors, peripherals are accessed through memory-mapped registers. Each peripheral (GPIO, UART, Timer) has a base address, and individual control/data registers are at fixed offsets.

// Direct register access via memory-mapped I/O
// GPIOA base address on STM32F4: 0x40020000

#include <stdint.h>

#define GPIOA_BASE    0x40020000UL
#define GPIOA_MODER   (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_ODR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

void toggle_led(void) {
    // Set PA5 as output
    GPIOA_MODER &= ~(3U << 10);
    GPIOA_MODER |=  (1U << 10);

    // Toggle PA5
    GPIOA_ODR ^= (1U << 5);
}

Development Platforms

Three platforms dominate the hobbyist and professional embedded landscape, each serving different complexity levels.

Arduino

Arduino Ecosystem:
  • Hardware: ATmega328P (Uno), SAMD21 (Zero), ESP32-based boards
  • IDE: Arduino IDE with simplified C++ wrappers
  • Strengths: Massive library ecosystem, beginner-friendly, huge community
  • Limitations: Abstracts hardware details, limited for production use
  • Best For: Prototyping, learning, hobby projects
// Arduino: Blink LED on pin 13
// Complete, standalone sketch

void setup() {
    pinMode(13, OUTPUT);       // Configure pin 13 as output
    Serial.begin(115200);      // Initialize serial for debugging
    Serial.println("LED Blink initialized");
}

void loop() {
    digitalWrite(13, HIGH);    // LED on
    delay(500);                // Wait 500ms
    digitalWrite(13, LOW);     // LED off
    delay(500);                // Wait 500ms
    Serial.println("Blink!");  // Debug output
}

ESP32

ESP32 Features:
  • CPU: Dual-core Xtensa LX6 @ 240 MHz
  • Connectivity: WiFi 802.11 b/g/n + Bluetooth 4.2/BLE built-in
  • Peripherals: 34 GPIO, 18 ADC channels, 2 DAC, 4 SPI, 2 I2C, 3 UART
  • Memory: 520 KB SRAM, 4 MB Flash (external)
  • Best For: IoT projects, WiFi-connected sensors, BLE devices
// ESP32: Read temperature sensor and send via WiFi
// Uses Arduino framework on ESP32

#include <WiFi.h>

const char* ssid = "YourNetwork";
const char* password = "YourPassword";
const int sensorPin = 34;  // ADC1 channel 6

void setup() {
    Serial.begin(115200);
    analogReadResolution(12);   // 12-bit ADC (0-4095)

    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
}

void loop() {
    int rawValue = analogRead(sensorPin);
    float voltage = rawValue * (3.3 / 4095.0);
    float tempC = (voltage - 0.5) * 100.0;  // LM35 formula

    Serial.printf("Raw: %d, Voltage: %.2fV, Temp: %.1f°C\n",
                  rawValue, voltage, tempC);
    delay(2000);
}

Raspberry Pi

Important Distinction: Raspberry Pi is a single-board computer (SBC), not a microcontroller. It runs a full Linux OS, which means it is NOT suitable for hard real-time applications. Use it for prototyping, data processing, UI, and networking — pair it with an MCU for time-critical sensor/actuator control.
#!/usr/bin/env python3
"""Raspberry Pi: GPIO LED blink using gpiozero library"""

from gpiozero import LED, Button
from time import sleep

# Setup hardware
led = LED(17)           # GPIO17 connected to LED
button = Button(27)     # GPIO27 connected to button

# Blink LED
print("Starting LED blink on GPIO17")
for i in range(10):
    led.on()
    print(f"LED ON  (cycle {i+1}/10)")
    sleep(0.5)
    led.off()
    print(f"LED OFF (cycle {i+1}/10)")
    sleep(0.5)

# React to button press
print("Waiting for button press on GPIO27...")
button.wait_for_press()
print("Button pressed!")
led.toggle()

Your First Embedded Programs

Every embedded engineer starts with three fundamental exercises: blinking an LED, reading a button, and serial communication. These exercises validate your toolchain and hardware setup.

The “Hello World” of embedded systems. If you can blink an LED, your toolchain, programmer, and hardware connections are all working correctly.

// Bare-metal LED blink on STM32F4 Nucleo (PA5)
#include "stm32f4xx.h"

int main(void) {
    // 1. Enable GPIOA clock
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 2. Set PA5 as general-purpose output
    GPIOA->MODER &= ~(3U << 10);   // Clear PA5 mode bits
    GPIOA->MODER |=  (1U << 10);   // Set output mode (01)

    // 3. Infinite blink loop
    while (1) {
        GPIOA->BSRR = (1U << 5);           // Set PA5 HIGH (LED on)
        for (volatile int i = 0; i < 500000; i++);

        GPIOA->BSRR = (1U << (5 + 16));    // Reset PA5 LOW (LED off)
        for (volatile int i = 0; i < 500000; i++);
    }
}

Read a Button

Reading digital inputs with proper debouncing is critical for reliable operation. Mechanical buttons produce electrical noise (bouncing) that can register multiple false presses.

// Button debounce using timer-based approach
#include "stm32f4xx.h"

#define DEBOUNCE_MS  50
extern volatile uint32_t tick_count;  // From timer ISR

typedef struct {
    uint8_t state;
    uint8_t last_raw;
    uint32_t last_change;
} Debounce_t;

uint8_t debounce_read(Debounce_t *btn, uint8_t raw_state) {
    if (raw_state != btn->last_raw) {
        btn->last_change = tick_count;
        btn->last_raw = raw_state;
    }
    if ((tick_count - btn->last_change) >= DEBOUNCE_MS) {
        btn->state = btn->last_raw;
    }
    return btn->state;
}

// Usage in main loop
Debounce_t user_button = {0, 0, 0};

void check_button(void) {
    uint8_t raw = (GPIOC->IDR & (1U << 13)) ? 0 : 1;  // Active low
    uint8_t stable = debounce_read(&user_button, raw);

    if (stable) {
        GPIOA->ODR ^= (1U << 5);   // Toggle LED on press
    }
}

Serial Debugging

UART serial communication is the embedded engineer’s primary debugging tool. It provides a text interface to monitor variables, state machines, and error conditions.

// UART printf-style debugging on STM32
#include "stm32f4xx.h"
#include <stdio.h>

void uart2_init(uint32_t baud) {
    // Enable clocks
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

    // PA2 = TX, PA3 = RX (Alternate Function 7)
    GPIOA->MODER  |= (2U << 4) | (2U << 6);   // AF mode
    GPIOA->AFR[0] |= (7U << 8) | (7U << 12);  // AF7

    // Configure USART2
    USART2->BRR = 84000000 / baud;  // 84 MHz APB1
    USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}

void uart2_putchar(char c) {
    while (!(USART2->SR & USART_SR_TXE));
    USART2->DR = c;
}

// Retarget printf to UART
int _write(int fd, char *ptr, int len) {
    for (int i = 0; i < len; i++) {
        uart2_putchar(ptr[i]);
    }
    return len;
}

int main(void) {
    uart2_init(115200);
    printf("System initialized at 84 MHz\r\n");
    printf("Flash: %lu KB, SRAM: %lu KB\r\n",
           *(uint16_t*)0x1FFF7A22,
           (SRAM_END - SRAM_BASE + 1) / 1024);

    uint32_t counter = 0;
    while (1) {
        printf("[%08lu] Heartbeat\r\n", counter++);
        for (volatile int i = 0; i < 1000000; i++);
    }
}

Conclusion & Next Steps

You now have a solid foundation in embedded systems. You understand the role of microcontrollers, how GPIO pins interface with the physical world, how interrupts enable real-time response, and how to write your first programs across multiple platforms.

Key Takeaways:
  • Embedded systems are task-specific, resource-constrained computing platforms found everywhere
  • MCUs integrate CPU, memory, and peripherals on a single chip — MPUs need external components
  • GPIO pins are your interface to sensors and actuators — master register-level control
  • Interrupts enable real-time response without wasting CPU cycles on polling
  • Start with Arduino for prototyping, graduate to bare-metal STM32 for professional work

In Part 2, we’ll dive deep into Understanding Sensors — exploring transducers, analog vs digital signals, sensor characteristics like accuracy and sensitivity, and the physics behind how sensors convert real-world phenomena into electrical signals.