Gem #103: Code Archetypes for Real-Time Programming — Part 5

by Marco Panunzio —University of Padua

Introduction

In the previous Ada Gem we completed the creation of a complete sporadic task. In this Ada Gem that ends our mini-series, we want to complete the example by adding the realization of the communication between different tasks. In particular, we investigate how we can correctly manage the calls of operations outside a task. Those calls are performed from an OPCS and they have to be correctly routed to the endpoint of the communication.

Intertask communication -- A producer-consumer example

Suppose we want to realize the simple producer-consumer collaboration pattern depicted in the figure below.

Producer_Consumer.png

The Producer is a cyclic task which, after some processing, produces some data that is sent to a Consumer sporadic task. Data is passed as a parameter of operation Op1.

This implies that we have to equip the Producer cyclic task with the means to communicate with the Consumer sporadic task. However, the task structure that we created encapsulates the functional code inside a structure called the OPCS. Therefore, inside the functional code of the Producer we cannot directly call Op1 of the Consumer (i.e., the functional/sequential code), but we have to call the appropriate provided interface of the whole Consumer task.

Let us see how we can achieve this goal when creating the package Producer (analogous to the package Consumer that we presented in the previous Ada Gem of the series).

package Producer is
   type Producer_FC is new Controlled with private;
   type Producer_FC_Ref is access all Producer_FC'Class;   
   type Producer_FC_Static_Ref is access all Producer_FC;
   -- [code omitted]
   overriding
   procedure Initialize(This : in out Producer_FC);
   procedure Op0 (This : in out Producer_FC); 
   procedure Set_x(This : in out Producer_FC; 
                   v : in Consumer.Consumer_FC_Ref);
private
   type Producer_FC is new Controlled with record
      x : Consumer.Consumer_FC_Ref;
   end record;
end Producer;

We are adding a member x in the record of Producer_FC that represents a reference to the Consumer that provides Op1, which consumes the data produced by the Producer. The reader should note that the static type of this reference is the OPCS of the Consumer (which was called Consumer_FC).

package body Producer is
   -- [procedure Initialize omitted]
   procedure Op0(This : in out Producer_FC) is
   begin
      This.x.Op1([T1_VALUE]);  
   end Op1;
  
   procedure Set_x(This : in out Producer_FC; 
                   v : in Consumer.Consumer_FC_Ref)
   is
   begin
      This.x := v;
   end Set_x;
end Producer;

In the body of procedure Op0 (where the sequential code executed by the Producer task is specified), we insert the call to Op1 that is performed using reference x.

-- Package spec
type s1_T is new Consumer.Consumer_FC with record
   Op1_Ref : access procedure (a : in Types.T1; b : in Types.T2);
   Op2_Ref : access procedure (a : in Types.T1);
end record;
overriding
procedure Op1(This : in out s1_T; a : in Types.T1; b : in Types.T2);
overriding
procedure Op2(This : in out s1_T; a : in Types.T1);
-- Package body
procedure Op1(This : in out s1_T; a : in Types.T1; b : in Types.T2) is
begin
   This.Op1_Ref.all(a,b);
end Op1;

Above, in another package, we create a new type s1_T. This type extends Consumer_FC and adds a pointer to a procedure with the signature of Op1, the operation that we call at the Producer side, and Op2. We also override Op1 for s1_T, so that a call to Op1 is reissued to the pointer just defined.

Analogously, suppose we create a new type s0_T that extends Producer_FC.

In the few remaining code excerpts below, we complete the example with the instantiation of the cyclic task for the Producer and the sporadic task for the Consumer.

s0_Instance : aliased s0_T;
package My_Cyclic_Producer_Task is new 
     Op0_Cyclic_Producer.My_Sporadic_Factory(
           Thread_Priority => 1,
           Period => 2000,
           OPCS_Instance =>
               Producer.Producer_FC(s0_Instance)'access);
s1_Instance : aliased s1_T;
package My_Sporadic_Consumer_Task is new
     Op1_Op2_Sporadic_Consumer.My_Sporadic_Factory(
           Thread_Priority => 2,
           Ceiling => 2,
           MIAT => 500,
           OPCS_Instance =>
               Consumer.Consumer_FC(s1_Instance)'access);

Note that we pass a pointer to s0_Instance (respectively s1_Instance) as the OPCS during the instantiation of the Producer (respectively Consumer) cyclic (respectively sporadic) task.

s1_Instance.Op1_Ref := My_Sporadic_Consumer_Task.Op1'access;
s1_Instance.Op2_Ref := My_Sporadic_Consumer_Task.Op2'access;

Finally, with the assignment above, we are able to impose that whenever Op1 is called on s1_Instance, then the call is directed to the operation Op1 on the provided interface of the Consumer Sporadic Task, and from there it follows the correct delegation chain (redirection to the OBCS and reification of the request in the OBCS queue).

s0_Instance.Set_x(Consumer.Consumer_FC_Ref(s1_Instance'access));

Finally, we establish the binding between the Producer and the Consumer with the call above. In fact, it ensures that the call of Op1 inside the OPCS of the Producer (Producer_FC) is a call to the provided interface of the Consumer sporadic task.

Conclusion

In this Ada Gems miniseries we described a set of Ravenscar-compliant code archetypes for the realization of recurrent patterns in real-time systems. We presented two basic patterns for the realization of cyclic and sporadic tasks, commented on their drawbacks, and showed how to improve them to realize sporadic operations with parameters and an example of complex queuing policy. Finally, we showed how to perform intertask communication.

The code archetypes we described were used for the code generation in the HRT-UML/RCM track of the EU-funded ASSERT project.