LEON Ada is highly suitable for hard real-time applications that require accurate timing and a fast and predictable response to interrupts from peripheral devices. This is achieved with the following features:
Ravenscar profile
The package Ada.Real_Time and a high-resolution real-time clock (one microsecond)
Preemptive priority scheduling with ceiling locking (120 microsecond task switch[1])
Low interrupt latency (15 microseconds)
The packages Ada.Dynamic_Priorities, Ada.Synchronous_Task_Control and Ada.Task_Identification
Support for periodic tasks and task deadlines, as required by ARINC 653
LEON Ada also offers reduced program size by:
Optimized code generation
Use of trap instructions to raise exceptions
Small run-time system size
Optimizations that permit interrupt handling without tasking
This chapter describes how to use Ada tasks, and the associated language features, in an example real-time program.
In support of safety-critcal applications, Ada 95 offers various restrictions that can be invoked by the programmer to prevent the use of language features that are thought to be unsafe. Restrictions can be set individually, or can be set collectively in what is called a profile. XGC Ada supports all the Ada 95 restrictions and supports the implementation-defined pragma Profile. To get the compiler to work with the Ravenscar profile, you should place the following line at the top of each compilation unit.
pragma Profile (Ravenscar);
By default, LEON Ada supports a limited form of tasking that is a superset of what is supported by the Ravenscar profile. The built-in restrictions allow for statically declared tasks to communicate using protected types, the Ada 83 rendezvous or the predefined package Ada.Synchronous_Task_Control.
The Ravenscar profile prohibits the rendezvous and several other unsafe features. When using this profile, application programs are guaranteed to be deterministic and may be analyzed using static analysis tools.
The relevant Ada language features are as follows:
The main task
The pragma Priority
Task specs and bodies
Protected objects
Interrupt handlers
The delay until statement
The package Ada.Real_Time
The main subprogram, which contains the entry point, and which is at the root of the compilation unit graph, runs as task number 1. This is known as the main task. The TCB[2] for this task is declared in the run-time system, and its stack is the main stack declared in the linker script file. Other tasks are numbered from 2 in the order in which they are elaborated.
For other than a trivial program, the main task should probably be regarded as the idle task or background task. You can make sure that it runs at the lowest priority by the use of the pragma Priority in the declarative part of the main subprogram.
Example 3-1. Main Subprogram with Idle Loop
procedure T1 is pragma Priority (0); begin loop null; end loop; end T1;
You might want the background task to continuously run some built-in tests, or you may wish to switch the CPU into low power mode until the next interrupt is raised.
Here is an example main subprogram that goes into low-power mode when
there is nothing else to do. Note that the function
__xgc_set_pwdn
is included in the standard library
libc.
Example 3-2. Idle Loop with Power-Down
pragma Profile (Ravenscar); procedure T1 is pragma Priority (0); procedure Power_Down; pragma Import (C, Power_Down, "__xgc_set_pwdn"); begin loop Power_Down; end loop; end T1;
The rest of the program comprises periodic and aperiodic tasks that are declared in packages mentioned in the with list of the main subprogram.
Important: In LEON Ada, there is no default idle task. If all of your application tasks become blocked, then the program will fail with Program_Error.
The package Ada.Real_Time declares types and subprograms for use by real-time application programs. In LEON Ada, this package is implemented to offer maximum timing precision with minimum overhead.
The resolution of the time-related types is one microsecond. With a 32-bit word size, the range is approximately +/- 35 minutes. This is far greater than the maximum delay period likely to be needed in practice. For a 20 MHz processor, the lateness of a delay is approximately 55 microseconds. That means that given a delay statement that expires at time T, and given that the delayed task has a higher priority than any ready task, then the delayed task will restart at T + 55 microseconds. This lateness is independent of the duration of the delay, and represents the time for a context switch plus the overhead of executing the delay mechanism.
It is therefore possible to run tasks at quite high frequencies, without an excessive overhead. On a 20 MHz LEON, you can run a task at 1000Hz, with an overhead (in terms of CPU time) of approximately 10 percent, leaving 90 percent for the application program.
The general form of a periodic task is given in the following example. You should note that tasks and protected objects must be declared in a library package, and not in a subprogram.
In this example, the task's three scheduling parameters are declared as constants, giving the example task a frequency of 100 Hz, and a phase lag of 3 milliseconds, and a priority of 3. You will have computed these parameters by hand, or using a commercial scheduling tool.
Example 3-3. A Periodic Task
T0 : constant Time := Clock; -- Gets set at elaboration time Task1_Priority : constant System.Priority := 3; Task1_Period : constant Time_Span := To_Time_Span (0.010); Task1_Offset : constant Time_Span := To_Time_Span (0.003); task Task1 is pragma Priority (Task1_Priority); end Task1; task body Task1 is Next_Time : Time := T0 + Task1_Offset; begin loop -- Do something Next_Time := Next_Time + Task1_Period; delay until Next_Time; end loop; end Task1;
The task must have an outer loop that runs for ever. The periodic running of the task is controlled by the delay statement, which gives the task a time slot defined by Offset, Period, and the execution time of the rest of the body.
The value of Task1_Period should be a whole number of microseconds, otherwise, through the accumulation of rounding errors, you may experience a gradual change in phase that may invalidate the scheduling analysis you did earlier.
Like periodic tasks, aperiodic tasks have an outer loop and a single statement to invoke the task body.
In the following example, we declare a task that runs in response to an interrupt. You can use this code with a main subprogram to build a complete application that will run on the LEON simulator.
Here is the code for the package and its body:
Example 3-4. An Interrupt-Driven Task
package EG4_Pack is task Task2 is pragma Priority (1); end Task2; end EG4_Pack; with Ada.Interrupts.Names; with Interfaces; with Text_IO; package body EG4_Pack is use Ada.Interrupts.Names; use Interfaces; use Text_IO; protected IO is procedure Handler; pragma Attach_Handler (Handler, UART_A_RX_TX); entry Get (C : out Character); private Rx_Ready : Boolean := False; end IO; protected body IO is procedure Handler is Status_Word : Unsigned_32; for Status_Word'Address use 16#01F800E8#; begin Rx_Ready := (Status_Word and 16#00000001#) /= 0; end Handler; entry Get (C : out Character) when Rx_Ready is Data_Word : Unsigned_32; for Data_Word'Address use 16#01F800E0#; begin C := Character'Val (Data_Word and 16#0000007f#); Rx_Ready := False; end Get; end IO; task body Task2 is C : Character; begin loop IO.Get (C); -- Do something with the character Put ("C = '"); Put (C); Put ('''); New_Line; end loop; end Task2; end EG4_Pack;
Points to note are as follows:
The package Ada.Interrupts.Names declares the names of the 15 LEON interrupts.
We use address clauses to declare memory-mapped IO locations.
The type Unsigned_32 permits bitwise operators such as 'and' and 'or'.
The interrupt handler runs in supervisor mode with the Processor Interrupt Level (PIL) set to the level of the interrupt.
[1] | Simulated TSC695 at 20 MHz |
[2] | Task Control Block, which holds the task state |