Gem #94: Code Archetypes for Real-Time Programming - Part 3
by Marco Panunzio —University of Padua
Introduction
In the previous Ada Gem we described a code archetype for a simple sporadic task. Nevertheless, we recognized that the archetype is not completely satisfactory for at least two reasons: (i) it is not possible to pass parameters to the sporadic operation; (ii) the synchronization agent (OBCS) is a simple counter of pending requests.
In this Ada Gem we illustrate a more complex archetype that supports the invocation of a sporadic operation with parameters. Additionally, we want to explore how it is possible to enrich the OBCS to support complex queueing policies for the incoming requests.
Sporadic task
The archetype of a sporadic task that we wish to illustrate is depicted in the figure below.
Suppose that we want to create a sporadic task that at each release can execute either operation Op1
or operation Op2
, according to incoming requests by clients. Executing different operations with the same task is not an unusual need in real-time systems, especially when the execution platform has scarce computational power and memory resources, and the excessive proliferation of tasks may tax the system too much.
Furthermore, in this archetype we may also want to establish some relative ordering of importance between Op1
and Op2
. We consider Op1
as the nominal operation of the sporadic task (the operation normally called by clients), and we call it the START operation; in contrast, we consider Op2
to be a modifier operation, called the ATC. Then, we stipulate that pending requests for execution of Op1
are served by the sporadic task in FIFO ordering, but requests for Op2
take precedence over pending requests of Op1
. This choice implies that modifier operations are allowed to cause a one-time (as opposed to permanent) modification of the nominal execution behavior of the task.
OBCS for a sporadic task
We want to encapsulate the implementation of this policy and simply expose to clients of this sporadic task a set of procedures with the signatures of Op1
and Op2
; the role of these procedures is to reify the corresponding execution requests. The invocation (type and actual parameters) is recorded in a language-level structure and stored in the OBCS. When the sporadic task fetches a request, it decodes the original request and calls the appropriate operation with the correct parameters.
Sporadic Task -- System Types and Task Type
Let us now have a look at the set of types we need to implement this archetype. They are declared in a modified version of the package System_Types
that we also used in the preceding Ada Gem.
with System; with Ada.Real_Time; use Ada.Real_Time; with System_Time; with Ada.Finalization; use Ada.Finalization; package System_Types is -- Abstract parameter type -- type Param_Type is abstract tagged record In_Use : Boolean := False; end record; -- Abstract functional procedure -- procedure My_OPCS (Self : in out Param_Type) is abstract; type Param_Type_Ref is access all Param_Type'Class; type Param_Arr is array(Integer range <>) of Param_Type_Ref; type Param_Arr_Ref is access all Param_Arr; -- Request type -- type Request_T is (NO_REQ, START_REQ, ATC_REQ); -- Request descriptor to reify an execution request type Request_Descriptor_T is record Request : Request_T; Params : Param_Type_Ref; end record; -- Parameter buffer type Param_Buffer_T(Size : Integer) is record Buffer : aliased Param_Arr(1..Size); Index : Integer := 1; end record; type Param_Buffer_Ref is access all Param_Buffer_T; procedure Increase_Index(Self : in out Param_Buffer_T);
We have declared a set of types to represent parameters, a type describing the kinds of requests (START_REQ, ATC_REQ, and an additional kind NO_REQ just for the sake of the explanation), and a request descriptor type to encapsulate the information about invocations of Op1
and Op2
. We also declare procedure My_OPCS(..)
, which is an abstract procedure that represents all possible operations that can be invoked by the sporadic task.
We now continue on with the remainder of the specification of package System_Types
:
-- Abstract OBCS -- type OBCS_T is abstract new Controlled with null record; type OBCS_T_Ref is access all OBCS_T'Class; procedure Put(Self : in out OBCS_T; Req : Request_T; P : Param_Type_Ref) is abstract; procedure Get(Self : in out OBCS_T; R : out Request_Descriptor_T) is abstract; -- Sporadic OBCS -- type Sporadic_OBCS(Size : Integer) is new OBCS_T with record START_Param_Buffer : Param_Arr(1..Size); START_Insert_Index : Integer; START_Extract_Index : Integer; START_Pending : Integer; ATC_Param_Buffer : Param_Arr(1..Size); ATC_Insert_Index : Integer; ATC_Extract_Index : Integer; ATC_Pending : Integer; Pending : Integer; end record; overriding procedure Initialize(Self : in out Sporadic_OBCS); overriding procedure Put(Self : in out Sporadic_OBCS; Req : Request_T; P : Param_Type_Ref); overriding procedure Get(Self : in out Sporadic_OBCS; R : out Request_Descriptor_T); end System_Types;
Above, we declare a root type to represent an abstract OBCS (OBCS_T
) and a Sporadic_OBCS
type that implements the queueing policy we previously described. START_Param_Buffer
and ATC_Param_Buffer
are two distinct circular buffers that are used to store the invocations of the respective types of operation. In addition, we create a buffer for parameters.
The package body follows:
package body System_Types is -- Sporadic OBCS -- procedure Initialize (Self : in out Sporadic_OBCS) is begin Self.START_Pending := 0; Self.START_Insert_Index := Self.START_Param_Buffer'First; Self.START_Extract_Index := Self.START_Param_Buffer'First; Self.ATC_Pending := 0; Self.ATC_Insert_Index := Self.ATC_Param_Buffer'First; Self.ATC_Extract_Index := Self.ATC_Param_Buffer'First; end Initialize; procedure Put(Self : in out Sporadic_OBCS; Req : Request_T; P : Param_Type_Ref) is begin case Req is when START_REQ => Self.START_Param_Buffer (Self.START_Insert_Index) := P; Self.START_Insert_Index := Self.START_Insert_Index + 1; if Self.START_Insert_Index > Self.START_Param_Buffer'Last then Self.START_Insert_Index := Self.START_Param_Buffer'First; end if; -- Increase the number of pending requests, but do not overcome -- the number of buffered ones if Self.START_Pending < Self.START_Param_Buffer'Last then Self.START_Pending := Self.START_Pending + 1; end if; when ATC_REQ => Self.ATC_Param_Buffer (Self.ATC_Insert_Index) := P; Self.ATC_Insert_Index := Self.ATC_Insert_Index + 1; if Self.ATC_Insert_Index > Self.ATC_Param_Buffer'Last then Self.ATC_Insert_Index := Self.ATC_Param_Buffer'First; end if; if Self.ATC_Pending < Self.ATC_Param_Buffer'Last then -- Increase the number of pending requests, but do not overcome -- the number of buffered ones Self.ATC_Pending := Self.ATC_Pending + 1; end if; when others => null; end case; Self.Pending := Self.START_Pending + Self.ATC_Pending; end Put; procedure Get(Self : in out Sporadic_OBCS; R : out Request_Descriptor_T) is begin if Self.ATC_Pending > 0 then R := (ATC_REQ, Self.ATC_Param_Buffer(Self.ATC_Extract_Index)); Self.ATC_Extract_Index := Self.ATC_Extract_Index + 1; if Self.ATC_Extract_Index > Self.ATC_Param_Buffer'Last then Self.ATC_Extract_Index := Self.ATC_Param_Buffer'First; end if; Self.ATC_Pending := Self.ATC_Pending - 1; else if Self.START_Pending > 0 then R := (START_REQ, Self.START_Param_Buffer(Self.START_Extract_Index)); Self.START_Extract_Index := Self.START_Extract_Index + 1; if Self.START_Extract_Index > Self.START_Param_Buffer'Last then Self.START_Extract_Index := Self.START_Param_Buffer'First; end if; Self.START_Pending := Self.START_Pending - 1; end if; end if; R.Params.In_Use := True; Self.Pending := Self.START_Pending + Self.ATC_Pending; end Get; procedure Increase_Index(Self : in out Param_Buffer_T) is begin Self.Index := Self.Index + 1; if Self.Index > Self.Buffer'Last then Self.Index := Self.Buffer'First; end if; end Increase_Index; end System_Types;
In the package body we implement the desired queuing policy. Procedure Put(..)
simply inserts the representation of the incoming request in the queue of the requested operation kind (START_REQ or ATC_REQ). The ordering among requests of the same operation kind is FIFO.
Procedure Get(..)
is used to extract a request descriptor. We can see that as long as there are pending ATC requests, they are selected based on their arrival order. When the ATC queue is empty, requests for START operations are fetched.
The task that uses this sporadic OBCS has a specification almost identical to the "simple sporadic task" we presented in the preceding Ada Gem. The only difference is that Get_Request
now also fetches a request descriptor.
with System_Types; use System_Types; with System; use System; with Ada.Real_Time; use Ada.Real_Time; generic with procedure Get_Request(Req : out Request_Descriptor_T; Release : out Time); package Sporadic_Task is task type Thread_T (Thread_Priority : Any_Priority; MIAT : Integer) is pragma Priority (Thread_Priority); end Thread_T; end Sporadic_Task;
The body for the task type follows:
with System_Time; use System_Time; package body Sporadic_Task is task body Thread_T is Req_Desc : Request_Descriptor_T; Release : Time; Next_Time : Time := System_Start_Time; begin loop delay until Next_Time; Get_Request (Req_Desc, Release); Next_Time := Release + Milliseconds (MIAT); case Req_Desc.Request is when NO_REQ => null; when START_REQ | ATC_REQ => My_OPCS (Req_Desc.Params.all); when others => null; end case; end loop; end Thread_T; end Sporadic_Task;
Notice that the descriptor of the fetched request can be used to discriminate the action to perform according to the type of operation (this is done with the case statement). In our case, if we fetch a request of kind START_REQ or ATC_REQ, we simply execute My_OPCS, that will dynamically dispatch to the requested operation. This mechanism will be clear when, in a later Gem, we complete the picture with the declaration of Op1
and Op2
as seen by their clients.