Skip to main content

Embedded Systems Programming

Learning Objectives

  • Define an embedded system and list its defining constraints (resources, real-time, dedicated function)
  • Distinguish hard real-time from soft real-time requirements with concrete examples
  • Explain why RTOSes exist and what a task, scheduler, and priority mean in that context
  • Write a simple FreeRTOS-style multitasking example and explain how it differs from Arduino's single loop()
  • Identify common embedded memory pitfalls: stack overflow, heap fragmentation, and static allocation as a mitigation
  • Explain power management techniques (sleep modes) and why they matter for battery-powered embedded products

Quick Answer

Embedded systems programming is writing software that runs on dedicated, resource-constrained hardware to perform one specific job reliably — often for years, without a user ever seeing "code" running. Unlike desktop software, embedded programs must work within tight RAM/flash budgets (kilobytes, not gigabytes), often must respond to physical events within guaranteed time limits (real-time constraints), and typically run with no operating system or a minimal Real-Time Operating System (RTOS) instead of Windows/Linux. This matters because almost every physical product with "smart" behavior — a car's ABS, a pacemaker, a washing machine, an industrial robot arm — depends on embedded code that must be correct, predictable, and efficient, since there's no "just restart it" option once it ships inside sealed hardware.

What Makes a System "Embedded"

A desktop application can afford to be a little slow, use gigabytes of RAM, and crash occasionally (you just restart it). An embedded system usually cannot afford any of that, because:

  • Resources are tiny. A typical MCU might have 32 KB of flash and 2 KB of RAM — not enough to hold even a single high-resolution photo, let alone a modern OS.
  • It runs forever, unattended. Many embedded products run for years without a reboot; a memory leak that a desktop app could hide for a day would crash an embedded device after weeks.
  • Timing often has real consequences. A car's airbag controller detecting a crash and failing to deploy within milliseconds isn't a "bug report" — it's a safety failure.
  • It does one job. Unlike a general-purpose computer, embedded firmware is usually built, tested, and shipped to do exactly one thing extremely reliably.

Real-World Example

An anti-lock braking system (ABS) controller reads wheel-speed sensors dozens of times per second and must decide whether to pulse brake pressure within a few milliseconds of detecting a skid. If the code takes too long — even due to something as innocent as an unbounded loop or a blocked interrupt — the delay could mean the difference between the car stopping safely and skidding into another vehicle. This is why embedded engineers care so intensely about worst-case execution time, not just average performance.

Hard Real-Time vs. Soft Real-Time

This distinction shows up constantly in exams and job interviews, and it's often misunderstood.

  • Hard real-time: missing a deadline is a system failure, full stop. Example: airbag deployment timing, pacemaker pacing signal, anti-lock brake pulse timing.
  • Soft real-time: missing a deadline degrades quality but isn't catastrophic. Example: a video streaming buffer occasionally stuttering, a temperature display updating a bit late.

Note that "real-time" does not mean "fast" — it means "predictable and bounded." A hard real-time system running at a slow 1 MHz clock that always meets its 10 ms deadline is real-time; a blazing-fast system that usually responds in 1 ms but occasionally spikes to 50 ms is not, if that spike would ever violate a hard deadline.

Why RTOSes Exist

Arduino-style firmware runs one loop() — great for simple, single-task devices. But real products often need to do several things that all feel "at once": read sensors on a schedule, respond to a touchscreen, blink a status LED, and manage a wireless connection — each with its own timing needs.

A Real-Time Operating System (RTOS) like FreeRTOS provides a scheduler that runs multiple independent tasks, switching between them based on priority and timing, giving the illusion of parallelism on a single CPU core.

Example: Bare Arduino vs. FreeRTOS

Bare Arduino, everything crammed into one loop (works, but gets unwieldy and timing-fragile as complexity grows):

unsigned long lastSensorRead = 0;
unsigned long lastBlink = 0;

void loop() {
unsigned long now = millis();
if (now - lastSensorRead >= 100) {
lastSensorRead = now;
readSensor();
}
if (now - lastBlink >= 500) {
lastBlink = now;
toggleLED();
}
checkButton(); // must be fast, or it delays the timers above
}

The same structure using FreeRTOS tasks (available on ESP32 and many ARM Cortex-M boards):

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

void sensorTask(void *pvParameters) {
for (;;) {
readSensor();
vTaskDelay(pdMS_TO_TICKS(100)); // yields CPU to other tasks for 100ms
}
}

void blinkTask(void *pvParameters) {
for (;;) {
toggleLED();
vTaskDelay(pdMS_TO_TICKS(500));
}
}

void setup() {
xTaskCreate(sensorTask, "Sensor", 2048, NULL, 2, NULL); // priority 2
xTaskCreate(blinkTask, "Blink", 1024, NULL, 1, NULL); // priority 1 (lower)
}

void loop() {
// left empty - FreeRTOS scheduler now runs the tasks
}

vTaskDelay() is crucial: unlike delay(), it doesn't just busy-wait — it tells the scheduler "this task has nothing to do for 100ms, let another task run." Higher-priority tasks (like sensorTask at priority 2) preempt lower-priority ones (blinkTask at priority 1) whenever both are ready to run, which is how the scheduler guarantees time-critical work gets CPU time first.

Why It Matters

As soon as a project needs more than two or three independent timed behaviors, hand-rolled millis() juggling becomes error-prone and hard to reason about — a missed edge case in one timer check can silently delay another. An RTOS formalizes "what runs when" with priorities and blocking calls, making the system's timing behavior explicit and testable.

Memory Management on Embedded Systems

Desktop programmers rarely think twice about malloc(). Embedded programmers avoid it almost entirely, for good reason.

// RISKY on an embedded system with 2KB of RAM:
char *buffer = (char *)malloc(500); // heap allocation
// ... use buffer ...
free(buffer);
// Repeated malloc/free of different sizes over a long-running device
// can fragment the tiny heap until a later malloc() fails unpredictably,
// often after weeks of "working fine" in testing.
// PREFERRED on embedded systems: static/global allocation, size known at compile time
char buffer[500]; // allocated once, at a fixed address, for the life of the program

Stack overflow is another silent killer: deeply nested function calls or unbounded recursion on a system with only a few hundred bytes to a few KB of stack can silently overwrite other variables (since MCUs typically lack memory protection), causing corruption that's very hard to trace back to its root cause.

Common Misunderstanding

Students coming from desktop programming assume malloc/free are simply "how you use memory," the same way they would in a normal C or C++ course. In embedded systems, dynamic allocation is a known risk factor for long-term reliability, and many safety-critical coding standards (like MISRA C, used in automotive) explicitly forbid or heavily restrict it.

Power Management

For battery-powered embedded devices, power consumption is often the single most important design constraint — more important than raw speed.

  • Sleep modes: putting the CPU core to sleep while keeping just enough hardware (like a real-time clock or a wake-up pin) active to resume later. An MCU that draws 10 mA while active might draw only a few microamps in deep sleep — a thousand-fold difference.
  • Clock gating: disabling the clock signal to peripherals that aren't currently needed, since unclocked circuitry consumes almost no dynamic power.
  • Dynamic voltage/frequency scaling: running the CPU at a lower voltage/clock speed when full performance isn't needed, trading speed for power savings.
#include <avr/sleep.h>
#include <avr/interrupt.h>

void enterSleep() {
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // deepest sleep mode on AVR
sleep_enable();
sleep_cpu(); // CPU halts here until an interrupt wakes it
sleep_disable(); // execution resumes here after wake-up
}

Real-World Example

A wireless soil-moisture sensor node designed to run for a year on two AA batteries spends over 99.9% of its life in deep sleep, waking briefly every 15 minutes to take a reading and transmit it, then going straight back to sleep. Get the sleep-mode configuration wrong (forgetting to disable an unused peripheral, for instance) and battery life can drop from a year to a few days.

Key Terms

TermDefinition
Embedded systemA dedicated computing device built into a larger product to perform a specific function
Real-time systemA system whose correctness depends not just on the result but on meeting timing deadlines
Hard real-timeMissing a deadline constitutes a system failure
Soft real-timeMissing a deadline degrades quality but is not catastrophic
RTOSReal-Time Operating System — provides task scheduling with priorities for multitasking on embedded hardware
TaskAn independent unit of work managed by an RTOS scheduler, similar to a lightweight thread
SchedulerThe RTOS component that decides which task runs next based on priority and readiness
PreemptionThe scheduler interrupting a lower-priority running task to run a higher-priority one that becomes ready
Stack overflow (embedded)Corruption caused when the call stack grows beyond its allocated memory region, often due to deep recursion
Sleep modeA low-power CPU state that halts execution while retaining enough state to resume later

Common Mistakes

  1. Misconception: "Real-time means fast." Why it's wrong: The everyday use of "real-time" (like "real-time chat") implies low latency, so students carry that meaning into embedded systems. Correct explanation: Real-time means predictable and bounded response time, not raw speed. A slow system that always meets its deadline is real-time; a fast system that occasionally misses a deadline is not, for hard real-time purposes.

  2. Misconception: "Using malloc() in embedded C is just normal programming practice." Why it's wrong: It's standard practice in desktop/application programming, so it seems like a safe default everywhere. Correct explanation: On memory-constrained, long-running embedded systems, repeated dynamic allocation risks heap fragmentation and unpredictable allocation failures after extended runtime; static/global allocation with sizes known at compile time is strongly preferred.

  3. Misconception: "An RTOS is required for any embedded project with more than one task." Why it's wrong: Students see RTOS discussed as "the professional way" and assume bare-metal millis()-based scheduling is always inadequate. Correct explanation: Many simple, well-understood multitasking needs (a handful of periodic tasks with generous timing tolerances) work fine with cooperative millis()-based scheduling on bare metal; an RTOS becomes valuable as task count, priority complexity, and timing guarantees grow.

Comparison and Connections

AspectBare-metal (millis() scheduling)RTOS (e.g., FreeRTOS)
Task switchingManual, cooperative, whatever you code by handAutomatic, priority-based, preemptive
Complexity for many tasksGrows messy quicklyScales more cleanly
Memory/flash overheadMinimalAdds RTOS kernel overhead
Timing guaranteesBest-effort, hand-verifiedFormal priority-based guarantees
Good forSimple projects, 8-bit MCUs with tiny RAMComplex, multi-task systems on capable 32-bit MCUs
AspectHard real-timeSoft real-time
Missed deadline consequenceSystem failure/safety hazardDegraded quality, generally recoverable
ExampleAirbag deployment, pacemaker pacingVideo buffering, sensor display refresh

Practice Questions

Recall

  1. List three defining constraints of an embedded system compared to a desktop computer. Answer guidance: Limited RAM/flash resources, real-time/timing requirements, and dedicated (single-purpose) function, often running unattended for long periods.
  2. What does an RTOS scheduler use to decide which task runs next? Answer guidance: Task priority and readiness — higher-priority ready tasks preempt lower-priority ones.

Understanding 3. Explain why "real-time" refers to predictability rather than raw speed. Answer guidance: A system can be real-time even if slow, as long as it consistently meets its required deadlines; conversely, an average-fast system that occasionally misses a critical deadline fails to be (hard) real-time regardless of typical speed. 4. Explain why vTaskDelay() in FreeRTOS is preferable to a plain busy-wait loop inside a task. Answer guidance: vTaskDelay() yields the CPU to the scheduler so other tasks can run during the wait, whereas a busy-wait loop would occupy the CPU the entire time, starving lower- and equal-priority tasks.

Application 5. You're designing firmware for a coin-cell-powered wireless sensor that must last a year. List two specific power management techniques you'd apply and explain their effect. Answer guidance: Deep sleep mode between readings (draws microamps instead of milliamps most of the time) and disabling unused peripherals/clock gating (avoids wasting current on hardware not actively needed), both directly extending battery life. 6. A team is building a device that needs to simultaneously read a keypad, update a display, and log data to flash. Would you recommend bare-metal millis() scheduling or an RTOS? Justify your recommendation. Answer guidance: An RTOS is likely the better choice given three independent, differently-timed tasks with potential priority differences (keypad response probably more time-sensitive than flash logging); it manages the complexity and timing guarantees more cleanly than hand-rolled scheduling.

Analysis 7. A pacemaker's firmware uses recursion in a data-processing routine that, under rare conditions, recurses deeply enough to overflow the stack. Analyze why this is especially dangerous in an embedded medical device compared to the same bug in a desktop application. Answer guidance: On many MCUs there is no memory protection, so stack overflow silently corrupts adjacent memory (possibly variables controlling pacing) rather than triggering a clean crash/exception as on a desktop OS; in a life-critical device, this could cause undetected malfunction rather than an obvious, recoverable failure. 8. Compare the risk profile of using dynamic memory allocation (malloc/free) in a short-lived desktop script versus in firmware for a smart thermostat expected to run for 10 years without reboot. Answer guidance: In a short-lived script, any heap fragmentation or leak is irrelevant because the process exits quickly; in decade-long embedded operation, gradual heap fragmentation from repeated allocation/deallocation can eventually cause allocation failures, making static allocation strategies far safer for long-running, unattended embedded devices.

FAQ

Q1: Is FreeRTOS the only RTOS used in embedded systems? No — FreeRTOS is popular and free, but alternatives include Zephyr, μC/OS-II, VxWorks (widely used in aerospace), and ThreadX. Choice often depends on licensing, certification needs (e.g., for medical/automotive), and vendor support.

Q2: Can I run an RTOS on an 8-bit Arduino Uno? Technically some minimal RTOS-like schedulers exist for AVR, but with only 2 KB of RAM, running a full RTOS with multiple tasks is impractical. RTOSes are far more common on 32-bit MCUs (ESP32, STM32) with tens to hundreds of KB of RAM.

Q3: What's the difference between a "task" in an RTOS and a "thread" in desktop programming? Conceptually very similar — both are independently schedulable units of execution. RTOS tasks are typically lighter-weight, with smaller stacks and simpler scheduling models tailored to constrained hardware.

Q4: How do I know if my embedded code is really meeting its real-time deadlines? By measuring worst-case execution time (WCET) — not average — often using a logic analyzer/oscilloscope toggling a spare GPIO pin at the start and end of a critical routine, or using specialized WCET analysis tools for certified safety-critical systems.

Q5: Why do embedded coding standards like MISRA C restrict things that are normal in regular C, like recursion and dynamic memory? Because these standards target safety-critical, long-running, resource-constrained systems where unpredictable memory behavior (fragmentation, stack overflow) or unpredictable timing (unbounded recursion) can cause real-world harm, not just a program crash.

Quick Revision

  • Embedded systems are constrained by limited RAM/flash, real-time deadlines, and a single dedicated purpose
  • Hard real-time = missing a deadline is a failure; soft real-time = missing a deadline just degrades quality
  • "Real-time" means predictable/bounded timing, not "fast"
  • RTOSes (FreeRTOS, Zephyr, VxWorks) provide task scheduling with priority-based preemption
  • vTaskDelay() yields the CPU to other tasks; delay()/busy-waiting does not
  • Avoid dynamic memory (malloc/free) in embedded C — prefer static/global allocation to avoid heap fragmentation
  • Stack overflow from deep recursion can silently corrupt memory on MCUs without memory protection
  • Sleep modes can cut current draw a thousand-fold, critical for battery-powered devices
  • Clock gating and dynamic voltage/frequency scaling are other key power-saving techniques
  • Choose bare-metal scheduling for simple projects, RTOS for complex multi-task timing needs on capable 32-bit MCUs

Prerequisites: Programming Microcontrollers, Microcontroller Peripherals

Related Topics: Real-Time Operating Systems, Digital Logic Design, Computer Architecture

Next Topics: Interfacing Microcontrollers, Troubleshooting Microcontroller Circuits