A4A : « Dual Port Memory » générique en Ada

Bonjour,

Dans un système constitué de multiples tâches collaborant il est nécessaire de mettre en œuvre une communication garantissant la cohérence des données échangées entre les tâches.

L’un des mécanismes préconisés en Ada pour fournir une solution est fondé sur les types protégés.
On en trouve quelques explications par exemple ici mais le livre de Monsieur John Barnes est certainement plus didactique.

Ce mécanisme est déjà utilisé dans « Ada for Automation » notamment par exemple pour les interfaces entre la tâche principale et les tâches clients Modbus TCP ou entre la tâche principale et la tâche serveur Modbus TCP.

Pour faire court, un type protégé encapsule les données privées et protégées et doit fournir les fonctions et procédures permettant la manipulation des données. Les fonctions permettent uniquement la lecture des données protégées et comme elles ne peuvent modifier celles-ci elles peuvent être appelées de façon concurrente sans problème tandis que les procédures permettent la lecture et l’écriture des données protégées et bloquent le temps de leur exécution les appels des fonctions et procédures concurrentes.

Cependant, il se trouve que le besoin était jusque là considéré comme spécifique et cela a donc conduit à des mises en œuvre spécifiques.
Mais si l’on considère le besoin d’une façon générale on peut aboutir à une solution plus générique qui peut convenir dans une grande majorité des cas d’utilisation.
Ainsi, dans « Ada for Automation », la tâche principale est par défaut associée à une tâche périodique. Ces tâches jusque là tournaient indépendamment l’une de l’autre et j’avais remis aux calendes de les faire communiquer entre elles autrement que par variables partagées, sans gestion de la cohérence donc.

En cette fin d’année les calendes sont donc arrivées !

J’ai nommé le mécanisme « Dual Port Memory » générique par déformation professionnelle, en référence à la fameuse Dual Port Memory Hilscher dont il est souvent question dans les pages de ce site. Et aussi bien sûr parce que la similitude est bien réelle.

Vous voudrez bien vous référer au schéma ci-après pour les explications qui suivent.

Si le mécanisme est générique, les données échangées sont structurées dans des types de données propres à l’application.
Le paquetage implémentant le mécanisme générique est instancié en lui fournissant les types de données Entrées et Sorties et figure dans la partie privée du paquetage « Interface ». Entrée et Sortie sont vues ainsi par l’une des tâches, l’autre voyant bien sûr l’inverse.

Ce paquetage contient également les zones d’échanges pour les programmes utilisateur de chacune des tâches, de façon similaire aux mémoires images process, ces zones étant aussi des instances des mêmes types Entrées et Sorties.
Il contient enfin les fonctions et procédures assurant la mise à jour des zones d’E/S, ces fonctions et procédures étant appelées par les tâches en amont et aval des traitements utilisateur.

A Generic DPM in Ada
A Generic DPM in Ada

Voici la spécification du paquetage générique DPM :

-----------------------------------------------------------------------
--                       Ada for Automation                          --
--                                                                   --
--              Copyright (C) 2012-2015, Stephane LOS                --
--                                                                   --
-- This library is free software; you can redistribute it and/or     --
-- modify it under the terms of the GNU General Public               --
-- License as published by the Free Software Foundation; either      --
-- version 2 of the License, or (at your option) any later version.  --
--                                                                   --
-- This library is distributed in the hope that it will be useful,   --
-- but WITHOUT ANY WARRANTY; without even the implied warranty of    --
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU --
-- General Public License for more details.                          --
--                                                                   --
-- You should have received a copy of the GNU General Public         --
-- License along with this library; if not, write to the             --
-- Free Software Foundation, Inc., 59 Temple Place - Suite 330,      --
-- Boston, MA 02111-1307, USA.                                       --
--                                                                   --
-- As a special exception, if other files instantiate generics from  --
-- this unit, or you link this unit with other files to produce an   --
-- executable, this  unit  does not  by itself cause  the resulting  --
-- executable to be covered by the GNU General Public License. This  --
-- exception does not however invalidate any other reasons why the   --
-- executable file  might be covered by the  GNU Public License.     --
-----------------------------------------------------------------------

generic
   type Inputs_Type is private;
   type Outputs_Type is private;

package A4A.Generic_Dual_Port_Memory is

   protected type Inputs_Dual_Port_Memory_Area is

      procedure Set_Data (Data_In : in Inputs_Type);

      function Get_Data return Inputs_Type;

   private
      Data : Inputs_Type;
   end Inputs_Dual_Port_Memory_Area;

   protected type Outputs_Dual_Port_Memory_Area is

      procedure Set_Data (Data_In : in Outputs_Type);

      function Get_Data return Outputs_Type;

   private
      Data : Outputs_Type;
   end Outputs_Dual_Port_Memory_Area;

   type Dual_Port_Memory is
      record
         Inputs : Inputs_Dual_Port_Memory_Area;

         Outputs : Outputs_Dual_Port_Memory_Area;
      end record;

   type Dual_Port_Memory_Access
     is access all Dual_Port_Memory;

end A4A.Generic_Dual_Port_Memory;

Et le corps :

-----------------------------------------------------------------------
--                       Ada for Automation                          --
--                                                                   --
--              Copyright (C) 2012-2015, Stephane LOS                --
--                                                                   --
-- This library is free software; you can redistribute it and/or     --
-- modify it under the terms of the GNU General Public               --
-- License as published by the Free Software Foundation; either      --
-- version 2 of the License, or (at your option) any later version.  --
--                                                                   --
-- This library is distributed in the hope that it will be useful,   --
-- but WITHOUT ANY WARRANTY; without even the implied warranty of    --
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU --
-- General Public License for more details.                          --
--                                                                   --
-- You should have received a copy of the GNU General Public         --
-- License along with this library; if not, write to the             --
-- Free Software Foundation, Inc., 59 Temple Place - Suite 330,      --
-- Boston, MA 02111-1307, USA.                                       --
--                                                                   --
-- As a special exception, if other files instantiate generics from  --
-- this unit, or you link this unit with other files to produce an   --
-- executable, this  unit  does not  by itself cause  the resulting  --
-- executable to be covered by the GNU General Public License. This  --
-- exception does not however invalidate any other reasons why the   --
-- executable file  might be covered by the  GNU Public License.     --
-----------------------------------------------------------------------

package body A4A.Generic_Dual_Port_Memory is

   protected body Inputs_Dual_Port_Memory_Area is

      procedure Set_Data  (Data_In : in Inputs_Type) is
      begin
         Data := Data_In;
      end Set_Data;

      function Get_Data return Inputs_Type is
      begin
         return Data;
      end Get_Data;

   end Inputs_Dual_Port_Memory_Area;

   protected body Outputs_Dual_Port_Memory_Area is

      procedure Set_Data  (Data_In : in Outputs_Type) is
      begin
         Data := Data_In;
      end Set_Data;

      function Get_Data return Outputs_Type is
      begin
         return Data;
      end Get_Data;

   end Outputs_Dual_Port_Memory_Area;

end A4A.Generic_Dual_Port_Memory;

Pour l’exemple d’instanciation voici ci-après la spécification du paquetage réalisant celle nécessaire pour l’exemple d’interface entre la tâche principale et la tâche périodique 1.

On y trouve :

  • la définition des types Entrées et Sorties,
  • la définition des zones mémoire correspondantes pour la tâche principale et la tâche périodique 1,
  • les procédures gérant les échanges entre les zones mémoire et les données protégées,
  • et dans la partie privée, l’instance du paquetage générique.

Pour votre application, il est nécessaire d’adapter les types de données Entrées et Sorties comme vous l’avez sans doute compris.

-----------------------------------------------------------------------
--                       Ada for Automation                          --
--                                                                   --
--              Copyright (C) 2012-2015, Stephane LOS                --
--                                                                   --
-- This library is free software; you can redistribute it and/or     --
-- modify it under the terms of the GNU General Public               --
-- License as published by the Free Software Foundation; either      --
-- version 2 of the License, or (at your option) any later version.  --
--                                                                   --
-- This library is distributed in the hope that it will be useful,   --
-- but WITHOUT ANY WARRANTY; without even the implied warranty of    --
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU --
-- General Public License for more details.                          --
--                                                                   --
-- You should have received a copy of the GNU General Public         --
-- License along with this library; if not, write to the             --
-- Free Software Foundation, Inc., 59 Temple Place - Suite 330,      --
-- Boston, MA 02111-1307, USA.                                       --
--                                                                   --
-- As a special exception, if other files instantiate generics from  --
-- this unit, or you link this unit with other files to produce an   --
-- executable, this  unit  does not  by itself cause  the resulting  --
-- executable to be covered by the GNU General Public License. This  --
-- exception does not however invalidate any other reasons why the   --
-- executable file  might be covered by the  GNU Public License.     --
-----------------------------------------------------------------------

with A4A.Generic_Dual_Port_Memory; use A4A;

package A4A.Application.Main_Periodic1 is

   --------------------------------------------------------------------
   --  Main / Periodic1 Tasks interface
   --------------------------------------------------------------------

   type Inputs is
      record
         A : Boolean := False;
         B : Word    :=     0;
         C : Byte    :=     0;
      end record;

   type Outputs is
      record
         X : Boolean := False;
         Y : Word    :=     0;
         Z : Byte    :=     0;
      end record;

   --------------------------------------------------------------------
   --  Memory areas available to Main task
   --------------------------------------------------------------------

   Main_Inputs  : Inputs;
   --  These are the Inputs from task Periodic1 to Main

   Main_Outputs : Outputs;
   --  These are the Outputs from task Main to Periodic1

   procedure Get_Main_Inputs;
   --  This procedure updates Main_Inputs from DPM
   --  Main task should call it before calling Main_Cyclic

   procedure Set_Main_Outputs;
   --  This procedure updates DPM from Main_Outputs
   --  Main task should call it after calling Main_Cyclic

   --------------------------------------------------------------------
   --  Memory areas available to Periodic1 task
   --------------------------------------------------------------------

   Periodic1_Inputs  : Outputs;
   --  These are the Inputs from task Main to Periodic1

   Periodic1_Outputs : Inputs;
   --  These are the Outputs from task Periodic1 to Main

   procedure Get_Periodic1_Inputs;
   --  This procedure updates Periodic1_Inputs from DPM
   --  Periodic1 task should call it before calling Periodic1_Run

   procedure Set_Periodic1_Outputs;
   --  This procedure updates DPM from Periodic1_Outputs
   --  Periodic1 task should call it after calling Periodic1_Run

private

   package DPM is new
     A4A.Generic_Dual_Port_Memory
       (Inputs_Type  => Inputs,
        Outputs_Type => Outputs);

   The_DPM : DPM.Dual_Port_Memory;

end A4A.Application.Main_Periodic1;

En voici le corps :

-----------------------------------------------------------------------
--                       Ada for Automation                          --
--                                                                   --
--              Copyright (C) 2012-2015, Stephane LOS                --
--                                                                   --
-- This library is free software; you can redistribute it and/or     --
-- modify it under the terms of the GNU General Public               --
-- License as published by the Free Software Foundation; either      --
-- version 2 of the License, or (at your option) any later version.  --
--                                                                   --
-- This library is distributed in the hope that it will be useful,   --
-- but WITHOUT ANY WARRANTY; without even the implied warranty of    --
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU --
-- General Public License for more details.                          --
--                                                                   --
-- You should have received a copy of the GNU General Public         --
-- License along with this library; if not, write to the             --
-- Free Software Foundation, Inc., 59 Temple Place - Suite 330,      --
-- Boston, MA 02111-1307, USA.                                       --
--                                                                   --
-- As a special exception, if other files instantiate generics from  --
-- this unit, or you link this unit with other files to produce an   --
-- executable, this  unit  does not  by itself cause  the resulting  --
-- executable to be covered by the GNU General Public License. This  --
-- exception does not however invalidate any other reasons why the   --
-- executable file  might be covered by the  GNU Public License.     --
-----------------------------------------------------------------------

package body A4A.Application.Main_Periodic1 is

   procedure Get_Main_Inputs is
   begin
      Main_Inputs := The_DPM.Inputs.Get_Data;
   end Get_Main_Inputs;

   procedure Set_Main_Outputs is
   begin
      The_DPM.Outputs.Set_Data (Main_Outputs);
   end Set_Main_Outputs;

   procedure Get_Periodic1_Inputs is
   begin
      Periodic1_Inputs := The_DPM.Outputs.Get_Data;
   end Get_Periodic1_Inputs;

   procedure Set_Periodic1_Outputs is
   begin
      The_DPM.Inputs.Set_Data (Periodic1_Outputs);
   end Set_Periodic1_Outputs;

end A4A.Application.Main_Periodic1;

Dans la tâche principale on rajoute les deux incantations autour de l’appel au programme utilisateur :

package body A4A.Kernel.Main is
...
            A4A.Application.Main_Periodic1.Get_Main_Inputs;
            A4A.Application.Main_Cyclic;
            A4A.Application.Main_Periodic1.Set_Main_Outputs;
...

Et on procède de même avec la tâche périodique 1 :

package body A4A.Kernel is
...

   procedure Periodic1_Run is
   begin
      A4A.Application.Main_Periodic1.Get_Periodic1_Inputs;
      A4A.Application.Periodic1_Cyclic;
      A4A.Application.Main_Periodic1.Set_Periodic1_Outputs;
   end Periodic1_Run;

...

Enfin, on peut jouer avec notre interface depuis la fonction principale… :

package body A4A.Application is
...

   procedure Main_Cyclic is
      My_Ident : constant String := "A4A.Application.Main_Cyclic";
   begin

      Map_Inputs;

      Map_HMI_Inputs;

      --  Playing with tasks interface
      Main_Outputs.X := Main_Inputs.A;
      Main_Outputs.Y := Main_Inputs.B;
      Main_Outputs.Z := Main_Inputs.C;

      Process_IO;

      Map_Outputs;

      Map_HMI_Outputs;

   exception

      when Error : others =>
         A4A.Log.Logger.Put (Who  => My_Ident,
                             What => Exception_Information (Error));

         Program_Fault_Flag := True;

   end Main_Cyclic;

...

… comme depuis la fonction périodique :

...

   procedure Periodic1_Cyclic is
      My_Ident : constant String := "A4A.Application.Periodic1_Cyclic";
   begin

      --  Playing with tasks interface
      Periodic1_Outputs.A := not Periodic1_Inputs.X;
      Periodic1_Outputs.B := Periodic1_Inputs.Y + 2;
      Periodic1_Outputs.C := Periodic1_Inputs.Z + 1;

   exception

      when Error : others =>
         A4A.Log.Logger.Put (Who  => My_Ident,
                             What => Exception_Information (Error));

         Program_Fault_Flag := True;

   end Periodic1_Cyclic;

...

Il y a également un programme de test qui permet de jouer avec cette DPM que vous trouverez dans le répertoire :

A4A/test/src/test_generic_dpm.adb

Comme ça fonctionne plutôt bien, je l’ai mis à l’épreuve un peu partout et j’ai donc pas mal bougrassé le code A4A en réorganisant les différents noyaux (kernels) afin d’en faciliter la réutilisation et l’évolution et en en rajoutant d’autres, ce dont je vous entretiendrai avec plaisir l’année prochaine.

Je vous remercie pour votre attention et vous souhaite de bonnes fêtes de fin d’année.

Cordialement,
Stéphane