Revision history (first version, published Jun 14, 2026)
- First published
Cite this article(DOI: 10.5281/zenodo.21614694)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Real-Time Systems Programming in Ada — Priorities, Periodic Execution, and CPU Time Control in Practice. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614694 https://comcomponent.com/en/blog/ada-real-time-systems/
- DOI (latest version)
- 10.5281/zenodo.21614694
- DOI (this version)
- 10.5281/zenodo.21614695
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 stands on its own conceptually, but examples 04 and 05 contain multiple compilation units and should be split with gnatchop before building with gnatmake.
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 |
For each task, WCET <= deadline is an important necessary condition. It is not, by itself, a system-wide deadline guarantee: blocking time, priority assignment, jitter, interrupts, and the behavior of the runtime and OS still need response-time or schedulability analysis. In practice, you normally target WCET < deadline to preserve margin. Ada’s real-time features provide a predictable execution model that makes that analysis easier to perform.
flowchart LR
HRT[Hard Real-Time] -->|Deadline miss = failure| Examples[Flight control<br/>Airbag<br/>Pacemaker]
SRT[Soft Real-Time] -->|Occasional miss tolerated| Examples2[Video streaming<br/>Games<br/>UI]
Ada[Ada Annex D<br/>Mechanisms for predictability] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[Real-Time Requirements]
D[Deadline<br/>Absolute time to complete]
P[Period<br/>Repetition interval]
W[WCET<br/>Worst-case execution time]
J[Jitter<br/>Timing variability]
end
D --> Analysis[Schedulability analysis]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[Necessary condition: WCET <= deadline<br/>Sufficiency checked by response-time analysis]
One of the most dangerous phenomena in real-time systems is priority inversion. This problem actually occurred on the Mars Pathfinder in 1997, causing the lander to reset repeatedly.
sequenceDiagram
participant S as Scheduler
participant L as Low-priority task
participant H as High-priority task
participant M as Medium-priority task
participant R as Shared resource
L->>R: Acquires lock
activate L
Note over L: Inside critical section
Note over S,L: H wakes, so the scheduler suspends L
deactivate L
activate H
H->>R: Tries to acquire lock
Note over H: Blocked! L still holds the lock
deactivate H
Note over S,L: H waits for the lock, so L resumes
activate L
Note over L: Continuing toward lock release...
Note over S,L: M wakes, so the scheduler suspends L
deactivate L
activate M
Note over L: L cannot release the lock
Note over M: M runs freely (L & H both stuck)
Note over H: 【PRIORITY INVERSION】High-priority blocked indefinitely
deactivate M
A low-priority task holding a lock gets preempted by a medium-priority task, leaving the high-priority task blocked indefinitely. The actual Mars Pathfinder mitigation enabled priority inheritance in VxWorks; Ada addresses the same class of problem with a different language-level mechanism, Ceiling_Locking.
3. Task Priority Basics — FIFO_Within_Priorities
FIFO_Within_Priorities is a standard priority-based dispatching policy that Ada Annex D lets you request explicitly. If no dispatching policy is specified, the behavior is implementation-defined; GNAT commonly uses this family of policies on many targets. 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.- This demo does not perform heavy 100ms or 500ms computation; both tasks mostly wait with
delay until. The point is that when both tasks are runnable, the higher-priority task gets the first execution opportunity. - This figure demonstrates the preemption side of
FIFO_Within_Priorities. Demonstrating FIFO order within the same priority level would require a separate example with multiple equal-priority tasks. - In practice, design priority levels relative to
System.Default_Priority.
sequenceDiagram
participant S as Scheduler
participant Main as Main task
participant HP as High-priority task<br/>(Priority=Last)
participant LP as Low-priority task<br/>(Priority=First)
Main->>HP: Create task
Main->>LP: Create task
Note over HP,LP: T=0ms: Both tasks are runnable
S->>HP: Pick HP, the highest priority
activate HP
Note over HP: Print start log
HP->>S: Block until T+100ms
deactivate HP
S->>LP: Run LP next
activate LP
Note over LP: Print start log
LP->>S: Block until T+500ms
deactivate LP
Note over S: T=100ms: HP wakes
S->>HP: Run HP
activate HP
Note over HP: Print completion log
deactivate HP
Note over S: T=500ms: LP wakes
S->>LP: Run LP
activate LP
Note over LP: Print completion log
deactivate LP
Note over Main: (T=800ms) Main done
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.
The diagram below is not a literal time trace of the preceding sample. It shows how the priority inversion pattern from Figure 2 is avoided when the shared resource is a protected object using Ceiling_Locking.
sequenceDiagram
participant S as Scheduler
participant L as Low-priority task<br/>(prio=10)
participant M as Medium-priority task<br/>(prio=20)
participant H as High-priority task<br/>(prio=30)
participant PO as Protected Object<br/>(ceiling=30)
Note over PO: A caller with active priority > ceiling raises Program_Error<br/>H(30) equals the ceiling(30), so it may enter
L->>PO: Enter protected op
activate L
Note over L,PO: Active priority raised to 30
Note over S: M wakes up
Note over S,L: L is running at ceiling 30<br/>M(20) cannot preempt it
Note over S: H wakes up
Note over S,H: H(30) passes the ceiling check<br/>but waits because L is using PO
L->>PO: Execute operation
L->>PO: Exit protected op
deactivate L
Note over L: Priority restored to 10
Note over S,H: Run H after PO is released
activate H
H->>PO: Enter protected op
Note over H,PO: H(30) = ceiling(30), so it may enter after contention clears
H->>PO: Exit protected op
deactivate H
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. If a task with an active priority higher than the ceiling calls a protected operation, Ada can detect the design error by raising
Program_Error.
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 |
However, delay until does not guarantee that the computation fits inside the period. If the work has already passed the next release time, the delay until returns almost immediately; the system should treat that as a deadline miss or overload condition.
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.
flowchart TB
subgraph Bad["delay Period - Cumulative Drift"]
B1[T=0ms: computation 15ms] --> B2[delay 100ms → wake at 115ms]
B2 --> B3[computation 10ms → wake at 125ms]
B3 --> B4[delay 100ms → wake at 225ms]
B4 --> B5[Actual intervals: 115ms, 110ms...]
end
subgraph Good["delay until - Absolute-Time Based"]
G1[Next = T+100ms] --> G2[computation 15ms]
G2 --> G3[delay until T+100ms → wake at 100ms]
G3 --> G4[computation 10ms]
G4 --> G5[Next = T+200ms → wake at 200ms]
G5 --> G6[Actual intervals: 100ms, 100ms...]
end
subgraph Overrun["Overrun - deadline miss"]
O1[Next = T+100ms] --> O2[computation 130ms]
O2 --> O3[delay until T+100ms returns immediately]
O3 --> O4[Detect lateness and treat as overload]
end
Bad --> Drift[Accumulating error over time]
Good --> Stable[Prevents cumulative drift]
Good --> Overrun
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, 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: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Ravenscar_Demo;
Ravenscar profile restrictions:
| Forbidden feature | Rationale |
|---|---|
Dynamic task creation (new or access types) |
Runtime allocation is non-deterministic |
select statements |
Not only multiple alternatives; the whole construct complicates control-flow analysis |
abort statement |
Asynchronous termination breaks state predictability |
Ada.Task_Attributes |
Runtime-dynamic behavior |
| Dynamic priority changes | Scheduling assumptions can change at runtime |
Relative delay (delay) |
Can accumulate drift; use absolute-time delay until instead |
| Multiple entries per protected object | Adds blocking conditions and analysis cases |
| Task termination | Ravenscar treats all tasks as nonterminating |
requeue statement |
Complex control flow tracking |
Under these restrictions, a Ravenscar-compliant program becomes much more 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). This is a representative subset of the restrictions; the full profile also includes rules such as No_Task_Hierarchy and Detect_Blocking, depending on the runtime profile and compiler configuration.
flowchart TB
Full[Full Ada Tasking] --> Profile[Ravenscar Profile]
Profile --> Restrict[Restrictions]
Profile --> Policy[Required policies]
Restrict --> R1[No dynamic task creation]
Restrict --> R2[No select statements]
Restrict --> R3[No abort statements]
Restrict --> R4[No Task_Attributes]
Restrict --> R5[Max 1 entry per protected object]
Restrict --> R6[No requeue]
Restrict --> R7[No relative delay<br/>use delay until]
Restrict --> R8[No dynamic priorities]
Restrict --> R9[No task termination<br/>all tasks nonterminating]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[Helps enable:<br/>Static timing analysis]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>Aviation software]
Benefit --> ISO26262[ISO 26262<br/>Automotive functional safety]
Benefit --> IEC62304[IEC 62304<br/>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
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;
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, this example explicitly uses Ceiling_Locking, and because Fire is a protected procedure, it executes at the ceiling priority of the protected object. A protected procedure used as a timing-event handler belongs in a protected object with an interrupt-level ceiling priority; this example uses System.Interrupt_Priority'Last. This prevents priority inversion 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 express this as barrier-based synchronization. The runtime still provides mutual exclusion internally; the application code does not manually use mutexes or condition variables.
-- 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 application-level mutexes, semaphores, or condition variables. The required waiting is expressed through protected-object entry barriers.
stateDiagram-v2
Empty: Empty / Count=0
Partial: Partial / Count=1..Buffer_Size-1
Full: Full / Count=Buffer_Size
[*] --> Empty: Initial state
Empty --> Partial: Put (add first item)
Partial --> Partial: Put / Get
Partial --> Empty: Get (remove last item)
Partial --> Full: Put (fill last slot)
Full --> Partial: Get (free one slot)
Empty --> Empty: Get blocks (barrier Count=0)
Full --> Full: Put blocks (barrier Count=Buffer_Size)
A successful Put rechecks waiting Get calls, and a successful Get rechecks waiting Put calls. That barrier re-evaluation happens when protected operations complete, regardless of which state is currently shown.
9. Measuring Execution Time — The First Step Toward Runtime Monitoring
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;
pragma Volatile (Dummy);
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 a foundation for runtime execution-time monitoring and for validating WCET assumptions. 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. Measurement does not guarantee the true WCET by itself; cache effects, pipeline behavior, and memory contention still require target-specific validation or static analysis.
flowchart LR
subgraph Wall[Wall Clock Time]
W1[Total elapsed: 500ms] --> W2[Includes: compute + idle + blocked + preempted]
end
subgraph CPU[CPU Time]
C1[Total CPU: 120ms] --> C2[Only: actual computation]
end
Wall --> Diff[The gap = time spent waiting or preempted]
CPU --> Diff
Diff --> Insight[CPU time observes real computation cost<br/>Useful for WCET validation and monitoring<br/>Excludes waiting, blocking, and preemption time]
Insight --> Caveat[Caution<br/>Measurement does not guarantee true WCET<br/>Static analysis or target validation is still needed]
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:
The schedule below uses the sample code’s release times: the fast sensor starts at 100ms and repeats every 100ms, while the slow controller starts at 150ms and repeats every 400ms. The execution durations are illustrative; the sample code itself does not contain an 80ms control computation. If the fast sensor has the higher priority, it preempts the slow controller when a release occurs during slow-controller execution. To keep the figure readable, it shows one representative preemption per slow-control cycle; in a real run, the fast sensor is released at every 100ms boundary.
flowchart TB
Assumption["Illustrative assumption<br/>Fast sensor: 10ms work<br/>Slow control: 80ms work"]
subgraph Cycle1["Slow control cycle 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>Fast sensor #1"] --> C1S1["150-200ms<br/>Slow control #1 part A"]
C1S1 --> C1F2["200-210ms<br/>Fast sensor #2<br/>P+3 preempts"]
C1F2 --> C1S2["210-240ms<br/>Slow control #1 part B"]
end
subgraph Cycle2["Slow control cycle 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>Slow control #2 part A"] --> C2F6["600-610ms<br/>Fast sensor #6<br/>P+3 preempts"]
C2F6 --> C2S2["610-640ms<br/>Slow control #2 part B"]
end
subgraph Cycle3["Slow control cycle 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>Slow control #3 part A"] --> C3F10["1000-1010ms<br/>Fast sensor #10<br/>P+3 preempts"]
C3F10 --> C3S2["1010-1040ms<br/>Slow control #3 part B"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
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:
flowchart TB
Ada[Ada Annex D<br/>Real-Time Features] --> Aero[Aerospace<br/>DO-178C]
Ada --> Rail[Railway<br/>EN 50128 family]
Ada --> Auto[Automotive<br/>ISO 26262]
Ada --> Medical[Medical Devices<br/>IEC 62304]
Ada --> Industrial[Industrial Control<br/>IEC 61508 family]
Ada --> Defense[Defense and high-integrity systems]
Aero --> A1[Flight control<br/>common Ada domain]
Aero --> A2[Satellite and spacecraft control]
Rail --> R1[Signaling systems]
Rail --> R2[Automatic Train Control]
Auto --> Au1[Candidate for safety-related ECUs]
Auto --> Au2[Selective use where C / MISRA-C<br/>remain dominant]
Medical --> M1[Pacemakers]
Medical --> M2[Infusion pumps]
Industrial --> I1[Robot controllers]
Industrial --> I2[CNC machines]
Defense --> D1[Mission computers]
Defense --> D2[Long-life operational systems]
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 |
Periodic execution that prevents cumulative drift |
| Ravenscar profile | Tasking subset that is more tractable for static analysis |
| Timing events | Polling-free timer-driven task wakeup |
| Protected queue | Barrier-based synchronization with protected objects |
| 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. Locking rules that limit priority inversion, absolute-time periodic execution, and execution-time monitoring are part of the language specification itself. Deadline achievement still has to be confirmed by design and analysis, but the language runtime gives you a predictable foundation for that work.
mindmap
root((Ada Annex D<br/>Real-Time Systems))
Scheduling
FIFO_Within_Priorities
Task priorities
Preemption
Priority Inversion Prevention
Ceiling_Locking protocol
Automatic priority elevation
Ceiling-priority rule
Periodic Execution
delay until
Prevents cumulative drift
Absolute time reference
Ravenscar Profile
Static task set
Deterministic analysis
No relative delay
DO-178C / ISO 26262
Timing Events
Polling-free wakeup
Protected handler registration
Handler runs at ceiling priority
Protected Objects / Queues
Entry barriers
Empty / partial / full state
Barrier-based synchronization
Monitoring
Per-task CPU time
Wall clock vs CPU
WCET validation and monitoring aid
Integration
Multi-periodic design
Protection via barriers
Language-level safety
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
How Ada builds concurrency into the language: tasks, rendezvous (entry/accept), and protected objects, shown through eight compilable cod...
Generic Programming in Ada — Contracts, Types, and Zero-Cost Reuse
A systematic introduction to generic programming in Ada. Covers generic subprograms, generic packages, formal subprogram parameters, type...
Introduction to Formal Verification with SPARK — From Ada Contracts to Mathematical Proof
A practical introduction to formal verification using the SPARK subset of Ada. Covers how to step up from contracts (Pre/Post) to proofs,...
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...
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.
Frequently Asked Questions
Common questions about the topic of this article.
- What is the Ravenscar profile in Ada?
- The Ravenscar profile restricts Ada's tasking model to a statically analyzable, deterministic subset for safety-critical systems. It forbids features that make worst-case timing analysis difficult, including dynamic task creation, select statements, abort statements, dynamic priority changes, relative delays, and task termination. Under these restrictions a program becomes much more tractable for static timing analysis, a property required by safety standards such as DO-178C for aviation, ISO 26262 for automotive, and IEC 62304 for medical devices. You enable it by placing pragma Profile (Ravenscar) in a gnat.adc file.
- How does Ada prevent priority inversion?
- Ada uses the Ceiling_Locking protocol, which is built directly into protected objects rather than bolted on as a library. Each protected object is given a ceiling priority, and any task entering it is immediately raised to that ceiling, so no medium-priority task can preempt a task inside the critical section; on exit, the task returns to its original priority. The ceiling must be at least as high as the highest priority of any task that uses the object, and Ada can detect violations by raising Program_Error. Achieving the same effect with C's pthread mutexes requires explicitly setting the PTHREAD_PRIO_PROTECT attribute, whereas in Ada it is a standard language feature.
- What is the difference between delay and delay until in Ada?
- A plain delay statement waits for a relative duration, so the processing time of each iteration accumulates and a periodic task drifts over time. delay until waits for an absolute time instead: you keep a Next_Release variable and advance it by the period each cycle, so even if one iteration overruns, the next release time is still correct. Note that delay until does not guarantee the computation fits inside the period — if the release time has already passed, it returns almost immediately, and the system should treat that as a deadline miss or overload condition.
- How do you measure a task's CPU time in Ada, and does it prove the WCET?
- The Ada.Execution_Time package provides per-task CPU time accounting, which counts only the time a task actually spends executing on the CPU, unlike Ada.Real_Time.Clock, which measures wall-clock time including blocked and preempted intervals. This distinction is the foundation for runtime execution-time monitoring and for validating worst-case execution time (WCET) assumptions. Measurement alone does not guarantee the true WCET, however: cache effects, pipeline behavior, and memory contention still require static analysis tools or target-specific validation.
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