Real-Time Systems Programming in Ada — Priorities, Periodic Execution, and CPU Time Control in Practice
· Go Komura · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage
1. Introduction — Ada’s Deep Relationship with Real-Time
Our previous article, “Safe Concurrency in Ada,” covered the fundamentals of Ada’s tasks and protected objects for safe concurrent programming. This time we go a step further, into a more constrained domain — real-time systems.
In a real-time system, “correctness” means not only logically correct computation results, but also that those results arrive within their deadline. A correct answer delivered one millisecond late is as dangerous as an incorrect one.
Ada addresses this requirement with a comprehensive set of real-time features standardized as Annex D (Real-Time Systems) of the language specification. This is not a library bolted on after the fact — it is a real-time guarantee built into the language runtime itself.
Ada's Real-Time Features (Annex D):
- Task priorities and preemption (FIFO_Within_Priorities)
- The Ceiling_Locking protocol (priority inversion prevention)
- delay until for absolute-time periodic execution
- The Ravenscar profile (safety-critical tasking subset)
- Timing events (polling-free timer-driven wakeup)
- Execution time monitoring (Ada.Execution_Time)
- Multi-periodic scheduling
This article walks through all of these with 8 practical, self-contained code examples. Each snippet can be compiled and run independently.
The code fragments in this article are organized as a reference collection on GitHub, one file per chapter.
ada-real-time-systems — komurasoft-blog-samples (GitHub)
2. What Is a Real-Time System?
Let us establish our terminology.
| Concept | Definition |
|---|---|
| Hard real-time | Missing a deadline means catastrophic system failure (flight control, airbags, pacemakers) |
| Soft real-time | Missing a deadline is undesirable but occasional misses are tolerable (video streaming, games) |
| Deadline | The absolute time by which a task must complete |
| Period | The fixed time interval at which a task is repeatedly activated |
| WCET (Worst-Case Execution Time) | The longest possible execution time of a task |
| Jitter | Variation in the actual timing of periodic activation |
A real-time system design must ensure that for every task, WCET < deadline. Ada’s real-time features help you perform this analysis and provide these guarantees at the language level.
3. Task Priority Basics — FIFO_Within_Priorities
Ada’s default task scheduling policy is FIFO_Within_Priorities. Within the same priority level, tasks run FIFO (first-in, first-out), and a higher-priority task preempts (interrupts) any lower-priority task.
-- 01_task_priority.ada
-- Task priority and FIFO_Within_Priorities fundamentals
-- Configuration pragma must precede all context clauses
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Task_Priority_Demo is
task High_Priority_Task is
pragma Priority (Priority'Last);
pragma Storage_Size (4 * 1024);
end High_Priority_Task;
task Low_Priority_Task is
pragma Priority (Priority'First);
pragma Storage_Size (4 * 1024);
end Low_Priority_Task;
task body High_Priority_Task is
begin
Put_Line ("[T=0.0s] High priority task started");
delay until Clock + Milliseconds (100);
Put_Line ("[T=0.1s] High priority task completed");
end High_Priority_Task;
task body Low_Priority_Task is
begin
Put_Line ("[T=0.0s] Low priority task started");
delay until Clock + Milliseconds (500);
Put_Line ("[T=0.5s] Low priority task completed");
end Low_Priority_Task;
begin
Put_Line ("=== Task Priority Demo (FIFO_Within_Priorities) ===");
Put_Line ("Main: waiting for tasks to complete...");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Task_Priority_Demo;
Key points:
pragma Priorityassigns a static priority to each task.Priority'Lastis the highest;Priority'Firstis the lowest.- The high-priority task begins and finishes its 100ms work immediately, and can preempt the low-priority task (500ms) at any time.
- In practice, design priority levels relative to
System.Default_Priority.
Ada priority range (GNAT default):
Priority'First = 0 (lowest)
Priority'Last = 30 (highest; OS-dependent)
4. Ceiling_Locking — Priority Inversion Prevented by the Language
One of the most insidious problems in real-time systems is priority inversion: a high-priority task waiting for a lock held by a low-priority task, while a medium-priority task preempts the low-priority task, causing the high-priority task to be blocked indefinitely.
Ada solves this with the Ceiling_Locking protocol, built directly into protected objects.
-- 02_ceiling_locking.ada
-- Ceiling_Locking protocol prevents priority inversion
-- Configuration pragma must precede all context clauses
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ceiling_Locking_Demo is
Ceiling : constant System.Any_Priority := System.Any_Priority'Last;
protected Shared_Data is
pragma Priority (Ceiling);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Data;
protected body Shared_Data is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Data;
task Producer is
pragma Priority (Priority'Last);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (Priority'First);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
begin
Put_Line ("[T=0.0s] Producer (high prio): about to write");
Shared_Data.Write (42);
Put_Line ("[T=0.0s] Producer (high prio): write done");
delay until Clock + Milliseconds (100);
end Producer;
task body Consumer is
begin
delay until Clock + Milliseconds (10);
Put_Line ("[T=0.01s] Consumer (low prio): about to read");
declare
V : Integer;
begin
V := Shared_Data.Read;
Put_Line ("[T=0.01s] Consumer (low prio): read done, got" &
Integer'Image (V));
end;
delay until Clock + Milliseconds (100);
end Consumer;
begin
Put_Line ("=== Ceiling_Locking Demo ===");
Put_Line ("Main: producer priority = Last, consumer priority = First");
Put_Line ("Ceiling = Any_Priority'Last, locking = Ceiling_Locking");
delay until Clock + Milliseconds (300);
Put_Line ("Main: done");
end Ceiling_Locking_Demo;
How Ceiling_Locking works:
- A ceiling priority is set on the protected object via
pragma Priority (Ceiling). - Whenever any task enters the protected object, it is immediately raised to the ceiling priority.
- This prevents any medium-priority task from preempting a task currently inside the protected object.
- Upon exiting, the task returns to its original priority.
Design rule: The ceiling priority of a protected object must be at least as high as the highest priority of any task that uses it. This is the cardinal rule of Ceiling_Locking.
Achieving the same effect with C’s pthread mutexes requires explicitly setting the PTHREAD_PRIO_PROTECT attribute. In Ada, it is a standard language feature.
5. delay until — Periodic Tasks Without Drift
The fundamental pattern of real-time systems is the periodic task — a task that runs repeatedly at a fixed interval. Preventing cumulative timing error (drift) is critically important.
Ada’s delay until solves this elegantly.
-- 03_periodic_task.ada
-- delay until for periodic tasks — prevents cumulative drift
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Periodic_Task_Demo is
Period_MS : constant Time_Span := Milliseconds (200);
Cycles : constant Positive := 5;
task Sensor_Reader is
pragma Priority (Priority'Last - 2);
pragma Storage_Size (4 * 1024);
end Sensor_Reader;
task body Sensor_Reader is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Period_MS;
Cycle_Count : Natural := 0;
begin
Put_Line ("[Sensor] Periodic task starts, period=" &
To_Duration (Period_MS)'Image & "s, cycles=" &
Natural'Image (Cycles));
for I in 1 .. Cycles loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Sensor] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Next_Release := Next_Release + Period_MS;
end loop;
Put_Line ("[Sensor] Periodic task finished. Actual elapsed:" &
Duration'Image (To_Duration (Clock - Start_Time)) & "s");
end Sensor_Reader;
begin
Put_Line ("=== Periodic Task Demo (delay until) ===");
Put_Line ("Main: waiting for" & Natural'Image (Cycles) & " cycles...");
delay until Clock + Milliseconds (1500);
Put_Line ("Main: done");
end Periodic_Task_Demo;
Why delay until versus plain delay:
| Approach | Problem |
|---|---|
delay Period; |
Processing time per iteration accumulates; the period drifts over time |
delay until Next_Release; Next_Release := Next_Release + Period; |
Anchored to absolute time; even if one iteration overruns, the next release time is still correct |
With delay:
T=0ms → work(15ms) → delay 100ms → T=115ms → work(10ms) → ...
Actual intervals: 115ms, 110ms, ... (processing time accumulates)
With delay until:
Next_Release: 100ms, 200ms, 300ms, ... (absolute time)
T=0ms → work(15ms) → delay until 100ms → T=100ms → work(10ms) → delay until 200ms
Actual intervals: 100ms, 100ms, ... (processing time does not affect timing)
This delay until pattern is used in every periodic task from here onward.
6. The Ravenscar Profile — A Verifiable Real-Time Subset
Ada’s tasking features are powerful, but in safety-critical systems, “too powerful” becomes a liability. Dynamic task creation, complex select statements, and abort statements make static worst-case timing analysis difficult or impossible.
The Ravenscar profile is Ada’s answer: it restricts the tasking model to a statically analyzable, deterministic subset.
-- 04_ravenscar_profile.ada
-- Ravenscar profile fundamentals
-- Enable with: pragma Profile (Ravenscar); in a gnat.adc file
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package Ravenscar_State is
protected Signal is
pragma Priority (System.Default_Priority + 5);
entry Wait_For_Release;
procedure Release;
private
Released : Boolean := False;
end Signal;
task Periodic_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Periodic_Worker;
task Monitor is
pragma Priority (System.Default_Priority);
pragma Storage_Size (4 * 1024);
end Monitor;
end Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package body Ravenscar_State is
protected body Signal is
entry Wait_For_Release when Released is
begin
Released := False;
end Wait_For_Release;
procedure Release is
begin
Released := True;
end Release;
end Signal;
task body Periodic_Worker is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle_Count : Natural := 0;
begin
Put_Line ("[Worker] Ravenscar periodic task starts");
for I in 1 .. 4 loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Signal.Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Periodic_Worker;
task body Monitor is
begin
Put_Line ("[Monitor] Waiting for signals...");
for I in 1 .. 4 loop
Signal.Wait_For_Release;
Put_Line ("[Monitor] Received signal" & Natural'Image (I));
end loop;
Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Monitor;
end Ravenscar_State;
with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ravenscar_Demo is
begin
Put_Line ("=== Ravenscar Profile Demo ===");
Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
Put_Line ("Main: waiting for Ravenscar tasks...");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Ravenscar_Demo;
Ravenscar profile restrictions:
| Forbidden feature | Rationale |
|---|---|
Dynamic task creation (new or access types) |
Runtime allocation is non-deterministic |
select with multiple open alternatives |
Static analysis of execution time is infeasible |
abort statement |
Asynchronous termination breaks state predictability |
Ada.Task_Attributes |
Runtime-dynamic behavior |
| Dynamic detection of task termination | Complicates deterministic termination analysis |
requeue statement |
Complex control flow tracking |
Under these restrictions, a Ravenscar-compliant program becomes tractable for static timing analysis — a property required by safety standards such as DO-178C (aviation software), ISO 26262 (automotive functional safety), and IEC 62304 (medical device software).
To enable the Ravenscar profile, place the following in a gnat.adc file:
pragma Profile (Ravenscar);
7. Timing Events — Polling-Free Timer-Driven Wakeup
A common real-time requirement is “wake up a high-priority task at a specific time.” A naive implementation would poll a timer, but Ada offers a more sophisticated mechanism — timing events.
-- 05_timing_events.ada
-- Timing events (Ada.Real_Time.Timing_Events)
-- Wakes up a high-priority task without polling
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package Signal_Pkg is
protected type Signal_Type is
pragma Priority (System.Interrupt_Priority'Last);
entry Wait_For_Event;
procedure Fire (Event : in out Timing_Event);
private
Fired : Boolean := False;
end Signal_Type;
S : Signal_Type;
end Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package body Signal_Pkg is
protected body Signal_Type is
entry Wait_For_Event when Fired is
begin
Fired := False;
end Wait_For_Event;
procedure Fire (Event : in out Timing_Event) is
begin
Fired := True;
end Fire;
end Signal_Type;
end Signal_Pkg;
with Signal_Pkg; use Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
procedure Timing_Events_Demo is
pragma Priority (29);
Timer_1 : Timing_Event;
Timer_2 : Timing_Event;
task Reactor is
pragma Priority (System.Default_Priority + 5);
pragma Storage_Size (4 * 1024);
end Reactor;
task body Reactor is
begin
Put_Line ("[Reactor] Waiting for timing events...");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #1");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #2");
Put_Line ("[Reactor] Done");
end Reactor;
begin
Put_Line ("=== Timing Events Demo ===");
Put_Line ("Scheduling two timers at +100ms and +250ms...");
Set_Handler (Timer_1, Clock + Milliseconds (100), S.Fire'Access);
Set_Handler (Timer_2, Clock + Milliseconds (250), S.Fire'Access);
delay until Clock + Milliseconds (500);
Put_Line ("Main: done");
end Timing_Events_Demo;
How timing events work:
1. Set_Handler(Timer_1, T+100ms, S.Fire'Access) — register handler at absolute time
2. T+100ms elapses — the runtime calls S.Fire at the ceiling priority
3. Fire sets the Fired flag to True — the barrier opens
4. The Reactor task wakes up from Wait_For_Event
Critically, because Fire is a protected procedure, it executes at the ceiling priority of the protected object. This means no priority inversion can occur during timing event handling.
8. A Real-Time Queue with Protected Objects
A recurring real-time pattern is producer-consumer — a sensor generates data, and a control task consumes it. The synchronization and mutual exclusion must be both efficient and safe.
Ada’s protected objects and entry barriers achieve this without any explicit locks.
-- 06_protected_queue.ada
-- Protected-object-based real-time data sharing
-- Pipeline: Producer → Bounded_Buffer → Consumer
-- Enable with: pragma Locking_Policy (Ceiling_Locking); in a gnat.adc file
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Protected_Queue_Demo is
Buffer_Size : constant := 4;
type Buf_Array is array (1 .. Buffer_Size) of Integer;
protected Bounded_Buffer is
pragma Priority (System.Any_Priority'Last);
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Buf : Buf_Array;
Count : Natural := 0;
Head : Positive := 1;
Tail : Positive := 1;
end Bounded_Buffer;
protected body Bounded_Buffer is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Buf (Tail) := Item;
Tail := (Tail mod Buffer_Size) + 1;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Buf (Head);
Head := (Head mod Buffer_Size) + 1;
Count := Count - 1;
end Get;
end Bounded_Buffer;
task Producer is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
Next_Release : Time := Clock + Milliseconds (50);
Period : constant Time_Span := Milliseconds (50);
begin
for I in 1 .. 6 loop
Bounded_Buffer.Put (I);
Put_Line ("[Producer] Put" & Integer'Image (I));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Producer] Done");
end Producer;
task body Consumer is
Item : Integer;
Next_Release : Time := Clock + Milliseconds (80);
Period : constant Time_Span := Milliseconds (80);
begin
delay until Clock + Milliseconds (30);
for I in 1 .. 6 loop
Bounded_Buffer.Get (Item);
Put_Line ("[Consumer] Got" & Integer'Image (Item));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Consumer] Done");
end Consumer;
begin
Put_Line ("=== Protected Queue Demo (Ceiling_Locking) ===");
Put_Line ("Buffer size = 4; Producer every 50ms, Consumer every 80ms");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Protected_Queue_Demo;
Design highlights:
entry Put when Count < Buffer_Size— the Producer blocks automatically when the buffer is full.entry Get when Count > 0— the Consumer blocks automatically when the buffer is empty.pragma Priority (System.Any_Priority'Last)— Ceiling_Locking ensures no priority inversion between Producer and Consumer.- Barrier conditions are defined in terms of the protected object’s internal state (
Count) and are automatically re-evaluated upon each lock release.
This code contains no mutexes, no semaphores, and no condition variables. All synchronization is handled by the protected object’s barriers.
9. Measuring Execution Time — The First Step Toward WCET Analysis
To assess the schedulability of a real-time system, you must know the CPU execution time of each task accurately. Ada’s Ada.Execution_Time package provides per-task CPU time accounting.
-- 07_execution_time.ada
-- Execution time control
-- Measures per-task CPU consumption
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Execution_Time;
use type Ada.Execution_Time.CPU_Time;
procedure Execution_Time_Demo is
package ET renames Ada.Execution_Time;
task Busy_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Busy_Worker;
task body Busy_Worker is
Wall_Start : Time;
Cpu_Start : ET.CPU_Time;
Dummy : Integer := 0;
begin
Wall_Start := Clock;
Cpu_Start := ET.Clock;
Put_Line ("[Worker] Starting compute-bound work...");
for I in 1 .. 20_000_000 loop
Dummy := Dummy + 1;
end loop;
Put_Line ("[Worker] Dummy =" & Integer'Image (Dummy));
declare
Wall_Elapsed : constant Duration :=
To_Duration (Clock - Wall_Start);
Cpu_Span : constant Time_Span :=
ET.Clock - Cpu_Start;
begin
Put_Line ("[Worker] Done, wall time:" &
Duration'Image (Wall_Elapsed) & "s");
Put_Line ("[Worker] CPU time consumed:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
end Busy_Worker;
Cpu_Start_Main : constant ET.CPU_Time := ET.Clock;
begin
Put_Line ("=== Execution Time Demo ===");
delay until Clock + Milliseconds (500);
declare
Cpu_Span : constant Time_Span := ET.Clock - Cpu_Start_Main;
begin
Put_Line ("Main: CPU time consumed after 500ms:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
Put_Line ("Main: done");
end Execution_Time_Demo;
Wall-clock time vs. CPU time:
Wall-clock time: Ada.Real_Time.Clock
→ Actual elapsed time. Includes time spent blocked or preempted.
CPU time (execution time): Ada.Execution_Time.Clock
→ Only the time the task was actively executing on the CPU.
→ Blocked or preempted intervals are not counted.
This distinction is the foundation of WCET analysis. While Busy_Worker is blocked in a delay until, its CPU time does not increase — it only accumulates during actual computation. The main task’s delay until Clock + Milliseconds(500) should likewise show nearly zero CPU time for the main procedure.
10. Integration Demo — A Multi-Periodic Real-Time System
Let us now integrate all the elements we have covered — priorities, Ceiling_Locking, delay until, and protected objects — into a complete multi-periodic real-time system.
-- 08_multiperiodic.ada
-- Multi-periodic real-time system integration demo
-- Fast sensor reader (100ms period)
-- Slow control task (400ms period)
-- Ceiling_Locking for shared data
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Multiperiodic_Demo is
package Int_IO is new Ada.Text_IO.Integer_IO (Integer);
protected Shared_Sensor is
pragma Priority (System.Any_Priority'Last);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Sensor;
protected body Shared_Sensor is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Sensor;
task Fast_Sensor is
pragma Priority (System.Default_Priority + 3);
pragma Storage_Size (4 * 1024);
end Fast_Sensor;
task body Fast_Sensor is
Next_Release : Time := Clock + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle : Natural := 0;
begin
Put_Line ("[Fast] Sensor reader starts (100ms period)");
for I in 1 .. 12 loop
delay until Next_Release;
Cycle := Cycle + 1;
Shared_Sensor.Write (Cycle * 10);
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Fast] Done");
end Fast_Sensor;
task Slow_Controller is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Slow_Controller;
task body Slow_Controller is
Next_Release : Time := Clock + Milliseconds (150);
Period : constant Time_Span := Milliseconds (400);
Cycle : Natural := 0;
Raw : Integer;
begin
Put_Line ("[Slow] Controller starts (400ms period)");
for I in 1 .. 3 loop
delay until Next_Release;
Cycle := Cycle + 1;
Raw := Shared_Sensor.Read;
Put_Line ("[Slow] Cycle" & Natural'Image (Cycle) &
" reads sensor =" & Integer'Image (Raw));
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Slow] Done");
end Slow_Controller;
begin
Put_Line ("=== Multiperiodic Real-Time System Demo ===");
Put_Line ("Fast sensor (100ms) x 12 + Slow controller (400ms) x 3");
Put_Line ("Ceiling_Locking prevents priority inversion on shared data");
delay until Clock + Milliseconds (2000);
Put_Line ("Main: done");
end Multiperiodic_Demo;
System architecture:
Priority order (high to low):
System.Any_Priority'Last — Shared_Sensor (Ceiling_Locking)
Default_Priority + 3 — Fast_Sensor (100ms period, 12 cycles)
Default_Priority + 2 — Slow_Controller (400ms period, 3 cycles)
Behavior:
Fast_Sensor writes 4 values for every 1 read by Slow_Controller.
Because Fast_Sensor has higher priority, its writes are never delayed.
Ceiling_Locking ensures that whichever task is inside the protected object,
no preemption by the other can occur.
This pattern — “fast sensor acquisition + slow control loop” — is ubiquitous in industrial control systems and robotics.
11. Where Ada’s Real-Time Features Shine
Ada’s real-time features deliver particular value in these domains:
Aerospace:
- Flight control systems (Airbus A380, Boeing 787)
- Satellite attitude control (numerous ESA satellites)
- Drone flight control
Railways:
- Signaling systems
- Automatic Train Control (ATC)
- Grade crossing control
Automotive:
- Engine Control Units (ECU)
- Brake-by-wire
- Advanced Driver Assistance Systems (ADAS)
Medical devices:
- Pacemakers
- Infusion pumps
- Radiotherapy devices
In these domains, compliance with safety standards such as DO-178C (aviation software), ISO 26262 (automotive functional safety), and IEC 62304 (medical device software) is mandatory — and the Ravenscar profile is a key enabler.
12. Limitations and Cautions
Ada’s real-time features are powerful, but they are not a panacea.
1. Platform dependence:
- The actual mapping of
pragma Prioritydepends on the execution environment (OS + GNAT runtime). On Linux it maps toSCHED_FIFO, but on Windows full preemption may not be guaranteed.
2. Ravenscar constraints:
- Dynamic task creation is forbidden, so all tasks must be statically declared at system startup. This constrains design flexibility.
3. WCET measurement limits:
Ada.Execution_Timeprovides measurement, not guarantee. True WCET, including cache misses and pipeline hazards, must be verified separately using static analysis tools.
4. Overhead:
- Protected object barrier evaluation runs automatically upon entry completion, cancellation, and exit. For frequently-called protected objects, this overhead must be accounted for.
5. Toolchain barriers:
- Fully leveraging Ada’s real-time features requires a suitable cross-compiler and runtime. For embedded targets in particular, you will depend on vendor-supplied runtimes.
13. Summary
This article has explored Ada’s Annex D real-time features through 8 progressive code examples.
| Feature | Value Provided |
|---|---|
| Task priorities | Preemptive priority-based scheduling |
| Ceiling_Locking | Language-built-in priority inversion prevention |
delay until |
Drift-free, precise periodic execution |
| Ravenscar profile | Statically analyzable, certifiable tasking subset |
| Timing events | Polling-free timer-driven task wakeup |
| Protected queue | Lock-free producer-consumer |
| Execution time measurement | Per-task CPU time monitoring |
| Multi-periodic integration | Safe coexistence of tasks with different periods |
The essence of Ada’s real-time features is that they are not bolted on. Priority inversion prevention, precise periodic execution, and WCET monitoring are all part of the language specification itself. This is a powerful design advantage: you can delegate real-time guarantees to the language runtime.
To try real-time Ada development yourself, install the GNAT toolchain via Alire and build the sample code in this article with gnatchop + gnatmake.
For the fundamentals of Ada concurrency (tasks, rendezvous, protected objects), see “Safe Concurrency in Ada.”
14. References
- Ada Reference Manual — Annex D: Real-Time Systems
- Ravenscar Profile Definition (ISO/IEC TR 24718:2005)
- GNAT Real-Time Topics (AdaCore)
- The Ravenscar Profile for High-Integrity Systems (AdaCore)
- Rate Monotonic Analysis (Liu & Layland, 1973)
- Alire — Ada Package Manager
- Ada Sample Code Collection (GitHub)
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Safe Concurrency with Ada — A Practical Guide to Tasks and Protected Objects
A practical introduction to Ada's built-in concurrency model — tasks, rendezvous, and protected objects. Covers the rendezvous pattern (e...
The Appeal of the Ada Language — Expressing Design Through Types and Powering Software That Runs for Decades
An introduction to the appeal of the Ada language: strong typing, range constraints, separation of specification and implementation via p...
Windows Processor Scheduling Settings - Background Services and P/E Cores
What actually changes with the Windows "Background services" setting, explained through quantum time, foreground favoritism, audio glitch...
Fable Is Gone — Don't Give Up: OpenRouter Fusion + Chinese LLMs + Review Layer
Fable is nowhere near replaceable. But combine OpenRouter Fusion with 5 Chinese LLMs, then add a review layer (GPT-5.5-Pro or Codex PR re...
What Is MFC on Windows? Foundational Knowledge for Maintaining Existing Assets
An overview of the Microsoft Foundation Classes (MFC): its relationship to Win32, application structure, message maps, Document/View, DDX...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links