Real-Time Systems Programming in Ada — Priorities, Periodic Execution, and CPU Time Control in Practice

· Updated: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage

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.

Real-Time RequirementsDeadline miss = failureOccasional miss toleratedDeadlineAbsolute time to completePeriodRepetition intervalWCETWorst-case execution timeJitterTiming variabilityHard Real-TimeFlight controlAirbagPacemakerSoft Real-TimeVideo streamingGamesUIAda Annex DMechanisms for predictabilityFIFO_Within_PrioritiesCeiling_Lockingdelay untilSchedulability analysisNecessary condition: WCET &lt;= deadlineSufficiency 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.

Shared resourceMedium-priority taskHigh-priority taskLow-priority taskSchedulerShared resourceMedium-priority taskHigh-priority taskLow-priority taskSchedulerInside critical sectionH wakes, so the scheduler suspends LBlocked! L still holds the lockH waits for the lock, so L resumesContinuing toward lock release...M wakes, so the scheduler suspends LL cannot release the lockM runs freely (L & H both stuck)【PRIORITY INVERSION】High-priority blocked indefinitelyAcquires lockTries to acquire lock

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 Priority assigns a static priority to each task. Priority'Last is the highest; Priority'First is 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.
Low-priority task(Priority=First)High-priority task(Priority=Last)Main taskSchedulerLow-priority task(Priority=First)High-priority task(Priority=Last)Main taskSchedulerT=0ms: Both tasks are runnablePrint start logPrint start logT=100ms: HP wakesPrint completion logT=500ms: LP wakesPrint completion log(T=800ms) Main doneCreate taskCreate taskPick HP, the highest priorityBlock until T+100msRun LP nextBlock until T+500msRun HPRun LP
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:

  1. A ceiling priority is set on the protected object via pragma Priority (Ceiling).
  2. Whenever any task enters the protected object, it is immediately raised to the ceiling priority.
  3. This prevents any medium-priority task from preempting a task currently inside the protected object.
  4. 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.

Protected Object(ceiling=30)High-priority task(prio=30)Medium-priority task(prio=20)Low-priority task(prio=10)SchedulerProtected Object(ceiling=30)High-priority task(prio=30)Medium-priority task(prio=20)Low-priority task(prio=10)SchedulerA caller with active priority > ceiling raises Program_ErrorH(30) equals the ceiling(30), so it may enterActive priority raised to 30M wakes upL is running at ceiling 30M(20) cannot preempt itH wakes upH(30) passes the ceiling checkbut waits because L is using POPriority restored to 10Run H after PO is releasedH(30) = ceiling(30), so it may enter after contention clearsEnter protected opExecute operationExit protected opEnter protected opExit protected op

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.

Overrun - deadline misscomputation 130msNext = T+100msdelay until T+100ms returns immediatelyDetect lateness and treat as overloaddelay until - Absolute-Time Basedcomputation 15msNext = T+100msdelay until T+100ms → wake at 100mscomputation 10msNext = T+200ms → wake at 200msActual intervals: 100ms, 100ms...delay Period - Cumulative Driftdelay 100ms → wake at 115msT=0ms: computation 15mscomputation 10ms → wake at 125msdelay 100ms → wake at 225msActual intervals: 115ms, 110ms...Accumulating error over timePrevents cumulative drift

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.

Full Ada TaskingRavenscar ProfileRestrictionsRequired policiesNo dynamic task creationNo select statementsNo abort statementsNo Task_AttributesMax 1 entry per protected objectNo requeueNo relative delayuse delay untilNo dynamic prioritiesNo task terminationall tasks nonterminatingFIFO_Within_PrioritiesCeiling_LockingHelps enable:Static timing analysisDO-178CAviation softwareISO 26262Automotive functional safetyIEC 62304Medical 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.

Initial statePut (add first item)Put / GetGet (remove last item)Put (fill last slot)Get (free one slot)Get blocks (barrier Count=0)Put blocks (barrier Count=Buffer_Size)Empty / Count=0Partial / Count=1..Buffer_Size-1Full / 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.

CPU TimeOnly: actual computationTotal CPU: 120msWall Clock TimeIncludes: compute + idle + blocked + preemptedTotal elapsed: 500msThe gap = time spent waiting or preemptedCPU time observes real computation costUseful for WCET validation and monitoringExcludes waiting, blocking, and preemption timeCautionMeasurement does not guarantee true WCETStatic 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.

Slow control cycle 3 (release=950ms)Slow control cycle 2 (release=550ms)Slow control cycle 1 (release=150ms)1000-1010msFast sensor #10P+3 preempts950-1000msSlow control #3 part A1010-1040msSlow control #3 part B600-610msFast sensor #6P+3 preempts550-600msSlow control #2 part A610-640msSlow control #2 part B150-200msSlow control #1 part A100-110msFast sensor #1200-210msFast sensor #2P+3 preempts210-240msSlow control #1 part BIllustrative assumptionFast sensor: 10ms workSlow control: 80ms work

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:

Ada Annex DReal-Time FeaturesAerospaceDO-178CRailwayEN 50128 familyAutomotiveISO 26262Medical DevicesIEC 62304Industrial ControlIEC 61508 familyDefense and high-integrity systemsFlight controlcommon Ada domainSatellite and spacecraft controlSignaling systemsAutomatic Train ControlCandidate for safety-related ECUsSelective use where C / MISRA-Cremain dominantPacemakersInfusion pumpsRobot controllersCNC machinesMission computersLong-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 Priority depends on the execution environment (OS + GNAT runtime). On Linux it maps to SCHED_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_Time provides 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.

Ada Annex DReal-Time SystemsSchedulingFIFO_Within_PrioritiesTask prioritiesPreemptionPriority InversionPreventionCeiling_Locking protocolAutomatic priority elevationCeiling-priority rulePeriodic Executiondelay untilPrevents cumulative driftAbsolute time referenceRavenscar ProfileStatic task setDeterministic analysisNo relative delayDO-178C / ISO 26262Timing EventsPolling-free wakeupProtected handlerregistrationHandler runs at ceilingpriorityProtected Objects / QueuesEntry barriersEmpty / partial / full stateBarrier-basedsynchronizationMonitoringPer-task CPU timeWall clock vs CPUWCET validation andmonitoring aidIntegrationMulti-periodic designProtection via barriersLanguage-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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

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.

Back to the Blog