Preface
To create a usable piece of software, you have to fight for every fix, every feature, every little accommodation that will get one more person up the curve. There are no shortcuts. Luck is involved, but you don’t win by being lucky, it happens because you fought for every inch. —Dave Winer
For many years, I had been looking for a book or a magazine article that would describe a truly practical way of coding modern state machines (UML1 statecharts) in a mainstream programming language such as C or C++. I have never found such a technique. In 2002, I wrote Practical Statecharts in C/C++: Quantum Programming for Embedded Systems (PSiCC), which was the first book to provide what had been missing thus far: a compact, efficient, and highly maintainable implementation of UML state machines in C and C++ with full support for hierarchical nesting of states. PSiCC was also the first book to offer complete C and C++ source code of a generic, state machine- based, real-time application framework for embedded systems. To my delight, PSiCC continues to be one of the most popular books about statecharts and event-driven programming for embedded systems. Within a year of its publication, PSiCC was translated into Chinese, and a year later into Korean. I’ve received and answered literally thousands of e-mails from readers who successfully used the published code in consumer, medical, industrial, wireless, networking, research, defense, robotics, automotive, space exploration, and many other applications worldwide. In 2003 I started to speak about the subject matter at
1 UML stands for Unified Modeling Language and is the trademark of Object Management Group.
www.newnespress.com xviii Preface the Embedded Systems Conferences on both U.S. coasts. I also began to consult to companies. All this gave me additional numerous opportunities to find out firsthand how engineers actually use the published design techniques in a wide range of application areas. What you’re holding in your hands is the second edition of PSiCC. It is the direct result of the plentiful feedback I’ve received as well as five years of the “massive parallel testing” and scrutiny that has occurred in the trenches.
What’s New in the Second Edition? As promised in the first edition of PSiCC, I continued to advance the code and refine the design techniques. This completely revised second edition incorporates these advancements as well the numerous lessons learned from readers.
New Code First of all, this book presents an entirely new version of the software, which is now called Quantum Platform (QP) and includes the hierarchical event processor (QEP) and the real-time framework (QF) as well as two new components. QP underwent several quantum leaps of improvement since the first publication six years ago. The enhancements introduced since the first edition of PSiCC are too numerous to list here, but the general areas of improvements include greater efficiency and testability and better portability across different processors, compilers, and operating systems. The two new QP components are the lightweight, preemptive, real-time kernel (QK) described in Chapter 10 and the software-tracing instrumentation (QS) covered in Chapter 11. Finally, I’m quite excited about the entirely new, ultralight, reduced-feature version of QP called QP-nano that scales the approach down to the lowest-end 8- and 16-bit MCUs. I describe QP-nano in Chapter 12.
Open Source and Dual Licensing In 2004, I decided to release the entire QP code as open source under the terms of the GNU General Public License (GPL) version 2, as published by the Free Software Foundation. Independent of the open-source licensing, the QP source code is also available under the terms of traditional commercial licenses, which expressly supersede the GPL and are specifically designed for users interested in retaining the proprietary www.newnespress.com Preface xix status of their applications based on QP. This increasingly popular strategy of combining open source with commercial licensing, called dual licensing, is explained in more detail in Appendix A.
C as the Primary Language of Exposition Most of the code samples in the first edition of PSiCC pertained to the C++ implementation. However, as I found out in the field, many embedded software developers come from a hardware background (mostly EE) and are often unnecessarily intimidated by C++. In this edition, I decided to exactly reverse the roles of C and C++. As before, the companion Website contains the complete source code for both C and C++ versions. But now, most of the code examples in the text refer to the C version, and the C++ code is discussed only when the differences between it and the C implementation become nontrivial and important. As far as the C source code is concerned, I no longer use the C+ object-oriented extension that I’ve applied and documented in the first edition. The code is still compatible with C+, but the C+ macros are not used.
More Examples Compared to the first edition, this book presents more examples of event-driven systems and the examples are more complete. I made a significant effort to come up with examples that are not utterly trivial yet don’t obscure the general principles in too many details. I also chose examples that don’t require any specific domain knowledge, so I don’t need to waste space and your attention explaining the problem specification.
Preemptive Multitasking Support An event-driven infrastructure such as QP can work with a variety of concurrency mechanisms, from a simple “superloop” to fully preemptive, priority-based multitasking. The previous version of QP supported the simple nonpreemptive scheduling natively but required an external RTOS to provide preemptive multitasking, if such capability was required. In Chapter 10, I describe the new real-time kernel (QK) component that provides deterministic, fully preemptive, priority-based multitasking to QP. QK is a very special,
www.newnespress.com xx Preface super-simple, run-to-completion, single-stack kernel that perfectly matches the universally assumed run-to-completion semantics required for state machine execution.
Testing Support A running application built of concurrently executing state machines is a highly structured affair where all important system interactions funnel through the event- driven framework that ties all the state machines together. By instrumenting just this tiny “funnel” code, you can gain unprecedented insight into the live system. In fact, the software trace data from an instrumented event-driven framework can tell you much more about the application than any traditional real-time operating system (RTOS) because the framework “knows” so much more about the application. Chapter 11 describes the new QS (“spy”) component that provides a comprehensive software-tracing instrumentation to the QP event-driven platform. The trace data produced by the QS component allows you to perform a live analysis of your running real-time embedded system with minimal target system resources and without stopping or significantly slowing down the code. Among other things, you can reconstruct complete sequence diagrams and detailed, timestamped state machine activities for all active objects in the system. You can monitor all event exchanges, event queues, event pools, time events (timers), and preemptions and context switches. You can also use QS to add your own instrumentation to the application-level code.
Ultra-Lightweight QP-nano Version The event-driven approach with state machines scales down better than any conventional real-time kernel or RTOS. To address really small embedded systems, a reduced QP version called QP-nano implements a subset of features supported in QP/C or QP/C++. QP-nano has been specifically designed to enable event-driven programming with hierarchical state machines on low-end 8- and 16-bit microcontrollers (MCUs), such as AVR, MSP430, 8051, PICmicro, 68HC(S)08, M16C, and many others. Typically, QP-nano requires around 1-2KB of ROM and just a few bytes of RAM per state machine. I describe QP-nano in Chapter 12.
Removed Quantum Metaphor In the first edition of PSiCC, I proposed a quantum-mechanical metaphor as a way of thinking about the event-driven software systems. Though I still believe that this www.newnespress.com Preface xxi analogy is remarkably accurate, it hasn’t particularly caught on with readers, even though providing such a metaphor is one of the key practices of eXtreme Programming (XP) and other agile methods. Respecting readers’ feedback, I decided to remove the quantum metaphor from this edition. For historical reasons, the word quantum still appears in the names of the software components, and the prefix Q is consistently used in the code for type and function names to clearly distinguish the QP code from other code, but you don’t need to read anything into these names.
What You Need to Use QP Most of the code supplied with this book is highly portable C or C++, independent of any particular CPU, operating system, or compiler. However, to focus the discussion I provide executable examples that run in a DOS console under any variant of Windows. I’ve chosen the legacy 16-bit DOS as a demonstration platform because it allows programming a standard x86-based PC at the bare-metal level. Without leaving your desktop, you can work with interrupts, directly manipulate CPU registers, and directly access the I/O space. No other modern 32-bit development environment for the standard PC allows this much so easily. The additional advantage of the legacy DOS platform is the availability of mature and free tools. To that end, I have compiled the examples with the legacy Borland Turbo C++ 1.01 toolset, which is available for a free download from Borland. To demonstrate modern embedded systems programming with QP, I also provide examples for the inexpensive2 ARM Cortex-M3-based Stellaris EV-LM3S811 evaluation kit from Luminary Micro. The Cortex-M3 examples use the exact same source code as the DOS counterparts and differ only in the board support package (BSP). The Cortex-M3 examples require the 32KB-limited KickStart edition of the IAR EWARM toolset, which is included in the Stellaris kit and is also available for a free download from IAR. Finally, some examples in this book run on Linux as well as any other POSIX- compliant operating system such as BSD, QNX, Mac OS X, or Solaris. You can also build the Linux examples on Windows under Cygwin.
2 At the time of this writing, the EKI-LM3S811 kit was available for $49 ( www.luminarymicro.com).
www.newnespress.com xxii Preface
The companion Website to this book at www.quantum-leaps .com/psicc2 provides the links for downloading all the tools used in the book, as well as other resources. The Website also contains links to dozens of QP ports to various CPUs, operating systems, and compilers. Keep checking this Website; new ports are added frequently.
Intended Audience This book is intended for the following software developers interested in event-driven programming and modern state machines: Embedded programmers and consultants will find a complete, ready-to-use, event-driven infrastructure to develop applications. The book describes both state machine coding strategies and, equally important, a compatible real-time framework for executing concurrent state machines. These two elements are synergistically complementary, and one cannot reach its full potential without the other. Embedded developers looking for a real-time kernel or RTOS will find that the QP event-driven platform can do everything one might expect from an RTOS and that, in fact, QP actually contains a fully preemptive real-time kernel as well as a simple cooperative scheduler. Designers of ultra low-power systems, such as wireless sensor networks, will find how to scale down the event-driven, state machine-based approach to fit the tiniest MCUs. The ultra-light QP-nano version (Chapter 12) combines a hierarchical event processor, a real-time framework, and either a cooperative or a fully preemptive kernel in just 1–2KB of ROM. On the opposite end of the complexity spectrum, designers of very large-scale, massively parallel server applications will find that the event-driven approach combined with hierarchical state machines scales up easily and is ideal for managing very large numbers of stateful components, such as client sessions. As it turns out, the “embedded” design philosophy of QP provides the critical per-component efficiency both in time and space. The open-source community will find that QP complements other open-source software, such as Linux or BSD. The QP port to Linux (and more generally to POSIX-compliant operating systems) is described in Chapter 8. www.newnespress.com Preface xxiii
GUI developers and computer game programmers using C or C++ will find that QP very nicely complements GUI libraries. QP provides the high-level “screen logic” based on hierarchical state machines, whereas the GUI libraries handle low-level widgets and rendering of the images on the screen. System architects might find in QP a lightweight alternative to heavyweight design automation tools. Users of design automation tools will gain deeper understanding of the inner workings of their tools. The glimpse “under the hood” will help them use the tools more efficiently and with greater confidence. Due to the code-centric approach, this book will primarily appeal to software developers tasked with creating actual, working code, as opposed to just modeling. Many books about UML already do a good job of describing model-driven analysis and design as well as related issues, such as software development processes and modeling tools. This book does not provide yet another CASE tool. Instead, this book is about practical, manual coding techniques for hierarchical state machines and about combining state machines into robust event-driven systems by means of a real-time framework. To benefit from the book, you should be reasonably proficient in C or C++ and have a general understanding of computer architectures. I am not assuming that you have prior knowledge of UML state machines, and I introduce the underlying concepts in a crash course in Chapter 2. I also introduce the basic real-time concepts of multitasking, mutual exclusion, and blocking in Chapter 6.
The Companion Websites This book has a companion Website at www.quantum-leaps.com/psicc2 that contains the following information: Source code downloads for QP/C, QP/C++, and QP-nano All QP ports and examples described in the book Reference manuals for QP/C, QP/C++, and QP-nano in HTML and CHM file formats Links for downloading compilers and other tools used in the book
www.newnespress.com xxiv Preface
Selected reviews and reader feedback Errata Additionally, the Quantum Leaps Website at www.quantum-leaps.com has been supporting the QP user community since the publication of the first edition of PSiCC in 2002. This Website offers the following resources: Latest QP downloads QP ports and development kits Programmer manuals Application notes Resources and goodies such as Visio stencils for drawing UML diagrams, design patterns, links to related books and articles, and more Commercial licensing and technical support information Consulting and training in the technology News and events Discussion forum Newsletter Blog Links to related Websites And more Finally, QP is also present on SourceForge.net—the world’s largest repository of open source code and applications. The QP project is located at https://sourceforge. net/projects/qpc/.
www.newnespress.com Acknowledgments
First and foremost, I’d like to thank my wonderful family for the unfading support over the years of creating the software and the two editions of this book. I would also like to thank the team at Elsevier, which includes Rachel Roumeliotis and Heather Scherer, and John (Jay) Donahue. Finally, I’m grateful to all the software developers who contacted me with thought- provoking questions, bug reports, and countless suggestions for improvements in the code and documentation. As a rule, a software system only gets better if it is used and scrutinized by many people in many different real-life projects.
www.newnespress.com Newnes is an imprint of Elsevier 30 Corporate Drive, Suite 400, Burlington, MA 01803, USA Linacre House, Jordan Hill, Oxford OX2 8DP, UK
Copyright # 2009, Elsevier Inc. All rights reserved.
No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher.
Permissions may be sought directly from Elsevier’s Science & Technology Rights Department in Oxford, UK: phone: (+44) 1865 843830, fax: (+44) 1865 853333, E-m ail: permi ssions@el sevier. com. You may also com plete your reque st online via the Else vier homepage ( http://el sevier. com), by selecting “Suppor t & Contact” then “Copyright and Permission” and then “Obtaining Permissions.”
Library of Congress Cataloging-in-Publication Data Application submitted.
British Library Cataloguing-in-Publication Data A catalogue record for this book is available from the British Library.
ISBN: 978-0-7506-8706-5
For information on all Newnes publications visit our Web site at www.elsevierdirect.com
0809101110987654321
Printed in the United States of America Introduction
Almost all computer systems in general, and embedded systems in particular, are event- driven, which means that they continuously wait for the occurrence of some external or internal event such as a time tick, an arrival of a data packet, a button press, or a mouse click. After recognizing the event, such systems react by performing the appropriate computation that may include manipulating the hardware or generating “soft” events that trigger other internal software components. (That’s why event-driven systems are alternatively called reactive systems.) Once the event handling is complete, the software goes back to waiting for the next event. You are undoubtedly accustomed to the basic sequential control, in which a program waits for events in various places in its execution path by either actively polling for events or passively blocking on a semaphore or other such operating system mechanism. Though this approach to programming event-driven systems is functional in many situations, it doesn’t work very well when there are multiple possible sources of events whose arrival times and order you cannot predict and where it is important to handle the events in a timely manner. The problem is that while a sequential program is waiting for one kind of event, it is not doing any other work and is not responsive to other events. Clearly, what we need is a program structure that can respond to a multitude of possible events, any of which can arrive at unpredictable times and in an unpredictable sequence. Though this problem is very common in embedded systems such as home appliances, cell phones, industrial controllers, medical devices and many others, it is also very common in modern desktop computers. Think about using a Web browser, a word processor, or a spreadsheet. Most of these programs have a modern graphical user interface (GUI), which is clearly capable of handling multiple events. All developers of
www.newnespress.com xxviii Introduction modern GUI systems, and many embedded applications, have adopted a common program structure that elegantly solves the problem of dealing with many asynchronous events in a timely manner. This program structure is generally called event-driven programming.
Inversion of Control Event-driven programming requires a distinctly different way of thinking than conventional sequential programs, such as “superloops” or tasks in a traditional RTOS. Most modern event-driven systems are structured according to the Hollywood principle, which means “Don’t call us, we’ll call you.” So an event-driven program is not in control while waiting for an event; in fact, it’s not even active. Only once the event arrives, the program is called to process the event and then it quickly relinquishes the control again. This arrangement allows an event-driven system to wait for many events in parallel, so the system remains responsive to all events it needs to handle. This scheme has three important consequences. First, it implies that an event-driven system is naturally divided into the application, which actually handles the events, and the supervisory event-driven infrastructure, which waits for events and dispatches them to the application. Second, the control resides in the event-driven infrastructure, so from the application standpoint the control is inverted compared to a traditional sequential program. And third, the event-driven application must return control after handling each event, so the execution context cannot be preserved in the stack-based variables and the program counter as it is in a sequential program. Instead, the event-driven application becomes a state machine, or actually a set of collaborating state machines that preserve the context from one event to the next in the static variables.
The Importance of the Event-Driven Framework The inversion of control, so typical in all event-driven systems, gives the event-driven infrastructure all the defining characteristics of an application framework rather than a toolkit. When you use a toolkit, such as a traditional operating system or an RTOS, you write the main body of the application and call the toolkit code that you want to reuse. When you use a framework, you reuse the main body and write the code it calls. Another important point is that an event-driven framework is actually necessary if you want to combine multiple event-driven state machines into systems. It really takes more than “just” an API, such as a traditional RTOS, to execute concurrent state machines. www.newnespress.com Introduction xxix
State machines require an infrastructure (framework) that provides, at a minimum, run-to-completion (RTC) execution context for each state machine, queuing of events, and event-based timing services. This is really the pivotal point. State machines cannot operate in a vacuum and are not really practical without an event-driven framework.
Active Object Computing Model This book brings together two most effective techniques of decomposing event-driven systems: hierarchical state machines and an event-driven framework. The combination of these two elements is known as the active object computing model. The term active object comes from the UML and denotes an autonomous object engaging other active objects asynchronously via events. The UML further proposes the UML variant of statecharts with which to model the behavior of event-driven active objects. In this book, active objects are implemented by means of the event-driven framework called QF, which is the main component of the QP event-driven platform. The QF framework orderly executes active objects and handles all the details of thread-safe event exchange and processing within active objects. QF guarantees the universally assumed RTC semantics of state machine execution, by queuing events and dispatching them sequentially (one at a time) to the internal state machines of active objects. The fundamental concepts of hierarchical state machines combined with an event- driven framework are not new. In fact, they have been in widespread use for at least two decades. Virtually all commercially successful design automation tools on the market today are based on hierarchical state machines (statecharts) and incorporate internally a variant of an event-driven, real-time framework similar to QF.
The Code-Centric Approach The approach I assume in this book is code-centric, minimalist, and low-level. This characterization is not pejorative; it simply means that you’ll learn how to map hierarchical state machines and active objects directly to C or C++ source code, without big tools. The issue here is not a tool—the issue is understanding. The modern design automation tools are truly powerful, but they are not for everyone. For many developers the tool simply can’t pull its own weight and gets abandoned. For such developers, the code-centric approach presented in this book can provide a lightweight alternative to the heavyweight tools.
www.newnespress.com xxx Introduction
Most important, though, no tool can replace conceptual understanding. For example, determining which exit and entry actions fire in which sequence in a nontrivial state transition is not something you should discover by running a tool-supported animation of your state machine. The answer should come from your understanding of the underlying state machine implementation (discussed in Chapters 3 and 4). Even if you later decide to use a design automation tool and even if that particular tool would use a different statechart implementation technique than discussed in this book, you will still apply the concepts with greater confidence and more efficiency because of your understanding of the fundamental mechanisms at a low level. In spite of many pressures from existing users, I persisted in keeping the QP event- driven platform lean by directly implementing only the essential elements of the bulky UML specification and supporting the niceties as design patterns. Keeping the core implementation small and simple has real benefits. Programmers can learn and deploy QP quickly without large investments in tools and training. They can easily adapt and customize the framework’s source code to the particular situation, including to severely resource-constrained embedded systems. They can understand, and indeed regularly use, all the provided features.
Focus on Real-Life Problems You can’t just look at state machines and the event-driven framework as a collection of features, because some of the features will make no sense in isolation. You can only use these powerful concepts effectively if you are thinking about design, not simply coding. And to understand state machines that way, you must understand the problems with event-driven programming in general. This book discusses event-driven programming problems, why they are problems, and how state machines and active object computing model can help. Thus, I begin most chapters with the programming problems the chapter will address. In this way, I hope to move you, a little at a time, to the point where hierarchical state machines and the event-driven framework become a much more natural way of solving the problems than the traditional approaches such as deeply nested IFs and ELSEs for coding stateful behavior or passing events via semaphores or event flags of a traditional RTOS.
www.newnespress.com Introduction xxxi
Object Orientation Even though I use C as the primary programming language, I also extensively use object-oriented design principles. Like virtually all application frameworks, QP uses the basic concepts of encapsulation (classes) and single inheritance as the primary mechanisms of customizing, specializing, and extending the framework to a particular application. Don’t worry if these concepts are new to you, especially in C. At the C language level, encapsulation and inheritance become just simple coding idioms, which I introduce in Chapter 1. I specifically avoid polymorphism in the C version because implementing late binding in C is a little more involved. Of course, the C++ version uses classes and inheritance directly and QP/C++ applications can use polymorphism.
More Fun When you start using the techniques described in this book, your problems will change. You will no longer struggle with 15 levels of convoluted if–else statements, and you will stop worrying about semaphores or other such low-level RTOS mechanisms. Instead, you’ll start thinking at a higher level of abstraction about state machines, events, and active objects. After you experience this quantum leap you will find, as I did, that programming can be much more fun. You will never want to go back to the “spaghetti” code or the raw RTOS.
How to Contact Me If you have comments or questions about this book, the code, or event-driven programming in general, I’d be pleased to hear from you. Please e-mail me at [email protected].
www.newnespress.com www.CartoonStock.com
PART I UML STATE MACHINES State machines are the best-known formalism for specifying and implementing event- driven systems that must react to incoming events in a timely fashion. The advanced UML state machines represent the current state of the art in state machine theory and notation. Part I of this book shows practical ways of using UML state machines in event-driven applications to help you produce efficient and maintainable software with well- understood behavior, rather than creating “spaghetti” code littered with convoluted IFs and ELSEs. Chapter 1 presents an overview of the method based on a working example.
www.newnespress.com 2 Part I
Chapter 2 introduces state machine concepts and the UML notation. Chapter 3 shows the standard techniques of coding state machines, and Chapter 4 describes a generic hierarchical event processor. Part I concludes with Chapter 5, which presents a mini- catalog of five state design patterns. You will learn that UML state machines are a powerful design method that you can use, even without complex code-synthesizing tools.
www.newnespress.com CHAPTER 1 Getting Started with UML State Machines and Event-Driven Programming
It is common sense to take a method and try it. If it fails, admit it frankly and try another. But above all, try something. —Franklin D. Roosevelt
This chapter presents an example project implemented entirely with UML state machines and the event-driven paradigm. The example application is an interactive “Fly ‘n’ Shoot”-type game, which I decided to include early in the book so that you can start playing (literally) with the code as soon as possible. My aim in this chapter is to show the essential elements of the method in a real, nontrivial program, but without getting bogged down in details, rules, and exceptions. At this point, I am not trying to be complete or even precise, although this example as well as all other examples in the book is meant to show a good design and the recommended coding style. I don’t assume that you know much about UML state machines, UML notation, or event-driven programming. I will either briefly introduce the concepts, as needed, or refer you to the later chapters of the book for more details. The example “Fly ‘n’ Shoot” game is based on the Quickstart application provided in source code with the Stellaris EV-LM3S811 evaluation kit from Luminary Micro [Luminary 06]. I was trying to make the “Fly ‘n’ Shoot” example behave quite similarly to the original Luminary Micro Quickstart application so that you can directly compare the event-driven approach with the traditional solution to essentially the same problem specification.
www.newnespress.com 4 Chapter 1
1.1 Installing the Accompanying Code The companion Website to this book at www.quantum-leaps.com/psicc2 contains the self-extracting archive with the complete source code of the QP event-driven platform and all executable examples described in this book; as well as documentation, development tools, resources, and more. You can uncompress the archive into any directory. The installation directory you choose will be referred henceforth as the QP Root Directory
NOTE Although in the text I mostly concentrate on the C implementation, the accompanying Web- site also contains the equivalent C++ version of virtually every element available in C. The C++ code is organized in exactly the same directory tree as the corresponding C code, except you need to look in the
Specifically to the “Fly ‘n’ Shoot” example, the companion code contains two versions1 of the game. I provide a DOS version for the standard Windows-based PC (see Figure 1.1) so that you don’t need any special embedded board to play the game and experiment with the code.
NOTE I’ve chosen the legacy 16-bit DOS platform because it allows programming a standard PC at the bare-metal level. Without leaving your desktop, you can work with interrupts, directly manipulate CPU registers, and directly access the I/O space. No other modern 32-bit devel- opment environment for the standard PC allows this much so easily. The ubiquitous PC run- ning under DOS (or a DOS console within any variant of Windows) is as close as it gets to emulating embedded software development on the commodity 80x86 hardware. Addition- ally, you can use free, mature tools, such as the Borland C/C++ compiler.
I also provide an embedded version for the inexpensive2 ARM Cortex-M3-based Stellaris EV-LM3S811 evaluation kit (see Figure 1.2). Both the PC and Cortex-M3
1 The accompanying code actually contains many more versions of the “Fly ‘n’ Shoot” game, but they are not relevant at this point. 2 At the time of this writing the EV-LM3S811 kit was available for $49 (www.luminarymicro.com). www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 5 versions use the exact same source code for all application components and differ only in the Board Support Package (BSP). 1.2 Let’s Play The following description of the “Fly ‘n’ Shoot” game serves the dual purpose of explaining how to play the game and as the problem specification for the purpose of designing and implementing the software later in the chapter. To accomplish these two goals I need to be quite detailed, so please bear with me. Your objective in the game is to navigate a spaceship through an endless horizontal tunnel with mines. Any collision with the tunnel or the mine destroys the ship. You can move the ship up and down with Up-arrow and Down-arrow keys on the PC (see Figure 1.1) or via the potentiometer wheel on the EV-LM3S811 board (see Figure 1.2). You can also fire a missile to destroy the mines in the tunnel by pressing the Spacebar on the PC or the User button on the EV-LM3S811 board. Score accumulates for survival (at the rate of 30 points per second) and destroying the mines. The game lasts for only one ship. The game starts in a demo mode, where the tunnel walls scroll at the normal pace from right to left and the “Press Button” text flashes in the middle of the screen. You need to generate the “fire missile” event for the game to begin (press Spacebar on the PC or the User button on the EV-LM3S811 board). You can have only one missile in flight at a time, so trying to fire a missile while it is already flying has no effect. Hitting the tunnel wall with the missile brings you no points, but you earn extra points for destroying the mines. The game has two types of mines with different behavior. In the original Luminary Quickstart application both types of mines behave the same, but I wanted to demonstrate how state machines can elegantly handle differently behaving mines. Mine type 1 is small, but can be destroyed by hitting any of its pixels with the missile. You earn 25 points for destroying a mine type 1. Mine type 2 is bigger but is nastier in that the missile can destroy it only by hitting its center, not any of the “tentacles.” Of course, the ship is vulnerable to the whole mine. You earn 45 points for destroying a mine type 2. When you crash the ship, by either hitting a wall or a mine, the game ends and displays the flashing “Game Over” text as well as your final score. After 5 seconds of flashing,
www.newnespress.com 6 Chapter 1 the “Game Over” screen changes back to the demo screen, where the game waits to be started again. Additionally the application contains a screen saver because the OLED display of the original EV-LM3S811 board has burn-in characteristics similar to a CRT. The screen saver only becomes active if 20 seconds elapse in the demo mode without starting the game (i.e., the screen saver never appears during game play). The screen saver is a simple random pixel type rather than the “Game of Life” algorithm used in the original Luminary Quickstart application. I’ve decided to simplify this aspect of the implementation because the more elaborate pixel-mixing algorithm does not contribute any new or interesting behavior. After a minute of running the screen saver, the display turns blank and only a single random pixel shows on the screen. Again, this is a little different from the original Quickstart application, which instead blanks the screen and starts flashing the User LED. I’ve changed this behavior because I have a better purpose for the User LED (to visualize the activity of the idle loop).
Mine Mine Tunnel Ship Missile Type 1 Explosion Type 2 wall
Figure 1.1: The “Fly ‘n’ Shoot” game running in a DOS window under Windows XP. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 7
Potentiometer Reset LM3S811 96 x 16 Power Wheel Switch Cortex-M3 MCU OLED Display LED
USB Cable LMI FTDI User User to PC Debugger LED Switch
Figure 1.2: The “Fly ‘n’ Shoot” game running on the Stellaris EV-LM3S811 evaluation board.
1.2.1 Running the DOS Version The “Fly ‘n’ Shoot” sample code for the DOS version (in C) is located in the
www.newnespress.com 8 Chapter 1 is used as a matrix of 80 Â 16 character-wide “pixels,” which is a little less than the 96 Â 16 pixels of the OLED display but still good enough to play the game. I specifically avoid employing any fancier graphics in this early example because I have bigger fish to fry for you than to worry about the irrelevant complexities of programming graphics. My main goal is to make it easy for you to understand the event-driven code and experiment with it. To this end, I chose the legacy Borland Turbo C++ 1.01 toolset to build this example as well as several other examples in this book. Even though Turbo C++ 1.01 is an older compiler, it is adequate to demonstrate all features of both the C and C++ versions. Best of all, it is available for a free download from the Borland “Museum” at http://bdn.borland.com/article/0,1410,21751,00.html. The toolset is very easy to install. After you download the Turbo C++ 1.01 files directly from Borland, you need to unzip the files onto your hard drive. Then you run the INSTALL.EXE program and follow the installation instructions it provides.
NOTE I strongly recommend that you install the Turbo C++ 1.01 toolset into the directory C:\tools\tcpp101\. That way you will be able to directly use the provided project files and make scripts.
Perhaps the easiest way to experiment with the “Fly ‘n’ Shoot” code is to launch the Turbo C++ IDE (TC.EXE) and open the provided project file GAME-DBG.PRJ, which is located in the directory
1.2.2 Running the Stellaris Version In contrast to the “Fly ‘n’ Shoot” version for DOS running in the ancient real mode of the 80x86 processor, the exact same source code runs on one of the most modern processors in the industry: the ARM Cortex-M3. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 9
The sample code for the Stellaris EV-LM3S811 board is located in the
NOTE
I strongly recommend that you install the IAR EWARM toolset into the directory C:\tools \iar\arm_ks_5.11. That way you will be able to directly use the provided EWARM work- space files and make scripts.
Before you program the “Fly ‘n’ Shoot” game to the EV-LM3S811 board, you might want to play a little with the original Quickstart application that comes preprogrammed with the EV-LM3S811 kit. To program the “Fly ‘n’ Shoot” game to the Flash memory of the EV-LM3S811 board, you first connect the EV-LM3S811 board to your PC with the USB cable provided in the kit and make sure that the Power LED is on (see Figure 1.2). Next, you need to launch the IAR Embedded Workbench and open the workspace game.eww located in the
www.newnespress.com 10 Chapter 1 that the Flash loader application provided by IAR will be first loaded to the RAM of the MCU, and this application will program the Flash with the image of your application. To start the Flash programming process, select the Project | Debug menu, or simply click the Debug button (see Figure 1.3) in the toolbar. The IAR Workbench should respond by showing the Flash programming progress bar for several seconds, as shown in Figure 1.3. Once the Flash programming completes, the IAR EWARM switches to the IAR C-Spy debugger and the program should stop at the entry to main(). You can start playing the game either by clicking the Go button in the debugger or you can close the debugger and reset the board by pressing the Reset button. Either way, the “Fly ‘n’ Shoot” game is now permanently programmed into the EV-LM3S811 board and will start automatically on every powerup.
Debug Build Configuration Button Selection
QP Libraries
Application Sources
Flash Programming Progress
Figure 1.3: Loading the “Fly ‘n’ Shoot” game into the flash of LM3S811 MCU with IAR EWARM IDE. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 11
The IAR Embedded Workbench environment allows you to experiment with the “Fly ‘n’ Shoot” code very easily. You can edit the files and recompile the application at a click of a button (F7). The only caveat is that the first time after the installation of the IAR toolset you need to build the Luminary Micro driver library for the LM3S811 MCU from the sources. You accomplish this by loading the workspace ek-lm3s811.eww located in the directory
NOTE To explain code listings, I place numbers in parentheses at the interesting lines in the left margin of the listing. I then use these labels in the left margin of the explanation section that immediately follows the listing. Occasionally, to unambiguously refer to a line of a particular listing from sections of text other than the explanation section, I use the full reference con- sisting of the listing number followed by the label. For example, Listing 1.1(21) refers to the label (21) in Listing 1.1.
Listing 1.1 The file main.c of the “Fly ‘n’ Shoot” game application
(1) #include "qp_port.h" /* the QP port */ (2) #include "bsp.h" /* Board Support Package */ (3) #include "game.h" /* this application */
/* Local-scope objects ------*/ (4) static QEvent const * l_missileQueueSto[2]; /* event queue */ (5) static QEvent const * l_shipQueueSto[3]; /* event queue */ (6) static QEvent const * l_tunnelQueueSto[GAME_MINES_MAX + 5]; /* event queue */
Continued onto next page
www.newnespress.com 12 Chapter 1
(7) static ObjectPosEvt l_smlPoolSto[GAME_MINES_MAX + 8]; /* small-size pool */ (8) static ObjectImageEvt l_medPoolSto[GAME_MINES_MAX + 8]; /* medium-size pool */ (9) static QSubscrList l_subscrSto[MAX_PUB_SIG]; /* publish-subscribe */
/*...... */ void main(int argc, char *argv[]) { /* explicitly invoke the active objects’ ctors... */ (10) Missile_ctor(); (11) Ship_ctor(); (12) Tunnel_ctor();
(13) BSP_init(argc, argv); /* initialize the Board Support Package */ (14) QF_init(); /* initialize the framework and the underlying RT kernel */
/* initialize the event pools... */ (15) QF_poolInit(l_smlPoolSto, sizeof(l_smlPoolSto), sizeof(l_smlPoolSto[0])); (16) QF_poolInit(l_medPoolSto, sizeof(l_medPoolSto), sizeof(l_medPoolSto[0]));
(17) QF_psInit(l_subscrSto, Q_DIM(l_subscrSto)); /* init publish-subscribe */
/* start the active objects... */ (18) QActive_start(AO_Missile,/* global pointer to the Missile active object */ 1, /* priority (lowest) */ l_missileQueueSto, Q_DIM(l_missileQueueSto), /* evt queue */ (void *)0, 0, /* no per-thread stack */ (QEvent *)0); /* no initialization event */ (19) QActive_start(AO_Ship, /* global pointer to the Ship active object */ 2, /* priority */ l_shipQueueSto, Q_DIM(l_shipQueueSto), /* evt queue */ (void *)0, 0, /* no per-thread stack */ (QEvent *)0); /* no initialization event */ (20) QActive_start(AO_Tunnel, /* global pointer to the Tunnel active object */ 3, /* priority */ l_tunnelQueueSto, Q_DIM(l_tunnelQueueSto), /* evt queue */ (void *)0, 0, /* no per-thread stack */ (QEvent *)0); /* no initialization event */
(21) QF_run(); /* run the QF application */ }
(1) The “Fly ‘n’ Shoot” game is an example of an application implemented with the QP event-driven platform. Every application C-file that uses QP must include the qp_port.h header file. This header file contains the specific adaptation of QP to the given processor, operating system, and compiler, which is called a port. Each QP port is located in a separate directory, and the C compiler finds the right qp_port.h header file through the include search path provided to the compiler www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 13
(typically via the –I compiler option). That way I don’t need to change the application source code to recompile it for a different processor or compiler. I only need to instruct the compiler to look in a different QP port directory for the qp_port.h header file. For example, the DOS version includes the qp_port.h header file from the directory
(2) The bsp.h header file contains the interface to the Board Support Package and is located in the application directory. (3) The game.h header file contains the declarations of events and other facilities shared among the components of the application. I will discuss this header file in the upcoming Section 1.6. This header file is located in the application directory. The QP event-driven platform is a collection of components, such as the QEP event processor that executes state machines according to the UML semantics and the QF real-time framework that implements the active object computing model. Active objects in QF are encapsulated state machines (each with an event queue, a separate task context, and a unique priority) that communicate with one another asynchronously by sending and receiving events, whereas QF handles all the details of thread-safe event exchange and queuing. Within an active object, the events are processed by the QEP event processor sequentially in a run-to-completion (RTC) fashion, meaning that processing of one event must necessarily complete before processing the next event. (See also Section 6.3.3 in Chapter 6.) (4-6) The application must provide storage for the event queues of all active objects used in the application. Here the storage is provided at compile time through the statically allocated arrays of immutable (const) pointers to events, because QF event queues hold just pointers to events, not events themselves. Events are represented as instances of the QEvent structure declared in the qp_port.h header file. Each event queue of an active object can have a different size, and you need to decide this size based on your knowledge of the application. Event queues are discussed in Chapters 6 and 7. (7,8) The application must also provide storage for event pools that the framework uses for fast and deterministic dynamic allocation of events. Each event pool
www.newnespress.com 14 Chapter 1
can provide only fixed-size memory blocks. To avoid wasting memory by using oversized blocks for small events, the QF framework can manage up to three event pools of different block sizes (for small, medium, and large events). The “Fly ‘n’ Shoot” application uses only two out of the three possible event pools (the small and medium pools). The QF real-time framework supports two event delivery mechanisms: the simple direct event posting to active objects and the more advanced mechanism called publish-subscribe that decouples event producers from the consumers. In the publish- subscribe mechanism, active objects subscribe to events by the framework. Event producers publish the events to the framework. Upon each publication request, the framework delivers the event to all active objects that had subscribed to that event type. One obvious implication of publish-subscribe is that the framework must store the subscriber information, whereas it must be possible to handle multiple subscribers to any given event type. The event delivery mechanisms are described in Chapters 6 and 7. (9) The “Fly ‘n’ Shoot” application uses the publish-subscribe event delivery mechanism supported by QF, so it needs to provide the storage for the subscriber lists. The subscriber lists remember which active objects have subscribed to which events. The size of the subscriber database depends on both the number of published events, which is specified in the MAX_PUB_SIG constant found in the game.h header file, and the maximum number of active objects allowed in the system, which is determined by the QF configuration parameter QF_MAX_ACTIVE. (10-12) These functions perform an early initialization of the active objects in the system. They play the role of static “constructors,” which in C you need to invoke explicitly. (C++ calls such static constructors implicitly before entering main()). (13) The function BSP_init() initializes the board and is defined in the bsp.c file. (14) The function QF_init() initializes the QF component and the underlying RTOS/kernel, if such software is used. You need to call QF_init() before you invoke any QF services. (15,16) The function QF_poolInit() initializes the event pools. The parameters of this function are the pointer to the event pool storage, the size of this storage, www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 15
and the block-size of this pool. You can call this function up to three times to initialize up to three event pools. The subsequent calls to QF_poolInit() must be made in the increasing order of block size. For instance, the small block-size pool must be initialized before the medium block-size pool. (17) The function QF_psInit() initializes the publish-subscribe event delivery mechanism of QF. The parameters of this function are the pointer to the subscriber-list array and the dimension of this array. The utility macro Q_DIM(a) provides the dimension of a one-dimensional array a[] computed as sizeof(a)/sizeof(a[0]), which is a compile-time constant. The use of this macro simplifies the code because it allows me to eliminate many #define constants that otherwise I would need to provide for the dimensions of various arrays. I can simply hard-code the dimension right in the definition of an array, which is the only place that I specify it. I then use the macro Q_DIM() whenever I need this dimension in the code.
(18-20) The function QActive_start() tells the QF framework to start managing an active object as part of the application. The function takes the following parameters: the pointer to the active object structure, the priority of the active object, the pointer to its event queue, the dimension (length) of that queue, and three other parameters that I explain in Chapter 7 (they are not relevant at this point). The active object priorities in QF are numbered from 1 to QF_MAX_ACTIVE, inclusive, where a higher-priority number denotes higher urgency of the active object. The constant QF_MAX_ACTIVE is defined in the QF port header file qf_port.h and currently cannot exceed 63. I like to keep the code and data of every active object strictly encapsulated within its own C-file. For example, all code and data for the active object Ship are encapsulated in the file ship.c, with the external interface consisting of the function Ship_ctor() and the pointer AO_Ship.
(21) At this point, you have provided to the framework all the storage and information it needs to manage your application. The last thing you must do is call the function QF_run() to pass the control to the framework. After the call to QF_run() the framework is in full control. The framework executes the application by calling your code, not the other way around. The function QF_run() never returns the control back to main(). In the DOS version of the
www.newnespress.com 16 Chapter 1
“Fly ‘n’ Shoot” game, you can terminate the application by pressing the Esc key, in which case QF_run() exits to DOS but not to main(). In an embedded system, such as the Stellaris board, QF_run() runs forever or till the power is removed, whichever comes first.
NOTE For best cross-platform portability, the source code consistently uses the UNIX end-of-line convention (lines are terminated with LF only, 0xA character). This convention seems to be working for all C/C++ compilers and cross-compilers, including legacy DOS-era tools. In contrast, the DOS/Windows end-of-line convention (lines terminated with the CR,LF, or 0xD,0xA pair of characters) is known to cause problems on UNIX-like platforms, especially in the multiline preprocessor macros.
1.4 The Design of the “Fly ‘n’ Shoot” Game To proceed further with the explanation of the “Fly ‘n’ Shoot” application, I need to step up to the design level. At this point I need to explain how the application has been decomposed into the active objects and how these objects exchange events to collectively deliver the functionality of the “Fly ‘n’ Shoot” game. In general, the decomposition of a problem into active objects is not trivial. As usual in any decomposition, your goal is to achieve possibly loose coupling among the active object components (ideally no sharing of any resources), and you also strive for minimizing the communication in terms of the frequency and size of exchanged events. In the case of the “Fly ‘n’ Shoot” game, I need to first identify all objects with reactive behavior (i.e., with a state machine). I applied the simplest object-oriented technique of identifying objects, which is to pick the frequently used nouns in the problem specification. From Section 1.2, I identified Ship, Missile, Mines, and Tunnel. However, not every state machine in the system needs to be an active object (with a separate task context, an event queue, and a unique priority level), and merging them is a valid option when performance or space is needed. As an example of this idea, I ended up merging the Mines into the Tunnel active object, whereas I preserved the Mines as independent state machine components of the Tunnel active object. By doing so I applied the “Orthogonal Component” design pattern described in Chapter 5. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 17
The next step in the event-driven application design is assigning responsibilities and resources to the identified active objects. The general design strategy for avoiding sharing of resources is to encapsulate each resource inside a dedicated active object and to let that object manage the resource for the rest of the application. That way, instead of sharing the resource directly, the rest of the application shares the dedicated active object via events. So, for example, I decided to put the Tunnel active object in charge of the display. Other active objects and state machine components, such as Ship, Missile, and Mines, don’t draw on the display directly, but rather send events to the Tunnel object with the request to render the Ship, Missile, or Mine bitmaps at the provided (x, y) coordinates of the display. With some understanding of the responsibilities and resource allocations to active objects I can move on to devising the various scenarios of event exchanges among the objects. Perhaps the best instrument to aid the thinking process at this stage is the UML sequence diagram, such as the diagram depicted in Figure 1.4. This particular sequence diagram shows the most common event exchange scenarios in the “Fly ‘n’ Shoot” game (the primary use cases, if you will). The explanation section immediately following the diagram illuminates the interesting points.
NOTE A UML sequence diagram like Figure 1.4 has two dimensions. Horizontally arranged boxes represent the various objects participating in the scenario, whereas heavy borders indicate active objects. As usual in the UML, the object name is underlined. Time flows down the page along the vertical dashed lines descending from the objects. Events are represented as horizontal arrows originating from the sending object and terminating at the receiving object. Optionally, thin rectangles around instance lines indicate focus of control.
NOTE To explain diagrams, I place numbers in parentheses at the interesting elements of the dia- gram. I then use these labels in the left margin of the explanation section that immediately follows the diagram. Occasionally, to unambiguously refer to a specific element of a partic- ular diagram from sections of text other than the explanation section, I use the full reference consisting of the figure number followed by the label. For example, Figure 1.4(12) refers to the element (12) in Figure 1.4.
www.newnespress.com 18 Chapter 1
Player QP Ship Missile Tunnel Mine[n]
(1) (2) (3) (4) TIME_TICK TIME_TICK (5) SHIP_IMG(x,y,bmp) MINE_IMG SHIP_IMG (6) (7) (8) (9) PLAYER_TRIGGER (10) (11) MISSILE_FIRE(x,y)
(12) TIME_TICK TIME_TICK SHIP_IMG MISSILE_IMG MISSILE_IMG DESTROYED_ HIT_MINE(type) MINE(score) (15) (14) (13)
(16) PLAYER_SHIP_MOVE(x,y)
TIME_TICK TIME_TICK SHIP_IMG SHIP_IMG HIT_WALL (18) (17)
Figure 1.4: The sequence diagram of the “Fly ‘n’ Shoot” game.
(1) The TIME_TICK is the most important event in the game. This event is generated by the QF framework from the system time tick interrupt at a rate of 30 times per second, which is needed to drive a smooth animation of the display. Because the TIME_TICK event is of interest to virtually all objects in the application, it is published by the framework to all active objects. (The publish-subscribe event delivery in QF is described in Chapter 6.) (2) Upon reception of the TIME_TICK event, the Ship object advances its position by one step and posts the event SHIP_IMG(x, y, bmp) to the Tunnel object. The www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 19
SHIP_IMG event has parameters x and y, which are the coordinates of the Ship on the display, as well as the bitmap number bmp to draw at these coordinates. (3) The Missile object is not in flight yet, so it simply ignores the TIME_TICK event this time. (4) The Tunnel object performs the heaviest lifting for the TIME_TICK event. First, Tunnel redraws the entire display from the current frame buffer. This action, performed 30 times per second, provides the illusion of animation of the display. Next, the Tunnel clears the frame buffer and starts filling it up again for the next time frame. The Tunnel advances the tunnel walls by one step and copies the walls to the frame buffer. The Tunnel also dispatches the TIME_TICK event to all its Mine state machine components. (5) Each Mine advances its position by one step and posts the MINE_IMG(x, y, bmp) event to the Tunnel to render the appropriate Mine bitmap at the position (x, y)in the current frame buffer. Mines of type 1 send the bitmap number MINE1_BMP, whereas mines of type 2 send MINE2_BMP. (6) Upon receipt of the SHIP_IMG(x, y, bmp) event from the Ship, the Tunnel object renders the specified bitmap in the frame buffer and checks for any collision between the ship bitmap and the tunnel walls. Tunnel also dispatches the original SHIP_IMG(x, y, bmp) event to all active Mines. (7) Each Mine determines whether the Ship is in collision with that Mine. (8) The PLAYER_TRIGGER event is generated when the Player reliably presses the button (button press is debounced). This event is published by the QF framework and is delivered to the Ship and Tunnel objects, which both subscribe to the PLAYER_TRIGGER event. (9) Ship generates the MISSILE_FIRE(x, y) event to the Missile object. The parameters of this event are the current (x, y) coordinates of the Ship, which are the starting point for the Missile. (10) Tunnel receives the published PLAYER_TRIGGER event as well because Tunnel occasionally needs to start the game or terminate the screen saver mode based on this stimulus. (11) Missile reacts to the MISSILE_FIRE(x, y) event by starting to fly, whereas it sets its initial position from the (x, y) event parameters delivered from the Ship.
www.newnespress.com 20 Chapter 1
(12) This time around, the TIME_TICK event arrives while Missile is in flight. Missile posts the MISSILE_IMG(x, y, bmp) event to the Tunnel. (13) Tunnel renders the Missile bitmap in the current frame buffer and dispatches the MISSILE_IMG(x, y, bmp) event to all the Mines to let the Mines test for the collision with the Missile. This determination depends on the type of the Mine. In this scenario a particular Mine[n] object detects a hit and posts the DESTROYED_MINE(score) (score) event to the Missile. The Mine provides the score earned for destroying this particular mine as the parameter of this event. (14) Missile handles the HIT_MINE(score) event by becoming immediately ready to launch again and lets the Mine do the exploding. Because I decided to make the Ship responsible for the scorekeeping, the Missile also generates the DESTROYED_MINE (score) event to the Ship, to report the score for destroying the Mine. (15) Upon reception of the DESTROYED_MINE(score) event, the Ship increments the score by the value received from the Missile. (16) The Ship object handles the PLAYER_SHIP_MOVE(x, y) event by updating its position from the event parameters. (17) When the Tunnel object handles the SHIP_IMG(x, y, bmp_id) event next time around, it detects a collision between the Ship and the tunnel wall. In that case it posts the event HIT_WALL to the Ship. (18) The Ship responds to the HIT_WALL event by transitioning to the “exploding” state. Even though the sequence diagram in Figure 1.4 shows merely some selected scenarios of the “Fly ‘n’ Shoot” game, I hope that the explanations give you a big picture of how the application works. More important, you should start getting the general idea about the thinking process that goes into designing an event-driven system with active objects and events. 1.5 Active Objects in the “Fly ‘n’ Shoot” Game I hope that the analysis of the sequence diagram in Figure 1.4 makes it clear that actions performed by an active object depend as much on the internal mode of the object as on the events it receives. For example, the Missile active object handles the TIME_TICK event very differently when the Missile is in flight (Figure 1.4(12)) compared to the time when it is not (Figure 1.4(3)). www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 21
The best-known mechanism for handling such modal behavior is through state machines because a state machine makes the behavior explicitly dependent on both the event and the state of an object. Chapter 2 introduces UML state machine concepts more thoroughly. In this section, I give a cursory explanation of the state machines associated with each object in the “Fly ‘n’ Shoot” game.
1.5.1 The Missile Active Object I start with the Missile state machine shown in Figure 1.5 because it turns out to be the simplest one. The explanation section immediately following the diagram illuminates the interesting points.
NOTE A UML state diagram like Figure 1.5 preserves the general form of the traditional state tran- sition diagrams, where states are represented as nodes and transitions as arcs connecting the nodes. In the UML notation the state nodes are represented as rectangles with rounded cor- ners. The name of the state appears in bold type in the name compartment at the top of the state. Optionally, right below the name, a state can have an internal transition compart- ment separated from the name by a horizontal line. The internal transition compartment can contain entry actions (actions following the reserved symbol “entry”), exit actions (actions following the reserved symbol “exit”), and other internal transitions (e.g., those trig- gered by TIME_TICK in Figure 1.5(3)). State transitions are represented as arrows originating at the boundary of the source state and pointing to the boundary of the target state. At a min- imum, a transition must be labeled with the triggering event. Optionally, the trigger can be followed by event parameters, a guard, and a list of actions.
(1) The state transition originating at the black ball is called the initial transition. Such transition designates the first active state after the state machine object is created. An initial transition can have associated actions, which in the UML notation are enlisted after the forward slash ( / ). In this particular case, the Missile state machine starts in the “armed” state and the actions executed upon the initialization consist of subscribing to the event TIME_TICK. Subscribing to an event means that the framework will deliver the specified event to the Missile active object every time the event is published to the framework. Chapter 7 describes the implementation of the publish-subscribe event delivery in QF.
www.newnespress.com 22 Chapter 1
(1) armed / QActive_subscribe(me, TIME_TICK);
MISSILE_FIRE(x, y) / me->x = e->x; (2) me->y = e->y; flying TIME_TICK [me->x + GAME_MISSILE_SPEED_X (4) < GAME_SCREEN_WIDTH] / TIME_TICK [else] me->x += GAME_MISSILE_SPEED_X; (3) QActive_postFIFO(Tunnel, DESTROYED_MINE(score) / (5) MISSILE_IMG(me->x, me->y, QActive_postFIFO(Ship, MISSILE_BMP)); DESTROYED_MINE(e->score)); HIT_WALL
exploding (6) entry / me->exp_ctr = 0; (7)
TIME_TICK [(me->x >= GAME_SPEED_X) && (me->exp_ctr < 16)] / (8) me->x -= GAME_SPEED_X; ++me->exp_ctr; QActive_postFIFO(Tunnel, (9) EXPLOSION_IMG(me->x + 3, me->y -4, TIME_TICK [else] EXPLOSION0_BMP + (me->exp_ctr >> 2)));
Figure 1.5: Missile state machine diagram.
(2) The arrow labeled with the MISSILE_FIRE(x, y) event denotes a state transition, that is, a change of state from “armed” to “flying.” The MISSILE_FIRE(x, y) event is generated by the Ship object when the Player triggers the Missile (see the sequence diagram in Figure 1.4). In the MISSILE_FIRE event, Ship provides Missile with the initial coordinates in the event parameters (x, y).
NOTE The UML intentionally does not specify the notation for actions. In practice, the actions are often written in the programming language used for coding the particular state machine. In all state diagrams in this book, I assume the C programming language. Furthermore, in the C expressions I refer to the data members associated with the state machine object through the “me->” prefix and to the event parameters through the “e->” prefix. For example, the action “me->x = e->x;” means that the internal data member x of the Missile active object is assigned the value of the event parameter x. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 23
(3) The event name TIME_TICK enlisted in the compartment below the state name denotes an internal transition. Internal transitions are simple reactions to events performed without a change of state. An internal transition, as well as a regular transition, can have a guard condition, enclosed in square brackets. Guard condition is a Boolean expression evaluated at runtime. If the guard evaluates to TRUE, the transition is taken. Otherwise, the transition is not taken and no actions enlisted after the forward slash ( / ) are executed. In this particular case, the guard condition checks whether the x-coordinate propagated by the Missile speed is still visible on the screen. If so, the actions are executed. These actions include propagation of the Missile position by one step and posting the MISSILE_IMG event with the current Missile position and the MISSILE_BMP bitmap number to the Tunnel active object. Direct event posting to an active object is accomplished by the QF function QActive_postFIFO(), which I discuss in Chapter 7. (4) The same event TIME_TICK with the [else] guard denotes a regular state transition with the guard condition complementary to the other occurrence of the TIME_TICK event in the same state. In this case, the TIME_TICK transition to “armed” is taken if the Missile object flies out of the screen. (5) The event HIT_MINE(score) triggers another transition to the “armed” state. The action associated with this transition posts the DESTROYED_MINE event with the parameter e->score to the Ship object, to report destroying the mine. (6) The event HIT_WALL triggers a transition to the “exploding” state, with the purpose of animating the explosion bitmaps on the display. (7) The label “entry” denotes the entry action to be executed unconditionally upon the entry to the “exploding” state. This action consists of clearing the explosion counter (me->exp_ctr) member of the Missile object. (8) The TIME_TICK internal transition is guarded by the condition that the explosion does not scroll off the screen and that the explosion counter is lower than 16. The actions executed include propagation of the explosion position and posting the EXPLOSION_IMG event to the Tunnel active object. Please note that the bitmap of the explosion changes as the explosion counter gets bigger. (9) The TIME_TICK regular transition with the complementary guard changes the state back to the “armed” state. This transition is taken after the animation of the explosion completes.
www.newnespress.com 24 Chapter 1
1.5.2 The Ship Active Object The state machine of the Ship active object is shown in Figure 1.6. This state machine introduces the profound concept of hierarchical state nesting. The power of state nesting derives from the fact that it is designed to eliminate repetitions that otherwise would have to occur. One of the main responsibilities of the Ship active object is to maintain the current position of the Ship. On the original EV-LM3S811 board, this position is determined by the potentiometer wheel (see Figure 1.2). The PLAYER_SHIP_MOVE(x, y) event is generated whenever the wheel position changes, as shown in the sequence diagram (Figure 1.4). The Ship object must always keep track of the wheel position, which means that all states of the Ship state machine must handle the PLAYER_SHIP_MOVE(x, y) event. In the traditional finite state machine (FSM) formalism, you would need to repeat the Ship position update from the PLAYER_SHIP_MOVE(x, y) event in every state. But such repetitions would bloat the state machine and, more important, would represent multiple points of maintenance both in the diagram and the code. Such repetitions go against the DRY (Don’t Repeat Yourself) principle, which is vital for flexible and maintainable code [Hunt+ 00]. Hierarchical state nesting remedies the problem. Consider the state “active” that surrounds all other states in Figure 1.6. The high-level “active” state is called the superstate and is abstract in that the state machine cannot be in this state directly but only in one of the states nested within, which are called the substates of “active.” The UML semantics associated with state nesting prescribe that any event is first handled in the context of the currently active substate. If the substate cannot handle the event, the state machine attempts to handle the event in the context of the next-level superstate. Of course, state nesting in UML is not limited to just one level and the simple rule of processing events applies recursively to any level of nesting. Specifically to the Ship state machine diagram shown in Figure 1.6, suppose that the event PLAYER_SHIP_MOVE(x, y) arrives when the state machine is in the “parked” state. The “parked” state does not handle the PLAYER_SHIP_MOVE(x, y) event. In the traditional finite state machine this would be the end of the story—the PLAYER_SHIP_MOVE(x, y) event would be silently discarded. However, the state machine in Figure 1.6 has another layer of the “active” superstate. Per the semantics of state nesting, this higher-level superstate handles the PLAYER_SHIP_MOVE(x, y) event, which is exactly what’s needed. The same exact reasoning applies for any other substate of the “active” superstate, such as “flying” www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 25 or “exploding,” because none of these substates handle the PLAYER_SHIP_MOVE(x, y) event. Instead, the “active” superstate handles the event in one single place, without repetitions.
/ QActive_subscribe(me, TIME_TICK); QActive_subscribe(me, PLAYER_TRIGGER); (1) active PLAYER_SHIP_MOVE(x, y) / (3) me->x = e->x; me->y = e->y; (2) parked TAKE_OFF (4) flying entry / (5) me->score = 0; QActive_postFIFO(Tunnel, SCORE(me->score));
TIME_TICK / (6) QActive_postFIFO(Tunnel, SHIP_IMG(me->x, me->y, SHIP_BMP)); ++me->score; if ((me->score % 10) == 0) QActive_postFIFO(Tunnel, SCORE(me->score)); HIT_WALL (9) PLAYER_TRIGGER / (7) QActive_postFIFO(Missile, MISSLE_FIRE(me->x, me->y)); HIT_MINE(type) DESTROYED_MINE(score) / (8) (10) me->score += e->score;
exploding (11) entry / me->exp_ctr = 0;
TIME_TICK [me->exp_ctr < 16] / TIME_TICK [else] / ++me->exp_ctr; QActive_postFIFO(Tunnel,(13) QActive_postFIFO(Tunnel, GAME_OVER(me->score)); EXPLOSION(me->x, me->y + SHIP_HEIGHT -1, EXPLOSION0_BMP + (me->exp_ctr >> 2))); (12)
Figure 1.6: Ship state machine diagram.
(1) Upon the initial transition, the Ship state machine enters the “active” superstate and subscribes to events TIME_TICK and PLAYER_TRIGGER. (2) At each level of nesting, a superstate can have a private initial transition that designates the active substate after the superstate is entered directly. Here the
www.newnespress.com 26 Chapter 1
initial transition of state “active” designates the substate “parked” as the initial active substate. (3) The “active” superstate handles the PLAYER_SHIP_MOVE(x, y) event as an internal transition in which it updates the internal data members me->x and me->y from the event parameters e->x and e->y, respectively. (4) The TAKE_OFF event triggers transition to “flying.” This event is generated by the Tunnel object when the Player starts the game (see the description of the game in Section 1.2). (5) The entry actions to “flying” include clearing the me->score data member and posting the event SCORE with the event parameter me->score to the Tunnel active object. (6) The TIME_TICK internal transition causes posting the event SHIP_IMG with current Ship position and the SHIP_BMP bitmap number to the Tunnel active object. Additionally, the score is incremented for surviving another time tick. Finally, when the score is “round” (divisible by 10) it is also posted to the Tunnel active object. This decimation of the SCORE event is performed just to reduce the bandwidth of the communication, because the Tunnel active object only needs to give an approximation of the running score tally to the user. (7) The PLAYER_TRIGGER internal transition causes posting the event MISSILE_FIRE with current Ship position to the Missile active object. The parameters (me->x, me->y) provide the Missile with the initial position from the Ship. (8) The DESTROYED_MINE(score) internal transition causes update of the score kept by the Ship. The score is not posted to the Tunnel at this point, because the next TIME_TICK will send the “rounded” score, which is good enough for giving the Player the score approximation. (9) The HIT_WALL event triggers transition to “exploding.” (10) The HIT_MINE(type) event also triggers transition to “exploding.” (11) The “exploding” state of the Ship state machine is very similar to the “exploding” state of Missile (see Figure 1.5(7-9)). (12) The TIME_TICK[else] transition is taken when the Ship finishes exploding. Upon this transition, the Ship object posts the event GAME_OVER(me->score) to the Tunnel active object to terminate the game and display the final score to the Player. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 27
1.5.3 The Tunnel Active Object The Tunnel active object has the most complex state machine, which is shown in Figure 1.7. Unlike the previous state diagrams, the diagram in Figure 1.7 shows only the high level of abstraction and omits a lot of details such as most entry/exit actions, internal transitions, guard conditions, or actions on transitions. Such a “zoomed out” view is always legal in the UML because UML allows you to choose the level of detail that you want to include in your diagram. The Tunnel state machine uses state hierarchy more extensively than the Ship state machine in Figure 1.6. The explanation section immediately following Figure 1.7 illuminates the new uses of state nesting as well as the new elements not explained yet in the other state diagrams.
active MINE_DISABLED(mine_id) / (4) me->mines[e->mine_id] = NULL; (1) demo entry / QTimeEvt_postIn(&me->screenTimeEvt, me, BSP_TICKS_PER_SEC*20); (5) SCREEN_TIMEOUT (7) exit / QTimeEvt_disarm(&me->screenTimeEvt); (6) (2) PLAYER_TRIGGER playing
GAME_OVER game_over
SCREEN_TIMEOUT
screen_saver
screen_saver_n_pixels
SCREEN_TIMEOUT screen_saver_1_pixel (8) PLAYER_TRIGGER
(3) PLAYER_QUIT
Figure 1.7: Tunnel state machine diagram.
www.newnespress.com 28 Chapter 1
(1) An initial transition can target a substate at any level of state hierarchy, not necessarily just the next-lower level. Here the topmost initial transition goes down two levels to the substate “demo.” (2) The superstate “active” handles the PLAYER_QUIT event as a transition to the final state (see explanation of element (3)). Please note that the PLAYER_QUIT transition applies to all substates directly or transitively nested in the “active” superstate. Because a state transition always involves execution of all exit actions from the states, the high-level PLAYER_QUIT transition guarantees the proper cleanup that is specific to the current state context, whichever substate happens to be active at the time when the PLAYER_QUIT event arrives. (3) The final state is indicated in the UML notation as the bull’s-eye symbol and typically indicates destruction of the state machine object. In this case, the PLAYER_QUIT event indicates termination of the game. (4) The MINE_DISABLED(mine_id) event is handled at the high level of the “active” state, which means that this internal transition applies to the whole sub- machine nested inside the “active” superstate. (See also the discussion of the Mine object in the next section.) (5) The entry action to the “demo” state starts the screen time event (timer) me->screenTimeEvt to expire in 20 seconds. Time events are allocated by the application, but they are managed by the QF framework. QF provides functions to arm a time event, such as QTimeEvt_postIn() for one-shot timeout, and QTimeEvt_postEvery() forperiodictimeevents.Armingatimeeventisineffect telling the QF framework, for instance, “Give me a nudge in 20 seconds.” QF then posts the time event (the event me->screenTimeEvt in this case) to the active object after the requested number of clock ticks. Chapters 6 and 7 talk about time events in detail. (6) The exit action from the “demo” state disarms the me->screenTimeEvt time event. This cleanup is necessary when the state can be exited by a different event than the time event, such as the PLAYER_TRIGGER transition. (7) The SCREEN_TIMEOUT transition to “screen_saver” is triggered by the expiration of the me->screenTimeEvt time event. The signal SCREEN_TIMEOUT is assigned to this time event upon initialization and cannot be changed later. (8) The transition triggered by PLAYER_TRIGGER applies equally to the two substates of the “screen_saver” superstate. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 29
1.5.4 The Mine Components Mines are also modeled as hierarchical state machines, but are not active objects. Instead, Mines are components of the Tunnel active object and share its event queue and priority level. The Tunnel active object communicates with the Mine components synchronously by directly dispatching events to them via the function QHsm_dispatch(). Mines communicate with Tunnel and all other active objects asynchronously by posting events to their event queues via the function QActive_postFIFO().
NOTE Active objects exchange events asynchronously, meaning that the sender of the event merely posts the event to the event queue of the recipient active object without waiting for the com- pletion of the event processing. In contrast, synchronous event processing corresponds to a function call (e.g., QHsm_dispatch()), which processes the event in the caller’s thread of execution.
As shown in Figure 1.8, Tunnel maintains the data member mines[], which is an array of pointers to hierarchical state machines (QHsm *). Each of these pointers can point either to a Mine1 object, a Mine2 object, or NULL, if the entry is unused. Note that Tunnel “knows” the Mines only as generic state machines (pointers to the QHsm structure defined in QP). Tunnel dispatches events to Mines uniformly, without differentiating between different types of Mines. Still, each Mine state machine handles the events in its specific way. For example, Mine type 2 checks for collision with the Missile differently than with the Ship, whereas Mine type 1 handles both identically.
Tunnel
Mine1 mines1[] QHsm *mines[] Mine2 mines2[]
[0] [0] [0]
[1] [1] [1]
[2] [2] NULL [2]
[3] [3] [3]
[4] [4] NULL [4]
Figure 1.8: The Table active object manages two types of Mines.
www.newnespress.com 30 Chapter 1
NOTE The last point is actually very interesting. Dispatching the same event to different Mine objects results in different behavior, specific to the type of the Mine, which in OOP is known as polymorphism. I’ll have more to say about this in Chapter 3.
Each Mine object is fairly autonomous. The Mine maintains its own position and is responsible for informing the Tunnel object whenever the Mine gets destroyed or scrolls out of the display. This information is vital for the Tunnel object so that it can keep track of the unused Mines. Figure 1.9 shows a hierarchical state machine of Mine2 state machine. Mine1 is very similar, except that it uses the same bitmap for testing collisions with the Missile and the Ship. (1) The Mine starts in the “unused” state. (2) The Tunnel object plants a Mine by dispatching the MINE_PLANT(x, y) event to the Mine. The Tunnel provides the (x, y) coordinates as the original position of the Mine. (3) When the Mine scrolls off the display, the state machine transitions to “unused.” (4) When the Mine hits the Ship, the state machine transitions to “unused.” (5) When the Mine finishes exploding, the state machine transitions to “unused.” (6) When the Mine is recycled by the Tunnel object, the state machine transitions to “unused.” (7) The exit action in the “used” state posts the MINE_DISABLDED(mine_id) event to the Tunnel active object. Through this event, the Mine informs the Tunnel that it’s becoming disabled, so that Tunnel can update its mines[] array (see also Figure 1.7(4)). The mine_id parameter of the event becomes the index into the mines[] array. Note that generating the MINE_DISABLDED(mine_id) event in the exit action from “used” is much safer and more maintainable than repeating this action in each individual transition (3), (4), (5), and (6).
www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 31
unused (1) (6) MINE_RECYCLE
(2) MINE_PLANT(x, y) / used me->x = e->x; exit / (7) me->y = e->y; QActive_postFIFO(Tunnel, MINE_DISABLED(MINE_ID(me)));
planted
TIME_TICK [me->x >= GAME_SPEED_X] (3) / TIME_TICK [else] me->x -= GAME_SPEED_X; postFIFO(Tunnel, SHIP_IMG [do_bitmaps_overlap( MISSILE_IMG(me->x, me->y, MINE2_BMP, (4) MINE2_BMP)); me->x, me->y, e->bmp, e->x, e->y)] / postFIFO(Ship, HIT_MINE(2));
MISSILE_IMG [do_bitmaps_overlap( MINE2_MISSILE_BMP, me->x, me->y,` e->bmp, e->x, e->y)] / postFIFO(Missile, DESTROYED_MINE(45));
exploding entry / me->exp_ctr = 0;
TIME_TICK [(me->x >= GAME_SPEED_X) && (me->exp_ctr < 16)] / me->x -= GAME_SPEED_X; ++me->exp_ctr; postFIFO(Tunnel, EXPLOSION(me->x + 3, me->y -4, (5) EXPLOSION0_BMP + (me->exp_ctr >> 2))); TIME_TICK [else]
Figure 1.9: Mine2 state machine diagram.
www.newnespress.com 32 Chapter 1
1.6 Events in the “Fly ‘n’ Shoot” Game The key events in the “Fly ‘n’ Shoot” game have been identified in the sequence diagram in Figure 1.4. Other events have been invented during the state machine design stage. In any case, you must have noticed that events consist really of two parts. The part of the event called the signal conveys the type of the occurrence (what happened). For example, the TIME_TICK signal conveys the arrival of a time tick, whereas the PLAYER_SHIP_MOVE signal conveys that the player wants to move the Ship. An event can also contain additional quantitative information about the occurrence in form of event parameters. For example, the PLAYER_SHIP_MOVE signal is accompanied by the parameters (x, y) that contain the quantitative information as to where exactly to move the Ship. In QP, events are represented as instances of the QEvent structure provided by the framework. Specifically, the QEvent structure contains the member sig, to represent the signal of that event. Event parameters are added in the process of inheritance, as described in the sidebar “Single Inheritance in C.”
SINGLE INHERITANCE IN C Inheritance is the ability to derive new structures based on existing structures in order to reuse and organize code. You can implement single inheritance in C very simply by literally embedding the base structure as the first member of the derived structure. For example, Figur e 1.10(A) show s the structure ScoreEvt derived from the base struct ure QEvent by embedding the QEvent instance as the first member of ScoreEvt. To make this idiom better stand out, I always name the base structure member super.
me typedef struct QEventTag { Instance of the QEvent QSignal sig; base struct sig : QSignal . . . super } QEvent; typedef struct ScoreEvtTag { Members QEvent super; added in ScoreEvt uint16_t score; the derived } ScoreEvt; struct score : uint16_t
ABC
Figure 1.10: (A) Derivation of structures in C, (B) memory alignment, and (C) the UML class diagram.
www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 33
As shown in Figure 1.10(B), such nesting of structures always aligns the data member super at the beginning of every instance of the derived structure, which is actually guaranteed by the C standard. Specifically, WG14/N1124 Section 6.7.2.1.13 says: “... A pointer to a struc- ture object, suitably converted, points to its initial member. There may be unnamed padding within a structure object, but not at its beginning” [ISO/IEC 9899:TC2]. The alignment lets you treat a pointer to the derived ScoreEvt structure as a pointer to the QEvent base struc- ture. All this is legal, portable, and guaranteed by the C standard. Consequently, you can always safely pass a pointer to ScoreEvt to any C function that expects a pointer to QEvent. (To be strictly correct in C, you should explicitly cast this pointer. In OOP such casting is called upcasting and is always safe.) Therefore, all functions designed for the QEvent structure are automatically available to the ScoreEvt structure as well as other structures derived from QEvent. Figure 1.10(C) shows the UML class diagram depicting the inheritance relationship between ScoreEvt and QEvent structures. QP uses single inheritance quite extensively not just for derivation of events with parameters, but also for derivation of state machines and active objects. Of course, the C++ version of QP uses the native C++ support for class inheritance rather than “derivation of structures.” You’ll see more examples of inheritance later in this chapter and throughout the book.
Because events are explicitly shared among most of the application components, it is convenient to declare them in the separate header file game.h shown in Listing 1.2. The explanation section immediately following the listing illuminates the interesting points.
Listing 1.2 Signals, event structures, and active object interfaces defined in file game.h
(1) enum GameSignals { /* signals used in the game */ (2) TIME_TICK_SIG = Q_USER_SIG, /* published from tick ISR */ PLAYER_TRIGGER_SIG, /* published by Player (ISR) to trigger the Missile */ PLAYER_QUIT_SIG, /* published by Player (ISR) to quit the game */ GAME_OVER_SIG, /* published by Ship when it finishes exploding */ /* insert other published signals here ... */ (3) MAX_PUB_SIG, /* the last published signal */
PLAYER_SHIP_MOVE_SIG, /* posted by Player (ISR) to the Ship to move it */ BLINK_TIMEOUT_SIG, /* signal for Tunnel’s blink timeout event */ SCREEN_TIMEOUT_SIG, /* signal for Tunnel’s screen timeout event */ TAKE_OFF_SIG, /* from Tunnel to Ship to grant permission to take off */ HIT_WALL_SIG, /* from Tunnel to Ship when Ship hits the wall */ HIT_MINE_SIG, /* from Mine to Ship or Missile when it hits the mine */ SHIP_IMG_SIG, /* from Ship to the Tunnel to draw and check for hits */ MISSILE_IMG_SIG, /* from Missile to the Tunnel to draw and check for hits */ MINE_IMG_SIG, /* sent by Mine to the Tunnel to draw the mine */
Continued onto next page
www.newnespress.com 34 Chapter 1
MISSILE_FIRE_SIG, /* sent by Ship to the Missile to fire */ DESTROYED_MINE_SIG, /* from Missile to Ship when Missile destroyed Mine */ EXPLOSION_SIG, /* from any exploding object to render the explosion */ MINE_PLANT_SIG, /* from Tunnel to the Mine to plant it */ MINE_DISABLED_SIG, /* from Mine to Tunnel when it becomes disabled */ MINE_RECYCLE_SIG, /* sent by Tunnel to Mine to recycle the mine */ SCORE_SIG, /* from Ship to Tunnel to adjust game level based on score */ /* insert other signals here ... */ (4) MAX_SIG /* the last signal (keep always last) */ };
(5) typedef struct ObjectPosEvtTag { (6) QEvent super; /* extend the QEvent class */ (7) uint8_t x; /* the x-position of the object */ (8) uint8_t y; /* new y-position of the object */ } ObjectPosEvt;
typedef struct ObjectImageEvtTag { QEvent super; /* extend the QEvent class */ uint8_t x; /* the x-position of the object */ int8_t y; /* the y-position of the object */ uint8_t bmp; /* the bitmap ID representing the object */ } ObjectImageEvt;
typedef struct MineEvtTag { QEvent super; /* extend the QEvent class */ uint8_t id; /* the ID of the Mine */ } MineEvt;
typedef struct ScoreEvtTag { QEvent super; /* extend the QEvent class */ uint16_t score; /* the current score */ } ScoreEvt;
/* opaque pointers to active objects in the application */ (9) extern QActive * const AO_Tunnel; (10) extern QActive * const AO_Ship; (11) extern QActive * const AO_Missile;
/* active objects’ "constructors" */ (12) void Tunnel_ctor(void); (13) void Ship_ctor(void); (14) void Missile_ctor(void);
(1) In QP, signals of events are simply enumerated constants. Placing all signals in a single enumeration is particularly convenient to avoid inadvertent overlap in the numerical values of different signals. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 35
(2) The application-level signals do not start from zero but rather are offset by the constant Q_USER_SIG. This is because QP reserves the lowest few signals for the internal use and provides the constant Q_USER_SIG as an offset from which user-level signals can start. Also note that by convention, I attach the suffix _SIG to all signals so that I can easily distinguish signals from other constants. I drop the suffix _SIG in the state diagrams to reduce the clutter. (3) The constant MAX_PUB_SIG delimits the published signals from the rest. The publish-subscribe event delivery mechanism consumes some RAM, which is proportional to the number of published signals. I save some RAM by providing the lower limit of published signals to QP (MAX_PUB_SIG) rather than the maximum of all signals used in the application. (See also Listing 1.1(9)). (4) The last enumeration MAX_SIG indicates the maximum of all signals used in the application. (5) The event structure ObjectPosEvt defines a “class” of events that convey the object’s position on the display in the event parameters. (6) The structure ObjectPosEvt derives from the base structure QEvent, as explained in the sidebar “Single Inheritance in C.” (7,8) The structure ObjectPosEvt adds parameters x and y, which are coordinates of the object on the display.
NOTE Throughout this book I use the following standard exact-width integer types (WG14/N843 C99 Standard, Section 7.18.1.1) [ISO/IEC 9899:TC2]:
Exact Size Unsigned Signed 8-bits uint8_t int8_t 16-bits uint16_t int16_t 32-bits uint32_t int32_t
If your (pre-standard) compiler does not provide the
www.newnespress.com 36 Chapter 1
(9-11) These global pointers represent active objects in the application and are used for posting events directly to active objects. Because the pointers can be initialized at compile time, I like to declare them const, so that they can be placed in ROM. The active object pointers are “opaque” because they cannot access the whole active object, only the part inherited from the QActive structure. I’ll have more to say about this in the next section. (12-14) These functions perform an early initialization of the active objects in the system. They play the role of static “constructors,” which in C you need to call explicitly, typically at the beginning of main(). (See also Listing 1.1 (10-12).)
1.6.1 Generating, Posting, and Publishing Events The QF framework supports two types of asynchronous event exchange: 1. The simple mechanism of direct event posting supported through the functions QActive_postFIFO() and QActive_postLIFO(), where the producer of an event directly posts the event to the event queue of the consumer active object. 2. A more sophisticated publish-subscribe event delivery mechanism supported through the functions QF_publish() and QActive_subscribe(), where the producers of the events “publish” them to the framework, and the framework then delivers the events to all active objects that had “subscribed” to these events. In QF, any part of the system, not necessarily only the active objects, can produce events. For example, interrupt service routines (ISRs) or device drivers can also produce events. On the other hand, only active objects can consume events, because only active objects have event queues.
NOTE
QF also provides “raw” thread-safe event queues (struct QEQueue), which can consume events as well. These “raw” thread-safe queues cannot block and are intended to deliver events to ISRs or device drivers. Refer to Chapter 7 for more details.
The most important characteristic of event management in QF is that the framework passes around only pointers to events, not the events themselves. QF never copies the www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 37 events by value (“zero-copy” policy); even in case of publishing events that often involves multicasting the same event to multiple subscribers. The actual event instances are either constant events statically allocated at compile time or dynamic events allocated at runtime from one of the event pools that the framework manages. Listing 1.3 provides examples of publishing static events and posting dynamic events from the ISRs of the “Fly ‘n’ Shoot” version for the Stellaris board (file
Listing 1.3 Generating, posting, and publishing events from the ISRs in bsp.c for the Stellaris board
(1) void ISR_SysTick(void) { (2) static QEvent const tickEvt = { TIME_TICK_SIG, 0 }; (3) QF_publish(&tickEvt); /* publish the tick event to all subscribers */ (4) QF_tick(); /* process all armed time events */ } /*...... */ (5) void ISR_ADC(void) { static uint32_t adcLPS = 0; /* Low-Pass-Filtered ADC reading */ static uint32_t wheel = 0; /* the last wheel position */ unsigned long tmp;
ADCIntClear(ADC_BASE, 3); /* clear the ADC interrupt */ (6) ADCSequenceDataGet(ADC_BASE, 3, &tmp); /* read the data from the ADC */
/* 1st order low-pass filter: time constant ~= 2^n samples * TF = (1/2^n)/(z-((2^n - 1)/2^n)), * e.g., n=3, y(k+1) = y(k) - y(k)/8 + x(k)/8 => y += (x - y)/8 */ (7) adcLPS += (((int)tmp - (int)adcLPS + 4) >> 3); /* Low-Pass-Filter */
/* compute the next position of the wheel */ (8) tmp = (((1 << 10) - adcLPS)*(BSP_SCREEN_HEIGHT - 2)) >> 10;
if (tmp != wheel) { /* did the wheel position change? */ (9) ObjectPosEvt *ope = Q_NEW(ObjectPosEvt, PLAYER_SHIP_MOVE_SIG); (10) ope->x = (uint8_t)GAME_SHIP_X; /* x-position is fixed */ (11) ope->y = (uint8_t)tmp; (12) QActive_postFIFO(AO_ship, (QEvent *)ope); /* post to the Ship AO */ wheel = tmp; /* save the last position of the wheel */ } ... }
www.newnespress.com 38 Chapter 1
(1) In the case of the Stellaris board, the function ISR_SysTick() services the system clock tick ISR generated by the Cortex-M3 system tick timer. (2) The TIME_TICK event never changes, so it can be statically allocated just once. This event is declared as const, which means that it can be placed in ROM. The initializer list for this event consists of the signal TIME_TICK_SIG followed by zero. This zero informs the QF framework that this event is static and should never be recycled to an event pool. (3) The ISR calls the framework function QF_publish(), which takes the pointer to the tickEvt event to deliver to all subscribers. (4) The ISR calls the function QF_tick(), in which the framework manages the armed time events. (5) The function ISR_ADC() services the ADC conversions, which ultimately deliver the position of the Ship. (6) The ISR reads the data from the ADC. (7,8) A low-pass filter is applied to the raw ADC reading and the potentiometer wheel position is computed. (9) The QF macro Q_NEW(ObjectPosEvt, PLAYER_SHIP_MOVE_SIG) dynamically allocates an instance of the ObjectPosEvt event from an event pool managed by QF. The macro also performs the association between the signal PLAYER_SHIP_MOVE_SIG and the allocated event. The Q_NEW() macro returns the pointer to the allocated event.
NOTE
The PLAYER_SHIP_MOVE(x, y) event is an example of an event with changing parameters. In general, such an event cannot be allocated statically (like the TIME_TICK event at label (2)) because it can change asynchronously next time the ISR executes. Some active objects in the system might still be referring to the event via a pointer, so the event should not be changing. Dynamic event allocation of QF solves all such concurrency issues because every time a new event is allocated. QF then recycles the dynamic events after it determines that all active objects are done with accessing the events.
(10,11) The x and y parameters of the event are assigned. (12) The dynamic event is posted directly to the Ship active object. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 39
1.7 Coding Hierarchical State Machines Contrary to widespread misconceptions, you don’t need big design automation tools to translate hierarchical state machines (UML statecharts) into efficient and highly maintainable C or C++. This section explains how to hand-code the Ship state machine from Figure 1.6 with the help of the QF real-time framework and the QEP hierarchical processor, which is also part of the QP event-driven platform. Once you know how to code this state machine, you know how to code them all. The source code for the Ship state machine is found in the file ship.c located either in the DOS version or the Stellaris version of the “Fly ‘n’ Shoot” game. I break the explanation of this file into three steps.
1.7.1 Step 1: Defining the Ship Structure In the first step you define the Ship data structure. Just as in the case of events, you use inheritance to derive the Ship structure from the framework structure QActive (see the sidebar “Single Inheritance in C”). Creating this inheritance relationship ties the Ship structure to the QF framework. The main responsibility of the QActive base structure is to store the information about the current active state of the state machine as well as the event queue and priority level of the Ship active object. In fact, QActive itself derives from a simpler QEP structure QHsm that represents just the current active state of a hierarchical state machine. On top of that information, almost every state machine must also store other “extended-state” information. For example, the Ship object is responsible for maintaining the Ship position as well as the score accumulated in the game. You supply this additional information by means of data members enlisted after the base structure member super, as shown in Listing 1.4.
Listing 1.4 Deriving the Ship structure in file ship.c
(1) #include "qp_port.h" /* the QP port */ (2) #include "bsp.h" /* Board Support Package */ (3) #include "game.h" /* this application */ /* local objects ------*/ (4) typedef struct ShipTag { (5) QActive super; /* derive from the QActive struct */ (6) uint8_t x; /* x-coordinate of the Ship position on the display */ (7) uint8_t y; /* y-coordinate of the Ship position on the display */ (8) uint8_t exp_ctr; /* explosion counter, used to animate explosions */
Continued onto next page
www.newnespress.com 40 Chapter 1
(9) uint16_t score; /* running score of the game */ (10) } Ship; /* the typedef-ed name for the Ship struct */
/* state handler functions... */ (11) static QState Ship_active (Ship *me, QEvent const *e); (12) static QState Ship_parked (Ship *me, QEvent const *e); (13) static QState Ship_flying (Ship *me, QEvent const *e); (14) static QState Ship_exploding(Ship *me, QEvent const *e);
(15) static QState Ship_initial (Ship *me, QEvent const *e);
(16) static Ship l_ship; /* the sole instance of the Ship active object */
/* global objects ------*/ (17) QActive * const AO_ship = (QActive *)&l_ship; /* opaque pointer to Ship AO */
(1) Every application-level C file that uses the QP platform must include the qp_port.h header file. (2) The bsp.h header file contains the interface to the Board Support Package. (3) The game.h header file contains the declarations of events and other facilities shared among the components of the application (see Listing 1.2). (4) This structure defines the Ship active object.
NOTE I like to keep active objects, and indeed all state machine objects (such as Mines), strictly encapsulated. Therefore, I don’t put the state machine structure definitions in header files; rather, I define them right in the implementation file, such as ship.c. That way I can be sure that the internal data members of the Ship structure are not known to any other parts of the application.
(5) The Ship active object structure derives from the framework structure QActive, as described in the sidebar “Single Inheritance in C.” (6,7) The x and y data members represent the position of the Ship on the display. (8) The exp_ctr member is used for pacing the explosion animation (see also the “exploding” state in the Ship state diagram in Figure 1.6). (9) The score member stores the accumulated score in the game. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 41
(10) I use the typedef to define the shorter name Ship equivalent to struct ShipTag. (11-14) These four functions are called state-handler functions because they correspond one to one to the states of the Ship state machine shown in Figure 1.6.For example, the Ship_active() function represents the “active” state. The QEP event processor calls the state-handler functions to realize the UML semantics of state machine execution. All state-handler functions have the same signature. A state-handler function takes the state machine pointer and the event pointer as arguments and returns the status of the operation back to the event processor—for example whether the event was handled or not. The return type QState of state-handler functions is typedef-ed to uint8_t as QState in the header file
NOTE I use a simple naming convention to strengthen the association between the structures and the functions designed to operate on these structures. First, I name the functions by combining the typedef’ed structure name with the name of the operation (e.g., Ship_active). Sec- ond, I always place the pointer to the structure as the first argument of the associated func- tion, and I always name this argument “me” (e.g., Ship_active(Ship *me, ...)).
(15) In addition to state-handler functions, every state machine must declare the initial pseudostate, which QEP invokes to execute the topmost initial transition (see Figure 1.6(1)). The initial pseudostate handler has a signature identical to the regular state-handler function. (16) In this line I statically allocate the storage for the Ship active object. Note that the object l_ship is defined as static so that it is accessible only locally at the file scope of the ship.c file. (17) In this line I define and initialize the global pointer AO_Ship to the Ship active object (see also Listing 1.2(10)). This pointer is “opaque” because it treats the Ship object as the generic QActive base structure rather than the specific Ship structure. The power of an “opaque” pointer is that it allows me to completely hide the definition of the Ship structure and make it inaccessible to the rest of the application. Still, the other application components can access the Ship object to post events directly to it via the QActive_postFIFO(QActive *me, QEvent const *e) function.
www.newnespress.com 42 Chapter 1
1.7.2 Step 2: Initializing the State Machine The state machine initialization is divided into the following two steps for increased flexibility and better control of the initialization timeline: 1. The state machine “constructor”; and 2. The top-most initial transition. The state machine “constructor,” such as Ship_ctor(), intentionally does not execute the topmost initial transition defined in the initial pseudostate because at that time some vital objects can be missing and critical hardware might not be properly initialized yet.3 Instead, the state machine “constructor” merely puts the state machine in the initial pseudostate. Later, the user code must trigger the topmost initial transition explicitly, which happens actually inside the function QActive_start() (see Listing 1.1(18-20)). Listing 1.5 shows the instantiation (the “constructor” function) and initialization (the initial pseudostate) of the Ship active object.
Listing 1.5 Instantiation and initialization of the Ship active object in ship.c
(1) void Ship_ctor(void) { /* instantiation */ (2) Ship *me = &l_ship; (3) QActive_ctor(&me->super, (QStateHandler)&Ship_initial); (4) me->x = GAME_SHIP_X; (5) me->y = GAME_SHIP_Y; } /*...... */ (6) QState Ship_initial(Ship *me, QEvent const *e) { /* initialization */ (7) QActive_subscribe((QActive *)me, TIME_TICK_SIG); (8) QActive_subscribe((QActive *)me, PLAYER_TRIGGER_SIG);
(9) return Q_TRAN(&Ship_active); /* top-most initial transition */ }
(1) The global function Ship_ctor() is prototyped in game.h and called at the beginning of main(). (2) The “me” pointer points to the statically allocated Ship object (see Listing 1.4(16)).
3 In C++, the static constructors run even before main(). www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 43
(3) Every derived structure is responsible for initializing the part inherited from the base structure. The “constructor” QActive_ctor() puts the state machine in the initial pseudostate &Ship_initial (see Listing 1.4(15)). (4,5) The Ship position is initialized. (6) The Ship_initial() function defines the topmost initial transition in the Ship state machine (see Figure 1.6(1)). (7,8) The Ship active object subscribes to signals TIME_TICK_SIG and PLAYER_TRIGGER_SIG, as specified in the state diagram in Figure 1.6(1). (9) The initial state “active” is specified by returning the QP macro Q_TRAN().
NOTE
The macro Q_TRAN() must always follow the return statement.
1.7.3 Step 3: Defining State-Handler Functions In the last step, you actually code the Ship state machine by implementing one state at a time as a state-handler function. To determine what elements belong to any given state-handler function, you follow around the state’s boundary in the diagram (Figure 1.6). You need to implement all transitions originating at the boundary, any entry and exit actions defined in the state, and all internal transitions enlisted directly in the state. Additionally, if there is an initial transition embedded directly in the state, you need to implement it as well. Take for example the state “flying” shown in Figure 1.6. This state has an entry action and two transitions originating at its boundary: HIT_WALL and HIT_MINE(type) as well as three internal transitions TIME_TICK, PLAYER_TRIGGER, and DESTROYED_MINE(score). The “flying” state nests inside the “active” superstate. Listing 1.6 shows two state-handler functions of the Ship state machine from Figure 1.6. The state-handler functions correspond to the states “active” and “flying,” respectively. The explanation section immediately following the listing highlights the important implementation techniques.
www.newnespress.com 44 Chapter 1
Listing 1.6 State-handler functions for states “active” and “flying” in ship.c
(1) QState Ship_active(Ship *me, QEvent const *e) { (2) switch (e->sig) { (3) case Q_INIT_SIG: { /* nested initial transition */ (4) /* any actions associated with the initial transition */ (5) return Q_TRAN(&Ship_parked); } (6) case PLAYER_SHIP_MOVE_SIG: { (7) me->x = ((ObjectPosEvt const *)e)->x; (8) me->y = ((ObjectPosEvt const *)e)->y; (9) return Q_HANDLED(); } } (10) return Q_SUPER(&QHsm_top); /* return the superstate */ } /*...... */ QState Ship_flying(Ship *me, QEvent const *e) { switch (e->sig) { (11) case Q_ENTRY_SIG: { (12) ScoreEvt *sev;
me->score = 0; /* reset the score */ (13) sev = Q_NEW(ScoreEvt, SCORE_SIG); (14) sev->score = me->score; (15) QActive_postFIFO(AO_Tunnel, (QEvent *)sev); (16) return Q_HANDLED(); } case TIME_TICK_SIG: { /* tell the Tunnel to draw the Ship and test for hits */ ObjectImageEvt *oie = Q_NEW(ObjectImageEvt, SHIP_IMG_SIG); oie->x = me->x; oie->y = me->y; oie->bmp = SHIP_BMP; QActive_postFIFO(AO_Tunnel, (QEvent *)oie);
++me->score; /* increment the score for surviving another tick */
if ((me->score % 10) == 0) { /* is the score "round"? */ ScoreEvt *sev = Q_NEW(ScoreEvt, SCORE_SIG); sev->score = me->score; QActive_postFIFO(AO_Tunnel, (QEvent *)sev); } return Q_HANDLED(); } case PLAYER_TRIGGER_SIG: { /* trigger the Missile */ ObjectPosEvt *ope = Q_NEW(ObjectPosEvt, MISSILE_FIRE_SIG); ope->x = me->x; ope->y = me->y + SHIP_HEIGHT - 1; www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 45
QActive_postFIFO(AO_Missile, (QEvent *)ope); return Q_HANDLED(); } case DESTROYED_MINE_SIG: { me->score += ((ScoreEvt const *)e)->score; /* the score will be sent to the Tunnel by the next TIME_TICK */ return Q_HANDLED(); } (17) case HIT_WALL_SIG: (18) case HIT_MINE_SIG: { (19) /* any actions associated with the transition */ (20) return Q_TRAN(&Ship_exploding); } } (21) return Q_SUPER(&Ship_active); /* return the superstate */ }
(1) Each state handler must have the same signature, that is, it must take two parameters: the state machine pointer “me” and the pointer to QEvent. The keyword const before the * in the event pointer declaration means that the event pointed to by that pointer cannot be changed inside the state-handler function (i.e., the event is read-only). A state-handler function must return QState, which conveys the status of the event handling to the QEP event processor. (2) Typically, every state handler is structured as a switch statement that discriminates based on the signal of the event e->sig. (3) This line of code pertains to the nested initial transition Figure 1.6(2). QEP provides a reserved signal Q_INIT_SIG that the framework passes to the state- handler function when it wants to execute the initial transition. (4) You can enlist any actions associated with this initial transition (none in this particular case). (5) You designate the target substate with the Q_TRAN() macro. This macro must always follow the return statement, through which the state-handler function informs the QEP event processor that the transition has been taken.
www.newnespress.com 46 Chapter 1
NOTE The initial transition must necessarily target a direct or transitive substate of a given state. An initial transition cannot target a peer state or go up in state hierarchy to higher-level states, which in the UML would represent a “malformed” state machine.
(6) This line of code pertains to the internal transition PLAYER_SHIP_MOVE_SIG (x, y) in Figure 1.6(3). (7,8) You access the data members of the Ship state machine via the “me” argument of the state-handler function. You access the event parameters via the “e” argument. You need to cast the event pointer from the generic QEvent base class to the specific event structure expected for the PLAYER_SHIP_MOVE_SIG, which is ObjectPosEvt in this case.
NOTE The association between the event signal and event structure (event parameters) is estab- lished at the time the event is generated. All recipients of that event must know about this association to perform the cast to the correct event structure.
(9) You terminate the case statement with “return QHANDLED ()”, which informs the QEP processor that the event has been handled but no transition has been taken. (10) The final return from a state-handler function designates the superstate of that state by means of the QEP macro Q_SUPER(). The final return statement from a state-handler function represents the single point of maintenance for changing the nesting level of a given state. The state “active” in Figure 1.6 has no explicit superstate, which means that it is implicitly nested in the “top” state. The “top” state is a UML concept that denotes the ultimate root of the state hierarchy in a hierarchical state machine. QEP provides the “top” state as a state-handler function QHsm_top(), and therefore the Ship_active() state handler uses the pointer &QHsm_top as the argument of the macro Q_SUPER().
www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 47
NOTE
In C and C++, a pointer-to-function QHsm_top() can be written either as QHsm_top or &QHsm_top. Even though the notation QHsm_top is more succinct, I prefer adding the amper- sand explicitly, to leave absolutely no doubt that I mean a pointer-to-function &QHsm_top.
(11) This line of code pertains to the entry action into state “flying” (Figure 1.6(5)). QEP provides a reserved signal Q_ENTRY_SIG that the framework passes to the state-handler function when it wants to execute the entry actions. (12) The entry action to “flying” posts the SCORE event to the Tunnel active object (Figure 1.6(5)). This line defines a temporary pointer to the event structure ScoreEvt.
(13) The QF macro Q_NEW(ScoreEvt, SCORE_SIG) dynamically allocates an instance of the ScoreEvt from an event pool managed by QF. The macro also performs the association between the signal SCORE_SIG and the allocated event. The Q_NEW() macro returns the pointer to the allocated event. (14) The score parameter of the ScoreEvt is set from the state machine member me->score.
(15) The sev event is posted directly to the Tunnel active object by means of the QP function QActive_postFIFO(). The arguments of this function are the recipient active object (AO_Tunnel in this case) and the pointer to the event (the temporary pointer sev in this case). (16) You terminate the case statement with return Q_HANDLED(), which informs QEP that the entry actions have been handled. (17,18) These two lines of code pertain to the state transitions from “flying” to “exploding” (Figure 1.6(9, 10)). (19) You can enlist any actions associated with the transition (none in this particular case). (20) You designate the target of the transition with the Q_TRAN() macro.
www.newnespress.com 48 Chapter 1
(21) The final return from a state-handler function designates the superstate of that state. The state “flying” in Figure 1.6 nests in the state “active,” so the state handler Ship_flying() returns the pointer &Ship_active. When implementing state-handler functions you need to keep in mind that the QEP event processor is in charge here rather than your code. QEP will invoke a state-handler function for various reasons: for hierarchical event processing, for execution of entry and exit actions, for triggering initial transitions, or even just to elicit the superstate of a given state handler. Therefore, you should not assume that a state handler would be invoked only for processing signals enlisted in the case statements. You should avoid any code outside the switch statement, especially code that would have side effects. 1.8 The Execution Model As you saw in Listing 1.1(21), the main() function eventually gives control to the event-driven framework by calling QF_run() to execute the application. In this section, I briefly explain how QF allocates the CPU cycles to various tasks within the system and what options you have in choosing the execution model.
1.8.1 Simple Nonpreemptive “Vanilla” Scheduler The “Fly ‘n’ Shoot” example uses the simplest QF configuration, in which QF runs on a bare-metal target processor without any underlying operating system or kernel.4 I call such a QF configuration “plain vanilla” or just “vanilla.” QF includes a simple nonpreemptive “vanilla” kernel, which executes one active object at a time in the infinite loop (similar to the “superloop”). The “vanilla” kernel is engaged after each event is processed in the run-to-completion (RTC) fashion to choose the next highest-priority active object ready to process the next event. The “vanilla” scheduler is cooperative, which means that all active objects cooperate to share a single CPU and implicitly yield to each other after every RTC step. The kernel is nonpreemptive, meaning that every active object must completely process an event before any other active object can start processing another event.
4 The 80Â86 version of the “Fly ‘n’ Shoot” game runs on top of DOS, but DOS does not provide any multitasking support. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 49
The ISRs can preempt the execution of active objects at any time, but due to the simplistic nature of the “vanilla” kernel, every ISR returns to exactly the preemption point. If the ISR posts or publishes an event to any active object, the processing of this event won’t start until the current RTC step completes. The maximum time an event for the highest-priority active object can be delayed this way is called the task-level response. With the nonpreemptive “vanilla” kernel, the task-level response is equal to the longest RTC step of all active objects in the system. Note that the task-level response of the “vanilla” kernel is still a lot better than the traditional “superloop” (a.k.a. main+ISRs) architecture. I’ll have more to say about this in the upcoming Section 1.9, where I compare the event-driven “Fly ‘n’ Shoot” example to the traditionally structured Quickstart application. The task-level response of the simple “vanilla” kernel turns out to be adequate for surprisingly many applications because state machines by nature handle events quickly without a need to busy-wait for events. (A state machine simply runs to completion and becomes dormant until another event arrives.) Also note that often you can make the task-level response as fast as you need by breaking up longer RTC steps into shorter ones (e.g., by using the “Reminder” state pattern described in Chapter 5).
1.8.2 The QK Preemptive Kernel In some cases, breaking up long RTC steps into short enough pieces might be very difficult, and consequently the task-level response of the nonpreemptive “vanilla” kernel might be too long. An example system could be a GPS receiver. Such a receiver performs a lot of floating-point number crunching on a fixed-point CPU to calculate the GPS position. At the same time, the GPS receiver must track the GPS satellite signals, which involves closing control loops in submillisecond intervals. It turns out that it’s not easy to break up the position-fix computation into short enough RTC steps to allow reliable signal tracking. But the RTC semantics of state machine execution do not mean that a state machine has to monopolize the CPU for the duration of the RTC step. A preemptive kernel can perform a context switch in the middle of the long RTC step to allow a higher-priority active object to run. As long as the active objects don’t share resources, they can run concurrently and complete their RTC steps independently (see also Section 6.3.3 in Chapter 6). The QP event-driven platform includes a tiny, fully preemptive, priority-based real-time kernel component called QK, which is specifically designed for processing
www.newnespress.com 50 Chapter 1 events in the RTC fashion. Configuring QP to use the preemptive QK kernel is very easy, but as with any fully preemptive kernel you must be very careful with any resources shared among active objects.5 The “Fly ‘n’ Shoot” example has been purposely designed to avoid any resource sharing among active objects, so the application code does not need to change at all to run on top of the QK preemptive kernel, or any other preemptive kernel or RTOS for that matter. The accompanying code contains the “Fly ‘n’ Shoot” example with QK in the following directory:
1.8.3 Traditional OS/RTOS QP can also work with a traditional operating system (OS), such as Windows or Linux, or virtually any real-time operating system (RTOS) to take advantage of the existing device drivers, communication stacks, and other middleware. QP contains a platform abstraction layer (PAL), which makes adapting QP to virtually any operating system easy. The carefully designed PAL allows tight integration with the underlying OS/RTOS by reusing any provided facilities for interrupt management, message queues, and memory partitions. I cover porting QP in Chapter 8.
1.9 Comparison to the Traditional Approach The “Fly ‘n’ Shoot” game behaves intentionally almost identically to the Quickstart application provided in source code with the Luminary Micro Stellaris EV-LM3S811 evaluation kit [Luminary 06]. In this section I’d like to compare the traditional approach represented by the Quickstart application with the state machine-based solution exemplified in the “Fly ‘n’ Shoot” game. Figure 1.11(A) shows schematically the flowchart of the Quickstart application; Figure 1.11(B) shows the flowchart of the “Fly ‘n’ Shoot” game running on top of the cooperative “vanilla” kernel. At the highest level, the flowcharts are similar in that they both consist of an endless loop surrounding the entire processing. But the internal structure of the main loop is very different in the two cases. As indicated by the heavy
5 QK provides a mutex facility for enforcing a mutually exclusive access to shared resources. The QK mutex uses the priority-ceiling protocol to avoid priority inversions. Refer to Chapter 10 for more information. www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 51 lines in the flowcharts, the Quickstart application spends most of its time in the tight “event loops” designed to busy-wait for certain events, such as the screen update event. In contrast, the “Fly ‘n’ Shoot” application spends most of its time right in the main loop. The QP framework dispatches any available event to the appropriate state machine that handles the event and returns quickly to the main loop without ever waiting for events internally.
QF_run() MainScreen()
QF_start(); Busy-wait for screen update event
event available
PlayGame();
Busy-wait no for screen events update event QHsm_dispatch();
ScreenSaver(); QF_onIdle();
A B
Figure 1.11: The control flow in the Quickstart application (A) and the “Fly ‘n’ Shoot” example (B). The heavy lines represent the most frequently exercised paths through the code.
The Quickstart application has much more convoluted flow of control than the “Fly ‘n’ Shoot” example because the traditional solution is very specific to the problem at hand whereas the state-machine approach is generic. The Quickstart application is structured very much like a traditional sequential program that tries to stay in control
www.newnespress.com 52 Chapter 1 from the beginning to the end. From time to time, the application pauses to busy-wait for a certain event, whereas the code is generally not ready to handle any other events than the one it chooses to wait for. All this contributes to the inflexibility of the design. Adding new events is hard because the whole structure of the intervening code is designed to accept only very specific events and would need to change dramatically to accommodate new events. Also, while busy-waiting for the screen update event (equivalent to the TIME_TICK event in “Fly ‘n’ Shoot” example), the application is really not responsive to any other events. The task-level response is hard to characterize and generally depends on the event type. The timing established by the hard-coded waiting for the existing events might not work well for new events. In contrast, the “Fly ‘n’ Shoot” application has a much simpler control flow that is purely event-driven and completely generic (see Figure 1.11(B)). The context of each active object component is represented as the current state of a state machine, rather than as a certain place in the code. That way, hanging in tight “event loops” around certain locations in the code corresponding to the current context is unnecessary. Instead, a state machine remembers the context very efficiently as a small data item (the state-variable; see Chapter 3). After processing of each event, the state machine can return to the common event loop that is designed generically to handle all kinds of events. For every event, the state machine naturally picks up where it left off and moves on to the next state, if necessary. Adding new events is easy in this design because a state machine is responsive to any event at any time. An event-driven, state-machine-based application is incomparably more flexible and resilient to change than the traditional one.
NOTE The generic event loop can also very easily detect the situation when no events are available, in which case the QP framework calls the QF_onIdle() function (see Figure 1.11(B)). This callback function is designed to be customized by the application and is the ideal place to put the CPU in a low-power sleep mode to conserve the power. In contrast, the traditional approach does not offer any single place to transition to the low-power sleep mode and con- sequently is much less friendly for implementing truly low-power designs.
1.10 Summary If you’ve never done event-driven programming before, the internal structure of the “Fly ‘n’ Shoot” game must certainly represent a big paradigm shift for you. In fact, www.newnespress.com Getting Started with UML State Machines and Event-Driven Programming 53
I hope that it actually blows your mind, because otherwise I’m not sure that you really appreciate the complete reversal of control of an event-driven program compared to the traditional sequential code. This reversal of control, known as the Hollywood Principle (don’t call us, we’ll call you), baffles many newcomers, who often find it “mind-boggling,” “backward,” or “weird.” The “Fly ‘n’ Shoot” game is by no means a big application, but at the same time it is definitely not trivial, either. You shouldn’t worry if you don’t fully understand it at the first reading. In the upcoming chapters, I will provide a closer look at the state machine design and coding techniques. In Part II, I discuss the features, implementation, and porting of the QF real-time framework. My main goal in this chapter was just to introduce you to the event-driven paradigm and the modern state machines to convince you that these powerful concepts aren’t particularly hard to implement directly in C or C++. Indeed, I hope you noticed that the actual coding of the nontrivial “Fly ‘n’ Shoot” game wasn’t a big deal at all. All you needed to know was just a few cookie-cutter rules for coding state machines and familiarity with a few framework services for implementing the actions. While the coding turned out to be essentially a nonissue; the bulk of the programming effort was spent on the design of the application. At this point, I hope that the “Fly ‘n’ Shoot” example helps you get the big picture of how the method works. Under the event-driven model, the program structure is divided into two rough groups: events and state machine components (active objects). An event represents the occurrence of something interesting. A state machine codifies the reactions to the events, which generally depend both on the nature of the event and on the state of the component. While events often originate from the outside of your program, such as time ticks or button presses in the “Fly ‘n’ Shoot” game, events can also be generated internally by the program itself. For example, the Mine components generate notification events when they detect a collision with the Missile or the Ship. An event-driven program executes by constantly checking for possible events and, when an event is detected, dispatching the event to the appropriate state machine component (see Figure 1.11(B)). For this approach to work, the events must be checked continuously and frequently. This implies that the state machines must execute quickly so that the program can get back to checking for events. To meet this requirement, a state machine cannot go into a condition where it is busy-waiting for some long or indeterminate time. The most common example of this would be a while loop inside a state-handler function, where the condition for termination was not under program
www.newnespress.com 54 Chapter 1 control—for instance, the button press. This kind of program structure, an indefinite loop, is referred to as “blocking” code,6 and you saw examples of it in the Quickstart application (see Figure 1.11(A)). For the event-driven programming model to work, you must only write “nonblocking” code [Carryer 05]. Finally, the “Fly ‘n’ Shoot” example demonstrates the use of the event-driven platform called QP, which is a collection of components for building event-driven applications. The QF real-time framework component embodies the Hollywood Principle by calling the application code, not the other way around. Such an arrangement is very typical for event-driven systems and application frameworks similar to QF are at the heart of virtually every design automation tool on the market today. The QF framework operates in the “Fly ‘n’ Shoot” game in its simplest configuration, in which QF runs on a bare-metal target processor without any operating system. QF can also be configured to work with the built-in preemptive real-time kernel called QK (see Chapter 10) or can be easily ported to almost any traditional OS or RTOS (see Chapter 8). In fact, you can view the QF framework itself as a high-level, event-driven, real-time operating system.
6 In the context of a multitasking operating system the “blocking” code corresponds to waiting on a semaphore, event flag, message mailbox, or other such operating system primitive. www.newnespress.com CHAPTER 2 A Crash Course in UML State Machines
One place we could really use help is in optimizing IF-THEN-ELSE constructs. Most programs start out fairly well structured. As bugs are found and features are grafted on, IFs and ELSEs are added until no human being really has a good idea how data flows through a function. Pretty printing helps, but does not reduce the complexity of 15 nested IF statements. —Jack Ganssle, “Break Points,” ESP Magazine, January 1991
Traditional, sequential programs can be structured as a single flow of control, using standard constructs such as loops and nested function calls. Such programs represent most of the execution context in the location of the program counter, in the procedure call tree, and in the temporary variables allocated on the stack. Event-driven programs, in contrast, require a series of fine-granularity event-handler functions for handling events. These event-handler functions must execute quickly and always return to the main event-loop, so no context can be preserved in the call tree and the program counter. In addition, all stack variables disappear across calls to the separate event-handlers. Thus, event-driven programs rely heavily on static variables to preserve the execution context from one event-handler invocation to the next. Consequently, one of the biggest challenges of event-driven programming lies in managing the execution context represented as data. The main problem here is that the context data must somehow feed back into the control flow of the event-handler code so that each event handler can execute only the actions appropriate in the current context. Traditionally, this dependence on the context very often leads to deeply nested if-else constructs that direct the flow of control based on the context data.
www.newnespress.com 56 Chapter 2
If you could eliminate even a fraction of these conditional branches (a.k.a. “spaghetti” code), the software would be much easier to understand, test, and maintain, and the sheer number of convoluted execution paths through the code would drop radically, typically by orders of magnitude. Techniques based on state machines are capable of achieving exactly this—a dramatic reduction of the different paths through the code and simplification of the conditions tested at each branching point. In this chapter I briefly introduce UML state machines that represent the current state of the art in the long evolution of these techniques. My intention is not to give a complete, formal discussion of UML state machines, which the official OMG specification [OMG 07] covers comprehensively and with formality. Rather, my goal in this chapter is to lay a foundation by establishing basic terminology, introducing basic notation,1 and clarifying semantics. This chapter is restricted to only a subset of those state machine features that are arguably most fundamental. The emphasis is on the role of UML state machines in practical, everyday programming rather than mathematical abstractions.
2.1 The Oversimplification of the Event-Action Paradigm The currently dominating approach to structuring event-driven software is the ubiquitous “event-action” paradigm, in which events are directly mapped to the code that is supposed to be executed in response. The event-action paradigm is an important stepping stone for understanding state machines, so in this section I briefly describe how it works in practice. I will use an example from the graphical user interface (GUI) domain, given that GUIs make exemplary event-driven systems. In the book Constructing the User Interface with Statecharts [Horrocks 99], Ian Horrocks discusses a simple GUI calculator application distributed in millions of copies as a sample program with Microsoft Visual Basic, in which he found a number of serious problems. As Horrocks notes, the point of this analysis is not to criticize this particular program but to identify the shortcomings of the general principles used in its construction. When you launch the Visual Basic calculator (available from the companion Website, in the directory
1 Appendix B contains a comprehensive summary of the notation. www.newnespress.com A Crash Course in UML State Machines 57
What’s not to like? However, play with the program for a while longer and you can discover many corner cases in which the calculator provides misleading results, freezes, or crashes altogether.
NOTE Ian Horrocks found 10 serious errors in the Visual Basic calculator after only an hour of test- ing. Try to find at least half of them.
For example, the Visual Basic calculator often has problems with the “–” event; just try the following sequence of operations: 2, –, –, –, 2, =. The application crashes with a runtime error (see Figure 2.1(B)). This is because the same button ( – ) is used to negate a number and to enter the subtraction operator. The correct interpretation of the “–” button-click event, therefore, depends on the context, or mode, in which it occurs. Likewise, the CE (Cancel Entry) button occasionally works erroneously—try 2, x, CE, 3, =, and observe that CE had no effect, even though it appears to cancel the 2 entry from the display. Again, CE should behave differently when canceling an operand than canceling an operator. As it turns out, the application handles the CE event always the same way, regardless of the context. At this point, you probably have noticed an
AB
Figure 2.1: Visual Basic calculator before the crash (A) and after a crash with a runtime error (B).
www.newnespress.com 58 Chapter 2 emerging pattern. The application is especially vulnerable to events that require different handling depending on the context. This is not to say that the Visual Basic calculator does not attempt to handle the context. Quite the contrary, if you look at the calculator code (available from the companion Website, in the directory
Listing 2.1 Fragment of Visual Basic code that attempts to determine whether the – (minus) button-click event should be treated as negation or subtraction Private Sub Operator_Click(Index As Integer) ... Select Case NumOps Case 0 If Operator(Index).Caption = "-" And LastInput <> "NEG" Then ReadOut = "-" & ReadOut LastInput = "NEG" End If Case 1 Op1 = ReadOut If Operator(Index).Caption = "-" And LastInput <> "NUMS" And OpFlag <> "=" Then ReadOut = "-" LastInput = "NEG" End If ... www.newnespress.com A Crash Course in UML State Machines 59
The approach exemplified in Listing 2.1 is a fertile ground for the “corner case” behavior (a.k.a. bugs) for at least three reasons: It always leads to convoluted conditional logic (a.k.a. “spaghetti” code). Each branching point requires evaluation of a complex expression. Switching between different modes requires modifying many variables, which can easily lead to inconsistencies. Convoluted conditional expressions like the one shown in Listing 2.1, scattered throughout the code, are unnecessarily complex and expensive to evaluate at runtime. They are also notoriously difficult to get right, even by experienced programmers, as the bugs still lurking in the Visual Basic calculator attest. This approach is insidious because it appears to work fine initially, but doesn’t scale up as the problem grows in complexity. Apparently, the calculator application (overall only seven event handlers and some 140 lines of Visual Basic code including comments) is just complex enough to be difficult to get right with this approach. The faults just outlined are rooted in the oversimplification of the event-action paradigm. The Visual Basic calculator example makes it clear, I hope, that an event alone does not determine the actions to be executed in response to that event. The current context is at least equally important. The prevalent event-action paradigm, however, recognizes only the dependency on the event-type and leaves the handling of the context to largely ad hoc techniques that all too easily degenerate into spaghetti code.
2.2 Basic State Machine Concepts The event-action paradigm can be extended to explicitly include the dependency on the execution context. As it turns out, the behavior of most event-driven systems can be divided into a relatively small number of chunks, where event responses within each individual chunk indeed depend only on the current event-type but no longer on the sequence of past events (the context). In other words, the event-action paradigm is still applied, but only locally within each individual chunk. A common and straightforward way of modeling behavior based on this idea is through a finite state machine (FSM). In this formalism, “chunks of behavior” are called states, and change of behavior (i.e., change in response to any event) corresponds to change of state and is called a state transition. An FSM is an efficient way to specify constraints of the overall behavior of a system. Being in a state means that the
www.newnespress.com 60 Chapter 2 system responds only to a subset of all allowed events, produces only a subset of possible responses, and changes state directly to only a subset of all possible states. The concept of an FSM is important in programming because it makes the event handling explicitly dependent on both the event-type and on the execution context (state) of the system. When used correctly, a state machine becomes a powerful “spaghetti reducer” that drastically cuts down the number of execution paths through the code, simplifies the conditions tested at each branching point, and simplifies the transitions between different modes of execution.
2.2.1 States A state captures the relevant aspects of the system’s history very efficiently. For example, when you strike a key on a keyboard, the character code generated will be either an uppercase or a lowercase character, depending on whether the Caps Lock is active.2 Therefore, the keyboard’s behavior can be divided into two chunks (states): the “default” state and the “caps_locked” state. (Most keyboards actually have an LED that indicates that the keyboard is in the “caps_locked” state.) The behavior of a keyboard depends only on certain aspects of its history, namely whether the Caps Lock key has been pressed, but not, for example, on how many and exactly which other keys have been pressed previously. A state can abstract away all possible (but irrelevant) event sequences and capture only the relevant ones. To relate this concept to programming, this means that instead of recording the event history in a multitude of variables, flags, and convoluted logic, you rely mainly on just one state variable that can assume only a limited number of a priori determined values (e.g., two values in case of the keyboard). The value of the state variable crisply defines the current state of the system at any given time. The concept of state reduces the problem of identifying the execution context in the code to testing just the state variable instead of many variables, thus eliminating a lot of conditional logic. Actually, in all but the most basic state machine implementation techniques, such as the “nested-switch statement” technique discussed in Chapter 3, even the explicit testing of the state variable disappears from the code, which reduces the “spaghetti” further still (you will experience this effect later in Chapters 3 and 4). Moreover, switching between different states is vastly simplified as well, because you need to reassign just one state variable instead of changing multiple variables in a self-consistent manner.
2 Ignore at this point the effects of the Shift, Ctrl, and Alt keys. www.newnespress.com A Crash Course in UML State Machines 61
2.2.2 State Diagrams FSMs have an expressive graphical representation in the form of state diagrams. These diagrams are directed graphs in which nodes denote states and connectors denote state transitions.3 For example, Figure 2.2 shows a UML state transition diagram corresponding to the computer keyboard state machine. In UML, states are represented as rounded rectangles labeled with state names. The transitions, represented as arrows, are labeled with the triggering events followed optionally by the list of executed actions. The initial transition originates from the solid circle and specifies the starting state when the system first begins. Every state diagram should have such a transition, which should not be labeled, since it is not triggered by an event. The initial transition can have associated actions.
initial states transition
default CAPS_LOCK caps_locked CAPS_LOCK
ANY_KEY / send_lower_case_scan_code(); ANY_KEY / send_upper_case_scan_code();
state transition trigger list of actions
Figure 2.2: UML state diagram representing the computer keyboard state machine.
2.2.3 State Diagrams versus Flowcharts Newcomers to the state machine formalism often confuse state diagrams with flowcharts. The UML specification [OMG 07] isn’t helping in this respect because it lumps activity graphs in the state machine package. Activity graphs are essentially elaborate flowcharts.
3 Appendix B contains a succinct summary of the graphical notations used throughout the book, including state transition diagrams.
www.newnespress.com 62 Chapter 2
Figure 2.3 shows a comparison of a state diagram with a flowchart. A state machine (panel (A)) performs actions in response to explicit triggers. In contrast, the flowchart (panel (B)) does not need explicit triggers but rather transitions from node to node in its graph automatically upon completion of activities.
s1
E1 / action1(); do X s2
E2 / action2(); do Y do Z s3
E3 / action3(); do W A B
Figure 2.3: Comparison of (A) state machine (statechart) with (B) activity diagram (flowchart).
Graphically, compared to state diagrams, flowcharts reverse the sense of vertices and arcs. In a state diagram, the processing is associated with the arcs (transitions), whereas in a flowchart, it is associated with the vertices. A state machine is idle when it sits in a state waiting for an event to occur. A flowchart is busy executing activities when it sits in a node. Figure 2.3 attempts to show that reversal of roles by aligning the arcs of the statecharts with the processing stages of the flowchart. You can compare a flowchart to an assembly line in manufacturing because the flowchart describes the progression of some task from beginning to end (e.g., transforming source code input into object code output by a compiler). A state machine generally has no notion of such a progression. A computer keyboard, for example, is not in a more advanced stage when it is in the “caps_locked” state, compared to being in the “default” state; it simply reacts differently to events. A state in a state machine is an efficient way of specifying a particular behavior, rather than a stage of processing. The distinction between state machines and flowcharts is especially important because these two concepts represent two diametrically opposed programming www.newnespress.com A Crash Course in UML State Machines 63 paradigms: event-driven programming (state machines) and transformational programming (flowcharts). You cannot devise effective state machines without constantly thinking about the available events. In contrast, events are only a secondary concern (if at all) for flowcharts.
2.2.4 Extended State Machines One possible interpretation of state for software systems is that each state represents one distinct set of valid values of the whole program memory. Even for simple programs with only a few elementary variables, this interpretation leads to an astronomical number of states. For example, a single 32-bit integer could contribute to over 4 billion different states. Clearly, this interpretation is not practical, so program variables are commonly dissociated from states. Rather, the complete condition of the system (called the extended state) is the combination of a qualitative aspect (the state) and the quantitative aspects (the extended state variables). In this interpretation, a change of variable does not always imply a change of the qualitative aspects of the system behavior and therefore does not lead to a change of state [Selic+ 94]. State machines supplemented with variables are called extended state machines. Extended state machines can apply the underlying formalism to much more complex problems than is practical without including extended state variables. For instance, suppose the behavior of the keyboard depends on the number of characters typed on it so far and that after, say, 1,000 keystrokes, the keyboard breaks down and enters the final state. To model this behavior in a state machine without memory, you would need to introduce 1,000 states (e.g., pressing a key in state stroke123 would lead to state stroke124, and so on), which is clearly an impractical proposition. Alternatively, you could construct an extended state machine with a key_count down-counter variable. The counter would be initialized to 1,000 and decremented by every keystroke without changing state. When the counter reached zero, the state machine would enter the final state. The state diagram from Figure 2.4 is an example of an extended state machine, in which the complete condition of the system (called the extended state) is the combination of a qualitative aspect—the “state”—and the quantitative aspects—the extended state variables (such as the down-counter key_count). In extended state machines, a change of a variable does not always imply a change of the qualitative aspects of the system behavior and therefore does not always lead to a change of state.
www.newnespress.com 64 Chapter 2
The obvious advantage of extended state machines is flexibility. For example, extending the lifespan of the “cheap keyboard” from 1,000 to 10,000 keystrokes would not complicate the extended state machine at all. The only modification required would be changing the initialization value of the key_count down-counter in the initial transition.
/ key_count = 1000;
default caps_locked CAPS_LOCK
CAPS_LOCK
ANY_KEY / --key_count; ANY_KEY / --key_count;
choice guard pseudostate conditions
[else] [key_count == 0] [key_count == 0] [else]
Figure 2.4: Extended state machine of “cheap keyboard” with extended state variable key_count and various guard conditions.
2.2.5 Guard Conditions This flexibility of extended state machines comes with a price, however, because of the complex coupling between the “qualitative” and the “quantitative” aspects of the extended state. The coupling occurs through the guard conditions attached to transitions, as shown in Figure 2.4. Guard conditions (or simply guards) are Boolean expressions evaluated dynamically based on the value of extended state variables and event parameters (see the discussion of events and event parameters in the next section). Guard conditions affect the behavior of a state machine by enabling actions or transitions only when they evaluate to TRUE and disabling them when they evaluate to FALSE. In the UML notation, guard conditions are shown in square brackets (e.g., [key_count == 0]). The need for guards is the immediate consequence of adding memory (extended state variables) to the state machine formalism. Used sparingly, extended state variables and guards make up an incredibly powerful mechanism that can immensely simplify www.newnespress.com A Crash Course in UML State Machines 65 designs. But don’t let the fancy name (“guard”) and the concise UML notation fool you. When you actually code an extended state machine, the guards become the same IFs and ELSEs that you wanted to eliminate by using the state machine in the first place. Too many of them, and you’ll find yourself back in square one (“spaghetti”), where the guards effectively take over handling of all the relevant conditions in the system. Indeed, abuse of extended state variables and guards is the primary mechanism of architectural decay in designs based on state machines. Usually, in the day-to-day battle, it seems very tempting, especially to programmers new to state machine formalism, to add yet another extended state variable and yet another guard condition (another if or an else) rather than to factor out the related behavior into a new qualitative aspect of the system—the state. From my experience in the trenches, the likelihood of such an architectural decay is directly proportional to the overhead (actual or perceived) involved in adding or removing states. (That’s why I don’t particularly like the popular state-table technique of implementing state machines that I describe in Chapter 3, because adding a new state requires adding and initializing a whole new column in the table.) One of the main challenges in becoming an effective state machine designer is to develop a sense for which parts of the behavior should be captured as the “qualitative” aspects (the “state”) and which elements are better left as the “quantitative” aspects (extended state variables). In general, you should actively look for opportunities to capture the event history (what happened) as the “state” of the system, instead of storing this information in extended state variables. For example, the Visual Basic calculator uses an extended state variable DecimalFlag to remember that the user entered the decimal point to avoid entering multiple decimal points in the same number. However, a better solution is to observe that entering a decimal point really leads to a distinct state “entering_the_fractional_part_of_a_number,” in which the calculator ignores decimal points. This solution is superior for a number of reasons. The lesser reason is that it eliminates one extended state variable and the need to initialize and test it. The more important reason is that the state-based solution is more robust because the context information is used very locally (only in this particular state) and is discarded as soon as it becomes irrelevant. Once the number is correctly entered, it doesn’t really matter for the subsequent operation of the calculator whether that number had a decimal point. The state machine moves on to another state and automatically “forgets” the previous context. The DecimalFlag extended state variable, on the other hand, “lays around” well past the time the information becomes irrelevant (and perhaps outdated!).
www.newnespress.com 66 Chapter 2
Worse, you must not forget to reset DecimalFlag before entering another number or the flag will incorrectly indicate that indeed the user once entered the decimal point, but perhaps this happened in the context of the previous number. Capturing behavior as the quantitative “state” has its disadvantages and limitations, too. First, the state and transition topology in a state machine must be static and fixed at compile time, which can be too limiting and inflexible. Sure, you can easily devise “state machines” that would modify themselves at runtime (this is what often actually happens when you try to recode “spaghetti” as a state machine). However, this is like writing self-modifying code, which indeed was done in the early days of programming but was quickly dismissed as a generally bad idea. Consequently, “state” can capture only static aspects of the behavior that are known a priori and are unlikely to change in the future. For example, it’s fine to capture the entry of a decimal point in the calculator as a separate state “entering_the_fractional_part_of_a_number,” because a number can have only one fractional part, which is both known aprioriandisnotlikelytochange in the future. However, implementing the “cheap keyboard” without extended state variables and guard conditions would be practically impossible. This example points to the main weakness of the quantitative “state,” which simply cannot store too much information (such as the wide range of keystroke counts). Extended state variables and guards are thus a mechanism for adding extra runtime flexibility to state machines.
2.2.6 Events In the most general terms, an event is an occurrence in time and space that has significance to the system. Strictly speaking, in the UML specification the term event refers to the type of occurrence rather than to any concrete instance of that occurrence [OMG 07]. For example, Keystroke is an event for the keyboard, but each press of a key is not an event but a concrete instance of the Keystroke event. Another event of interest for the keyboard might be Power-on, but turning the power on tomorrow at 10:05:36 will be just an instance of the Power-on event. An event can have associated parameters, allowing the event instance to convey not only the occurrence of some interesting incident but also quantitative information regarding that occurrence. For example, the Keystroke event generated by pressing a key on a computer keyboard has associated parameters that convey the character scan code as well as the status of the Shift, Ctrl, and Alt keys. www.newnespress.com A Crash Course in UML State Machines 67
An event instance outlives the instantaneous occurrence that generated it and might convey this occurrence to one or more state machines. Once generated, the event instance goes through a processing life cycle that can consist of up to three stages. First, the event instance is received when it is accepted and waiting for processing (e.g., it is placed on the event queue). Later, the event instance is dispatched to the state machine, at which point it becomes the current event. Finally, it is consumed when the state machine finishes processing the event instance. A consumed event instance is no longer available for processing.
2.2.7 Actions and Transitions When an event instance is dispatched, the state machine responds by performing actions, such as changing a variable, performing I/O, invoking a function, generating another event instance, or changing to another state. Any parameter values associated with the current event are available to all actions directly caused by that event. Switching from one state to another is called state transition, and the event that causes it is called the triggering event,orsimplythetrigger. In the keyboard example, if the keyboard is in the “default” state when the Caps Lock key is pressed, the keyboard will enter the “caps_locked” state. However, if the keyboard is already in the “caps_locked” state, pressing Caps Lock will cause a different transition—from the “caps_locked” to the “default” state. In both cases, pressing Caps Lock is the triggering event. In extended state machines, a transition can have a guard, which means that the transition can “fire” only if the guard evaluates to TRUE. A state can have many transitions in response to the same trigger, as long as they have nonoverlapping guards; however, this situation could create problems in the sequence of evaluation of the guards when the common trigger occurs. The UML specification intentionally does not stipulate any particular order; rather, UML puts the burden on the designer to devise guards in such a way that the order of their evaluation does not matter. Practically, this means that guard expressions should have no side effects, at least none that would alter evaluation of other guards having the same trigger.
2.2.8 Run-to-Completion Execution Model All state machine formalisms, including UML statecharts, universally assume that a state machine completes processing of each event before it can start processing the next event. This model of execution is called run to completion, or RTC.
www.newnespress.com 68 Chapter 2
In the RTC model, the system processes events in discrete, indivisible RTC steps. New incoming events cannot interrupt the processing of the current event and must be stored (typically in an event queue) until the state machine becomes idle again. These semantics completely avoid any internal concurrency issues within a single state machine. The RTC model also gets around the conceptual problem of processing actions associated with transitions,4 where the state machine is not in a well-defined state (is between two states) for the duration of the action. During event processing, the system is unresponsive (unobservable), so the ill-defined state during that time has no practical significance. Note, however, that RTC does not mean that a state machine has to monopolize the CPU until the RTC step is complete. The preemption restriction only applies to the task context of the state machine that is already busy processing events. In a multitasking environment, other tasks (not related to the task context of the busy state machine) can be running, possibly preempting the currently executing state machine. As long as other state machines do not share variables or other resources with each other, there are no concurrency hazards. The key advantage of RTC processing is simplicity. Its biggest disadvantage is that the responsiveness of a state machine is determined by its longest RTC step.5 Achieving short RTC steps can often significantly complicate real-time designs.
2.3 UML Extensions to the Traditional FSM Formalism Though the traditional FSMs are an excellent tool for tackling smaller problems, it’s also generally known that they tend to become unmanageable, even for moderately involved systems. Due to the phenomenon known as state explosion, the complexity of a traditional FSM tends to grow much faster than the complexity of the reactive system it describes. This happens because the traditional state machine formalism inflicts repetitions. For example, if you try to represent the behavior of the Visual Basic calculator introduced in Section 2.1 with a traditional FSM, you’ll immediately notice that many events (e.g., the Clear event) are handled identically in many states. A conventional FSM, however, has no means of capturing such a commonality and requires repeating the same actions and transitions in many states. What’s missing in the traditional state machines is the mechanism for factoring out the common behavior in order to share it across many states.
4 State machines that associate actions with transitions are classified as Mealy machines. 5 A state machine can improve responsiveness by breaking up the CPU-intensive processing into sufficiently short RTC steps (see also the “Reminder” state pattern in Chapter 5). www.newnespress.com A Crash Course in UML State Machines 69
The formalism of statecharts, invented by David Harel in the 1980s [Harel 87], addresses exactly this shortcoming of the conventional FSMs. Statecharts provide a very efficient way of sharing behavior so that the complexity of a statechart no longer explodes but tends to faithfully represent the complexity of the reactive system it describes. Obviously, formalism like this is a godsend to embedded systems programmers (or any programmers working with event-driven systems) because it makes the state machine approach truly applicable to real-life problems. UML state machines, known also as UML statecharts [OMG 07], are object-based variants of Harel statecharts and incorporate several concepts defined in ROOMcharts, a variant of the statechart defined in the Real-time Object-Oriented Modeling (ROOM) language [Selic+ 94]. UML statecharts are extended state machines with characteristics of both Mealy and Moore automata. In statecharts, actions generally depend on both the state of the system and the triggering event, as in a Mealy automaton. Additionally, UML statecharts provide optional entry and exit actions, which are associated with states rather than transitions, as in a Moore automaton.
2.3.1 Reuse of Behavior in Reactive Systems All reactive systems seem to reuse behavior in a similar way. For example, the characteristic look and feel of all GUIs results from the same pattern, which the Windows guru Charles Petzold calls the “Ultimate Hook” [Petzold 96]. The pattern is brilliantly simple: A GUI system dispatches every event first to the application (e.g., Windows calls a specific function inside the application, passing the event as an argument). If not handled by the application, the event flows back to the system. This establishes a hierarchical order of event processing. The application, which is conceptually at a lower level of the hierarchy, has the first shot at every event; thus the application can choose to react in any way it likes. At the same time, all unhandled events flow back to the higher level (i.e., to the GUI system), where they are processed according to the standard look and feel. This is an example of programming by difference because the application programmer needs to code only the differences from the standard system behavior.
2.3.2 Hierarchically Nested States Harel statecharts bring the “Ultimate Hook” pattern to the logical conclusion by combining it with the state machine formalism. The most important innovation of statecharts over the classical FSMs is the introduction of hierarchically nested states
www.newnespress.com 70 Chapter 2
(that’s why statecharts are also called hierarchical state machines,orHSMs).The semantics associated with state nesting are as follows (see Figure 2.5(A)): If a system is in the nested state “s11” (called the substate), it also (implicitly) is in the surrounding state “s1” (called the superstate). This state machine will attempt to handle any event in the context of state “s11,” which conceptually is at the lower level of the hierarchy. However, if state “s11” does not prescribe how to handle the event, the event is not quietly discarded as in a traditional “flat” state machine; rather, it is automatically handled at the higher level context of the superstate “s1.” This is what is meant by the system being in state “s11” as well as “s1.” Of course, state nesting is not limited to one level only, and the simple rule of event processing applies recursively to any level of nesting.
s1 heating OPEN_DOOR superstate toasting s11 door_open substate baking
AB
Figure 2.5: UML notation for hierarchically nested states (A), and a state model of a toaster oven in which states “toasting” and “baking” share the common transition from state “heating” to “door_open” (B).
States that contain other states are called composite states; conversely, states without internal structure are called simple states or leaf states. A nested state is called a direct substate when it is not contained by any other state; otherwise, it is referred to as a transitively nested substate. Because the internal structure of a composite state can be arbitrarily complex, any hierarchical state machine can be viewed as an internal structure of some (higher-level) composite state. It is conceptually convenient to define one composite state as the ultimate root of state machine hierarchy. In the UML specification, every state machine has a top state (the abstract root of every state machine hierarchy), which contains all the other elements of the entire state machine. The graphical rendering of this all- enclosing top state is optional [OMG 07]. As you can see, the semantics of hierarchical state decomposition are designed to facilitate sharing of behavior through the direct support for the “Ultimate Hook” pattern. The substates (nested states) need only define the differences from the www.newnespress.com A Crash Course in UML State Machines 71 superstates (surrounding states). A substate can easily reuse the common behavior from its superstate(s) by simply ignoring commonly handled events, which are then automatically handled by higher-level states. In this manner, the substates can share all aspects of behavior with their superstates. For example, in a state model of a toaster oven shown in Figure 2.5(B), states “toasting” and “baking” share a common transition DOOR_OPEN to the “door_open” state, defined in their common superstate “heating.” The aspect of state hierarchy emphasized most often is abstraction—an old and powerful technique for coping with complexity. Instead of facing all aspects of a complex system at the same time, it is often possible to ignore (abstract away) some parts of the system. Hierarchical states are an ideal mechanism for hiding internal details because the designer can easily zoom out or zoom in to hide or show nested states. Although abstraction by itself does not reduce overall system complexity, it is valuable because it reduces the amount of detail you need to deal with at one time. As Grady Booch [Booch 94] notes:
... we are still constrained by the number of things that we can comprehend at one time, but through abstraction, we use chunks of information with increasingly greater semantic content.
However valuable abstraction in itself might be, you cannot cheat your way out of complexity simply by hiding it inside composite states. However, the composite states don’t simply hide complexity, they also actively reduce it through the powerful mechanism of reuse (the “Ultimate Hook” pattern). Without such reuse, even a moderate increase in system complexity often leads to an explosive increase in the number of states and transitions. For example, if you transform the statechart from Figure 2.5(B) to a classical flat state machine,6 you must repeat one transition (from heating to “door_open”) in two places: as a transition from “toasting” to “door_open” and from “baking” to “door_open.” Avoiding such repetitions allows HSMs to grow proportionally to system complexity. As the modeled system grows, the opportunity for reuse also increases and thus counteracts the explosive increase in states and transitions typical for traditional FSMs.
2.3.3 Behavioral Inheritance Hierarchical states are more than merely the “grouping of [nested] state machines together without additional semantics” [Mellor 00]. In fact, hierarchical states have
6 Such a transformation is always possible because HSMs are mathematically equivalent to classical FSMs.
www.newnespress.com 72 Chapter 2 simple but profound semantics. Nested states are also more than just “great diagrammatic simplification when a set of events applies to several substates” [Douglass 99]. The savings in the number of states and transitions are real and go far beyond less cluttered diagrams. In other words, simpler diagrams are just a side effect of behavioral reuse enabled by state nesting. The fundamental character of state nesting comes from the combination of abstraction and hierarchy, which is a traditional approach to reducing complexity and is otherwise known in software as inheritance. In OOP, the concept of class inheritance describes relations between classes of objects. Class inheritance describes the “is a ...” relationship among classes. For example, class Bird might derive from class Animal.Ifanobject is a bird (instance of the Bird class), it automatically is an animal, because all operations that apply to animals (e.g., eating, eliminating, reproducing) also apply to birds. But birds are more specialized, since they have operations that are not applicable to animals in general. For example, flying() applies to birds but not to fish. The benefits of class inheritance are concisely summarized by Gamma and colleagues [Gamma+ 95]:
Inheritance lets you define a new kind of class rapidly in terms of an old one, by reusing functionality from parent classes. It allows new classes to be specified by difference rather than created from scratch each time. It lets you get new implementations almost for free, inheriting most of what is common from the ancestor classes.
As you saw in the previous section, all these basic characteristics of inheritance apply equally well to nested states (just replace the word class with state), which is not surprising because state nesting is based on the same fundamental “is a ...” classification as object-oriented class inheritance. For example, in a state model of a toaster oven, state “toasting” nests inside state “heating.” If the toaster is in the “toasting” state, it automatically is in the “heating” state because all behavior pertaining to “heating” applies also to “toasting” (e.g., the heater must be turned on). But “toasting” is more specialized because it has behaviors not applicable to “heating” in general. For example, setting toast color (light or dark) applies to “toasting” but not to “baking.” In the case of nested states, the “is a ...” (is-a-kind-of) relationship merely needs to be replaced by the “is in ...” (is-in-a-state) relationship; otherwise, it is the same fundamental classification. State nesting allows a substate to inherit state behavior from its ancestors (superstates); therefore, it’s called behavioral inheritance. www.newnespress.com A Crash Course in UML State Machines 73
NOTE The term “behavioral inheritance” does not come from the UML specification. Note too that behavioral inheritance describes the relationship between substates and superstates, and you should not confuse it with traditional (class) inheritance applied to entire state machines.
The concept of inheritance is fundamental in software construction. Class inheritance is essential for better software organization and for code reuse, which makes it a cornerstone of OOP. In the same way, behavioral inheritance is essential for efficient use of HSMs and for behavior reuse, which makes it a cornerstone of event-driven programming. In Chapter 5, a mini-catalog of state patterns shows ways to structure HSMs to solve recurring problems. Not surprisingly, behavioral inheritance plays the central role in all these patterns.
2.3.4 Liskov Substitution Principle for States Identifying the relationship among substates and superstates as inheritance has many practical implications. Perhaps the most important is the Liskov Substitution Principle (LSP) applied to state hierarchy. In its traditional formulation for classes, LSP requires that a subclass can be freely substituted for its superclass. This means that every instance of the subclass should be compatible with the instance of the superclass and that any code designed to work with the instance of the superclass should continue to work correctly if an instance of the subclass is used instead. Because behavioral inheritance is just a special kind of inheritance, LSP can be applied to nested states as well as classes. LSP generalized for states means that the behavior of a substate should be consistent with the superstate. For example, all states nested inside the “heating” state of the toaster oven (e.g., “toasting” or “baking”) should share the same basic characteristics of the “heating” state. In particular, if being in the “heating” state means that the heater is turned on, then none of the substates should turn the heater off (without transitioning out of the “heating” state). Turning the heater off and staying in the “toasting” or “baking” state would be inconsistent with being in the “heating” state and would indicate poor design (violation of the LSP). Compliance with the LSP allows you to build better (more correct) state hierarchies and make efficient use of abstraction. For example, in an LSP-compliant state hierarchy, you can safely zoom out and work at the higher level of the “heating” state (thus
www.newnespress.com 74 Chapter 2 abstracting away the specifics of “toasting” and “baking”). As long as all the substates are consistent with their superstate, such abstraction is meaningful. On the other hand, if the substates violate basic assumptions of being in the superstate, zooming out and ignoring the specifics of the substates will be incorrect.
2.3.5 Orthogonal Regions Hierarchical state decomposition can be viewed as exclusive-OR operation applied to states. For example, if a system is in the “heating” superstate (Figure 2.5(B)), it means that it’s either in “toasting” substate OR the “baking” substate. That is why the “heating” superstate is called an OR-state. UML statecharts also introduce the complementary AND-decomposition. Such decomposition means that a composite state can contain two or more orthogonal regions (orthogonal means independent in this context) and that being in such a composite state entails being in all its orthogonal regions simultaneously [Harel+ 98]. Orthogonal regions address the frequent problem of a combinatorial increase in the number of states when the behavior of a system is fragmented into independent, concurrently active parts. For example, apart from the main keypad, a computer keyboard has an independent numeric keypad. From the previous discussion, recall the two states of the main keypad already identified: “default” and “caps_locked” (Figure 2.2). The numeric keypad also can be in two states—“numbers” and “arrows”— depending on whether Num Lock is active. The complete state space of the keyboard in the standard decomposition is the cross-product of the two components (main keypad and numeric keypad) and consists of four states: “default–numbers,” “default–arrows,” “caps_locked–numbers,” and “caps_locked–arrows.” However, this is unnatural because the behavior of the numeric keypad does not depend on the state of the main keypad and vice versa. Orthogonal regions allow you to avoid mixing the independent behaviors as a cross-product and, instead, to keep them separate, as shown in Figure 2.6. Note that if the orthogonal regions are fully independent of each other, their combined complexity is simply additive, which means that the number of independent states needed to model the system is simply the sum k + l + m + ..., where k, l, m, ...denote numbers of OR-states in each orthogonal region. The general case of mutual dependency, on the other hand, results in multiplicative complexity, so in general, the number of states needed is the product k  l  m  .... www.newnespress.com A Crash Course in UML State Machines 75
keyboard main_keypad numeric_keypad default numbers
ANY_KEY NUM_KEY CAPS_LOCK NUM_LOCK ANY_KEY CAPS_LOCK NUM_KEY NUM_LOCK
caps_locked arrows
Figure 2.6: Two orthogonal regions (main keypad and numeric keypad) of a computer keyboard.
In most real-life situations, however, orthogonal regions are only approximately orthogonal (i.e., they are not independent). Therefore, UML statecharts provide a number of ways for orthogonal regions to communicate and synchronize their behaviors. From these rich sets of (sometimes complex) mechanisms, perhaps the most important is that orthogonal regions can coordinate their behaviors by sending event instances to each other. Even though orthogonal regions imply independence of execution (i.e., some kind of concurrency), the UML specification does not require that a separate thread of execution be assigned to each orthogonal region (although it can be implemented that way). In fact, most commonly, orthogonal regions execute within the same thread. The UML specification only requires that the designer not rely on any particular order in which an event instance will be dispatched to the involved orthogonal regions.
NOTE The HSM implementation described in this book (see Chapter 4) does not directly support orthogonal regions. Chapter 5 describes the “Orthogonal Component” state pattern, which emulates orthogonal regions by composition of HSMs.
2.3.6 Entry and Exit Actions Every state in a UML statechart can have optional entry actions, which are executed upon entry to a state, as well as optional exit actions, which are executed upon exit from
www.newnespress.com 76 Chapter 2 a state. Entry and exit actions are associated with states, not transitions.7 Regardless of how a state is entered or exited, all its entry and exit actions will be executed. Because of this characteristic, statecharts behave like Moore automata. The UML notation for state entry and exit actions is to place the reserved word “entry” (or “exit”) in the state right below the name compartment, followed by the forward slash and the list of arbitrary actions (see Figure 2.7). The value of entry and exit actions is that they provide means for guaranteed initialization and cleanup, very much like class constructors and destructors in OOP. For example, consider the “door_open” state from Figure 2.7, which corresponds to the toaster oven behavior while the door is open. This state has a very important safety- critical requirement: Always disable the heater when the door is open.8 Additionally, while the door is open, the internal lamp illuminating the oven should light up. Of course, you could model such behavior by adding appropriate actions (disabling the heater and turning on the light) to every transition path leading to the “door_open” state (the user may open the door at any time during “baking” or “toasting” or when the oven is not used at all). You also should not forget to extinguish the internal lamp
heating entry / heater_on(); exit / heater_off(); toasting DOOR_OPEN entry / arm_time_event(me->toast_color); exit / disarm_time_event(); door_open entry / internal_lamp_on(); exit / internal_lamp_off(); DO_BAKING DO_TOASTING
baking entry / set_temperature(me->temperature); DOOR_CLOSE exit / set_temperature(0);
Figure 2.7: Toaster oven state machine with entry and exit actions.
7 State machines are classified as Mealy machines if actions are associated with transitions and as Moore machines if actions are associated with states. UML state machines have characteristics of both Mealy machines and Moore machines. 8 Commonly, such a safety-critical function is (and should be) redundantly safeguarded by mechanical interlocks, but for the sake of this discussion, suppose you need to implement it entirely in software. www.newnespress.com A Crash Course in UML State Machines 77 with every transition leaving the “door_open” state. However, such a solution would cause the repetition of actions in many transitions. More important, such an approach is error-prone in view of changes to the state machine (e.g., the next programmer working on a new feature, such as top-browning, might simply forget to disable the heater on transition to “door_open”). Entry and exit actions allow you to implement the desired behavior in a much safer, simpler, and more intuitive way. As shown in Figure 2.7, you could specify that the exit action from “heating” disables the heater, the entry action to “door_open” lights up the oven lamp, and the exit action from “door_open” extinguishes the lamp. The use of entry and exit action is superior to placing actions on transitions because it avoids repetitions of those actions on transitions and eliminates the basic safety hazard of leaving the heater on while the door is open. The semantics of exit actions guarantees that, regardless of the transition path, the heater will be disabled when the toaster is not in the “heating” state. Because entry actions are executed automatically whenever an associated state is entered, they often determine the conditions of operation or the identity of the state, very much as a class constructor determines the identity of the object being constructed. For example, the identity of the “heating” state is determined by the fact that the heater is turned on. This condition must be established before entering any substate of “heating” because entry actions to a substate of “heating,” like “toasting,” rely on proper initialization of the “heating” superstate and perform only the differences from this initialization. Consequently, the order of execution of entry actions must always proceed from the outermost state to the innermost state. Not surprisingly, this order is analogous to the order in which class constructors are invoked. Construction of a class always starts at the very root of the class hierarchy and follows through all inheritance levels down to the class being instantiated. The execution of exit actions, which corresponds to destructor invocation, proceeds in the exact reverse order, starting from the innermost state (corresponding to the most derived class).
2.3.7 Internal Transitions Very commonly, an event causes only some internal actions to execute but does not lead to a change of state (state transition). In this case, all actions executed comprise the internal transition. For example, when you type on your keyboard, it responds by generating different character codes. However, unless you hit the Caps Lock key, the
www.newnespress.com 78 Chapter 2 state of the keyboard does not change (no state transition occurs). In UML, this situation should be modeled with internal transitions, as shown in Figure 2.8. The UML notation for internal transitions follows the general syntax used for exit (or entry) actions, except instead of the word entry (or exit) the internal transition is labeled with the triggering event (e.g., see the internal transition triggered by the ANY_KEY event in Figure 2.8).
default ANY_KEY / send_lower_case_scan_code();
internal CAPS_LOCK CAPS_LOCK transitions caps_locked ANY_KEY / send_upper_case_scan_code();
trigger list of actions
Figure 2.8: UML state diagram of the keyboard state machine with internal transitions.
In the absence of entry and exit actions, internal transitions would be identical to self- transitions (transitions in which the target state is the same as the source state). In fact, in a classical Mealy automaton, actions are associated exclusively with state transitions, so the only way to execute actions without changing state is through a self-transition (depicted as a directed loop in Figure 2.2). However, in the presence of entry and exit actions, as in UML statecharts, a self-transition involves the execution of exit and entry actions and therefore it is distinctively different from an internal transition. In contrast to a self-transition, no entry or exit actions are ever executed as a result of an internal transition, even if the internal transition is inherited from a higher level of the hierarchy than the currently active state. Internal transitions inherited from superstates at any level of nesting act as if they were defined directly in the currently active state.
2.3.8 Transition Execution Sequence State nesting combined with entry and exit actions significantly complicates the state transition semantics in HSMs compared to the traditional FSMs. When dealing with hierarchically nested states and orthogonal regions, the simple term current state can be quite confusing. In an HSM, more than one state can be active at once. If the state www.newnespress.com A Crash Course in UML State Machines 79 machine is in a leaf state that is contained in a composite state (which is possibly contained in a higher-level composite state, and so on), all the composite states that either directly or transitively contain the leaf state are also active. Furthermore, because some of the composite states in this hierarchy might have orthogonal regions, the current active state is actually represented by a tree of states starting with the single top state at the root down to individual simple states at the leaves. The UML specification refers to such a state tree as state configuration [OMG 07].
main source Active state main target Active state of T1 before transition LCA(s1, s2) of T1 after transition
s
s1 s2 exit / b(); entry / c(); s11 s21 T1 [g()] / t(); / d(); exit / a(); entry / e();
Figure 2.9: State roles in a state transition.
In UML, a state transition can directly connect any two states. These two states, which may be composite, are designated as the main source and the main target of a transition. Figure 2.9 shows a simple transition example and explains the state roles in that transition. The UML specification prescribes that taking a state transition involves executing the following actions in the following sequence [OMG 07, Section 15.3.13]: 1. Evaluate the guard condition associated with the transition and perform the following steps only if the guard evaluates to TRUE. 2. Exit the source state configuration. 3. Execute the actions associated with the transition. 4. Enter the target state configuration. The transition sequence is easy to interpret in the simple case of both the main source and the main target nesting at the same level. For example, transition T1 shown in Figure 2.9 causes the evaluation of the guard g(); followed by the sequence of actions: a(); b(); t(); c(); d(); and e(), assuming that the guard g() evaluates to TRUE.
www.newnespress.com 80 Chapter 2
However, in the general case of source and target states nested at different levels of the state hierarchy, it might not be immediately obvious how many levels of nesting need to be exited. The UML specification prescribes that a transition involves exiting all nested states from the current active state (which might be a direct or transitive substate of the main source state) up to, but not including, the least common ancestor (LCA) state of the main source and main target states. As the name indicates, the LCA is the lowest composite state that is simultaneously a superstate (ancestor) of both the source and the target states. As described before, the order of execution of exit actions is always from the most deeply nested state (the current active state) up the hierarchy to the LCA but without exiting the LCA. For instance, the LCA(s1, s2) of states “s1” and “s2” shown in Figure 2.9 is state “s.” Entering the target state configuration commences from the level where the exit actions left off (i.e., from inside the LCA). As described before, entry actions must be executed starting from the highest-level state down the state hierarchy to the main target state. If the main target state is composite, the UML semantics prescribes to “drill” into its submachine recursively using the local initial transitions. The target state configuration is completely entered only after encountering a leaf state that has no initial transitions.
NOTE The HSM implementation described in this book (see Chapter 4) preserves the essential order of exiting the source configuration followed by entering the target state configuration, but executes the actions associated with the transition entirely in the context of the source state, that is, before exiting the source state configuration. Specifically, the implemented transition sequence is as follows: 1. Evaluate the guard condition associated with the transition and perform the following steps only if the guard evaluates to TRUE. 2. Execute the actions associated with the transition. 3. Atomically exit the source state configuration and enter the target state configuration. For example, the transition T1 shown in Figure 2.9 will cause the evaluation of the guard g(); followed by the sequence of actions: t(); a(); b(); c(); d(); and e(), assuming that the guard g() evaluates to TRUE.
One big problem with the UML transition sequence is that it requires executing actions associated with the transition after destroying the source state configuration but before creating the target state configuration. In the analogy between exit actions in www.newnespress.com A Crash Course in UML State Machines 81 state machines and destructors in OOP, this situation corresponds to executing a class method after partially destroying an object. Of course, such action is illegal in OOP. As it turns out, it is also particularly awkward to implement for state machines. Executing actions associated with a transition is much more natural in the context of the source state—the same context in which the guard condition is evaluated. Only after the guard and the transition actions execute, the source state configuration is exited and the target state configuration is entered atomically. That way the state machine is observable only in a stable state configuration, either before or after the transition, but not in the middle.
2.3.9 Local versus External Transitions Before UML 2, the only transition semantics in use was the external transition,in which the main source of the transition is always exited and the main target of the transition is always entered. UML 2 preserved the “external transition” semantics for backward compatibility, but also introduced a new kind of transition called local transition [OMG 07, Section 15.3.15]. For many transition topologies, external and local transitions are actually identical. However, a local transition doesn’t cause exit from the main source state if the main target state is a substate of the main source. In addition, local state transition doesn’t cause exit and reentry to the target state if the main target is a superstate of the main source state. Figure 2.10 contrasts local (A) and external (B) transitions. In the top row, you see the case of the main source containing the target. The local transition does not cause exit
Local transition External transitions
AB
Figure 2.10: Local (A) versus external transitions (B). QP implements only the local transitions.
www.newnespress.com 82 Chapter 2 from the source, while the external transition causes exit and re-entry to the source. In the bottom row of Figure 2.10, you see the case of the target containing the source. The local transition does not cause entry to the target, whereas the external transition causes exit and reentry to the target.
NOTE The HSM implementation described in Chapter 4 of this book (as well as the HSM imple- mentation described in the first edition) supports exclusively the local state transition semantics.
2.3.10 Event Types in the UML The UML specification defines four kinds of events, each one distinguished by a specific notation. SignalEvent represents the reception of a particular (asynchronous) signal. Its format is signal-name ’(’ comma-separated-parameter- list ’)’. TimeEvent models the expiration of a specific deadline. It is denoted with the keyword ‘after’ followed by an expression specifying the amount of time. The time is measured from the entry to the state in which the TimeEvent is used as a trigger. CallEvent represents the request to synchronously invoke a specific operation. Its format is operation-name ’(’ comma-separated-parameter- list ’)’. ChangeEvent models an event that occurs when an explicit Boolean expression becomes TRUE. It is denoted with the keyword ‘when’ followed by a Boolean expression. A SignalEvent is by far the most common event type (and the only one used in classical FSMs). Even here, however, the UML specification extends traditional FSM semantics by allowing the specified signal to be a subclass of another signal, resulting in polymorphic event triggering. Any transition triggered by a given signal event is also triggered by any subevent derived directly or indirectly from the original event. www.newnespress.com A Crash Course in UML State Machines 83
NOTE The HSM implementation described in this book (see Chapter 4) supports only the SignalEvent type. The real-time framework described in Part II also adds support for the TimeEvent type, but TimeEvents in QF require explicit arming and disarming, which is not com- patible with the UML ‘after’ notation. The polymorphic event triggering for SignalEvents is not supported, due to its inherent complexity and very high performance costs.
2.3.11 Event Deferral Sometimes an event arrives at a particularly inconvenient time, when a state machine is in a state that cannot handle the event. In many cases, the nature of the event is such that it can be postponed (within limits) until the system enters another state, in which it is much better prepared to handle the original event. UML state machines provide a special mechanism for deferring events in states. In every state, you can include a clause ‘deferred / [event list]’. If an event in the current state’s deferred event list occurs, the event will be saved (deferred) for future processing until a state is entered that does not list the event in its deferred event list. Upon entry to such state, the UML state machine will automatically recall any saved event(s) that are no longer deferred and process them as if they have just arrived.
NOTE The HSM implementation described in this book (see Chapter 4) does not directly support the UML-style event deferral. However, the “Deferred Event” state pattern presented in Chapter 5 shows how to approximate this feature in a much less expensive way by explicitly deferring and recalling events.
2.3.12 Pseudostates Because statecharts started as a visual formalism [Harel 87], some nodes in the diagrams other than the regular states turned out to be useful for implementing various features (or simply as a shorthand notation). The various “plumbing gear” nodes are collectively called pseudostates. More formally, a pseudostate is an abstraction that
www.newnespress.com 84 Chapter 2 encompasses different types of transient vertices (nodes) in the state machine graph. The UML specification [OMG 07] defines the following kinds of pseudostates: The initial pseudostate (shown as a black dot) represents a source for initial transition. There can be, at most, one initial pseudostate in a composite state. The outgoing transition from the initial pseudostate may have actions but not a trigger or guard. The choice pseudostate (shown as a diamond or an empty circle) is used for dynamic conditional branches. It allows the splitting of transitions into multiple outgoing paths, so the decision as to which path to take could depend on the results of prior actions performed in the same RTC step. The shallow-history pseudostate (shown as a circled letter H) is a shorthand notation that represents the most recent active direct substate of its containing state. A transition coming into the shallow-history vertex (called a transition to history) is equivalent to a transition coming into the most recent active substate of a state. A transition can originate from the history connector to designate a state to be entered in case a composite state has no history yet (has never been active before). The deep-history pseudostate (shown as a circled H*) is similar to shallow- history except it represents the whole, most recent state configuration of the composite state that directly contains the pseudostate. The junction pseudostate (shown as a black dot) is a semantics-free vertex that chains together multiple transitions. A junction is like a Swiss Army knife: It performs various functions. Junctions can be used both to merge multiple incoming transitions (from the same concurrent region) and to split an incoming transition into multiple outgoing transition segments with different guards. The latter case realizes a static conditional branch because the use of a junction imposes static evaluation of all guards before the transition is taken. The join pseudostate (shown as a vertical bar) serves to merge several transitions emanating from source vertices in different orthogonal regions. The fork pseudostate (represented identically as a join) serves to split an incoming transition into two or more transitions terminating in different orthogonal regions. www.newnespress.com A Crash Course in UML State Machines 85
2.3.13 UML Statecharts and Automatic Code Synthesis UML statecharts provide sufficiently well-defined semantics for building executable state models. Indeed, several design automation tools on the market support various versions of statecharts (see the sidebar “Design Automation Tools Supporting Statecharts”). The commercially available design automation tools typically not only automatically generate code from statecharts but also enable debugging and testing of the state models at the graphical level [Douglass 99]. But what does automatic code generation really mean? And more important, what kind of code is actually generated by such statechart-based tools? Many people understand automatic code synthesis as the generation of a program to solve a problem from a statement of the problem specification. Statechart-based tools cannot provide this because a statechart is just a higher-level (mostly visual) solution rather than the statement of the problem. As far as the automatically generated code is concerned, the statechart-based tools can autonomously generate only so-called “housekeeping code” [Douglass 99]. The modeler explicitly must provide all the application-specific code, such as action and guard expressions, to the tool. The role of housekeeping code is to “glue” the various action and guard expressions together to ensure proper state machine execution in accordance with the statechart semantics. For example, synthesized code typically handles event queuing, event dispatching, guard evaluation, or transition chain execution (including exit and entry of appropriate states). Almost universally, the tools also encompass some kind of real-time framework (see Part II of this book) that integrates tightly with the underlying operating system.
DESIGN AUTOMATION TOOLS SUPPORTING STATECHARTS Some of the computer-aided software-engineering (CASE) tools with support for statecharts currently available on the market are (see also Queens University CASE tool index [Queens 07]): Tele logic Statem ate, www .telelogi c.com (the tool originally devel oped by I-Log ix, Inc. acquired by Tele logic in 2006, which in turn is in the proce ss of being acqui red by IBM) Telelogic Rhapsody, www.telelogic.com
Continued onto next page
www.newnespress.com 86 Chapter 2
Rational Suite Develo pment Studio Real- Time, Rational Software Corpor ation, www . ibm.com /software/ rational (Rationa l was acqui red by IBM in 2006) ARTiSAN Studio, ARTiSAN Software Tools, Ltd., www.artisansw.com Stateflow, The Mathworks, www.mathworks.com VisualState, IAR Systems, www.iar.com
2.3.14 The Limitations of the UML State Diagrams Statecharts have been invented as “a visual formalism for complex systems” [Harel 87], so from their inception, they have been inseparably associated with graphical representation in the form of state diagrams. However, it is important to understand that the concept of HSMs transcends any particular notation, graphical or textual. The UML specification [OMG 07] makes this distinction apparent by clearly separating state machine semantics from the notation. However, the notation of UML statecharts is not purely visual. Any nontrivial state machine requires a large amount of textual information (e.g., the specification of actions and guards). The exact syntax of action and guard expressions isn’t defined in the UML specification, so many people use either structured English or, more formally, expressions in an implementation language such as C, C++, or Java [Douglass 99b]. In practice, this means that UML statechart notation depends heavily on the specific programming language. Nevertheless, most of the statecharts semantics are heavily biased toward graphical notation. For example, state diagrams poorly represent the sequence of processing, be it order of evaluation of guards or order of dispatching events to orthogonal regions. The UML specification sidesteps these problems by putting the burden on the designer not to rely on any particular sequencing. But, as you will see in Chapters 3 and 4, when you actually implement UML state machines, you will always have full control over the order of execution, so the restrictions imposed by UML semantics will be unnecessarily restrictive. Similarly, statechart diagrams require a lot of plumbing gear (pseudostates, like joins, forks, junctions, choicepoints, etc.) to represent the flow of control graphically. These elements are nothing but the old flowchart in disguise, which structured programming techniques proved far less significant9 a long time ago. In other words, the graphical notation does not add much value in representing flow of control as compared to plain structured code.
9 You can find a critique of flowcharts in Brooks [Brooks 95]. www.newnespress.com A Crash Course in UML State Machines 87
This is not to criticize the graphical notation of statecharts. In fact, it is remarkably expressive and can scarcely be improved. Rather, I want merely to point out some shortcomings and limitations of the pen-and-paper diagrams. The UML notation and semantics are really geared toward computerized design automation tools. A UML state machine, as represented in a tool, is a not just the state diagram, but rather a mixture of graphical and textual representation that precisely captures both the state topology and the actions. The users of the tool can get several complementary views of the same state machine, both visual and textual, whereas the generated code is just one of the many available views.
2.3.15 UML State Machine Semantics: An Exhaustive Example The very rich UML state machine semantics might be quite confusing to newcomers, and even to fairly experienced designers. Wouldn’t it be great if you could generate the exact sequence of actions for every possible transition so that you know for sure what actions get executed and in which order? In this section, I present an executable example of a hierarchical state machine shown in Figure 2.11 that contains all possible transition topologies up to four levels of state nesting. The state machine contains six states: “s,” “s1,” “s11,” “s2,” “s21,” and “s211.” The state machine recognizes nine events A through I, which you can generate by typing either uppercase or lowercase letters on your keyboard. All the actions of these state machines consist only of printf() statements that report the status of the state machine to the screen. The executable console application for Windows is located in the directory
www.newnespress.com 88 Chapter 2
TERMINATE / me->foo = 0; s entry / exit / I [me->foo] / me->foo = 0; s2 entry / exit / s1 I [!me->foo] / me->foo = 1; entry / C D [!me->foo ] / exit / s21 me->foo = 1; I / C entry / s11 exit / B entry / G A exit / s211 G E entry / F exit / B
A F D D [me->foo] / H me->foo = 0; H
Figure 2.11: Hypothetical state machine that contains all possible state transition topologies up to four levels of state nesting.
(1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)
Figure 2.12: QHSMTST.EXE example application running in the command window. The line numbers in brackets to the right are added for reference. www.newnespress.com A Crash Course in UML State Machines 89
Per the semantics of UML state machines, the event G injected in line (2) is handled in the following way. First, the active state (“s211”) attempts to handle the event. However, as you can see in the state diagram in Figure 2.11, state “s211” does not prescribe how to handle event G. Therefore the event is passed on to the next higher- level state in the hierarchy, which is state “s21.” The superstate “s21” prescribes how to handle event G because it has a state transition triggered by G. State “s21” executes the actions associated with the transition (printout “s21-G;” in line (2) of Figure 2.12). Next the state executes the transition chain that exits the source state configuration and enters the target state configuration. The transition chain starts with execution of exit actions from the active state through higher and higher levels of hierarchy. Next the target state configuration is entered in exact opposite order, that is, starting from highest levels of state hierarchy down to the lowest. The transition G from state “s21” terminates at state “s1.” However, the transition chain does not end at the direct target of the transition but continues via the initial transition defined in the direct target state “s1.” Finally, a new active state is established by entering state “s11” that has no initial transition. In line (3) of Figure 2.12, you see how the statechart handles an internal transition. Event I is injected while state machine is in state “s11.” Again, the active state does not prescribe how to handle event I, so it is passed on to the next level of state hierarchy, that is, to state “s1.” State “s1” has an internal transition triggered by I defined in its internal transition compartment; therefore “s1” handles the event (printout “s1-I;” in line (3)). And at this point the processing ends. No change of state ever occurs in the internal transition, even if such transition is inherited from higher levels of state hierarchy. In the UML state machines internal transitions are different from self-transitions. Line (4) of Figure 2.12 demonstrates the difference. The state machine is in state “s11” when event A is injected. As in the case of the internal transition, the active state “s11” does not prescribe how to handle event A, so the event is passed on to the superstate “s1.” The superstate has a self-transition triggered by A and so it executes actions associated with the transition (printout “s1-A;” in line (4)). This time, however, a regular transition is taken, which requires exiting the source state configuration and entering the target state configuration. UML statecharts are extended state machines, meaning that in general, the actions executed by the state machine can depend also on the value of the extended state variables. Consider for example event D injected in line (5). The active state “s11” has
www.newnespress.com “[12.50]s[21.10]”
90 Chapter 2
transition D, but this transition has a guard [me->foo]. The variable me->foo is an extended state variable of the state machine from Figure 2.11. You can see that me->foo is initialized to 0 on the topmost initial transition. Therefore the guard [me->foo], which is a test of me->foo against 0, evaluates to FALSE. The guard condition temporarily disables the transition D in state “s11,” which is handled as though state “s11” did not define the transition in the first place. Therefore, the event D is passed on to the next higher level, that is, to state “s1.” State “s1” has transition D with a complementary guard [!me->foo]. This time, the guard evaluates to TRUE and the transition D in state “s1” is taken. As indicated in the diagram, the transition action changes the value of the extended state variable me->foo to 1. Therefore when another event D is injected again in line (6), the guard condition [me->foo] on transition D in state “s11” evaluates to TRUE and this transition is taken, as indicated in line (6) of Figure 2.12. Line (7) of Figure 2.12 demonstrates that all exit and entry actions are always executed, regardless of the exit and entry path. The main target of transition C from “s1” is “s2.” The initial transition in the main target state goes “over” the substate “s21” all the way to the substate “s211.” However, the entry actions don’t skip the entry to “s21.” This example demonstrates the powerful concept of guaranteed cleanup of the source state configuration and guaranteed initialization of the target state configuration, regardless of the complexity of the exit or entry path. Interestingly, in hierarchical state machines, the same transition can cause different sequences of exit actions, depending on which state inherits the transition. For example, in lines (8) and (9) of Figure 2.12, event E triggers the exact same state transition defined in the superstate “s.” However, the responses in lines (8) and (9) are different because transition E fires from different state configurations—once when “s211” is active (line (8)) and next when “s11” is active (line (9)). Finally, lines (11) and (12) of Figure 2.12 demonstrate that guard conditions can also be used on internal transitions. States “s2” and "s"both define internal transition I with complementary guard conditions. In line (11), the guard [!me->foo] enables internal transition in state “s2.” In line (12) the same guard disables the internal transition in “s2,” and therefore the internal transition defined in the superstate d is executed. You can learn much more about the semantics of UML state machines by injecting various events to the QHSMTST.EXE application and studying its output. Because the state machine from Figure 2.11 has been specifically designed to contain all possible state transition configurations up to level 4 of nesting, this example can “answer” virtually all
www.newnespress.com A Crash Course in UML State Machines 91 your questions regarding the semantics of statecharts. Moreover, you can use the source code that actually implements the state machine (located in
2.4 Designing a UML State Machine Designing a UML state machine, as any design, is not a strict science. Typically it is an iterative and incremental process: You design a little, code a little, test a little, and so on. In that manner you may converge at a correct design in many different ways, and typically also, more than one correct HSM design satisfies a given problem specification. To focus the discussion, here I walk you through a design of an UML state machine that implements correctly the behavior of a simple calculator similar to the Visual Basic calculator used at the beginning of this chapter. Obviously, the presented solution is just one of the many possible.
Figure 2.13: A simple electronic calculator used as a model for the statechart example.
2.4.1 Problem Specification The calculator (see Figure 2.13) operates broadly as follows: a user enters an operand, then an operator, then another operand, and finally clicks the equals button to get a result. From the programming perspective, this means that the calculator needs to parse numerical expressions, defined formally by the following BNF grammar:
www.newnespress.com 92 Chapter 2
expression ::= operand1 operator operand2 ’=’ operand1 ::= expression | [’+’ | ’-’] number operand2 ::= [’+’ | ’-’] number number ::= {’0’ | ’1’ | ... ’9’}* [’.’ {’0’ | ’1’ | ... ’9’}*] operator ::= ’+’ | ’-’ | ’*’ | ’/’
The problem is not only to correctly parse numerical expressions, but also to do it interactively (“on the fly”). The user can provide any symbol at any time, not necessarily only the symbols allowed by the grammar in the current context. It is up to the application to ignore such symbols. (This particular application ignores invalid inputs. Often an even better approach is to actively prevent generation of the invalid inputs in the first place by disabling invalid options, for example.) In addition, the application must handle inputs not related to parsing expressions, for example Cancel (C) or Cancel Entry (CE). All this adds up to a nontrivial problem, which is difficult to tackle with the traditional event-action paradigm (see Section 2.1) or even with the traditional (nonhierarchical) FSM.
2.4.2 High-Level Design Figure 2.14 shows first steps in elaborating the calculator statechart. In the very first step (panel (a)), the state machine attempts to realize the primary function of the system (the primary use case), which is to compute expressions: operand1 operator operand2 equals... The state machine starts in the “operand1” state, whose function is to ensure that the user can only enter a valid operand. This state obviously needs some internal submachine to accomplish this goal, but we ignore it for now. The criterion for transitioning out of “operand1” is entering an operator (+, –, *, or /). The statechart then enters the “opEntered” state, in which the calculator waits for the second operand. When the user clicks a digit (0 .. 9) or a decimal point, the state machine transitions to the “operand2” state, which is similar to “operand1.” Finally, the user clicks =, at which point the calculator computes and displays the result. It then transitions back to the “operand1” state to get ready for another computation. The simple state model from Figure 2.14(A) has a major problem, however. When the user clicks = in the last step, the state machine cannot transition directly to “operand1” because this would erase the result from the display (to get ready for the first operand). We need another state “result” in which the calculator pauses to display www.newnespress.com A Crash Course in UML State Machines 93
operand1 operand1 DIGIT_0_9, POINT OPER PLUS, MINUS, EQUALS C result MULT, DIVIDE OPER opEntered operand2 EQUALS opEntered operand2 0, 1, 2, 3, 4 5, 6, 7, 8, 9, POINT DIGIT_0_9, POINT A B
Figure 2.14: The first two steps in elaborating the calculator statechart 10. the result (Figure 2.14(B)). Three things can happen in the “result” state: (1) the user may click an operator button to use the result as the first operand of a new computation (see the recursive production in line 2 of the calculator grammar), (2) the user may click Cancel (C) to start a completely new computation, or (3) the user may enter a number or a decimal point to start entering the first operand.
TIP Figure 2.14(B) illustrates a trick worth remembering: the consolidation of signals PLUS, MINUS, MULTIPLY, and DIVIDE into a higher-level signal OPER (operator). This transfor- mation avoids repetition of the same group of triggers on two transitions (from “operand1” to “opEntered” and from “result” to “opEntered”). Although most events are generated exter- nally to the statechart, in many situations it is still possible to perform simple transformations before dispatching them (e.g., a transformation of raw button presses into the calculator events). Such transformations often simplify designs more than the trickiest state and transi- tion topologies.
2.4.3 Scavenging for Reuse The state machine from Figure 2.14(B) accepts the C (Cancel) command only in the result state. However, the user expects to be able to cancel and start over at any time. Similarly, the user expects to be able to turn the calculator off at any time. Statechart in Figure 2.15(A) adds these features in a naı¨ve way. A better solution is to factor
10 In this section, I am using a shorthand notation to represent many transitions with the same source and target as just one transition arrow with a multiple triggers.
www.newnespress.com 94 Chapter 2
C OFF on operand1 DIGIT_0_9, operand1 OPER C C C POINT DIGIT_0_9, POINT OPER OPER C OPER result result EQUALS opEntered OFF EQUALS opEntered operand2 OFF operand2 OFF DIGIT_0_9, POINT OFF DIGIT_0_9, POINT A B
Figure 2.15: Applying state nesting to factor out the common Cancel transition (C). out the common transition into a higher-level state named On and let all substates reuse the Cancel and Off transitions through behavioral inheritance, as shown in Figure 2.15(B).
2.4.4 Elaborating Composite States The states “operand1” and “operand2” need submachines to parse floating-point numbers. Figure 2.16 refers to both these states simultaneously as “operandX” state.
DIGIT_0 / clear(); DIGIT_1_9 (keyId) / clear(); POINT / clear(); insert(keyId); insert(‘0’); insert(‘.’); operandX
zeroX intX fracX DIGIT_0 /; DIGIT_0, DIGIT_1_9 (keyId) / DIGIT_0, DIGIT_1_9 (keyId) / insert(keyId); insert(keyId); POINT / ; DIGIT_1_9 (keyId) / insert(keyId); POINT / insert(‘.’); POINT / insert(‘.’);
Figure 2.16: Internal submachine of states “operand1” and “operand2.”
These submachines consist of three substates. The “zero” substate is entered when the user clicks 0. Its function is to ignore additional zeros that the user may try to enter (so that the calculator displays only one 0). Note my notation for explicitly ignoring an www.newnespress.com A Crash Course in UML State Machines 95 event. I use the internal transition (DIGIT_0 in this case) followed by an explicitly empty list of actions (a semicolon in C). The function of the “int” substate is to parse integer part of a number. This state is entered either from outside or from the “zero” peer substate (when the user clicks 1 through 9). Finally, the substate “frac” parses the fractional part of the number. It is entered from either outside or both peer substates when the user clicks a decimal point ( . ). Again, note that the “frac” substate explicitly ignores the decimal point POINT event, so that the user cannot enter multiple decimal points in the fractional part of a number.
2.4.5 Refining the Behavior The last step brings the calculator statechart to the point at which it can actually compute expressions. However, it can handle only positive numbers. In the next step, I will add handling of negative numbers. This turns out to be perhaps the toughest problem in this design because the same button, – (minus), represents in some contexts the binary operator of subtraction and sometimes the unary operator of negation. There are only two possible contexts in which – can unambiguously represent the negation rather than the subtraction: (1) in the “opEntered” state (as in the expression: 2 * À2 = ), and (2) at the beginning of a new computation (as in the expression: À2*2=).
OPER [keyId == ‘-‘] OPER [keyId == ‘-‘] EQUALS
negated2 result begin negated1 OPER [keyId == ‘-‘] / ; OPER [keyId == ‘-‘] / ;
opEntered 0, 1_9, 0, 1_9, POINT 0, 1_9, 0, 1_9, POINT POINT POINT EQUALS 0, 1_9, POINT operand2 operand1 A B
Figure 2.17: Two cases of handling negative numbers.
The solution to the first case (shown in Figure 2.17(A)) is simpler. We need one more state “negated2,” which is entered when the operator is MINUS (note the use of the guard). Upon entry, this state sets up the display to show –0 and subsequently does not clear the display when transitioning to the “operand2” state. This is a different behavior
www.newnespress.com 96 Chapter 2 from “opEntered” because in this latter state the display must be cleared to prepare for entering of the second operand. The second case in which – represents the negation is trickier because the specification “beginning of new computation” is much more subtle. Here it indicates the situation just after launching the application or after the user clicks Cancel but not when the calculator displays the result from the previous computation. Figure 2.17(B) shows the solution. A new state “begin” is created to capture the behavior specific to the “beginning of new computation” (note the initial transition pointing now to “begin” rather than to “operand1”). The rest of the solution is analogous as in the first case, except now the state “begin” plays the role of “opEntered.”
2.4.6 Final Touches The calculator is almost ready now. The final touches (which I leave as an exercise) include adding Cancel-Entry transitions in appropriate contexts and adding an “error” state to capture overflows and division by zero. Figure 2.18 shows the final calculator state diagram. The actual C++ implementation of this state machine will be described in Chapter 4.
2.5 Summary The main challenge in programming event-driven systems is to identify the appropriate actions to execute in response to a given event. In general, the actions are determined by two factors: by the nature of the event and by the current context (i.e., by the sequence of past events in which the system was involved). The traditional techniques, such as the event- action paradigm, neglect the context and result in code riddled with a disproportionate amount of convoluted conditional logic that programmers call “spaghetti” code. Techniques based on state machines are capable of achieving a dramatic reduction of the different paths through the code and simplification of the conditions tested at each branching point. A state machine makes the event handling explicitly dependent on both the nature of the event and on the context (state) of the system. States are “chunks of behavior,” whereas the event-action paradigm is applied locally within each state. The concept of state is a very useful abstraction of system history, capable of capturing only relevant sequences of stimuli (and ignoring all irrelevant ones). In extended state machines (state machines with “memory”), state corresponds to qualitative aspects of system behavior, whereas extended state variables (program memory) correspond to the quantitative aspects. www.newnespress.com A Crash Course in UML State Machines 97
C OFF
on entry / exit / OPER [e->keId == '-'] ready
result begin negated1 CE entry / negate(); OPER [‘-‘] / ;
DIGIT_0 DIGIT_1_9 POINT DIGIT_0 DIGIT_1_9 CE POINT OPER operand1
zero1 int1 frac1 DIGIT_0 / ; DIGIT_0, DIGIT_1_9 / DIGIT_0, DIGIT_1_9 / insert(); insert(); POINT / ; POINT POINT DIGIT_1_9 [else]
error [error] OPER OPER [e->keId == '-']
[error] opEntered negated2 [else] CE entry / negate(); OPER [‘-‘] / ;
DIGIT_0 DIGIT_1_9 POINT DIGIT_0 DIGIT_1_9 CE POINT
operand2
zero2 int2 frac2 DIGIT_0 / ; DIGIT_0, DIGIT_1_9 / DIGIT_0, DIGIT_1_9 / OPER insert(); insert(); POINT / ; EQUALS POINT DIGIT_1_9 POINT
Figure 2.18: The final calculator statechart.
www.newnespress.com 98 Chapter 2
An event is a type of instantaneous occurrence that can cause a state machine to perform actions. Events can have parameters, which convey the quantitative information regarding that occurrence. Upon reception of an event instance, a state machine responds by performing actions (executing code). The response might include changing state, which is called a state transition. Classical FSMs have two complementary interpretations of actions and transitions. In Mealy automata, actions are associated with transitions, whereas in Moore automata, actions are associated with states. State machine formalisms universally assume the run-to-completion (RTC) execution model. In this model, all actions triggered by an event instance must complete before the next event instance can be dispatched to the state machine, meaning that the state machine executes uninterruptible steps (RTC steps) and starts processing each event in a stable state configuration. UML state machines are an advanced formalism for specifying state machines, which extends the traditional automata theory in several ways. The UML state machine formalism is a variant of extended state machines with characteristics of both Mealy and Moore automata. UML state machines include notations of nested hierarchical states and orthogonal regions as well as extending the notation of actions. The most important innovation of UML state machines over classical FSMs is the introduction of hierarchically nested states. The value of state nesting lies in avoiding repetitions, which are inevitable in the traditional “flat” FSM formalism. The semantics of state nesting allow substates to define only the differences in behavior from the superstates, thus promoting sharing and reuse of behavior. The relation between a substate and its superstate has all the characteristics of inheritance and is called behavioral inheritance in this book. Behavioral inheritance is as fundamental as class inheritance and allows building whole hierarchies of states, which correspond to class taxonomies in OOP. Properly designed state hierarchies comply with the Liskov Substitution Principle (LSP) extended for states. UML state machines support state entry and exit actions for states, which provide the means for guaranteed initialization and cleanup, very much as constructors and destructors do for classes. Entry actions are always executed starting with the outermost state, which is analogous to class constructors executed from the most general class. The exit actions, similar to destructors, are always executed in exact reverse order. Entry and exit actions combined with state nesting complicate transition sequence. The precise semantics of state transitions can be confusing. The exhaustive example www.newnespress.com A Crash Course in UML State Machines 99
QHSMTST.EXE discussed in this chapter can precisely “answer” virtually all your questions regarding the semantics of state machine execution. Statecharts were first invented as a visual formalism; therefore, they are heavily biased toward graphical representation. However, it is important to distinguish the underlying concept of the HSM from the graphical notation. It is also important to distinguish between statecharts and flowcharts. Designing effective UML state machines is not trivial and, as with most designs, typically requires incremental, iterative process. Reuse does not come automatically, but you must actively look for it. Chapter 5 presents a mini-catalog of proven, effective state machine designs called state patterns. CHAPTER 3 Standard State Machine Implementations
An expert is a man who has made all the mistakes which can be made, in a narrow field. — Niels Bohr
This chapter discusses standard state machine implementation techniques, which you can find in the literature or in the working code. They are mostly applicable to the traditional nonhierarchical extended finite state machines (FSMs) because hardly any standard implementations of hierarchical state machines (HSMs) are intended for manual coding.1 Typical implementations of state machines in high-level programming languages, such as C or C++, include: The nested switch statement The state table The object-oriented State design pattern Other techniques that are often a combination of the above
1 Design automation tools often use standard state machine implementation techniques to generate code for hierarchical state machines. The resulting code, however, is typically intended not for manual maintenance but rather to be regenerated every time you change the state diagram inside the tool.
www.newnespress.com 102 Chapter 3
3.1 The Time-Bomb Example To focus the discussion and allow meaningful comparisons, in all following state machine implementation techniques I’ll use the same time-bomb example. As shown Figure 3.1, the time bomb has a control panel with an LCD that shows the current value of the timeout and three buttons: UP, DOWN, and ARM. The user begins with setting up the time bomb using the UP and DOWN buttons to adjust the timeout in one-second steps. Once the desired timeout is selected, the user can arm the bomb by pressing the ARM button. When armed, the bomb starts decrementing the timeout every second and explodes when the timeout reaches zero. An additional safety feature is the option to defuse an armed bomb by entering a secret code. The secret defuse code is a certain combination of the UP and DOWN buttons terminated with the ARM button press. Of course, the defuse code must be correctly entered before the bomb times out.
LCD showing the timeout value
UP button ARM button DOWN button
Figure 3.1: Time bomb controller user interface.
Figure 3.2 shows FSM that models the time-bomb behavior. The following explanation section illuminates the interesting points. (1) The initial transition initializes the me->timeout extended state variable and enters the “setting” state. (2) If the timeout is below the 60-second limit, the internal transition UP in state “setting” increments the me->timeout variable and displays it on the LCD. (3) If the timeout is above the 1 second limit, the internal transition DOWN in state “setting” decrements the me->timeout variable and displays it on the LCD. (4) The transition ARM to state “timing” clears the me->code extended state variable to make sure that the code for defusing the bomb is wiped clean before entering the “timing” state. www.newnespress.com Standard State Machine Implementations 103
(5) The internal transition UP in state “timing” shifts the me->code variable and inserts the 1-bit into the least-significant-bit position. (6) The internal transition DOWN in state “timing” just shifts the me->code variable and leaves the least-significant-bit at zero. (7) If the entered defuse code me->code matches exactly the secret code me->defuse given to the bomb in the constructor, the transition ARM to state “setting” disarms the ticking bomb. Note that the “setting” state does not handle the TICK event, which means that TICK is ignored in this state. (8) The handling of the most important TICK event in state “timing” is the most complex. To make the time-bomb example a little more interesting, I decided to generate the TICK event 10 times per second and to include an event parameter fine_time with the TICK event. The fine_time parameter contains the fractional part of a second in 1/10 s increments, so it cycles through 0, 1, .., 9 and back to 0. The guard [e->fine_time == 0] checks for the full-second rollover condition, at which time the me->timeout variable is decremented and displayed on the LCD. (9) The choice pseudostate is evaluated only if the first segment of the TICK transition fires. If the me->timeout variable is zero, the transition segment is executed that calls the BSP_boom() function to trigger the destruction of the bomb (transition to the final state). (10) Otherwise the choice pseudostate selects the transition back to “timing.”
(1) (8) TICK (fine_time) [e->fine_time == 0] / / me->timeout = INIT_TIMEOUT; --me->timeout; BSP_display(me->timeout); setting timing UP [me->timeout < 60] / (2) UP / (5) ++me->timeout; me->code <<= 1; (10) (4) BSP_display(me->timeout); ARM / me->code |= 1; [else] me->code = 0; (9) DOWN [me->timeout > 1] / (3) DOWN / (5)(6) [me->timeout == 0] / --me->timeout; me->code <<= 1; BSP_boom(); BSP_display(me->timeout); (7) ARM [me->code == me->defuse]
Figure 3.2: UML state diagram representing the time-bomb state machine.
www.newnespress.com 104 Chapter 3
3.1.1 Executing the Example Code Every state machine implementation technique described in this chapter comes with the complete source code for the time-bomb example and the precompiled executable program. The C version of the code is located in the directory
Timeout adjusted Bomb armed
Bomb defused
Bomb armed
Bomb destroyed
Figure 3.3: Time-bomb example running in the Turbo C++ IDE.
www.newnespress.com Standard State Machine Implementations 105
NOTE Because all the examples in this chapter are simple console applications, they can easily be compiled with just about any C/C++ compiler for your favorite desktop workstation, includ- ing Linux.2 The code accompanying this book provides the project files for the legacy Turbo C++ 1.01 compiler — the same one that I’ve used in Chapter 1. You can run the generated executables in any variant of Windows.
You generate events by pressing appropriate keys on the keyboard (see the legend of recognized keypresses at the top of the output window in Figure 3.3.). The time bomb responds by printing every event it receives (the TICK event is represented as T)as well as every update to the LCD, which is shown in square brackets. The application terminates automatically when the bomb explodes. You can also quit the program at any time by pressing the Esc key.
3.2 A Generic State Machine Interface The majority of published state machine code presents state machines intimately intertwined with a specific concurrency model and a particular event-passing method. For example, embedded systems engineers3 often present their state machines inside polling loops or interrupt service routines (ISRs) that extract events and event parameters directly from hardware registers or global variables. GUI programmers are typically more disciplined in this respect because the GUI system already provides a consistent interface, but then again GUI programmers seldom use state machines, as demonstrated in the Visual Basic Calculator example in Chapter 2. However, it is far better to separate the state machine code from a particular concurrency model and to provide a flexible and uniform way of passing events with arbitrary parameters. Therefore, implementations in this chapter use a simple and
2 The Linux platform requires slightly different techniques for interacting with the console, but the state machine code can be exactly the same. 3 Judging by 20 years of articles (1988–2008) published on the subject in Embedded Systems Design magazine (formerly Embedded Systems Programming).
www.newnespress.com 106 Chapter 3 generally applicable interface to a state machine.4 The interface I propose consists of just two functions: init(), to trigger the top-level initial transition in a state machine, and dispatch(), to dispatch an event to the state machine. In this simple model, a state machine is externally driven by invoking init() once and dispatch() repetitively, for each event.
3.2.1 Representing Events To nail down the signature of the dispatch() function, we need a uniform representation of events. Again, this is where the standard approaches vary the most. For example, the GUI systems, such as Microsoft Windows, provide only events with fixed sets of event parameters that are passed to the WinMain() function and thus are not generally applicable outside the GUI domain (after all, the most complex event parameters that a GUI needs to handle are the parameters of the mouse-click event). As described in Chapter 2, events consist really of two parts: the signal of the event conveys the type of the occurrence (such as arrival of the time tick), and event parameters convey the quantitative information about the occurrence (such as the fractional part of a second in the time tick event). In event-driven systems, event instances are frequently passed around, placed in event queues, and eventually consumed by state machines. Consequently, it is very convenient to represent events as event objects that combine the signal and the event parameters into one entity. The following Event structure represents event objects. The scalar data member sig contains the signal information, which is an integer number that identifies the event, such as UP, DOWN, ARM, or TICK:
typedef struct EventTag { uint16_t sig; /* signal of the event */ /* add event parameters by derivation from the Event structure... */ } Event;
4 Because of the special nature, the object-oriented State design pattern uses a different interface for dispatching events. www.newnespress.com Standard State Machine Implementations 107
NOTE Throughout this book I use the following standard exact-width integer types (WG14/N843 C99 Standard, Section 7.18.1.1) [ISO/IEC 9899:TC2]:
Exact Size Unsigned Signed
8 bits uint8_t int8_t
16 bits uint16_t int16_t
32 bits uint32_t int32_t
If your (prestandard) compiler does not provide the
You can add arbitrary event parameters to an event in the process of “derivation of structures” described in the sidebar “Single Inheritance in C” (Chapter 1, Section 1.6). For example, the following TickEvt structure declares an event wit h the p ara m ete r fine_time used i n the TICK(fine_t ime) event (se e Figure 3.2(8)).
typedef struct TickEvtTag { Event super; /* derive from the Event structure */ uint8_t fine_time; /* the fine 1/10 s counter */ } TickEvt;
As shown in Figure 3.4, such nesting of structures always aligns the data member super at the beginning of every instance of the derived structure. In particular, this alignment lets you treat a pointer to the derived TickEvt structure as a pointer to the Event base structure. Consequently, you can always safely pass a pointer to TickEvt to any C function that expects a pointer to Event. With this representation of events, the signature of the dispatch() function looks as follows:
void dispatch(StateMachine *me, Event const *e);
www.newnespress.com 108 Chapter 3
The first argument ‘StateMachine *me’ is the pointer to the state machine object. Different state machine implementation techniques will define the StateMachine structure differently. The second argument ‘Event const *e’ is the pointer to the event object, which might point to a structure derived from Event and thus might contain arbitrary event parameters (see Figure 3.4(B)). This interface becomes clearer when you see how it is used in the concrete implementation techniques.
e typedef struct EventTag { Instance of the Event uint16_t sig; base struct sig : uint16_t . . . super } Event; B C typedef struct TickEvtTag { Members Event super; added in A TickEvt uint8_t fine_time; the derived } TimeEvt; struct fine_time : uint8_t
Figure 3.4: Adding parameters to events in the process of derivation of structures (inheritance). 3.3 Nested Switch Statement Perhaps the most popular and straightforward technique of implementing state machines is the nested switch statement, with a scalar state variable used as the discriminator in the first level of the switch and the signal of the event used in the second level.
3.3.1 Example Implementation Listing 3.1 shows a typical implementation of the time bomb FSM from Figure 3.2. The explanation section immediately following the listing illuminates the interesting points.
Listing 3.1 Time bomb state machine implemented using the nested switch statement technique (see file bomb1.c)
(1) enum BombSignals { /* all signals for the Bomb FSM */ UP_SIG, DOWN_SIG, ARM_SIG, TICK_SIG }; /*...... */ www.newnespress.com Standard State Machine Implementations 109
(2) enum BombStates { /* all states for the Bomb FSM */ SETTING_STATE, TIMING_STATE }; /*...... */ (3) typedef struct EventTag { (4) uint16_t sig; /* signal of the event */ /* add event parameters by derivation from the Event structure... */ } Event;
(5) typedef struct TickEvtTag { (6) Event super; /* derive from the Event structure */ (7) uint8_t fine_time; /* the fine 1/10 s counter */ } TickEvt; /*...... */ (8) typedef struct Bomb1Tag { /* the Bomb FSM */ (9) uint8_t state; /* the scalar state-variable */ (10) uint8_t timeout; /* number of seconds till explosion */ (11) uint8_t code; /* currently entered code to disarm the bomb */ (12) uint8_t defuse; /* secret defuse code to disarm the bomb */ } Bomb1; /* macro for taking a state transition */ (13) #define TRAN(target_) (me->state = (uint8_t)(target_)) /*...... */ (14) void Bomb1_ctor(Bomb1 *me, uint8_t defuse) { /* the "constructor" */ (15) me->defuse = defuse; /* the defuse code is assigned at instantiation */ } /*...... */ (16) void Bomb1_init(Bomb1 *me) { /* initial transition */ (17) me->timeout = INIT_TIMEOUT;/* timeout is initialized in initial tran. */ (18) TRAN(SETTING_STATE); } /*...... */ (19) void Bomb1_dispatch(Bomb1 *me, Event const *e) { /* dispatching */ (20) switch (me->state) { (21) case SETTING_STATE: { (22) switch (e->sig) { (23) case UP_SIG: { /* internal transition with a guard */ (24) if (me->timeout < 60) { /* guard condition */ (25) ++me->timeout; (26) BSP_display(me->timeout); } (27) break; } case DOWN_SIG: { /* internal transition with a guard */ if (me->timeout > 1) { --me->timeout;
Continued onto next page
www.newnespress.com 110 Chapter 3
BSP_display(me->timeout); } break; } case ARM_SIG: { /* regular transition */ me->code = 0; /* transition action */ (28) TRAN(TIMING_STATE); /* transition to "timing" */ break; } } (29) break; } case TIMING_STATE: { switch (e->sig) { case UP_SIG: { me->code <<= 1; me->code j=1; break; } case DOWN_SIG: { me->code <<= 1; break; } case ARM_SIG: { /* regular transition with a guard */ (30) if (me->code == me->defuse) { (31) TRAN(SETTING_STATE); /* transition to "setting" */ } break; } case TICK_SIG: { (32) if (((TickEvt const *)e)->fine_time == 0) { --me->timeout; BSP_display(me->timeout); (33) if (me->timeout == 0) { BSP_boom(); /* destroy the bomb */ } else { TRAN(TIMING_STATE); } } break; } } break; } } }
www.newnespress.com Standard State Machine Implementations 111
(1) Event signals are typically represented as an enumeration. (2) States are also typically represented as an enumeration. (3) The Event structure represents signal events without parameters. (4) The scalar data member sig holds the signal. Here it’s declared as the C99- standard 16-bit unsigned integer with the dynamic range of 64K signals. (5) The TickEvt structure represents TICK events with the fine_time parameter described in the explanation to Figure 3.2(8). (6) The TickEvt structure derives from Event structure, as described in the Sidebar “Single Inheritance in C” in Chapter 1. By convention, I name the base structure member super . (7) The event parameter(s) are added after the member super . (8) The Bomb1 structure represents the time-bomb state machine implemented with the nested switch statement technique. (9) The data member state is the scalar state variable in this implementation. Here it’s declared as the C99-standard 8-bit unsigned integer with the dynamic range of 256 states. You can adjust it to suit your needs. (10-12) The data members timeout, defuse, and code are the extended state variables used in the state diagram shown in Figure 3.2 . (13) The TRAN() macro encapsulates the transition, which in this method consists of reassigning the state variable state . (14) The state machine “constructor” performs just a basic initialization but does not trigger the initial transition. In C, you need to call the “constructor” explicitly at the beginning of main(). (15) Here, the “constructor” initializes the secret defuse code, which is assigned to the bomb at instantiation. (16) This is the init() function of the generic state machine interface. Calling this function triggers the initial transition in the state machine. (17) The initial transition initializes the me->timeout extended state variable, as prescribed in the diagram in Figure 3.2(1).
www.newnespress.com 112 Chapter 3
(18) The initial transition changes the state to the “setting” state by means of the TRAN() macro. (19) This is the dispatch() function of the generic state machine interface. Calling this function dispatches one event to the state machine. (20) The first level of switch statement discriminates based on the scalar state variable me->state . (21) Each state corresponds to one case statement in the first level of the switch . (22) Within each state the second level of switch discriminates based on the event signal e->sig. (23) For example, the internal transition UP is coded as a nested case statement. (24) The guard condition (see Figure 3.2(2)) is coded by means of the if statement. (25,26) The actions associated with the transition are coded directly. (27) Handling of each event case must be terminated with the break statement. (28) A state transition is coded by reassigning the state variable, here achieved by the TRAN() macro. (29) Handling of each state case must be terminated with the break statement. (30,31) A regular transition with a guard is coded with an if statement and TRAN() macro. (32) Events with parameters, such as the TICK event, require explicit casting from the genericbasestructureEvent to the specific derived structure TickEvt,inthiscase. (33) The choice pseudostate is coded as an if statement that tests all the outgoing guards of the choice point.
3.3.2 Consequences The nested switch statement implementation has the following consequences: It is simple. It requires enumerating both signals and states. It has a small memory footprint, since only one small scalar state variable is necessary to represent the current state of a state machine. www.newnespress.com Standard State Machine Implementations 113