Embedded Systems Tutorial: Learn Embedded from Scratch (2026)
Embedded systems control everything from your car's braking system to medical implants to satellite communications. After designing embedded firmware for industrial and consumer products, I have learned that embedded development demands a deep understanding of hardware constraints — limited memory, real-time deadlines, power budgets — that desktop programmers rarely consider. This tutorial bridges the gap between software engineering and electrical engineering, covering microcontrollers, RTOS, sensor integration, and IoT connectivity.
You will learn to read datasheets, configure peripherals via memory-mapped registers, write interrupt handlers, and design energy-efficient firmware.
Microcontroller Architecture and Memory Map
A microcontroller integrates a CPU core, memory (flash for code, SRAM for data), and programmable peripherals on a single chip. Unlike a desktop CPU, microcontrollers have a flat memory map where peripherals are accessed through memory-mapped registers. The ARM Cortex-M series dominates with its Thumb-2 instruction set, vector table for interrupt handling, and sleep modes for power saving.
#include
extern uint32_t _estack;
extern int main(void);
void Reset_Handler(void);
void Default_Handler(void) { while(1); }
__attribute__((section(".isr_vector")))
void (* const vector_table[])(void) = {
(void(*)(void))(&_estack), Reset_Handler, Default_Handler, Default_Handler,
};
void Reset_Handler(void) {
extern uint32_t _sdata, _edata, _sidata;
for (uint32_t *src=&_sidata,*dst=&_sdata; dst<&_edata;) { *dst++ = *src++; }
extern uint32_t _sbss, _ebss;
for (uint32_t *dst=&_sbss; dst<&_ebss;) { *dst++ = 0; }
main(); while(1);
}
GPIO, Timers, and Interrupts
General Purpose Input/Output (GPIO) pins are the most basic way to interact with the physical world. Each GPIO port is configured via registers: MODER sets input/output, OTYPER selects push-pull or open-drain, and PUPDR enables pull-up/pull-down resistors. Timers generate precise delays, PWM signals, or capture input pulse widths. The NVIC prioritizes and dispatches interrupts with minimal latency.
#include "stm32f4xx.h"
void GPIO_Init(void) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
GPIOD->MODER &= ~GPIO_MODER_MODER12_Msk;
GPIOD->MODER |= GPIO_MODER_MODER12_0;
GPIOD->OTYPER &= ~GPIO_OTYPER_OT12;
}
void SysTick_Handler(void) {
static uint32_t ticks = 0;
if (++ticks >= 500) { ticks = 0; GPIOD->ODR ^= GPIO_ODR_OD12; }
}
int main(void) {
GPIO_Init(); SystemCoreClockUpdate();
SysTick_Config(SystemCoreClock / 1000);
while(1) { __WFI(); }
}
Real-Time Operating Systems (FreeRTOS)
A real-time operating system (RTOS) provides deterministic task scheduling, inter-task communication, and synchronization primitives. FreeRTOS is the most widely used embedded RTOS. Tasks have priorities, and the scheduler always runs the highest-priority ready task. Queues enable task-to-task and ISR-to-task communication. Semaphores protect shared resources.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
QueueHandle_t sensor_queue;
void SensorTask(void *pv) {
int val;
while(1) { val = read_adc(); xQueueSend(sensor_queue, &val, portMAX_DELAY); vTaskDelay(100); }
}
void ProcTask(void *pv) {
int val;
while(1) {
if (xQueueReceive(sensor_queue, &val, portMAX_DELAY) == pdTRUE) {
if (apply_filter(val) > THRESHOLD) activate_actuator();
}
}
}
int main(void) {
hardware_init();
sensor_queue = xQueueCreate(10, sizeof(int));
xTaskCreate(SensorTask, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(ProcTask, "Proc", 256, NULL, 1, NULL);
vTaskStartScheduler(); while(1);
}
Communication Protocols: I2C, SPI, and UART
I2C, SPI, and UART are the three most common serial communication protocols. I2C uses two wires (SCL, SDA) with addressing. SPI uses four wires for full-duplex communication at higher speeds. UART is asynchronous — both sides must agree on a baud rate. Protocol selection involves trade-offs in speed, pin count, and complexity.
void i2c_read_sensor(uint8_t dev, uint8_t reg, uint8_t* data, uint8_t len) {
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = dev << 1; while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2;
I2C1->DR = reg; while (!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = (dev << 1) | 1; while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2;
for (uint8_t i = 0; i < len; i++) {
if (i == len-1) I2C1->CR1 &= ~I2C_CR1_ACK;
while (!(I2C1->SR1 & I2C_SR1_RXNE)); data[i] = I2C1->DR;
}
I2C1->CR1 |= I2C_CR1_STOP;
}
Power Management and Energy Efficiency
Energy efficiency is critical for battery-powered embedded systems. Microcontrollers offer multiple power modes: Run, Sleep, Stop, and Standby. The key strategy is to spend as much time as possible in low-power modes, waking only to process data. Techniques include event-driven interrupts instead of polling, running at the lowest acceptable clock frequency, and power-gating external sensors.
void enter_low_power(void) {
RTC->CR |= RTC_CR_ALRAE; RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_GPIOBEN);
PWR->CSR |= PWR_CSR_EWUP1; PWR->CR |= PWR_CR_PDDS;
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; __WFI();
}
// I_avg = (10mA*5ms + 0.002mA*9995ms)/10000ms = 7uA
// Battery life (200mAh) = 200/0.007 = 28571h = 3.26 years
IoT Connectivity: Wi-Fi, BLE, and LoRaWAN
IoT devices must communicate wirelessly with gateways or cloud services. Wi-Fi offers high bandwidth but high power. Bluetooth Low Energy (BLE) is ideal for short-range battery-operated devices using advertising events with duty-cycled radios. LoRaWAN provides long-range communication at very low data rates for agricultural and infrastructure sensors.
#include
#include
BLECharacteristic* pChar;
class MyCB: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* p) {
std::string val = p->getValue();
if (val.length() > 0) digitalWrite(LED_PIN, val[0]==1 ? HIGH : LOW);
}
};
void setup() {
BLEDevice::init("IoT_Sensor");
BLEServer* srv = BLEDevice::createServer();
BLEService* svc = srv->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
pChar = svc->createCharacteristic("beb5483e-36e1-4688-b7f5-ea07361b26a8",
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
pChar->setCallbacks(new MyCB()); svc->start(); BLEDevice::getAdvertising()->start();
}
Frequently Asked Questions
What is the difference between a microprocessor and a microcontroller?
A microprocessor is just a CPU requiring external RAM, ROM, and peripherals. A microcontroller integrates CPU, RAM, flash, and peripherals on a single chip for embedded applications.
What is a watchdog timer?
A watchdog timer is a hardware counter that resets the system if the firmware fails to periodically reset it. This protects against software hangs in safety-critical systems.
What is priority inversion and how does FreeRTOS handle it?
Priority inversion occurs when a high-priority task waits on a resource held by a low-priority task. FreeRTOS mutexes implement priority inheritance to prevent medium-priority tasks from interfering.
How do you debug an embedded system without a display?
Common techniques: printf over UART (serial console), toggling GPIO pins for timing, JTAG/SWD debugger, and logging to flash for post-mortem analysis.
Originally published on Ayodhyyya. Last updated June 1, 2026.