M1750 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:
The package Ada.Real_Time and a high-resolution real-time clock (a precision of one microsecond)
Preemptive priority scheduling with ceiling locking (120 microsecond task switch)
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
M1750 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 example real-time programs.
In support of safety-critical 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 to the Ravenscar profile, you should place the following line at the top of each compilation unit.
pragma Profile (Ravenscar);
By default, M1750 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 pragma Priority
Task specs and bodies
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. The TCB for this task is created in the run-time system, and the 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 environment 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. Note that the default priority for the main program and for any tasks is 63.
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. Note that lower power mode requires support from art0.S and may not be supportable on your target computer.
Example 3-2. Idle Loop with Power-Down
with Built_In_Tests; procedure T1 is pragma Priority (0); procedure Power_Down; pragma Import (C, Power_Down, "__xgc_set_pwdn"); begin loop Built_In_Tests.Run; Power_Down; end loop; end T1;
The rest of the program comprises periodic and aperiodic tasks that are declared in packages, that are with-ed from the main subprogram.
Important: In M1750 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 M1750 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 10 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 one 10 MHz 1750, you can run a task at 1000Hz, with an overhead (in terms of CPU time) of approximately 20 percent, leaving 80 percent for the application program.
Note: A delay statement that gives a time that is already passed has missed its deadline, and will raise a soft deadline fault. The default system call handler logs deadline fault to the console. You may wish to modify this code to log the fault in non-volatile memory.
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 the following example, the task's three scheduling parameters are declared as constants, giving 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
package body Example is 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; end Example;
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 simulator.
The code for the package and its body is given in the following example.
Example 3-4. An Interrupt-Driven Task
package Example is task Task2 is pragma Priority (1); end Task2; end Example; with Ada.Interrupts.Names; with Interfaces; with Text_IO; package body Example is use Ada.Interrupts.Names; use Interfaces; use Text_IO; protected IO is procedure Handler; pragma Attach_Handler (Handler, SPARE2); entry Get (C : out Character); private Rx_Ready : Boolean := False; end IO; protected body IO is procedure Handler is Status_Word : Unsigned_16; begin Asm (Template => "xio %0,0x8501", Outputs => (Unsigned_16'Asm_Output ("=r", Status_Word)), Volatile => True); Rx_Ready := (Status_Word and 16#0002#) /= 0; end Handler; entry Get (C : out Character) when Rx_Ready is Data_Word : Unsigned_16; begin Asm (Template => "xio %0,0x8500", Outputs => (Unsigned_16'Asm_Output ("=r", Data_Word)), Volatile => True); C := Character'Val (Data_Word and 16#007f#); 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 Example;
Points to note are as follows:
The package Ada.Interrupts.Names declares the names of the M1750 interrupts.
We use machine code statements to perform IO.
The type Unsigned_16 permits bitwise operators such as 'and' and 'or'.
The interrupt handler runs in supervisor mode with the mask register set appropriately for the level of interrupt.
Simulated generic M1750 at 10 MHz