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...