Exponential improvements in hardware capacity made possible by Moore’s Law have substantially expanded the size and complexity of embedded-system software. For many embedded software products, the amount of software deployed in each new revision of the product doubles every 18 to 36 months. The challenges of managing this rapid expansion of software size require innovative approaches and improved software engineering disciplines.
One key to tackling the growing size and complexity of modern embedded-system software is to divide these large systems into many smaller components, each isolated from its neighbors by abstract encapsulation and isolation barriers. These barriers assure each component’s developers that other components won't interfere with correct operation.
The general strategy of using divide-and-conquer to managing software complexity is a fundamental tenet of software engineering. However, its relevance to embedded and real-time software has, until recently, not been fully appreciated. There are two reasons for this circumstance:
- Most embedded systems are much smaller than their desktop and “mainframe” counterparts.
- Many early attempts to adopt object-oriented encapsulation and isolation disciplines from the mainstream (non-embedded) software engineering community overlooked the reality that embedded real-time software engineering has special needs that aren't adequately served by mainstream abstractions.
In particular, developers of non-embedded software are almost always told to ignore resource constraint issues in order to focus on functional correctness, generality, and ease of software maintenance. However, developers of embedded real-time software can't ignore these issues. They are precisely what make development and maintenance of embedded real-time software so much more challenging.
For this reason, it's essential that the encapsulation and isolation mechanisms in effective embedded real-time software engineering assist developers with isolating and encapsulating resource constraint issues at the level of abstraction required by each component developer. This article describes some of the mechanisms available to assist developers of embedded real-time Java software with these challenges.
In typical embedded real-time software systems, each critical software component represents different requirements and tradeoffs. For developers who use Java in their real-time embedded system design, it's necessary to carefully balance hard real-time versus soft real-time performance with the tradeoffs to achieve specific goals.
Hard real-time constraints are those for which an action performed at the wrong time will have zero or possibly negative value. The connotation of “hard real-time” is that compliance with all timing constraints is proven using theoretical static analysis techniques prior to deployment. Soft real-time constraints are those for which an action performed at the wrong time (either too early or too late) has some positive value even though it would have had greater value if performed at the proper time. The expectation is that soft real-time systems use empirical (statistical) measurements and heuristic enforcement of resource budgets to improve the likelihood that software complies with timing constraints.
Note that the difference between hard real-time and soft real-time doesn't depend on the time ranges specified for deadlines or periodic tasks. A soft real-time system might have a deadline of 100 µs, while a hard real-time system's deadline may be 3 seconds.
Satisfying real-time constraints in high-assurance Java applications is possible. Here, the developer must recognize the strengths and weaknesses of particular Java methodologies, such as automatic garbage collection, and carefully select the most appropriate mechanisms to address each particular system requirement. For those pursuing use of the Java language to improve developer productivity and decrease the costs of software maintenance, a collection of recommendations can be found in “Draft Guidelines for Scalable Java Development of Real-Time Systems,” authored by Kelvin Nilsen (http://research.aonix.com/jsc/rtjava.guidelines.5-6-05.pdf).
Practically speaking, guidelines for soft real-time Java development are usually preferred, unless there's a specific constraint that precludes them. This is because soft real-time Java offers the best developer and software maintenance productivity. Guidelines for soft real-time Java involve the use of traditional JSE-style Java APIs with virtual machines that are specially implemented to support real-time behavior. These virtual machines support preemptible and incremental real-time garbage collection, fixed priority scheduling, and priority inheritance in the implementation of all synchronization locks.
In contrast, hard real-time Java technologies don't employ automatic garbage collection. Instead, dynamic memory is allocated and deallocated under more explicit programmer control. The hard real-time Java guidelines enable deployments that run up to three times faster in less than a tenth the memory required by traditional Java implementations. Furthermore, deterministic response latencies are over a thousand times better. For safety-critical development, Java programmers use a restricted subset of the hard real-time technologies.Memory Allocation for Temporary Objects Because Java is an object-oriented programming language, all structured data is represented as objects. In a traditional Java run-time environment, all objects are allocated within a region of memory known as the heap, and the memory for these objects is reclaimed by an automatic garbage collector. In the proposed guidelines for hard real-time development, there's no automatic garbage collector.
In traditional block-structured languages like Ada, Modula, and Pascal, records (the equivalent of Java’s objects) that are declared local to a given procedure are visible within inner-nested procedures. As a result, it's not possible to explicitly copy the address of an object into programmer-declared variables. This provides a safe mechanism for stack allocation of temporary objects. But Java isn't a block-structured language. Rather, it derives more directly from C and C++ in that it doesn't support declaration of nested procedures.
In C and C++, programmers can also declare structures that are local to a function. These structures are allocated on the run-time stack and their memory is automatically reclaimed upon return of the enclosing function. Since C and C++ don't allow for inner-nested functions, the only way for a function to share access to its stack-allocated local structures is by passing the addresses of these objects to called subroutines in global variables or as input parameters.
One of the dangers with this common C and C++ practice is the lack of compiler-enforced protection. Such protection ensures that references to stack-allocated objects don't outlast the objects to which they refer. With C and C++, it's far too easy to create dangling pointers to objects that no longer exist. Whenever this occurs, these programming errors are among the most difficult to debug.
According to the hard real-time Java guidelines, special provisions are made to allow safety-critical Java components to allocate objects on the run-time stack using programming constructs similar to those of the C and C++ programming languages. Unlike C and C++, statically enforced programmer annotations enable the safety-critical Java compiler to ensure that the use of stack-allocated memory doesn't create dangling pointers (Figures 1 and 2).
Each of the statements in lines 3 through 6 of Figure 1 allocates a new Complex object. According to the conventions established for hard real-time Java development, all four allocated objects are allocated within the stack activation frame of the method that contains this code.
In Figure 2, the various @ScopedPure annotations denote that all of the method’s incoming reference arguments, including its implicit this argument, may refer to objects residing within the stack-allocated activation frame of some outer-nested method. Upon recognizing this annotation, the safety-critical Java compiler ensures that these incoming arguments are never copied to any variable that might outlive the object to which the reference arguments refer.
Key benefits of safe stack allocation of temporary objects include:
- Very fast temporary memory allocation and deallocation
- Very reliable temporary memory allocation, because the stack never becomes fragmented and the maximum stack size can be determined through static analysis
- Performance and footprint overhead of the Real-Time Specification for Java’s (RTSJ) run-time checks and the risk that critical RTSJ program components won't be aborted as they violate scoped memory protocols
This gives the safety-critical Java programmer capabilities comparable to more traditional languages like Ada, C, and C++.Cooperation Between Hard and Soft Real-Time Components Most mission-critical software systems consist of multiple software layers, with the lower layers being more static, having tighter real-time constraints and more demanding performance constraints. The higher software layers generally need to support more dynamic behavior, requiring dynamic allocation of data structures and even dynamic loading and unloading of code. Guidelines for high-assurance, mission-critical Java support a synergy between hard and soft real-time components. Soft real-time mission-critical components are implemented using standard JSE libraries running on a real-time-enhanced virtual machine.
Hard real-time components will typically run with footprint and throughput efficiency that's very close (within 5% to 10%) to that of optimized C. This represents a threefold improvement over typical optimized Java performance on important CPU-intensive benchmarks. Many important mission-critical needs can be addressed by this capability, such as:
- Portable and very efficient device drivers (possibly, but not necessarily, having hard real-time constraints) can be implemented using the safety-critical Java programming notations.
- Compared with the use of the Java Native Interface (JNI), interfaces to legacy (“native”) components written in other languages are much more efficient and much safer if implemented using the safety-critical Java environment as an intermediary between traditional Java and native code.
- Performance-critical code (e.g., Fourier analysis and matrix manipulation) can be provided much more efficiently as safety-critical Java components than as traditional Java code or as legacy C code interfaced to Java through JNI.
Because these are hard real-time objects, they will never be subject to relocation. This greatly simplifies the implementation and improves execution efficiency.Sample Implementation of A Device Driver In this section, we describe a simple interrupt-handling device driver software component, written entirely in Java. Figure 3 provides interface declarations that serve as a bridge between the soft and hard real-time domains. For any hard real-time Java object that implements these interfaces and happens to be shared with the traditional Java domain, the traditional Java environment can execute only the methods defined in these interfaces.
Figure 4 defines the DeviceBuffer class, and Figure 5 supplies the declarations of class constants and instance variables for the Interrupt class that represents the interrupt service routine. Figures 6 and 7 complete the implementation of the Interrupt class.
The ceilingPriority() method, shown on lines 24 through 26 of Figure 5, is required because this class implements the javax.realtime.util.sc.Atomic interface. For classes that implement such an interface, programmers are required to provide this method—it establishes a syntactic marker within the body of the class, which can be used to establish static properties regarding the object’s synchronization behavior.
The constructor, shown on lines 28 through 43 of Figure 6, instantiates the three I/O ports required for operation of the interrupt handler. For any given hardware configuration, certain ranges of memory and I/O address space will be eligible to be treated as I/O ports that are accessible from hard real-time Java components.
To ensure that this hard real-time Java component has permission to access the requested I/O ports, range checking is performed when these I/O ports are instantiated. Once instantiated, no additional checking is required when reading or writing the I/O ports. Besides instantiating the necessary I/O ports, the Interrupt constructor also allocates memory to represent a pair of input buffers.
Lines 45 through 58 of Figure 6 provide the actual interrupt-handling code. This method is declared as synchronized because, while running, all other interrupts of equal or lower priority are forbidden from running. Because this class is declared to implement the Atomic interface, the safety-critical Java byte-code verifier assures that the body of every synchronized method is execution-time bounded. This restricts the set of services that can be invoked from within an interrupt handler.
The byte-code verifier prohibits interrupt-handling code from invoking services that might block while holding the priority ceiling lock. The byte-code verifier also ensures that only objects implementing the Atomic interface can set their ceiling priority to ranges that correspond to hardware-dispatched interrupt handling.
In Figure 7, we present the method that enables efficient streaming of bytes from the hard real-time interrupt handler to the traditional Java domain. This method returns a reference to a DeviceBuffer object. Note that DeviceBuffer implements the TraditionalJavaBuffer interface. Thus, it’s possible to share a reference to the returned DeviceBuffer with the traditional Java domain. If shared, the traditional Java environment is able to invoke the validBytes() and byteAt() methods.
The Interrupt class doesn’t implement the TraditionalJavaDevice interface. Consequently, it’s not possible to share instances of Interrupt with the traditional Java environment. Figure 8 provides a definition of Interrupt subclass SharedDevice, which implements the TraditionalJavaDevice interface. Instances of SharedDevice can be shared with the traditional Java domain.
Note that the traditional Java getBuffer() method invokes this.getReadBuffer() indirectly, by way of NoHeapRealtimeThread.transfigure(). The transfigure() service has the effect of converting the currently running traditional Java thread into a hard real-time thread. Therefore, the thread could perform hard real-time synchronization operations.
If we allowed the traditional Java thread to perform hard real-time synchronization without first transfiguring the thread, we would introduce many scheduling complexities into the hard real-time environment, which would be very difficult to analyze. Though the transfigure operation is very efficient, thread transfiguration and synchronization is more expensive than simple method calls. This is why the implementation of this shared device driver processes the contents of each buffer without requiring any further synchronization.Conclusion With proper attention to resource management details, developers can implement hard real-time tasks using the Java language. The hard real-time Java technologies offer improved performance and determinism as well as a much smaller memory footprint than traditional Java technologies. Hard real-time Java components integrate cleanly and efficiently within large systems that typically comprise a combination of soft real-time and hard real-time capabilities. Development and maintenance of low-level software components using portable stylized Java rather than assembler, C, or C++ will yield significant productivity improvements and cost savings over the complete life cycle of typical critical software systems.