Ada and Java: real-time advantages
Ada and Java offer strengths for real-time programming and built-in support for multithreading. Join us for a look at what these two under-appreciated languages offer developers of embedded software.
Although C wins the popularity contest as the language most typically chosen for real-time embedded systems, other languages are worth considering. One of them, Ada, offers advantages over the C family, especially in high-reliability environments. Another, Java (as extended with real-time enhancements), may be especially attractive in highly dynamic systems.
In this article, I'll review the language features you'll need for real-time and embedded applications and look at how C, Ada, and Java's real-time extensions address these requirements. I'll use a typical example to describe and compare the Ada and Java approaches. For summaries of Ada and Real-Time Java, see the Beginner's Corner articles listed as further reading at the end of this article.
What C ain't got
For reasons both technical and historical, C is the obvious choice for programming embedded applications. It is processor independent but low-level enough for programmers to get down and dirty with the machine. C can be implemented on almost every architecture imaginable, has reasonable run-time performance, is an international standard, and is familiar to almost all embedded systems programmers. Nonetheless, C has a number of drawbacks that may be significant:
- Language insecurities. The loopholes in C's type model are well known; for example, allowing pointers to be treated as integral data and vice versa. Although it's possible to avoid such loopholes by adhering to a subset such as MISRA C or by applying a style-enforcement tool, more secure languages will prevent the hard-to-find bugs that loopholes create and will thereby decrease development time and promote higher reliability.
- Lack of support for large-scale systems. C has weak facilities for "programming in the large," that is, developing applications comprising hundreds of thousands, indeed perhaps millions of lines of code. A relatively small portion of most embedded systems needs the low-level facilities provided by C. The majority of the logic is higher-level processing that's better supported by languages that offer greater type safety and better capabilities for module structuring, namespace management, encapsulation, and object orientation.
- Absence of needed functionality. C has no language support for concurrency (multithreading), a serious omission for real-time systems. The programmer needs to use an external application programming interface or real-time operating system (RTOS), thus compromising portability. The POSIX C binding, while addressing this concern to some degree, still leaves quite a bit of functionality implementation-dependent. Moreover, the programmer needs to be sensitive to whether otherwise-safe libraries can be used in a multithreaded application. Languages with an integrated concurrency model can better deal with these problems.
Real-time embedded applications require the following:
- General features for promoting reliability, maintainability, reusability, and other broad software engineering goals.
- Specific features for real-time and embedded applications.
- A lack of features that interfere with goals such as predictability of program execution and performance.
Several kinds of specialized features are required for real-time programming. For our purposes, we'll assume that a real-time system may be composed of a collection of periodic activities that cooperatively access shared resources, together with a set of activities that respond to external or software-initiated events. The programmer's job is to produce a system that satisfies various time-based metrics; for example, ensuring that all deadlines are met or that average throughput is optimized. Essential to this job is predictability of program execution. The relevant features include the following:
- A thread/task model for expressing concurrent activities, mutual exclusion for shared resources, inter-thread or task coordination, and responses to asynchronous events including hardware interrupts.
- Facilities for assigning priorities to threads/tasks and for establishing appropriate scheduling behavior (for example, controlling priority inversions).
- Mechanisms for dealing with time: hardware clocks, timeouts, periodicity.
On the other side, the real-time thread facility in the RTSJ is more dynamic and more flexible than Ada's. With the RTSJ, a programmer can define different priority-inversion control mechanisms for different shared objects and can supply different scheduling algorithms for different groups of real-time threads.
Beyond what is required for real-time programming, other features are needed for embedded systems development, such as the ability to program at the machine level (dealing with addresses and "raw storage"), write interrupt service routines, and execute similar functionality. This is where C does provide the needed features. Ada offers comparable functionality, specifically through its Systems Programming Annex. The Java language is (rightly) weak in this area, since the ability to directly access hardware resources would compromise both the language's safety and portability. The RTSJ addresses this concern in two ways. It allows "peeking" and "poking" of integral and floating-point data at specified addresses ("raw memory"), and it allows interrupt handlers to be expressed as handlers for asynchronous events.
Interestingly, more features don't automatically mean better functionality. If a language feature is more general than is required, there's the danger it may incur a run-time cost even if the feature is not used. Other features may interfere with the requirement for safety or predictability:
- The ease with which type checking can be defeated in C is an example of unwanted generality that compromises reliability. The solution is for the programmer to adhere to certain stylistic guidelines (possibly detected/enforced by an external tool).
- Ada has a general run-time model, and the full support may incur an unwanted cost. Anticipating such an issue, the language also includes a directive (pragma Restrictions) that identifies features that aren't used by the program. A program that uses a feature that's excluded by pragma Restrictions is rejected by the compiler. The compiler's implementation can thus provide a specialized version of the run-time support—more efficient, perhaps even amendable to safety-critical certification—knowing that certain features will not be used.
- The main issue with Java derives from its foundation in object-oriented programming. The problem isn't so much with efficiency—the overhead of dynamic binding needn't be more than an extra level of indirection on the method call—but rather with the apparent unpredictability or latency incurred by garbage collection. The approach taken in the RTSJ is to allow the program to define memory areas that aren't subject to garbage collection and to define certain kinds of threads that aren't allowed to access the garbage-collected heap; such threads may preempt the garbage collector.
To help you grasp some of these concepts more thoroughly, here's an example of Ada and Java programming of a standard real-time embedded system: a sensor reporter. Figure 1 illustrates the main data flows among the various components of the sensor reporter.
Figure 1: Data flow in sensor reporter example
A sensor on a vehicle transmits position data (X and Y coordinates) 10 times per second. Each position value comprises a pair of signed 32-bit integers. Receipt is signaled to the CPU via a sensor input interrupt at level 31; the data is then available at the eight bytes located at hexadecimal addresses 100 through 107. X occupies locations 100 through 103, and Y is at locations 104 through 107. (Endianness issues are being ignored here.)
The input data are to be reported (displayed to the console) every 500ms, but this is not to begin until after the first input has come in. It's important that each displayed value be consistent (in other words, if new data are coming in while a position is being reported, it's not acceptable to display, for example, the old X and the new Y).
On request (triggered by the user entering a line starting with "?" at the keyboard), the program is to display the number of interrupts that have thus far occurred. When the user enters a line starting with "!" the program is to be terminated. Any other input from the user is ignored.
Although this example is simply stated, and although it omits details such as hardware initialization and fault detection/recovery, it's typical of many real-time embedded applications. It illustrates multithreading, interrupt handling, periodic behavior, access to low-level data, the need to protect shared data from simultaneous access by multiple threads of control, and the need for synchronization. It's therefore a useful example for comparing the features of candidate languages for such applications.
Listing 1 Ada version of sensor reporter
1 pragma Task_Dispatching_Policy(FIFO_Within_Priorities);
3 with Interfaces;
4 package Position_Pkg is
5 Sensor_Interrupt_Level : constant := 31;
7 protected Position is
8 procedure Sensor_ISR;
9 pragma Interrupt_Priority(Sensor_Interrupt_Level);
10 pragma Interrupt_Handler(Sensor_ISR);
11 pragma Attach_Handler(Sensor_ISR, Sensor_Interrupt_Level);
13 entry First_Fetch(X, Y: out Interfaces.Integer_32);
14 procedure Fetch(X, Y : out Interfaces.Integer_32);
15 function Num_Interrupts return Natural;
17 --protected components:
18 X, Y : Interfaces.Integer_32;
19 Count : Integer := 0;
20 end Position;
21 end Position_Pkg;
23 with System.Storage_Elements; use System.Storage_Elements;
24 package body Position_Pkg is
25 protected body Position is
26 procedure Sensor_ISR is
27 X, Y : Integer_32;
28 for X'Address use To_Address(16#100#);
29 for Y'Address use To_Address(16#104#);
31 Position.X := X;
32 Position.Y := Y;
33 Count := Count+1;
34 --HW-specific processing here, e.g. reenabling interrupts
35 end Sensor_ISR;
37 entry First_Fetch(X, Y: out Integer_32) when Count > 0 is
39 X := Position.X;
40 Y := Position.Y;
41 end First_Fetch;
43 procedure Fetch(X, Y : out Integer_32) is
45 X := Position.X;
46 Y := Position.Y;
47 end Fetch;
49 function Num_Interrupts return Natural is
51 return Count;
52 end Num_Interrupts;
53 end Position;
54 end Position_Pkg;
56 with Interfaces; use Interfaces;
57 with Ada.Text_IO; use Ada.Text_IO;
58 with Ada.Real_Time; use Ada.Real_Time;
59 with Position_Pkg; use Position_Pkg;
60 procedure Sensor_ISR_Reporter is
62 Shutdown : Boolean := False;
63 pragma Atomic(Shutdown);
65 task Position_Reporter is
66 pragma Priority(15);
67 end Position_Reporter;
69 task body Position_Reporter is
70 X : Integer_32;
71 Y : Integer_32;
73 Next_Time : Time;
74 Period : constant Time_Span := To_Time_Span(0.500); -- .5 s
77 Position.First_Fetch(X, Y);
78 Next_Time := Clock; -- Function call
82 Next_Time := Next_Time+Period;
83 delay until Next_Time;
84 exit when Shutdown;
85 Position.Fetch(X, Y); -- Invoke protected operation
86 end loop;
87 end Position_Reporter;
89 task Count_Reporter is
90 pragma Priority(20);
91 end Count_Reporter;
93 task body Count_Reporter is
94 Char : Character;
95 Count : Natural;
98 Put("Enter '?' to see statistics, '!' to quit: ");
101 if Char = '?' then
102 Count := Position.Num_Interrupts;
103 Put_Line("Count: " & Integer'Image(Count));
104 elsif Char = '!' then
105 Shutdown := True;
107 end if;
108 end loop;
109 end Count_Reporter;
112 end Sensor_ISR_Reporter;
The program (shown as Listing 1) comprises a pragma (an implementation directive) at Line 1, the specification (Lines 3 through 21) and body (Lines 23 through 54) for the package Position_Pkg, and a main procedure Sensor_ISR_Reporter (Lines 56 through 112). The package specification contains the interface of the package to other parts of the program; the package body contains the implementation.
The pragma at Line 1 establishes the task dispatching policy to be used by the run-time scheduler. FIFO_Within_Priorities means that when multiple tasks are ready to run, the task with highest priority is chosen. If several tasks are set at this priority, the one that has been waiting the longest is selected. This policy implies preemption: a higher-priority task awakening after a delay will preempt a lower-priority running task. Thus, informally, FIFO_Within_Priorities means "run until blocked or preempted." An effect of FIFO_Within_Priorities is to establish the priority ceiling policy as the mechanism for managing object locking.
The with clause on Line 3 identifies Interfaces as a module that is needed by Position_Pkg. Interfaces is a predefined package. It contains, among other things, the declaration of the 32-bit signed integer type Integer_32 referenced at Lines 13, 14, and 18.
The main content of Position_Pkg is the protected object Position (Lines 7 through 20). A protected object comprises "protected components" and "protected operations." The term protected refers to the fact that the implementation needs to ensure mutually exclusive access to the object across multiple threads of control (tasks). The protected components are declared in the private part of the object and are inaccessible except through the protected operations; thus, the protected object fully enforces encapsulation.
The three kinds of protected operations are all illustrated in the Position object. A protected procedure (Sensor_ISR, Fetch) is allowed to "read" or "write" the protected components. A protected function (Num_Interrupts) is allowed to read but not write the protected components. A protected entry (First_Fetch) is similar to a protected procedure but has an accompanying "barrier condition" (Line 37) that is checked before the calling task is allowed to execute the entry body (Lines 38 through 41).
The various pragmas on Lines 9 through 11 relate to the protected object's usage for interrupt handling. The Interrupt_Priority pragma sets the "ceiling priority" for Position; when any of the protected operations is invoked, the invoking task's priority will be raised to the value given (Sensor_Interrupt_Level). In the case of the protected procedure Sensor_ISR, the invoker will not be a software task but rather an interrupt handling context.
If a task calls a protected operation (for example Line 77) it must first acquire the lock on the object; it releases the lock on completion of the operation. If the operation is a protected entry, then the calling task must acquire the lock before evaluating the entry barrier. If the barrier condition is true, the calling task simply executes the entry body. If the barrier condition is false, then the calling task is placed in a queue associated with the entry and it releases the lock on the object. At the end of a protected procedure or protected entry, the barrier conditions are evaluated for any entries with nonempty queues. For some such entry whose condition is now true, one of the waiting tasks is made ready, and the corresponding entry body is executed on behalf of that task, with the object still locked. The program can specify the entry queuing policy; by default it's FIFO. In this example the queuing policy is not relevant, since only one task (Position_Reporter) is calling the First_Fetch entry.
Entry barriers are analogous to condition variables in POSIX but are at a higher level; an explicit signal isn't needed since it's done automatically as part of the barrier reevaluation.
The protected procedure Sensor_ISR (Lines 26 through 35) is invoked as the interrupt handler for the Sensor_Interrupt_Level interrupt; it updates the protected components X and Y and increments the protected component Count.
The package body contains the body of the protected object, which in turn contains the implementation of the protected operations. The logic for most of these operations should be relatively straightforward. The Sensor_ISR procedure fetches the values of the 32-bit integers stored at locations 100 and 104 and assigns them into the corresponding components of the protected object. The semantics of protected objects and priority ceilings prevent problems caused by nested interrupts.
The package System.Storage_Elements is "with"ed by Position_Pkg (Line 23) since the function To_Address is required. The use clause allows the code to reference this function without the System.Storage_Elements prefix that would otherwise be needed.
The main procedure declares two tasks, Position_Reporter (Lines 65 through 87) and Count_Reporter (Lines 89 through 109). Each task is assigned an explicit priority in its specification. Position_Reporter communicates with Count_Reporter via a shared Boolean variable Shutdown. pragma Atomic(Shutdown) at Line 63 checks that the Shutdown variable is atomically accessible (no chance of a task switch when the variable is being fetched or stored) and also inhibits the compiler from caching the variable in the local memory of the tasks that access it.
Both tasks are activated just after the "begin" of the enclosing procedure (Line 110). Position_Reporter waits until some data has been set by the interrupt handler (Line 77) and then periodically displays the Position components and fetches the next Position value. The loop is exited when Shutdown is true at Line 84. The periodicity idiom uses the absolute delay statement (Line 87) to implement the 500ms heartbeat.
The Count_Reporter task is expressed as a loop that displays a prompt, blocks until the user enters some input, and then interprets the input as required (displaying the count when the line begins with "?" and setting Shutdown and exiting when the line begins with "!"). Get(Char) inputs a character; Skip_Line flushes the input through the next end-of-line.
For simplicity, let's ignore the fact that the Position_Reporter and the Count_Reporter tasks may simultaneously call Put_Line (which may lead to intermixed output). There are several techniques for avoiding this situation, but these are beyond the scope of this article.