What Makes a System Embedded?
An embedded system is a computer built into a larger device to perform a dedicated function. Users rarely see it as a computer. It runs the engine, the brakes, the thermostat, or the infusion pump. In a cyber-physical system, the embedded computer is the "cyber" half of the loop introduced in Introduction to Cyber-Physical Systems.
What separates embedded design from general-purpose programming is a set of constraints that are all active at the same time:
| Constraint | What it means in practice |
|---|---|
| Timing | Results must be produced before a deadline set by physics, not by user patience |
| Resources | Kilobytes to megabytes of RAM, modest clock rates, often no memory management unit or file system |
| Power and energy | Battery or energy-harvesting operation; the processor sleeps most of the time and wakes on events |
| Reliability | No one is there to reboot it; failures may be dangerous or expensive |
| Lifetime | Deployed for 10 to 30 years, outliving its toolchain, its developers, and sometimes its manufacturer |
| Cost | Produced in volumes where a few cents per unit matters |
Computing platforms
Microcontroller (MCU)
Processor, memory, timers, and peripherals on one chip (for example ARM Cortex-M, RISC-V, ESP32, RP2040). Usually bare-metal or a small RTOS. Simple pipelines give fairly predictable timing.
Application processor / SoC
Multicore chips with caches and a memory management unit that run Linux (for example Cortex-A on a Raspberry Pi). Much more capable, but timing is much harder to bound.
Digital signal processor (DSP)
Optimized for multiply-accumulate loops in filtering, audio, radio, and motor control.
FPGA
Reconfigurable hardware. Logic runs in true parallel with cycle-exact timing, at the cost of harder development.
GPU / NPU accelerators
Run perception and machine-learning workloads in vehicles and robots. They have high throughput but are notoriously difficult to analyze for worst-case timing.
Programmable logic controller (PLC)
Ruggedized industrial computers with a fixed scan cycle. Covered in Control Systems and Industrial Applications.
The Hardware/Software Interface
Software reaches the physical world through peripherals: hardware blocks that the processor controls by reading and writing special addresses called memory-mapped registers. Writing a bit to a register can start a motor; reading one can return a sensor value.
Sensing: sampling and quantization
An analog-to-digital converter (ADC) measures a voltage at discrete instants (sampling) and rounds it to one of 2n levels (quantization). A 12-bit ADC on a 3.3 V range resolves about 0.8 mV.
Sampling too slowly causes aliasing: a fast signal looks like a slow one. The Nyquist rule (sample faster than twice the highest frequency present) is the minimum, and control loops usually sample 10 to 30 times faster than the plant's dynamics.
Actuation: PWM and DACs
Most actuators are driven by pulse-width modulation (PWM): a digital pin switches on and off quickly, and the fraction of time it is on (the duty cycle) sets the average power delivered to a motor, heater, or LED.
A digital-to-analog converter (DAC) produces a true analog voltage when one is needed. Either way, the physical actuator then has its own dynamics, saturation limits, and delay.
Getting data in and out: polling, interrupts, and DMA
| Mechanism | How it works | Strength | Weakness |
|---|---|---|---|
| Polling | Software repeatedly checks a status register | Simple, timing is easy to reason about | Wastes cycles; can miss short events between checks |
| Interrupts | Hardware signals the processor, which pauses current work and runs an interrupt service routine (ISR) | Fast response; the processor can sleep until needed | Preempts other code at unpredictable points; shared data needs protection |
| DMA | A direct memory access controller moves data between peripherals and memory without the processor | High throughput with little CPU load | Competes with the CPU for the memory bus, which adds timing interference |
volatile in C).
Common buses and networks
| Bus | Typical use | Timing character |
|---|---|---|
| UART | Debug consoles, GPS modules | Point-to-point, asynchronous |
| I²C | Low-speed sensors on one board | Shared two-wire bus with addressing; slow |
| SPI | Fast sensors, displays, flash memory | Full-duplex, clocked by the controller; fast and predictable |
| CAN / CAN FD | Automotive and industrial control networks | Priority-based arbitration by message ID, which makes worst-case latency analyzable |
| EtherCAT, PROFINET | Factory automation | Deterministic industrial Ethernet with cycle times in microseconds to milliseconds |
| Time-Sensitive Networking (TSN) | Converged vehicle and industrial Ethernet | IEEE 802.1 standards for clock synchronization and scheduled traffic with bounded latency |
What "Real-Time" Really Means
A real-time system is one whose correctness depends on when results are produced as well as what they are. Real-time does not mean fast. A system that responds in 10 ms every time is more real-time than one that responds in 1 ms on average and 200 ms occasionally. The goal is predictability.
| Class | Consequence of a missed deadline | Examples |
|---|---|---|
| Hard | Failure, possibly catastrophic; a late result is a wrong result | Airbag deployment, flight control surfaces, pacemaker pacing |
| Firm | A late result is useless and discarded, but occasional misses are tolerable | Video frame decoding, a sensor sample used only if fresh |
| Soft | Value degrades with lateness; quality of service suffers | Infotainment, telemetry logging, user interfaces |
Timing vocabulary
Release time: when a job becomes ready to run.
Deadline: when it must be finished, often measured relative to the release.
Response time: finish time minus release time. The worst-case response time is what must fit within the deadline.
Latency: delay from an event (a sensor edge, an interrupt) until the system reacts.
Jitter: variation in latency or period from one cycle to the next. Control loops are sensitive to jitter, not just to average delay.
WCET: worst-case execution time, the longest a piece of code can take on a given processor in isolation. Every schedulability guarantee rests on it.
Why execution time varies
The same code on the same input can take different amounts of time from run to run. Each item below is a source of timing variability that a WCET bound has to cover:
- Caches: a cache hit takes a few cycles; a miss to DRAM can take hundreds.
- Pipelines and branch prediction: mispredicted branches flush work already in progress.
- Interrupts and preemption: other code runs in the middle of yours.
- Multicore interference: cores share caches, memory controllers, and buses, so one core's workload slows another's.
- Operating system and runtime: page faults, garbage collection, and lock contention.
- Input-dependent paths: loops and branches whose length depends on the data.
WCET can be estimated by measurement (run many times and add a safety margin, which is never a proof) or bounded by static analysis of the binary against a model of the processor (sound, but pessimistic and only possible for analyzable hardware). Safety-critical systems often disable caches or pin tasks to cores just to make timing analyzable.
The Periodic Task Model
Real-time scheduling theory abstracts software into tasks. Each periodic task τi releases a new job every Ti time units, and each job needs up to Ci units of processor time (its WCET) before a relative deadline Di. In the simplest and most common case, Di = Ti: each job must finish before the next one is released.
Utilization
The fraction of processor time a task set demands:
If U > 1, no scheduler on one processor can meet every deadline. If U ≤ 1, whether deadlines are met depends on the scheduling policy.
Hyperperiod
The least common multiple of all periods. The schedule repeats after every hyperperiod, so simulating one is enough to check a periodic task set.
Sporadic tasks have a minimum gap between releases instead of a fixed period. Aperiodic tasks arrive unpredictably and are usually handled by a reserved server budget.
Real-Time Scheduling
Cyclic executive
A precomputed table says exactly which function runs in each time slot. It is fully deterministic and easy to certify, which is why it is common in avionics. It is also brittle: changing one task can mean rebuilding the whole table.
Fixed priority (RM, DM)
Each task gets a static priority, and the highest-priority ready job always runs. Rate monotonic (RM) gives higher priority to shorter periods; deadline monotonic (DM) to shorter deadlines. Both are optimal among fixed-priority policies for their task models. Fixed priority is what nearly every RTOS implements.
Earliest deadline first (EDF)
Priorities are dynamic: the job with the nearest absolute deadline runs. On one processor with D = T, EDF meets every deadline whenever U ≤ 1, so it is optimal. In exchange, its overload behavior is harder to predict.
Schedulability tests
A schedulability test decides, before deployment, whether a task set will always meet its deadlines. The table below lists the classic tests for one processor with independent tasks and D = T.
| Policy | Test | Type |
|---|---|---|
| EDF | U ≤ 1 | Exact (necessary and sufficient) |
| RM | U ≤ n(21/n − 1), which falls toward ln 2 ≈ 0.693 as n grows (Liu & Layland bound) | Sufficient only; failing it is inconclusive |
| Any fixed priority | Response-time analysis: iterate Ri = Ci + Σj ∈ hp(i) ⌈Ri / Tj⌉ Cj until it converges; schedulable if every Ri ≤ Di | Exact for this task model |
Response-time analysis says that task i's worst case happens when every higher-priority task j releases at the same instant (the critical instant). Then i must wait for its own Ci plus every higher-priority job released while it is still waiting.
Example: Schedulability analysis
The code below applies all three tests to two task sets. Each task is (name, C, T) with D = T, and RM priorities are assigned by period.
import math
def utilization(tasks):
return sum(c / t for _, c, t in tasks)
def liu_layland_bound(n):
return n * (2 ** (1 / n) - 1)
def response_times(tasks):
ordered = sorted(tasks, key=lambda task: task[2])
results = []
for i, (name, c, t) in enumerate(ordered):
higher = ordered[:i]
r = c + sum(hc for _, hc, _ in higher)
while True:
nxt = c + sum(math.ceil(r / ht) * hc for _, hc, ht in higher)
if nxt == r or nxt > t:
break
r = nxt
results.append((name, nxt, t, nxt <= t))
return results
def analyze(label, tasks):
u = utilization(tasks)
bound = liu_layland_bound(len(tasks))
print(f'{label}: U = {u:.3f}, RM bound = {bound:.3f}, EDF bound = 1.000')
print(f' RM utilization test: {"pass" if u <= bound else "inconclusive"}')
print(f' EDF utilization test: {"pass" if u <= 1 else "fail"}')
for name, r, t, ok in response_times(tasks):
print(f' RM response time {name:<10} R = {r:>2} deadline = {t:>2} {"ok" if ok else "MISS"}')
analyze('Set A', [('imu', 1, 4), ('control', 2, 6), ('logger', 3, 12)])
analyze('Set B', [('control', 2, 5), ('vision', 4, 7)])
Output:
Set A: U = 0.833, RM bound = 0.780, EDF bound = 1.000
RM utilization test: inconclusive
EDF utilization test: pass
RM response time imu R = 1 deadline = 4 ok
RM response time control R = 3 deadline = 6 ok
RM response time logger R = 10 deadline = 12 ok
Set B: U = 0.971, RM bound = 0.828, EDF bound = 1.000
RM utilization test: inconclusive
EDF utilization test: pass
RM response time control R = 2 deadline = 5 ok
RM response time vision R = 8 deadline = 7 MISS
What to notice
- Set A exceeds the Liu & Layland bound, yet response-time analysis proves it schedulable under RM. The bound is a quick sufficient check, not a verdict.
- Set B fails under RM: the vision task's worst-case response time (8) exceeds its deadline (7). EDF can still schedule it because U ≤ 1.
- All of this depends on C being a true upper bound. An underestimated WCET silently invalidates every guarantee.
Example: Watching RM and EDF schedule the same tasks
This discrete-time simulator runs Set B for one hyperperiod (35 time units). Each character is one time unit: c for control, v for vision, . for idle. A job still unfinished at its deadline is counted as a miss and dropped.
from math import lcm
def simulate(tasks, policy):
horizon = lcm(*(t for _, _, t in tasks))
jobs, timeline, misses = [], [], []
for now in range(horizon):
for name, c, t in tasks:
if now % t == 0:
jobs.append({'name': name, 'left': c, 'deadline': now + t, 'period': t})
for job in [j for j in jobs if j['deadline'] <= now]:
misses.append((job['name'], job['deadline']))
jobs.remove(job)
if not jobs:
timeline.append('.')
continue
key = (lambda j: j['period']) if policy == 'RM' else (lambda j: j['deadline'])
job = min(jobs, key=key)
job['left'] -= 1
timeline.append(job['name'][0])
if job['left'] == 0:
jobs.remove(job)
return ''.join(timeline), misses
tasks = [('control', 2, 5), ('vision', 4, 7)]
for policy in ('RM', 'EDF'):
timeline, misses = simulate(tasks, policy)
print(f'{policy:<3} {timeline}')
print(f' misses: {misses if misses else "none"}')
Output:
RM ccvvvccvvvccv.vccvvvccvvvccvvvccvv.
misses: [('vision', 7)]
EDF ccvvvvccvvvvccvccvvvccvvvvccvvvvcc.
misses: none
What to notice
- At t = 5, RM preempts vision for the second control job because control always has the higher priority. Vision has 1 unit left at its deadline at t = 7, and misses.
- At t = 5, EDF instead lets vision finish, because vision's deadline (7) is earlier than the new control job's deadline (10). Control still finishes by t = 8, well before 10.
- The same processor, tasks, and total work produce a different physical outcome. The only thing that changed is the policy.
Shared Resources and Priority Inversion
Tasks share data, buses, and devices, usually protected by a mutex. Locking breaks the simple rule that the highest-priority ready task runs. A high-priority task can be forced to wait for a low-priority task that holds a lock it needs. A short wait of that kind is bounded blocking and is unavoidable.
Unbounded priority inversion happens when medium-priority tasks, which do not need the lock at all, preempt the low-priority lock holder. The high-priority task then waits for all of them as well.
Case study: Mars Pathfinder, 1997
After landing, the Pathfinder lander began resetting itself repeatedly. A low-priority meteorological data task held a mutex inside the VxWorks inter-process communication mechanism, and medium-priority tasks kept preempting it. A higher-priority bus distribution task blocked on that mutex and missed its cycle. The highest-priority scheduler task detected the missed cycle and reset the system. Engineers reproduced the problem on a ground replica, enabled priority inheritance on the mutex by changing a configuration setting, and uploaded the fix to Mars. (Glenn Reeves' account of the fix)
Priority inheritance protocol (PIP)
While a task holds a lock that a higher-priority task is waiting for, it temporarily runs at the waiter's priority. Medium-priority tasks can no longer preempt it. This is widely supported, for example in POSIX mutexes with PTHREAD_PRIO_INHERIT and in FreeRTOS mutexes.
Priority ceiling protocols (PCP, SRP)
Each lock gets a ceiling equal to the highest priority of any task that uses it. A task's access is restricted by those ceilings, which prevents deadlock and limits blocking to at most one critical section. That bound can be added directly to response-time analysis.
Example: Priority inversion with and without inheritance
Three jobs: L (low) locks a shared resource for 4 units, M (medium) needs 6 units of CPU but no lock, and H (high) needs the lock for 2 units. Each segment is ('cpu', n) or ('cs', n) for a critical section. Each character in the timeline is the job running in that time unit.
def run(jobs, inheritance, horizon=20):
state = {n: {'seg': 0, 'left': j['segs'][0][1], 'done': None} for n, j in jobs.items()}
owner, timeline = None, []
for now in range(horizon):
ready = [n for n, j in jobs.items() if j['release'] <= now and state[n]['done'] is None]
def wants_lock(n):
return jobs[n]['segs'][state[n]['seg']][0] == 'cs'
def blocked(n):
return wants_lock(n) and owner not in (None, n)
def priority(n):
p = jobs[n]['prio']
if inheritance and n == owner:
waiting = [jobs[w]['prio'] for w in ready if blocked(w)]
p = max([p] + waiting)
return p
runnable = [n for n in ready if not blocked(n)]
if not runnable:
timeline.append('.')
continue
n = max(runnable, key=priority)
if wants_lock(n):
owner = n
s = state[n]
s['left'] -= 1
timeline.append(n)
if s['left'] == 0:
if wants_lock(n):
owner = None
s['seg'] += 1
if s['seg'] == len(jobs[n]['segs']):
s['done'] = now + 1
else:
s['left'] = jobs[n]['segs'][s['seg']][1]
return ''.join(timeline), {n: s['done'] - jobs[n]['release'] for n, s in state.items()}
jobs = {
'L': {'prio': 1, 'release': 0, 'segs': [('cpu', 1), ('cs', 4), ('cpu', 1)]},
'M': {'prio': 2, 'release': 2, 'segs': [('cpu', 6)]},
'H': {'prio': 3, 'release': 3, 'segs': [('cpu', 1), ('cs', 2), ('cpu', 1)]},
}
for inheritance in (False, True):
timeline, response = run(jobs, inheritance)
label = 'with inheritance ' if inheritance else 'without inheritance'
print(f'{label} {timeline} H response time = {response["H"]}')
Output:
without inheritance LLMHMMMMMLLLHHHL.... H response time = 12
with inheritance LLMHLLLHHHMMMMML.... H response time = 7
What to notice
- Without inheritance, H becomes blocked at t = 4, and then M runs for five straight units while L, which holds the lock, cannot run. H's response time grows to 12, and it would grow without limit if there were more medium tasks.
- With inheritance, L runs at H's priority from t = 4, releases the lock at t = 7, and H finishes at t = 10. M is delayed, which is correct, because M has lower priority than H.
- The total work is identical in both runs. Only the order changed.
Software Platforms
| Approach | Description | When it fits |
|---|---|---|
| Bare-metal super-loop | One infinite loop polls inputs and calls functions in order, with interrupts for urgent events | Small, simple devices; easy to analyze, hard to extend |
| RTOS | A small kernel provides preemptive fixed-priority scheduling, mutexes with priority inheritance, queues, and timers (for example FreeRTOS, Zephyr, VxWorks, QNX, INTEGRITY) | Most microcontroller-based CPS with several concurrent activities |
| Real-time Linux | The PREEMPT_RT patches make most of the Linux kernel preemptible. They have been part of the mainline kernel since version 6.12 (late 2024). | Robots, gateways, and industrial PCs that need Linux's ecosystem with millisecond-to-microsecond latency targets |
| Hypervisor / partitioning | Isolated partitions with guaranteed time and memory budgets (for example ARINC 653 in avionics) | Mixed-criticality systems combining certified and uncertified software on one processor |
Where Python fits
Standard CPython on a desktop operating system is not a hard real-time platform. The OS scheduler, garbage collection, and the interpreter itself add unbounded delays. Python is still valuable in CPS work: for analysis and simulation (like the examples on this page), for supervisory layers above a real-time core, and on microcontrollers through MicroPython, which offers direct register, timer, and interrupt access for prototyping.
One technique that matters in any language is how a periodic loop waits. Sleeping for a fixed period after doing the work lets every cycle's execution time and wake-up delay accumulate into drift. Sleeping until an absolute release time keeps the long-run rate exact, so only per-cycle jitter remains.
Example: Drift versus jitter in a periodic loop
import statistics
import time
def naive(period, cycles):
start = time.perf_counter()
stamps = []
for _ in range(cycles):
stamps.append(time.perf_counter())
sum(i * i for i in range(2000))
time.sleep(period)
return start, stamps
def absolute(period, cycles):
start = time.perf_counter()
stamps = []
for k in range(cycles):
stamps.append(time.perf_counter())
sum(i * i for i in range(2000))
delay = start + (k + 1) * period - time.perf_counter()
if delay > 0:
time.sleep(delay)
return start, stamps
def report(label, period, start, stamps):
ideal = [start + k * period for k in range(len(stamps))]
lateness = [(s - i) * 1e3 for s, i in zip(stamps, ideal)]
gaps = [(b - a) * 1e3 for a, b in zip(stamps, stamps[1:])]
print(f'{label:<9} mean period {statistics.mean(gaps):6.3f} ms '
f'jitter (stdev) {statistics.stdev(gaps):5.3f} ms '
f'drift after {len(stamps)} cycles {lateness[-1]:7.2f} ms')
period, cycles = 0.005, 400
report('naive', period, *naive(period, cycles))
report('absolute', period, *absolute(period, cycles))
Sample output (your numbers will differ by machine and load):
naive mean period 5.247 ms jitter (stdev) 0.111 ms drift after 400 cycles 98.39 ms
absolute mean period 5.001 ms jitter (stdev) 0.210 ms drift after 400 cycles 0.23 ms
What to notice
- The naive loop's period is about 5% too long, so after 400 cycles (2 seconds) it is about 100 ms behind schedule. A controller designed for a 5 ms period is effectively running at a different rate.
- The absolute-time loop has essentially no drift. Each cycle absorbs the previous cycle's delay.
- Jitter remains in both cases. Removing it requires a real-time kernel, hardware timers, or isolated cores, which is exactly what an RTOS provides.
Beyond a Single Processor
Multicore scheduling
Partitioned scheduling assigns each task to one core and runs a uniprocessor scheduler on each. It is simple and analyzable, but assigning tasks to cores is a bin-packing problem. Global scheduling lets tasks migrate between cores. It has better average utilization, but the uniprocessor optimality results no longer hold, and interference through shared caches and memory complicates WCET.
Mixed criticality
Modern platforms combine functions of different criticality, such as braking control and infotainment, on shared hardware. Mixed-criticality scheduling guarantees high-criticality tasks under pessimistic WCET assumptions and drops or degrades low-criticality tasks if those assumptions are ever reached.
Time-triggered and logical-time approaches
Instead of reacting to events whenever they happen, time-triggered architectures act at globally synchronized instants. Logical execution time and reactor-based languages such as Lingua Franca separate the timing a program specifies from how long the hardware takes. These ideas return in System Design and Modeling.
Distributed timing
When sensing, computing, and actuation are spread across networked nodes, end-to-end latency includes network delay and clock offsets. Clock synchronization (IEEE 1588 PTP) and scheduled networks (TSN) extend real-time guarantees across a vehicle or a factory floor.
A Practical Real-Time Design Checklist
- Derive each deadline and period from the physics (plant dynamics, safety margins), not from convenience.
- Bound the WCET of every task, and state how it was obtained (measurement with margin, or static analysis).
- Choose a scheduling policy and run an exact schedulability test, including blocking time from shared resources and interrupt overhead.
- Use mutexes with priority inheritance or ceiling protocols. Never use plain binary semaphores for mutual exclusion between priorities.
- Keep ISRs short and defer work to tasks; protect every variable shared with an ISR.
- Schedule periodic loops against absolute time, and measure the jitter on target hardware under worst-case load.
- Add runtime monitoring: watchdogs, deadline-miss counters, and a defined safe behavior when a deadline is missed (see Safety in Cyber-Physical Systems).
The Series
Textbook Reading
Lee & Seshia, Introduction to Embedded Systems: A Cyber-Physical Systems Approach, 2nd ed.
The chapters in the book's Design part cover this page's material: 7 Sensors and Actuators, 8 Embedded Processors, 9 Memory Architectures, 10 Input and Output, 11 Multitasking, and 12 Scheduling. Chapter 16, Quantitative Analysis, covers execution-time analysis.
Book website and free PDFTutorials, Tools, and Interactive Learning
Wokwi
A browser-based simulator for ESP32, Raspberry Pi Pico, STM32, and Arduino boards with sensors, displays, and motors. Projects can be written in MicroPython, so you can experiment with timers, interrupts, and PWM without any hardware.
Open WokwiMicroPython
Python 3 for microcontrollers, with a machine module for pins, ADCs, PWM, timers, and interrupts. It includes an online board emulator.
FreeRTOS
The most widely used open-source RTOS for microcontrollers (MIT license, 40+ architectures). Its beginner's guide explains tasks, priorities, queues, and mutexes with priority inheritance.
Home Beginner's guideZephyr RTOS
A Linux Foundation RTOS with broad board support, a device-tree hardware model, and built-in networking and security subsystems.
Documentation Getting startedReal-Time Linux (PREEMPT_RT)
The Linux Foundation's Real-Time Linux project, which coordinated the mainlining of PREEMPT_RT and documents real-time configuration.
Real-Time Linux wikiSimSo
A Python scheduling simulator (built on SimPy) with over 25 uniprocessor and multiprocessor schedulers, including cache and overhead models. Use it to extend the simulators on this page.
SimSo projectLingua Franca Embedded Systems Labs
A hands-on lab sequence on timing, concurrency, and modal models with the RP2040 robot platform, designed to accompany Lee & Seshia.
Embedded Systems Labs