Programmation de systèmes temps réel avec Ada ── Priorités, exécution périodique et contrôle du temps d'exécution en pratique

· · Ada, Temps Réel, Ravenscar, CeilingLocking, Tâches, Ordonnancement, Inversion de Priorité, Langage de Programmation

1. Introduction ── La relation profonde entre Ada et le temps réel

Notre article précédent, « Concurrence sécurisée en Ada », a présenté les bases de la concurrence sûre reposant sur les tâches et les objets protégés d’Ada. Cette fois, nous allons plus loin, dans un domaine encore plus contraint — les systèmes temps réel.

Dans un système temps réel, la « correction » ne se limite pas à l’exactitude logique du résultat du calcul : elle inclut aussi le fait que ce résultat soit obtenu dans les délais. Une réponse correcte obtenue avec une milliseconde de retard est aussi dangereuse qu’une réponse erronée.

Ada répond à cette exigence avec un ensemble complet de fonctionnalités temps réel normalisées dans l’Annexe D (Real-Time Systems) de la spécification du langage. Il ne s’agit pas d’une bibliothèque « ajoutée après coup », mais d’une garantie temps réel intégrée directement au runtime du langage.

Fonctionnalités temps réel d'Ada (Annexe D) :
- Priorités de tâches et préemption (FIFO_Within_Priorities)
- Protocole Ceiling_Locking (prévention de l'inversion de priorité)
- Exécution périodique à temps absolu avec delay until
- Profil Ravenscar (sous-ensemble critique pour la sûreté)
- Événements temporisés (réveil à heure fixe sans scrutation)
- Surveillance du temps d'exécution (Ada.Execution_Time)
- Ordonnancement multi-périodique

Cet article présente ces fonctionnalités de façon progressive à travers 8 exemples de code pratiques. Chaque extrait peut être considéré comme un exemple indépendant, mais les exemples 04 et 05, qui contiennent plusieurs unités de compilation, doivent d’abord être séparés avec gnatchop avant d’être compilés avec gnatmake.

Les extraits de code de cet article sont par ailleurs disponibles, organisés par chapitre, dans un dépôt de référence sur GitHub.

ada-real-time-systems - komurasoft-blog-samples (GitHub)

2. Qu’est-ce qu’un système temps réel ?

Commençons par clarifier le vocabulaire.

Concept Description
Temps réel dur (Hard real-time) Le dépassement d’une échéance constitue un échec catastrophique du système (commandes de vol, airbags, stimulateurs cardiaques)
Temps réel souple (Soft real-time) Le dépassement d’une échéance est indésirable mais des dépassements occasionnels sont tolérés (streaming vidéo, jeux)
Échéance (deadline) L’instant absolu auquel une tâche doit être terminée
Période (period) L’intervalle de temps auquel une tâche est déclenchée de façon répétée
WCET (Worst-Case Execution Time) Le temps d’exécution le plus défavorable d’une tâche
Gigue (jitter) La variation dans l’exécution périodique

Dans la conception d’un système temps réel, il est essentiel que « WCET <= échéance » soit vérifié pour chaque tâche. Cela ne garantit toutefois pas, à soi seul, que l’ensemble du système respecte ses échéances : il faut également une analyse du temps de réponse prenant en compte le temps de blocage, l’attribution des priorités, la gigue, les interruptions, ainsi que le comportement du runtime et de l’OS. En pratique, on vise plutôt WCET < échéance afin de conserver une marge. Les fonctionnalités temps réel d’Ada fournissent, au niveau du langage, un modèle d’exécution prévisible qui facilite cette analyse.

Exigences temps réelÉchéance non tenue = échec critiqueDépassement occasionnel toléréÉchéanceInstant absolu à respecterPériodeIntervalle de répétitionWCETTemps d'exécution pire casGigueVariation de la périodeTemps réel durCommandes de volAirbagStimulateur cardiaqueTemps réel soupleStreaming vidéoJeuxUIAda Annexe DMécanismes au service de la prévisibilitéFIFO_Within_PrioritiesCeiling_Lockingdelay untilAnalyse d'ordonnançabilitéCondition nécessaire : WCET &lt;= échéanceLa suffisance se vérifie par l'analyse du temps de réponse

L’un des phénomènes les plus dangereux dans les systèmes temps réel est l’inversion de priorité. Ce problème s’est réellement produit sur la sonde Mars Pathfinder en 1997, provoquant des redémarrages répétés de l’engin.

Ressource partagéeTâche priorité moyenneTâche haute prioritéTâche basse prioritéOrdonnanceurRessource partagéeTâche priorité moyenneTâche haute prioritéTâche basse prioritéOrdonnanceurExécution de la section critiqueH se réveille, l'ordonnanceur suspend LBloquée ! (L le détient toujours)H attend le verrou, donc L reprendContinue vers la libération du verrou...M se réveille, l'ordonnanceur suspend LL ne peut pas libérer le verrouM s'exécute librement (H et L bloquées)【INVERSION DE PRIORITÉ】Haute priorité bloquée indéfinimentAcquiert le verrouTente d'acquérir le verrou

Une tâche de faible priorité qui détient un verrou se fait préempter par une tâche de priorité intermédiaire, et la tâche de haute priorité se retrouve bloquée indéfiniment. Sur Mars Pathfinder, la contre-mesure effectivement appliquée fut l’activation de l’héritage de priorité (priority inheritance) de VxWorks ; Ada, lui, répond à ce même type de problème avec un mécanisme différent fourni comme fonctionnalité du langage : Ceiling_Locking.

3. Les bases des priorités de tâches ── FIFO_Within_Priorities

FIFO_Within_Priorities est une politique d’ordonnancement standard basée sur les priorités, que l’Annexe D d’Ada permet de spécifier explicitement. Si aucune politique n’est précisée, le comportement par défaut dépend de l’implémentation ; sous GNAT, cette famille de politiques est utilisée sur de nombreuses cibles. Au sein d’un même niveau de priorité, les tâches s’exécutent en FIFO (premier arrivé, premier servi), et une tâche de priorité supérieure préempte (interrompt) toute tâche de priorité inférieure.

-- 01_task_priority.ada
-- Bases de la priorité de tâche et de FIFO_Within_Priorities
-- Le pragma de configuration doit précéder toutes les clauses de contexte

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;

Points clés :

  • pragma Priority attribue une priorité statique à chaque tâche. Priority'Last est la plus élevée, Priority'First la plus basse.
  • Le corps des tâches de cette démonstration n’effectue pas de calcul lourd ; il attend simplement jusqu’à l’instant indiqué avec delay until. Ce que l’on souhaite vérifier ici, c’est que la tâche de priorité supérieure obtient la première opportunité d’exécution dès que les deux tâches deviennent exécutables simultanément.
  • Ce schéma illustre le volet préemption entre priorités différentes de FIFO_Within_Priorities. Pour vérifier l’ordre FIFO au sein d’un même niveau de priorité, il faudrait un autre exemple avec plusieurs tâches de même priorité.
  • Dans un système réel, il est courant de concevoir les priorités de façon relative par rapport à System.Default_Priority.
Tâche basse priorité(Priority=First)Tâche haute priorité(Priority=Last)Tâche principaleOrdonnanceurTâche basse priorité(Priority=First)Tâche haute priorité(Priority=Last)Tâche principaleOrdonnanceurT=0ms : les deux tâches sont exécutablesAffiche le message de démarrageAffiche le message de démarrageT=100ms : HP se réveilleAffiche le message de finT=500ms : LP se réveilleAffiche le message de fin(T=800ms) Fin de la tâche principaleCréation de la tâcheCréation de la tâcheSélectionne HP, priorité la plus élevéeSe bloque jusqu'à T+100msExécute LP ensuiteSe bloque jusqu'à T+500msExécute HPExécute LP
Plage de priorités d'Ada (valeurs par défaut de GNAT) :
  Priority'First  = 0   (la plus basse)
  Priority'Last   = 30  (la plus élevée, dépend toutefois de l'OS)

4. Ceiling_Locking ── Quand le langage empêche l’inversion de priorité

L’un des problèmes les plus délicats des systèmes temps réel est l’inversion de priorité (priority inversion) : une tâche de haute priorité attend un verrou détenu par une tâche de faible priorité, et cette dernière se fait préempter par une tâche de priorité intermédiaire, ce qui bloque indéfiniment la tâche de haute priorité.

Ada répond à ce problème en intégrant directement le protocole Ceiling_Locking dans les objets protégés.

-- 02_ceiling_locking.ada
-- Le protocole Ceiling_Locking empêche l'inversion de priorité
-- Le pragma de configuration doit précéder toutes les clauses de contexte

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;

Fonctionnement de Ceiling_Locking :

  1. Une priorité plafond (ceiling priority) est fixée sur l’objet protégé avec pragma Priority (Ceiling).
  2. Quelle que soit la tâche qui entre dans l’objet protégé, elle est automatiquement élevée à la priorité plafond dès son entrée.
  3. Cela empêche toute tâche de priorité intermédiaire de préempter la tâche en train d’utiliser l’objet protégé.
  4. En sortant de l’objet protégé, la tâche retrouve sa priorité d’origine.

Le schéma ci-dessous n’est pas une trace temporelle exacte du code d’exemple précédent : c’est un schéma conceptuel montrant comment le motif d’inversion de priorité de la Figure 2 est contenu grâce à Ceiling_Locking.

Objet protégé(plafond=30)Tâche haute priorité(priorité=30)Tâche priorité moyenne(priorité=20)Tâche basse priorité(priorité=10)OrdonnanceurObjet protégé(plafond=30)Tâche haute priorité(priorité=30)Tâche priorité moyenne(priorité=20)Tâche basse priorité(priorité=10)OrdonnanceurUn appelant dont la priorité active > priorité plafond déclenche Program_ErrorH(30) est égal au plafond (30), donc elle peut entrerPriorité active élevée à 30M se réveilleL s'exécute au plafond 30M(20) ne peut pas la préempterH se réveilleH(30) passe le contrôle de plafondmais attend car L utilise POPriorité restaurée à 10H s'exécute après la libération de POH(30) = plafond (30), donc elle peut entrer une fois la contention résolueEntre dans l'opération protégéeExécute l'opérationSort de l'opération protégéeEntre dans l'opération protégéeSort de l'opération protégée

Règle de conception : la priorité plafond d’un objet protégé doit être fixée au moins aussi haut que la priorité la plus élevée parmi toutes les tâches qui utilisent cet objet. Si une tâche dont la priorité active dépasse le plafond appelle une opération protégée, Ada peut détecter cette erreur de conception en déclenchant Program_Error.

Obtenir le même résultat avec les mutex pthread du langage C nécessite de définir explicitement l’attribut PTHREAD_PRIO_PROTECT, alors qu’en Ada, il s’agit d’une fonctionnalité standard du langage.

5. delay until ── Exécuter une tâche périodique sans dérive

Le motif fondamental des systèmes temps réel est la tâche périodique : une tâche exécutée de façon répétée à intervalle fixe. Il est absolument essentiel d’éviter l’erreur temporelle cumulative (la dérive).

Le delay until d’Ada résout ce problème avec élégance.

-- 03_periodic_task.ada
-- delay until pour une tâche périodique ── empêche la dérive cumulative

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;

Pourquoi delay until plutôt qu’un simple delay :

Méthode Problème
delay Period; Le temps de traitement de chaque itération s’accumule ; la période dérive progressivement (dérive cumulative)
delay until Next_Release; Next_Release := Next_Release + Period; Basé sur un temps absolu : même si un traitement prend du retard, la prochaine date de réveil reste correcte

Cependant, delay until ne garantit pas automatiquement que le traitement tienne dans la période. Si le traitement dépasse la prochaine date de réveil, ce delay until revient presque immédiatement, et le système se retrouve dans un état qui doit être traité comme un dépassement d’échéance (deadline miss).

Avec delay :
  T=0ms → traitement(15ms) → delay 100ms → T=115ms → traitement(10ms) → ...
  Intervalles réels : 115ms, 110ms, ... (le temps de traitement s'accumule)

Avec delay until :
  Next_Release : 100ms, 200ms, 300ms, ... (temps absolu)
  T=0ms → traitement(15ms) → delay until 100ms → T=100ms → traitement(10ms) → delay until 200ms
  Intervalles réels : 100ms, 100ms, ... (indépendants du temps de traitement)

Ce motif delay until est utilisé dans toutes les tâches périodiques présentées par la suite.

Dépassement de période - deadline misscalcul 130msProchaine = T+100msdelay until T+100ms revient immédiatementLe retard est détecté et traité comme une surchargedelay until - Basé sur le temps absolucalcul 15msProchaine = T+100msdelay until T+100ms → réveil à 100mscalcul 10msProchaine = T+200ms → réveil à 200msIntervalles réels : 100ms, 100ms...delay Period - Dérive cumulativedelay 100ms → réveil à 115msT=0ms : calcul 15mscalcul 10ms → 125msdelay 100ms → réveil à 225msIntervalles réels : 115ms, 110ms...L'erreur s'accumule avec le tempsEmpêche la dérive cumulative

6. Le profil Ravenscar ── Un sous-ensemble temps réel vérifiable

Les fonctionnalités de tâches d’Ada sont puissantes, mais dans les systèmes où la sûreté est absolument critique, cette puissance devient elle-même un problème. La création dynamique de tâches, les instructions select, les instructions abort, etc., rendent difficile l’analyse statique du temps d’exécution pire cas.

Le profil Ravenscar est la réponse qu’apporte Ada à ce problème : il restreint les fonctionnalités de tâches à un sous-ensemble statiquement analysable et déterministe.

-- 04_ravenscar_profile.ada
-- Bases du profil Ravenscar
-- Activer avec : pragma Profile (Ravenscar); dans un fichier gnat.adc

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;

Restrictions du profil Ravenscar :

Fonctionnalité interdite Raison
Création dynamique de tâches (new ou types accès) L’allocation mémoire à l’exécution est non déterministe
Instructions select Pas seulement les alternatives multiples : l’instruction select dans son ensemble complique l’analyse du flux de contrôle
Instructions abort L’interruption asynchrone rend l’état imprévisible
Ada.Task_Attributes Comportement dynamique à l’exécution
Changement dynamique de priorité Les hypothèses de l’analyse d’ordonnancement changent à l’exécution
delay relatif Tend à produire une dérive cumulative ; utiliser le delay until à temps absolu
Plusieurs entrées par objet protégé Augmente les conditions de blocage et le périmètre de l’analyse
Terminaison de tâche Ravenscar traite toutes les tâches comme non terminantes
Instruction requeue Complique le suivi du flux de contrôle

Ces restrictions rendent un programme conforme à Ravenscar beaucoup plus propice à l’analyse temporelle statique — une propriété exigée par des normes de sûreté telles que DO-178C (logiciel aéronautique) ou ISO 26262 (sécurité fonctionnelle automobile). La liste ci-dessus n’est qu’un extrait des principales restrictions ; le profil réel comprend également des règles supplémentaires liées au runtime et à l’analysabilité, telles que No_Task_Hierarchy ou Detect_Blocking.

Fonctionnalités complètes de tâches d'AdaProfil RavenscarRestrictionsPolitiques obligatoiresInterdiction de la création dynamique de tâchesInterdiction de l'instruction selectInterdiction de l'instruction abortInterdiction de Task_AttributesLimité à 1 entrée par objet protégéInterdiction de l'instruction requeueInterdiction du delay relatifutiliser delay untilInterdiction du changement dynamique de prioritéInterdiction de la terminaison de tâchetoutes les tâches non terminantesFIFO_Within_PrioritiesCeiling_LockingFacilite :l'analyse temporelle statiqueDO-178CLogiciel aéronautiqueISO 26262Sécurité fonctionnelle automobileIEC 62304Logiciel de dispositif médical

Pour activer le profil Ravenscar, il suffit d’inscrire ceci dans un fichier gnat.adc :

pragma Profile (Ravenscar);

7. Événements temporisés ── Un réveil piloté par le temps, sans scrutation

De nombreux systèmes temps réel ont besoin de « réveiller une tâche de haute priorité à un instant donné ». Une implémentation naïve reposerait sur la scrutation (polling) d’un minuteur, mais Ada propose un mécanisme plus élégant : les événements temporisés (timing events).

-- 05_timing_events.ada
-- Événements temporisés (Ada.Real_Time.Timing_Events)
-- Réveille une tâche de haute priorité sans scrutation

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;

Fonctionnement des événements temporisés :

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  ── enregistre un gestionnaire à un instant absolu
2. T+100ms écoulé ── le runtime appelle S.Fire à **la priorité plafond**
3. Fire positionne le drapeau Fired à True ── la barrière s'ouvre
4. La tâche Reactor se réveille depuis Wait_For_Event

Ce qui compte ici, c’est que cet exemple spécifie explicitement Ceiling_Locking, et que le gestionnaire Fire étant une procédure protégée, il s’exécute à la priorité plafond de l’objet protégé. Une procédure protégée utilisée comme gestionnaire d’événement temporisé doit se trouver dans un objet protégé dont la priorité plafond est de niveau interruption, ici System.Interrupt_Priority'Last. Cela garantit qu’aucune inversion de priorité ne se produit lors du traitement de l’événement temporisé.

8. Une file d’attente temps réel avec les objets protégés

Un motif fréquent dans les systèmes temps réel est le schéma producteur-consommateur : un capteur génère des données, et une tâche de contrôle les consomme. L’exclusion mutuelle et le blocage du tampon doivent alors être gérés efficacement.

Les objets protégés et les barrières d’entrée d’Ada permettent d’implémenter cela comme une synchronisation basée sur des barrières. Le runtime gère l’exclusion mutuelle en interne, sans que le code applicatif n’ait à écrire directement de mutex ni de variable de condition.

-- 06_protected_queue.ada
-- Partage de données temps réel via un objet protégé
-- Pipeline : Producer -> Bounded_Buffer -> Consumer
-- Activer avec : pragma Locking_Policy (Ceiling_Locking); dans un fichier gnat.adc

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;

Points clés de la conception :

  • entry Put when Count < Buffer_Size ── si le tampon est plein, le Producer se bloque automatiquement.
  • entry Get when Count > 0 ── si le tampon est vide, le Consumer se bloque automatiquement.
  • pragma Priority (System.Any_Priority'Last) ── grâce au ceiling locking, aucune inversion de priorité ne se produit entre le Producer et le Consumer.
  • Les conditions de barrière sont définies à partir de l’état interne de l’objet protégé (Count), et sont réévaluées automatiquement à chaque libération du verrou.

Ce code ne comporte aucun mutex, sémaphore ou variable de condition côté application. Les mises en attente nécessaires sont exprimées uniquement à travers les barrières d’entrée de l’objet protégé.

État initialPut (ajoute 1 élément)Put / GetGet (retire le dernier élément)Put (comble la dernière place)Get (libère une place)Get se bloque (barrière Count=0)Put se bloque (barrière Count=Buffer_Size)Vide / Count=0Partiel / Count=1..Buffer_Size-1Plein / Count=Buffer_Size

Un Put réussi réévalue les Get en attente, et un Get réussi réévalue les Put en attente. Cette réévaluation de barrière a lieu à la fin de l’opération protégée, indépendamment de l’état représenté dans le schéma.

9. Mesurer le temps d’exécution ── Le premier pas vers la surveillance du temps d’exécution

Pour évaluer l’ordonnançabilité d’un système temps réel, il faut connaître précisément le temps d’exécution (temps CPU) de chaque tâche. Le paquetage Ada.Execution_Time d’Ada fournit le décompte du temps CPU consommé, par tâche.

-- 07_execution_time.ada
-- Contrôle du temps d'exécution (Execution_Time)
-- Mesure la consommation CPU par tâche

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;

Temps horloge murale vs temps CPU :

Temps horloge murale (Wall Clock) : Ada.Real_Time.Clock
  → Temps réellement écoulé. Inclut les périodes de blocage et de préemption.

Temps CPU (Execution Time) : Ada.Execution_Time.Clock
  → Seulement le temps où la tâche a réellement été exécutée sur le CPU.
  → Les périodes de blocage et de préemption ne sont pas comptées.

Cette distinction constitue le point de départ de la surveillance du temps d’exécution et de la validation du WCET. Pendant que Busy_Worker attend dans un delay until, son temps CPU n’augmente pas ; il n’augmente que pendant le calcul effectif. De même, pendant le delay until Clock + Milliseconds(500) de la tâche principale, le temps CPU devrait rester quasiment nul. Cependant, la mesure du temps CPU ne garantit pas le véritable WCET : le cache, le pipeline, la contention mémoire, etc., nécessitent une analyse statique ou une validation séparée sur l’environnement cible.

Temps CPUUniquement : le calcul réelTotal CPU : 120msTemps horloge muraleComprend : calcul + attente + blocage + préemptionTotal écoulé : 500msÉcart = temps d'attente, de blocage et de préemptionLe temps CPU observe le coût de calcul réelAide à la validation et à la surveillance du WCETExclut l'attente, le blocage et la préemptionAttentionLa mesure ne garantit pas le véritable WCETUne analyse statique ou une validation cible reste nécessaire

10. Démonstration intégrée ── Un système temps réel multi-périodique

Intégrons maintenant tous les éléments vus jusqu’ici — priorités, Ceiling_Locking, delay until, objets protégés — pour construire un système temps réel multi-périodique typique.

-- 08_multiperiodic.ada
-- Démonstration intégrée d'un système temps réel multi-périodique
-- Tâche de lecture de capteur à cycle rapide (100ms)
-- Tâche de contrôle à cycle lent (400ms)
-- Partage de données via Ceiling_Locking

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;

Architecture du système :

Le schéma ci-dessous s’appuie sur les instants de release du code d’exemple (capteur rapide à cycle de 100ms, contrôle lent à cycle de 400ms avec un décalage de 150ms), en supposant des temps d’exécution donnés à titre illustratif. Le code lui-même ne contient pas de calcul de contrôle de 80ms ; ce n’est donc pas un relevé de mesure réel. Si le capteur rapide a la priorité la plus élevée, une release survenant pendant l’exécution du contrôle lent interrompt momentanément ce dernier. Pour rester lisible, le schéma ne représente qu’une interruption représentative par cycle de contrôle lent, alors qu’en réalité le capteur rapide est relâché à chaque frontière de 100ms.

Contrôle lent cycle 3 (release=950ms)Contrôle lent cycle 2 (release=550ms)Contrôle lent cycle 1 (release=150ms)1000-1010msCapteur rapide #10P+3, donc interruption950-1000msContrôle lent #3 partie A1010-1040msContrôle lent #3 partie B600-610msCapteur rapide #6P+3, donc interruption550-600msContrôle lent #2 partie A610-640msContrôle lent #2 partie B150-200msContrôle lent #1 partie A100-110msCapteur rapide #1200-210msCapteur rapide #2P+3, donc interruption210-240msContrôle lent #1 partie BHypothèse illustrativeCapteur rapide : traitement 10msContrôle lent : traitement 80ms

Ce motif — « acquisition rapide par capteur + boucle de contrôle lente » — est un schéma classique fréquemment rencontré dans les systèmes de contrôle industriel et la robotique.

11. Les domaines où les fonctionnalités temps réel d’Ada excellent

Les fonctionnalités temps réel d’Ada apportent une valeur particulière dans les domaines suivants :

Ada Annexe DFonctionnalités temps réelAérospatialDO-178CFerroviaireFamille EN 50128AutomobileISO 26262Dispositifs médicauxIEC 62304Contrôle industrielFamille IEC 61508Défense et systèmes à haute fiabilitéCommandes de voldomaine d'application historiqueContrôle de satellites et d'engins spatiauxSystèmes de signalisationContrôle automatique des trainsCandidat pour les ECU liés à la sécuritéUsage limité et sélectif dans un domainedominé par le C / MISRA-CStimulateurs cardiaquesPompes à perfusionContrôle de robotsMachines-outils à commande numériqueCalculateurs de missionSystèmes à exploitation de longue durée

12. Points d’attention et limites

Les fonctionnalités temps réel d’Ada sont puissantes, mais elles ne sont pas une solution miracle.

1. Dépendance à la plateforme :

  • Le mappage réel de pragma Priority dépend de l’environnement d’exécution (OS + runtime GNAT). Sous Linux, il est mappé sur SCHED_FIFO, mais sous Windows, une préemption complète n’est pas toujours garantie.

2. Contraintes de Ravenscar :

  • La création dynamique de tâches étant interdite, toutes les tâches doivent être déclarées statiquement au démarrage du système. Cela limite la liberté de conception.

3. Limites de la mesure du WCET :

  • Ada.Execution_Time fournit une mesure, pas une garantie. Le véritable WCET, incluant les défauts de cache et les aléas de pipeline, doit être vérifié séparément avec des outils d’analyse statique.

4. Surcoût :

  • L’évaluation des barrières d’un objet protégé s’exécute automatiquement lors de l’achèvement ou de l’annulation d’une entrée, ainsi qu’à la sortie de l’objet protégé. Pour des objets protégés appelés très fréquemment, ce surcoût doit être pris en compte.

5. Barrière de la chaîne d’outils :

  • Exploiter pleinement les fonctionnalités temps réel d’Ada nécessite un compilateur croisé et un runtime adaptés. Sur les cibles embarquées en particulier, on dépendra du runtime fourni par le fournisseur.

13. Conclusion

Cet article a présenté, à travers 8 exemples de code progressifs, les fonctionnalités temps réel offertes par l’Annexe D d’Ada.

Fonctionnalité Valeur apportée
Priorités de tâches Ordonnancement préemptif basé sur les priorités
Ceiling_Locking Prévention de l’inversion de priorité intégrée au langage
delay until Exécution périodique sans dérive cumulative
Profil Ravenscar Sous-ensemble de tâches propice à l’analyse statique
Événements temporisés Réveil piloté par le temps, sans scrutation
File protégée Synchronisation basée sur les barrières d’un objet protégé
Mesure du temps d’exécution Surveillance du temps CPU par tâche
Intégration multi-périodique Conception permettant à des tâches de périodes différentes de cohabiter en toute sécurité

L’essence des fonctionnalités temps réel d’Ada tient au fait qu’elles ne sont pas ajoutées après coup. Les règles de verrouillage qui limitent l’inversion de priorité, la spécification temporelle pour l’exécution périodique, la surveillance du temps d’exécution — tout cela est fourni comme partie intégrante de la spécification du langage. Bien entendu, le respect effectif des échéances doit être vérifié par la conception et l’analyse, mais le fait que le runtime du langage en pose les fondations constitue un atout majeur.

Ada Annexe DSystèmes temps réelOrdonnancementFIFO_Within_PrioritiesPriorités de tâchesPréemptionPrévention de l'inversion deprioritéProtocole Ceiling_LockingÉlévation automatique deprioritéRègle de la priorité plafondExécution périodiquedelay untilEmpêche la dérivecumulativeBasé sur le temps absoluProfil RavenscarEnsemble de tâchesstatiqueAnalyse déterministeInterdiction du delay relatifDO-178C / ISO 26262Événements temporisésRéveil sans scrutationEnregistrement d'ungestionnaire protégéGestionnaire exécuté à lapriorité plafondObjets protégés / filesBarrières d'entréeGestion des étatsvide/partiel/pleinSynchronisation basée surles barrièresSurveillance du tempsd'exécutionTemps CPU par tâcheHorloge murale vs tempsCPUAide à la validation et à lasurveillance du WCETConception intégréeConception multi-périodiqueProtection par barrièresSûreté au niveau dulangage

Pour aller plus loin et essayer concrètement le développement de systèmes temps réel en Ada, installez la chaîne d’outils GNAT via Alire, puis compilez les exemples de code de cet article avec gnatchop + gnatmake.

Pour les bases de la concurrence en Ada (tâches, rendez-vous, objets protégés), reportez-vous à notre article précédent, « Concurrence sécurisée en Ada ».

14. Références

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Qu'est-ce que l'Annexe D d'Ada ?
C'est un ensemble de fonctionnalités pour les systèmes temps réel, normalisé comme partie intégrante de la spécification du langage Ada. Il comprend l'ordonnancement préemptif basé sur les priorités avec FIFO_Within_Priorities, le protocole Ceiling_Locking qui prévient l'inversion de priorité, l'exécution périodique à temps absolu avec delay until, le profil Ravenscar, les événements temporisés (timing events), ainsi que la mesure du temps d'exécution par tâche avec Ada.Execution_Time. Sa particularité est de ne pas être une bibliothèque ajoutée après coup, mais d'être intégré directement dans le runtime du langage lui-même.
Qu'est-ce que l'inversion de priorité, et comment Ada l'empêche-t-elle ?
C'est un phénomène dans lequel une tâche de faible priorité, tout en détenant un verrou, se fait préempter par une tâche de priorité intermédiaire, ce qui bloque indéfiniment une tâche de haute priorité en attente de ce même verrou. Ce problème s'est réellement produit sur la sonde Mars Pathfinder en 1997, provoquant des redémarrages répétés de l'engin. En Ada, le protocole Ceiling_Locking est fourni comme fonctionnalité du langage : toute tâche qui entre dans un objet protégé est automatiquement élevée à la priorité plafond (ceiling), ce qui empêche toute préemption par une tâche de priorité intermédiaire.
Qu'est-ce que le profil Ravenscar ?
C'est un profil qui restreint le modèle de tâches d'Ada à un sous-ensemble statiquement analysable et déterministe, destiné aux systèmes où la sûreté est absolument critique. Il interdit notamment la création dynamique de tâches, les instructions select, les instructions abort, les delay relatifs et les instructions requeue. Ces restrictions facilitent l'analyse temporelle statique et permettent de satisfaire plus facilement les propriétés exigées par des normes de sûreté telles que DO-178C (logiciel aéronautique) ou ISO 26262 (sécurité fonctionnelle automobile). Sous GNAT, on l'active en plaçant pragma Profile (Ravenscar) dans un fichier gnat.adc.
Pourquoi utiliser delay until plutôt que delay pour une tâche périodique ?
Avec delay, qui spécifie une durée relative, le temps de traitement de chaque itération s'accumule et le cycle dérive progressivement (dérive cumulative). delay until fixe la prochaine date de réveil sur une base de temps absolu : même si un traitement prend du retard, les dates de réveil suivantes restent correctes. Cependant, si le traitement dépasse la prochaine date de réveil, delay until revient presque immédiatement ; il faut alors concevoir séparément une détection de ce dépassement (deadline miss) et le traiter comme une situation de surcharge.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog