In recent years, the Unified Modeling Language (UML) has been successfully used to develop many embedded applications, including avionics, automotive, medical, industrial automation, and telecommunications. Its success is due to its simple notation, its well-defined execution semantics, and its widespread availability as a standard supported by many tool vendors, consultants, and trainers.
For designers, the UML offers a well-defined language for capturing the structure, behavior, and functionality of embedded applications. It easily captures all subtle nuances of an embedded application and specific constraints that are the hallmark of embedded systems.
The UML core comprises use cases to model requirements and classes and objects to model system structure, while statecharts and sequence diagrams model behavior. This article discusses how to use these semantic elements and their diagrammatic representations to effectively specify, design, and build embedded applications.
Resulting from a significant number of man-centuries of effort by leading software modelers, the UML can be considered as several different parts. Just the key elements will be discussed here: elements used to capture requirements, to specify design structure, and to specify design behavior.
Although in its entirety, the UML is quite large (some 800 pages of rather dense text and diagrams), most engineering applications really only need to employ a core subset of what the UML has to offer: structural elements such as classes, objects, and packages; behavioral elements like states, statecharts, and sequence diagrams; and requirements capture and organization with use cases. The UML's other elements certainly have their uses, but they can be safely ignored, if desired. The resulting "UML Lite" provides a powerful set of conceptual tools for designing complex systems with an easy-to-read notation.
Design Structure With UML: At its core, the UML is an object-oriented language based on the notion of objects and classes. An object is just pieces of information tightly coupled together with operations that act on it. An object occupies some particular location in memory for a specific period of time during system execution.
An object is said to be an instance of a class, which is the specification of a set of objects. In the UML and other object-oriented languages, we design the structure of the system by identifying the objects, how they relate to each other, and their classes.
Because the UML is a graphical language, diagrams are used to depict the object structure of the system (Fig. 1). It's more common to show class diagrams specifying the structure of object collaborations that will exist at runtime, rather than the object diagrams themselves.
The rectangles in the figure are the structural elements of the system—either classes (roughly the "types" of the objects), or specific objects that exist in the system during execution. It's easy to tell the difference. If the name is underlined or contains a colon, as in UpButton:Button, it's an object. Otherwise, it's a class, which represents one or more objects at runtime.
Classes are typically shown either as rectangles with simple names like backlight, rectangles containing other rectangles like Elevator, or a rectangle with multiple partitions like PositionSensor. This depends on what you want to show about the class in question.
The containment of one class within another is one way to show the composition relationship among classes—that is, that one (whole) class is composed of smaller (part) classes. Composition is a strong form of the whole-part relation known as aggregation. The weaker form is shown using a diamond on the whole end of the relation, such as between the DestinationQueue object and the Destination objects that it contains. The multiple partition box lets you show some or all of the attributes (information known by objects of the class), and some or all of the operations (services that may be requested of the class).
In the case of PositionSensor, the class has an attribute called position, which is of type int. It shows a single operation getPosition, which returns an int. These different notations let designers decide exactly how much to expose on any particular diagram. Note that PositionSensor may have additional attributes such as a calibrationConstant, and additional operations such as executeSelfTest(). But this diagram doesn't show them.
Most times, only classes are shown on the diagram. Sometimes, though, it's useful to show the named instances of those classes as well. For example, the Floor object clearly contains two objects of class Button—one called upButton and one called downButton. Both are structurally the same with the same set of operations because they're different instances of the same class. But they serve different roles in the executing system.
Armed with this information, we can easily interpret the structure of the elevator system shown in Figure 1:
- Major classes include Door, DoorOwner, Elevator, Floor, and Elevator Dispatcher.
- There are two kinds of DoorOwners: Elevators and Floors, which both aggregate a Door object.
- The Door is composed of instances of PressureSensor, PositionSensor, and DCMotor.
- The Elevator aggregates one Door and one Button Panel.
- Two buttons and their associated backlights are parts of the Floor, which also aggregates one Door.
- The Elevator Dispatcher receives messages from the Floor's UpButton and DownButton objects. It sends messages to objects of class Elevator, which aggregates an UnhandledDestinationQueue object of class Queue.
Design Behavior With UML: The other side of design is, of course, how the system behaves, or acts over time. Here, the UML supplies ways of illustrating the behavior of individual structural elements and how they behave in the context of a collaboration.
A structural element may behave in one of three ways: simply, continuously, or reactively. Simple behavior means that the object's behavior does not depend on its history. For example, if I call PositionSensor.getPosition(), it will probably always give me a valid response. The value will change depending on the position of the Door, but the kind of behavior executed will remain the same.
Continuous behavior depends on the object's history, but the object behaves in a smooth way. For example, the DCMotor will likely use control laws or differential equations to control the speed of the Door in a smooth fashion. Because the UML doesn't provide specific means for modeling continuous behavior, many developers find that they have to go beyond the UML itself and use continuous mathematics-based tools to develop these algorithms.
It's a simple matter to tie that behavior back into the structural model, however. The implementation of DCMotor.setSpeed() is provided by tools outside of the standard UML. It's linked so that when that behavior is invoked, the separately generated code is executed. Sometimes, both simple and continuous behavior are modeled with activity graphs, a topic beyond this basic introduction, yet within the UML.
Reactive behavior means that the object reacts to events (occurrences of interest that are sent to the object after they happen). The special thing about reactive behavior is that primarily the system waits for incoming events, then changes the kinds of things it does. Reactive behavior is modeled with finite state machines, and hence has the alternative name of stateful behavior.
The UML supplies rich finite state-machine semantics via statecharts. These are an expanded notation, which includes the traditional Mealy-Moore state-machine semantics as a subset. But a statechart also is a state machine that supports additional constructs over the more traditional Mealy-Moore state machines, including:
- Nested states
- Or-states (exclusive)
- And-states (concurrent)
- Conditional pseudostates (branching)
There's not room to discuss the detailed semantics of a statechart here. (For a more detailed introduction to statecharts, see the author's books, Doing Hard Time: Developing Real-Time Systems with UML, Objects, Frameworks, and Patterns, Addison-Wesley, 1999; Real-Time UML 2nd Edition: Developing Efficient Objects for Embedded Systems, Addison-Wesley, 1999.)
In their basic form, statecharts comprise conditions of existence (states) and responses to events (transitions) that can change state. Additionally, the object can execute primitive behaviors (actions) at various points during the execution of the states to open doors, flash lights, read sensors, engage locking clamps, and so on. States are shown as rounded rectangles, while transitions are arrowed lines (Fig. 2).
The statechart in the figure shows how the door behaves. Actions are executed either on the transition between states, such as the two actions executed on the OpenedEvent transition from the Opening::SlidingOpen state to the Open state, or on the entry into the state itself, like the execution of the setInterlock(OFF) in the Closing::DisconnectingDoors state. Actions may also be executed when an object leaves a state. A transition with a single rounded end is called an initial state connector, or an initial pseudostate. It indicates the default state when the object comes into existence or enters a state with substates.
The general form for a transition is:
Event name '(' parameter-list ')' '\[' guard '\]' '/' action list
- Event name is the name of the event received by the object that invokes the transition.
- Events may pass information around in the form of parameters, if desired.
- The guard is a Boolean expression. If present, the transition is only taken if the named event occurs and the guard evaluates to true.
- The action list specifies a list of actions to be performed when the transition is taken. This can be primitive statements like ++x, operations on the object or other objects with which this object has an association, or the creation of another event, such as using the GEN() statement in Figure 2.
All of these transition elements are optional. If the transition is unnamed, it fires immediately after the entry actions for the state are executed. Such an "anonymous" transition occurs between the states ConnectingDoors and SlidingOpen in Figure 2.
We can see how the Door processes the events that it receives. The Door starts in the Open state. After a period of time (DoorOpenTime), a timeout fires and the Door transitions to its Closing state.
The Closing state has two substates. The first (indicated by the initial pseudostate) is the SlidingClosed state. While in this state, the Door commands the DCMotor to close the door. When that action is complete, the object transitions to the DisconnectingDoors state. If during the Closed state an obstruction is detected (indicated by an event sent to the Door from the PressureSensor), the Door transitions to the Opening state, and so on.
Statecharts work very well for defining the behavior of reactive objects. But what if you want to understand how a set of objects works in collaboration? The UML provides sequence diagrams to show how objects collaborate by sending messages (calling operations) and events to each other.
Figure 3 is an example of a sequence diagram. The vertical lines represent objects, not classes, during system execution. The arrowed lines are messages (either calls to operations on objects, or events sent to objects). The hatched area on the left is called a "collaboration boundary." It represents all objects in the universe other than those explicitly represented as instance lines on the diagram. Time flows down the page. It's easy to see how these objects work together to produce the desired system effect—picking up and delivering a person, upon request, from one floor to another.
The scenario isn't complete, but it shows most major elements of sequence diagrams: instance lines, messages, events, comments, and even a timing constraint. (The 100 ms in curly braces is expressing the time between two message invocations.) Although you can very easily see how the objects collaborate, the internal behavior of individual objects isn't readily apparent.
Requirements Capture With UML: Use cases are the way that the UML organizes requirements. A use case organizes a cohesive set of requirements around a single (named) system capability, but doesn't imply anything about internal implementation. An oval represents a use case on a use case diagram (Fig. 4).
In this example, the system is an elevator with the use cases as the primary capabilities of the elevator. The stick figures, called actors, represent any object outside the scope of the system that interacts with the system in ways of interest to us. The lines connecting the actors to the use case mean that the actor interacts in some interesting way with the system as the system executes that use case. Notice that the association line connecting Passenger with Auto Optimize System is a directed line. This indicates that messages flow in a single direction from the actor to the use case.
Use cases are named containers for more detailed requirements. What exactly does Monitor System Health for the elevator system mean? This use case internally contains more details about what it means. The UML provides two primary means to cover what's called "detailing the requirements," namely by example scenarios (shown with sequence diagrams), or by specification (captured with statecharts).
This article has covered the basic semantic elements of the UML and their notational representation. Basic embedded-systems development requires only three diagrams: class diagrams, statecharts, and sequence diagrams. (Use cases can be added to implement the UML for requirements capture.) These three diagrams enable easy specification of the system's structural design elements and how they relate and coordinate with each other.
Statecharts and se-quence diagrams let you specify and illustrate the behavior of individual structural elements, and the collaborations of those elements, respectively. While there's a great deal more to the UML than this, many successful projects have used just these UML elements.
Also discussed was how the UML fits into a potential development process, using the ROPES process as an example. (See Doing Hard Time.) The UML captures most of the analysis and design artifacts typically created during a project lifecycle. The executability of the UML aids in early identification of defects via testing. Finally, the automatic generation of source code from the UML model ensures that the model and code always remain in sync, while simultaneously removing sources of defects and saving significant time and effort.
Designers can create these diagrams using Rhapsody, a design automation tool from I-Logix, and then automatically generate code and debug their systems on their desktops or on the target hardware.