Designing Real-time Software
Designing Realtime software involves several steps. The basic steps are listed below:
Software Architecture Definition
This is the first stage of Realtime Software design. Here the software team understands the system that is being designed. The team also reviews at the proposed hardware architecture and develops a very basic software architecture. This architecture definition will be further refined in Co-Design.
Use Cases are also used in this stage to analyze the system. Use cases are used to understand the interactions between the system and its users. For example, use cases for a telephone exchange would specify the interactions between the telephone exchange, its subscribers and the operators which maintain the exchange.
Once the software architecture has been defined, the hardware and software teams should work together to associate software functionality to hardware modules. The software handling is partitioned between different processors and other hardware resources with the following key considerations:
- The software functionality should be partitioned in such a fashion that processors and links in the system do not get overloaded when the system is operating at peak capacity. This involves simulating the system with the proposed software and hardware architecture.
- The system should be designed for future growth by considering a scalable architecture, i.e. system capacity can be increased by adding new hardware modules. The system will not scale very well if some hardware or software module becomes a bottleneck in increasing system capacity.
- Software modules that interact very closely with each other should be placed on the same processor, this will reduce delays in the system. Higher system performance can be achieved by this approach as inter-processor message communication taxes the CPU as well as link resources.
This stage is sometimes referred to as Co-Design as the hardware and software teams work together to define the final system architecture. This is an iterative process. Changes in system architecture might result in changes in hardware and/or software architecture.
The next step in Realtime system design is the careful analysis of the system to define the software modules.
Defining Software Subsystems
- Determine all the features that the system needs to support.
- Group the various features based on the type of work they perform. Identify various sub-systems by assigning one subsystem for one type of features.
- Identify the tasks that will implement the software features. Clearly define the role of each task in its subsystem.
Within each subsystem, classify and group the features appropriately and associate the various tasks constituting the subsystem. For example, the Call Handling subsystem in a switching system would support features like:
- V5.2 Originating to ISUP Outgoing Call
- V5.2 Originating to V5.2 Terminating Call
- Conference Call
- Toll free call
A typical Realtime system is composed of various task entities distributed across different processors and all the inter-processor communication takes place mainly through messages. Feature Design defines the software features in terms of message interactions between tasks. This involves detailed specification of message interfaces. The feature design is generally carried out in the following steps:
- Specify the message interactions between different tasks in the system
- Identify the tasks that would be controlling the feature. The controlling tasks would be keeping track of progress of feature. Generally this is achieved by running timers.
- The message interfaces are defined in detail. All the fields and their possible values are identified.
Feature Design Guidelines
- Keep the design simple and provide a clear definition of the system composition.
- Do not involve too many tasks in a feature.
- Disintegrate big and complex features into small sub features.
- Draw message sequence charts for a feature carefully. Classify the legs of a scenario for a feature in such a way that similar message exchanges are performed by taking the common leg.
- Provide a clear and complete definition of each message interface.
- To check possible message loss, design timer based message exchanges.
- Always consider recovery and rollback cases at each stage of feature design. One way of doing this is to keep a timer for each feature at the task that controls the mainline activity of the feature. And then insert the timeout leg in the message sequence charts of the feature.
- To avoid overloading of message links choose design alternatives that include fewer message exchanges.
Designing a task requires that all the interfaces that the task needs to support should be very well defined. Make sure all the message parameters and timer values have been finalized.
Selecting the Task Type
Once the external interfaces are frozen, select the type of task/tasks that would be most appropriate to handle the interfaces:
- Single State Machine: The tasks functionality can be implemented in a single state machine.
- Multiple State Machines: The task manages multiple state machines. Such tasks would typically include a dispatcher to distribute the received messages to an appropriate state machine. Such tasks would create and delete state machine objects as and when required.
- Multiple Tasks: This type of tasks are similar to the multiple state machine tasks discussed above. The main difference is that the task now manages multiple tasks. Each of the managed tasks implements a single state machine. The manager task is also responsible for creating and deleting the single state machine tasks.
- Complex Task: This type of task would be required in really complex scenarios. Here the manager task manages other tasks which might be managing multiple state machines.
Selecting the State Machine Design
After choosing the type of task, the designer should consider dividing the message interface handling into a sequence of state transitions. Two different type of state machines can be supported:
- Flat State Machines: This is the most frequently used type of state transition. For example, the states of a call would be "Awaiting Digits", "Awaiting Connect", "Awaiting Release", "Awaiting On-hook" etc. This type of state division does not scale very well with increasing complexity. For a complex system this technique results in a state explosion, with the task requiring hundreds of states.
- Hierarchical State Machines: Here the states are viewed as a hierarchy. For example the states covered above would map to "Originating : Awaiting Digits", "Originating: Awaiting Connect", "Releasing : Awaiting Release", "Releasing : Awaiting On-hook". The total number of states is the same here, but the main difference is that some states inherit from an "Originating" base state and others inherit from "Releasing" base state. The total number of message handlers in each state would be reduced drastically, as all the messages that have common handling in all the originating states would be just handled in the "Originating" base state. In addition to this, the handlers of inheriting states can further refine the base state handling by taking additional actions.
Task Design Guidelines
- Do not complicate the design by introducing too many states. Such designs are very difficult to understand. Follow a simple rule of thumb, if you are having difficulty choosing the name of state, you may have identified the wrong state.
- Do not complicate the design by having too few states. If all the states in the system have not been captured in the state machine design, you will end up with lot of flags and strange looking variables which will be needed to control the message flow in the jumbo states.
- Keep the data-structure definitions simple. See if a simpler data-structure would do the same job just as well.
- Out of memory conditions should be handled. Tasks can and will run out of memory. So handle the out of memory conditions gracefully. This can lead to a lot of "if clutter" so consider exception handling as an option.
- All of the legs of the defined scenarios should be handled. This is easier said than done. Many times all the scenario legs identified in the feature design stage may not cover all the possible error scenarios for your task.
- Make sure that all the allocated resources are de-allocated at the end. Again it is very easy to miss out on this one. Many times designers forget to release resources in the error legs.
- Consider using a hierarchical state machine to simplify the state machine design.
- Consider using Object Oriented programming languages like C++. Contrary to popular belief, languages like C++ might turn out to be more efficient in runtime performance because of better locality of reference. (Most objects would be referring to data that is contained in the same object, thus improving the locality of reference)