Gem #13: Interrupt Handling Idioms (Part 1)

by Pat Rogers —AdaCore

Let's get started…

Recall that, in Ada, protected procedures are the standard interrupt-handling mechanism. This approach has a number of advantages over the “traditional” use of non-protected procedures. First, normal procedures don’t have a priority, but protected objects can have an interrupt priority assigned and are thus integrated with the overall priority semantics. Execution of entries and procedures within the protected object will execute at that level and only higher-level interrupts can preempt that execution. Thus no race conditions are possible either. Additionally, condition synchronization is expressed directly, via entry barriers, making interaction with other parts of the system easy to express and understand. Finally, protected objects support localization of data and their manipulating routines, as well as localization of multiple interrupt handlers within one protected object when they each need access to the local data.

The response to interrupts is often arranged in levels, with a first-level handler providing a very fast response that does limited processing and a secondary-level handler that does more expensive processing outside of the interrupt context, at application-level priority. A natural expression of this structure is to use a protected procedure as the first-level handler and a task as the secondary level. The protected procedure responds to the interrupt and then signals the task when it should run.

For example, consider message handling over a UART (Universal Asynchronous Receiver Transmitter), in which an interrupt signals arrival of the first character. The interrupt handler procedure would capture that character, place it into a buffer within the protected object, and then either poll for the remaining characters (if appropriate) or reset for the next interrupt. Once the entire message is received the protected procedure could then signal the secondary handler task to parse the message and respond accordingly.

We will implement such a message processor using both design idioms. In each case we encapsulate both levels of the interrupt handling code inside the body of a package named Message_Processor. All the processing is done in the package body and nothing is exported that requires a completion. Hence we need a pragma Elaborate_Body in the declaration to make the package body legal. The content of the package declaration, as shown below, would probably go in the body as well, but for the purpose of this gem we will leave them here.

package Message_Processor is

  pragma Elaborate_Body;

  subtype Message_Size is Integer range 1 .. 256; -- arbitrary

  type Contents is array (Message_Size range <>) of Character;

  type Message (Size : Message_Size) is
    record
      Value  : Contents (1..Size);
      Length : Natural := 0;
    end record;

end Message_Processor;

First Design Idiom
In the first idiom the first-level protected procedure handler signals the second-level task handler by enabling a barrier on a protected entry in the same protected object. The second-level task suspends on the entry call and, when allowed to resume execution, performs the secondary processing. Entry parameters can be used to pass information to the task, for example the message received on the UART. The code for this idiom results in a package body structured as follows:

with UART;
with System;
with Ada.Interrupts;

package body Message_Processor is

   Port : UART.Device;

   protected Receiver is ... -- the first-level handler

   protected body Receiver is ...

   task Process_Messages is ...

   task body Process_Messages is ...

begin
   UART.Configure (Port, UART_Data_Arrival, UART_Priority);
   UART.Enable_Interrupts (Port);
end Message_Processor;

In the above, the protected object Receiver is the first-level handler; the secondary handler is the task Process_Messages. The UART hardware is represented by the object named Port, of a type defined by package UART (not shown). The package body executable part automatically configures the UART and enables its interrupts after the protected object and task are elaborated.

The protected object Receiver contains the interrupt-handling procedure, the entry to be called by the secondary handler task, a buffer containing the currently received characters, and a boolean variable used for the entry barrier:

   UART_Priority     : constant System.Interrupt_Priority   := ...
   UART_Data_Arrival : constant Ada.Interrupts.Interrupt_Id := ...

   protected Receiver is
      entry Wait (Msg : access Message);
      pragma Interrupt_Priority (UART_Priority);
   private
      procedure Handle_Incoming_Data;
      pragma Attach_Handler (Handle_Incoming_Data, UART_Data_Arrival);
      Buffer        : Contents (Message_Size);
      Length        : Natural := 1;
      Message_Ready : Boolean := False;
   end Receiver;

The pragma Interrupt_Priority assigns the given priority to the whole protected object. No other interrupts at or below that level will be enabled whenever the procedure is executing. Note that the procedure is declared in the private part of the protected object. Placement there precludes “accidental” calls from client software in future maintenance activities. Note also the pragma Attach_Handler that permanently ties the procedure to the interrupt.

In the body of the protected object we have the bodies for the entry and the procedure. The entry is controlled by the boolean Message_Ready that is set to True when the interrupt handler determines that all the characters have been received for a given message. The entry body copies the buffer content directly into the caller’s Message object and then resets the buffer for the next message arrival.

   protected body Receiver is

      entry Wait (Msg : access Message) when Message_Ready is
      begin
         Msg.Value (1 .. Length) := Buffer (1 .. Length);
         Msg.Length := Length;
         -- reset for next arrival
         Length := 1;
         Message_Ready := False;
      end Wait;

      procedure Handle_Incoming_Data is
      begin
         UART.Disable_Interrupts (Port);
         Buffer (1) := UART.Data (Port);
         -- poll for all remaining
         while UART.Data_Available (Port) loop
            Length := Length + 1;
            Buffer (Length) := UART.Data (Port);
         end loop;
         UART.Enable_Interrupts (Port);
         -- wake up the task
         Message_Ready := True;
      end Handle_Incoming_Data;

   end Receiver;

The interrupt handler procedure uses the polling approach in this example. It first disables further interrupts from the UART and then captures all the incoming characters. Finally, in re-enables the device interrupts and enables the entry by setting Message Ready to True.

The second-level handler task has no entries of its own because nothing calls it. We only need to set the priority of the task, as specified in package Config (not shown) that defines all the priorities of the application.

   task Process_Messages is
      pragma Priority (Config.Process_Messages_Priority);
   end Process_Messages;

   task body Process_Messages is
      Next_Message : aliased Message (Size => Message_Size'Last);
   begin
      -- any initialization code
      loop
         Receiver.Wait (Next_Message'Access);
         -- process Next_Message ...
      end loop;
   end Process_Messages;

The task suspends until the entry is executed and then processes the message is some application-defined way.

Next week we will explore the second design idiom and then compare the two. Stay tuned for more...


About the Author

Pat Rogers has been a computing professional since 1975, primarily working on microprocessor-based real-time applications in Ada, C, C++ and other languages, including high-fidelity flight simulators and Supervisory Control and Data Acquisition (SCADA) systems controlling hazardous materials. Having first learned Ada in 1980, he was director of the Ada9X Laboratory for the U.S. Air Force’s Joint Advanced Strike Technology Program, Principle Investigator in distributed systems and fault tolerance research projects using Ada for the U.S. Air Force and Army, and Associate Director for Research at the NASA Software Engineering Research Center. He has B.S. and M.S. degrees in computer systems design and computer science from the University of Houston and a Ph.D. in computer science from the University of York, England. As a member of the Senior Technical Staff at AdaCore, he specializes in supporting real-time/embedded systems developers, creates and provides training courses, and is project leader and a developer of the GNATbench Eclipse plug-in for Ada. He also has a 3rd Dan black belt in Tae Kwon Do and is founder of the AdaCore club “The Wicked Uncles”.