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.
Embedded Systems Everywhere
Embedded systems are the invisible backbone of modern technology. Consider the sheer scale:
Embedded Systems by the Numbers
- 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:
- 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
| Feature | Microcontroller (MCU) | Microprocessor (MPU) |
|---|---|---|
| Integration | CPU + RAM + Flash + Peripherals on single chip | CPU only; external memory and peripherals |
| Clock Speed | 8 MHz – 480 MHz typical | 1 GHz – 4+ GHz typical |
| RAM | 2 KB – 1 MB on-chip | 256 MB – 16 GB external |
| Power | Milliwatts to microwatts | Watts to tens of watts |
| OS | Bare-metal or RTOS | Linux, Android, Windows |
| Cost | $0.10 – $15 | $5 – $100+ |
| Boot Time | Milliseconds | Seconds to minutes |
| Example | STM32, ATmega, ESP32 | Raspberry Pi, BeagleBone |
MCU Architecture
A microcontroller integrates everything needed to run a program on a single silicon die. The typical architecture includes:
- 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
Popular MCU Families
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-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
- Event occurs — external pin change, timer overflow, UART byte received
- NVIC detects the interrupt request and checks priority
- CPU stacks context — pushes R0-R3, R12, LR, PC, xPSR onto the stack
- ISR executes — runs the handler function for that interrupt vector
- CPU restores context — pops saved registers, resumes main code
Latency: ARM Cortex-M achieves 12-cycle interrupt latency — from event to first ISR instruction.
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.
- 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 Type | Purpose | Size (Typical) | Speed |
|---|---|---|---|
| Flash | Program code & constants | 16 KB – 2 MB | ~24 MHz wait states |
| SRAM | Variables, stack, heap | 4 KB – 1 MB | Zero wait state |
| EEPROM | Persistent settings, calibration | 256 B – 64 KB | ~5 ms write cycle |
| Backup SRAM | Battery-backed data | 4 KB | Zero 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
- 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
- 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
#!/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.
Blink an LED
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.
- 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.