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

· · 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 Priority assigns a static priority to each task. Priority'Last is the highest; Priority'First is 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:

  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.

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 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 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

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.

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