Chapter 1: What Is Embedded Programming? – Design Patterns for Embedded Systems in C

Chapter 1 What Is Embedded Programming?

What you will learn

  • Basics of embedded systems

  • OO versus structured programming

  • Implementing classes, inheritance, and state machines in C

This book focuses solely on embedded systems development. In doing so, it is drawing a distinction between “embedded” systems and “others.” Before we get into the depth of the discussion, we need to understand what this difference is so that we can appreciate what it forebodes with respect to the patterns and technologies we will use to develop embedded systems.

1.1 What’s Special About Embedded Systems?

This book focuses solely on embedded systems development. In doing so, it is drawing a distinction between “embedded” systems and “others.” Before we get into the depth of the discussion, we need to understand what this difference is so that we can appreciate what it forebodes with respect to the patterns and technologies we will use to develop embedded systems.

I define an embedded system as “a computerized system dedicated to performing a specific set of real-world functions, rather than to providing a generalized computing environment.” Clearly, this is a broad categorization that includes tiny 8-bit computers embedded in cardiac pacemakers, linked 32-bit computers controlling avionics, communications, fire control for aircraft, and wide-area networks composed of hundreds of powerful computer systems for battlefield management in C4ISR (Command, Control, Communications, Computers, Intelligence, Surveillance, and Reconnaissance) systems. Many embedded systems have no disks, human interface, and barely any memory but the scope of the embedded systems market is far broader than such simple devices.

Embedded systems are everywhere:

  • In the medical field, embedded systems include implantable devices (e.g., cardiac pacemakers, defibrillators, and insulin pumps), monitoring equipment (e.g., ECG/EKG monitors, blood gas monitors, blood pressure monitors, EMG monitors), imaging systems (e.g., CT, SPECT, PET, TEM, and x-ray imagers), and therapy delivery devices (e.g., patient ventilator, drug vaporizers, and infusion pumps).

  • In the telecom market, there are devices ranging from cell phones, switching systems, routers, modems, and satellites.

  • In automotive environments, embedded systems optimize engine combustion, manage power delivery in transmissions, monitor sensor data, control anti-lock braking, provide security, and offer infotainment services such as CD and DVD players, and GPS routing (in some locations, they can offer radar and laser detection and even active radar and laser countermeasures).

  • In the office, embedded systems manage phones, printers, copies, fax machines, lights, digital projectors, security systems, and fire detection and suppression systems.

  • In the home, examples include ovens, televisions, radios, washing machines, and even some vacuum cleaners.

Embedded systems already control, augment, monitor, and manage virtually every high-tech device we have from televisions to trains to factory automation, and their use is on the rise.

An important subset of embedded systems are real-time systems. Many people have the mistaken impression that “real time” means “real fast” but that is not true. A real-time system is one in which timeliness constraints must be satisfied for system correctness. A common, if simplistic, categorization of real-time systems is into two groups. “Hard” real-time systems are ones in which timeliness constraints are modeled as deadlines, points in time by which the execution of specific actions are required to be complete. “Soft” real-time systems are those that are not “hard”1 ; that is, some other (usually stochastic) measure than deadlines is used to determine timeliness. This may include average throughput, average execution time, maximum burst length, or some other measure. All systems may be modeled as hard real-time systems, but this often results in “over-designing” the system to be faster or have more available resources than is necessary, raising the recurring cost (approximately “manufacturing cost”) of the system.

Even though all systems may be modeled as hard real-time systems, in actual fact, most are not. If the response is occasionally delayed or even an entire input event is missed, most systems will continue to function properly. The primary reason for modeling real-time systems as “hard” is because it eases the assurance of the system’s timeliness through mathematical analysis.

1.1.1 Embedded Design Constraints

From the inside, one of the most striking characteristics of embedded systems is severity of their constraints. Unlike writing software for a general-purpose computer, an embedded system is usually shipped already integrated with all the hardware it needs. The hardware platform is not usually user-extensible, so resources such as memory, power, cooling, or computing power contribute to the per-unit cost (known as recurring cost). To maintain profitability, there is almost always tremendous pressure on the developer to minimize the use of such hardware resources. This means that embedded systems often require additional optimization efforts far beyond that required for desktop applications.

Beyond the need to minimize hardware, performance concerns are often critical to the success of a system. There are many aspects to performance, and different systems value these aspects differently. In some systems, throughput is a critical criterion. Throughput is normally measured in terms of the number of transactions, samples, connections, or messages that can be processed per unit time. In other systems, handling each request as quickly as possible is more important, a quality known as responsiveness, usually captured as a worst case execution time. Other systems value predictability of performance over maximum throughput or responsiveness. Predictability is usually measured as occurring within a range or as being drawn from a probability density function.

Reliability, robustness, and safety are other kinds of constraints levied on embedded systems. The reliability of a system is a (stochastic) measure of the likelihood that the system will deliver the correct functionality. Robustness refers to the ability of a system to deliver services properly when its preconditions (such as operating conditions or input data rates) are violated. Safety denotes the level of risk of a system, that is, the likelihood that using the system will result in an accident or loss. These concerns often require additional hardware and software measures to maintain the operation of the system within acceptable limits. For example, most embedded systems have a power on self-test (POST) as well as a periodic or continuous built-in test (BIT).

Collectively, these constraints on the system are known as the qualities of services (QoS) provided by the system. In addition to the various QoS constraints, to reduce recurring cost, it is common to create custom hardware that requires specialized device driver software.

1.1.2 The Embedded Tool Chain

Most embedded systems are created on a different system (the “host”) than they execute on (the “target”). This has a number of effects on the developer and the tools used in the creation of embedded systems. The most obvious such tool is the cross-compiler. This is a compiler that runs on the host but creates executable code that runs on a different computer and operating environment. Many real-time operating systems (RTOSs) provide their own proprietary compilers or provide customizations for open-source compilers such as GCC (Gnu Compiler Collection)2 .

A linker is a program that combines a set of executable codes together into an executable for a target. Some operating systems don’t require an explicit link step because the OS loader dynamically links the program elements as it loads into memory. This is true of Unix-style embedded operating systems but most embedded OSs require an explicit linking step. The linker often relocates the program as well, meaning that the start address is specified during the link step and assembly language jump instructions must be updated to reflect the actual starting base address.

A loader is a tool that loads the object image output from the linking step into the memory of the target environment. This may be done via a serial or network link or by burning the software image into nonvolatile memory such as Flash or EPROM. As an alternative to loading the software image on a target platform, many developers use simulators for the target that execute on their host development systems. It is common, for example, to use Z80 or 8051 simulators running on Windows to begin to run, debug, and test your software even before target boards are available.

So far, the tools mentioned in the tool chain have been limited to getting software onto the system. Beyond that, we need to ensure that the software works properly. The next set of tools are debuggers – tools that give us a great measure over the execution of the software, including the ability to step into (execute line by line) or step over (execute in entirety) functions, set breakpoints, and to examine and modify variables. These debuggers may work over standard serial and network links or over JTAG3 ports.

Modern-day Integrated Development Environments (IDEs) link together most or all of the tools in the embedded tool chain to facilitate and automate the development process. The latest industry trend is to host the IDEs in the Eclipse platform4 because of the power of the integrated environment and the availability of third-party and open-source plug-ins. The Jazz Foundation takes the Eclipse platform further to integrate management, measurement, reporting, and other tools to better support collaborative software development and delivery.

1.1.3 OS, RTOS, or Bareback?

An operating system (OS) provides a set of system and platform execution services for the application developer, especially around the management and use of target system resources. These resources include memory, concurrency units (processes, tasks, or threads), event queues, interrupts, hardware, and application programs. Most OSs do not provide any guarantees about timeliness, and desktop OSs may invoke unpredictable delays due to internal processing, memory management, or garbage collection at unforeseeable times. This unpredictability, and the fact that most desktop OSs are huge (compared to available memory), makes them unsuitable for real-time and embedded environments that are constrained in both time and resources.

A real-time operating system (RTOS) is a multitasking operating system intended for real-time and embedded applications. RTOSs are generally written to provide services with good efficiency and performance, but usually the predictability of the performance is more important than the maximum throughput. In addition, RTOSs are smaller and often less capable than desktop OSs. RTOSs don’t guarantee real-time performance but they provide an application environment so that appropriate developed applications can achieve real-time performance.

RTOSs run applications and tasks using one of three basic design schemas. Event-driven systems handle events as they arise and schedule tasks to handle the processing. Most such systems use task priority as a quantitative means by which to determine which task will run if multiple tasks are ready to run. Task priorities are most often static (i.e., specified at design time as such with rate-monotonic scheduling) but some are dynamic, varying the task priorities to account for current operating conditions (such as earliest deadline first scheduling). The other two approaches to task scheduling implement a “fairness doctrine” either by giving all tasks a periodic time slice in which to run (time-base schemas, such as round robin scheduling) or by running the task set cyclically (sequence-based schemas, such as cyclic executive scheduling).

Twenty years ago, most development houses specializing in real-time systems wrote their own proprietary RTOSs but the RTOS market has undergone a consolidation, enabling such houses to spend a greater percentage of their development efforts on their in-house specialties by buying a commercial RTOS instead.

Some of the challenges of using RTOSs include the fact that while there are some big players, such as Wind River (maker of VxWorks™) and Green Hills Software (maker of Integrity™), there are literally hundreds of RTOSs in use today, and each has its own Application Program Interface (API). Further, although RTOSs have a smaller footprint and timeliness impact than their desktop brethren, that is not the same as having no impact. Many embedded systems run on small 8-bit processors with only a few tens of kilobytes of memory – too little for a standard RTOS. Many RTOSs support a scalable set of services (based on the Microkernel Architecture Pattern5 ) but the degree of fidelity of that scalability may not meet the needs of a tightly-constrained application.

Some systems having too few resources to support both an RTOS and the actual application software opt to go bareback – that is, function without a commercial or proprietary RTOS at all. This means that the application code itself must replicate any RTOS services or functionality needed by the application. With the drop in hardware costs, systems formerly done with tiny 8-bit processors have moved up to larger 16- and 32-bit processors. This is needed to add the additional complexity of behavior required in modern embedded devices and is enabled by the lower recurring costs of parts and manufacturing.

For very simple embedded systems, there may be no explicit operating system functionality at all. The application may simply be a set of interrupt handlers that communicate via a shared resource scheme such as queuing or shared memory. Alternatively, a simple task loop implementation of a cyclic executive might suffice. Somewhat more complex systems may introduce operating system features such as memory or task management as they need them.

From one point of view, an operating system is an integrated set of design patterns that provides certain kinds of services and resources to applications. Chapter 3 in this book discusses patterns for accessing hardware resources, such as timers and memory while Chapter 4 focuses on ways of managing concurrency in your embedded system.

1.1.4 Embedded Middleware

Middleware is software that connects software components together in some way. Middleware as a term dates back to the 1968 NATO Software Engineering Conference6 . Like operating systems, there is commercial support for middleware, but in small systems it may be developed as a part of the application software. The most common middlewares for real-time and embedded systems include Common Object Request Broker Architecture (CORBA) and its many variants, and the Data Distribution Service (DDS). Both these middleware architectures are based on standards published by the Object Management Group (OMG)7 . In the case of CORBA, there are many specialized variant standards from which to choose including Real-Time CORBA, Embedded CORBA, and Minimum CORBA.

As you might guess, middleware is appropriate for larger-scale embedded applications – those consisting of multiple software components or applications that may be distributed across multiple processors and networks. Particularly when the components are to be developed by different organizations, the system will be extended by different organizations, or when the system is particularly long-lived, using standard middleware provides significant benefit to the developer

These middleware architectures, again like operating systems, are integrated collections of design patterns, such as Proxy, Data Bus, and Broker Pattern.

1.1.5 Codevelopment with Hardware

Many embedded projects involve the development of electronic and mechanical hardware simultaneously with the software. This introduces special challenges for the software developer who must develop software solely on the basis of predesign specifications of how the hardware is intended to work. All too often, those specifications are nothing more than notions of functionality, making it impossible to create the correct software until hardware is eventually delivered. Any slips in the hardware schedule directly induce slips in the software schedule, for which the software developers “take the heat.”

I am reminded of working against a hardware specification for a telemetry system in the 1980s. In this system, the hardware was supposed to pulse encode bits – eight pulses for a “0” bit and 15 pulses for a “1” bit, with a specific time period between pulses and a longer specific time between bits. I wrote the software predicated on that specification but much to my surprise8 , when the actual hardware arrived I could only intermittently communicate with the device. Despite intensive debugging, I couldn’t figure out where the software was failing – until I got out the oscilloscope, that is. With the oscilloscope I could see that when the software put a “0” bit out, the coil pulsed anywhere from six to 12 times. For a “1” bit, it pulsed anywhere from 10 to 18 times (note that the ranges for “0” and “1” bits actually overlap). The solution was to write predictor-corrector algorithm that estimated probabilities of bit error and corrected on-the-fly based on message CRC, the bit values, and their certainties. The software was certainly more complex than I had expected it to be! Such is the life of the embedded programmer.

Although much of the hardware-software integration pain can be eliminated with good specifications and early delivery of prototype hardware, much of it cannot. One of the truths of software development is that reengineering is a necessary consequence of codevelopment.

Agile methods address this concern in a couple of different ways9 . First, with agile methods, software is constantly being developed throughout the software lifecycle. Each day, software is being written, debugged, unit tested, and delivered. And each day, different software units are integrated together and tested as a cohesive whole in a workflow known as continuous integration. This allows software defects to be found as early as possible. This integration can (and should) include early hardware as well.

1.1.6 Debugging and Testing

The Prussian general von Clausewitz wrote “Everything in war is very simple, but the simplest thing is very difficult10 .” He might as well have said it about software!

The simplest thing that is very difficult about software isn’t writing the software – it’s writing the right software with the right functionality. The state of the art in developing defect-free software is an agile practice known as test-driven development (TDD). In TDD, the unit tests for a piece of software are written simultaneously with, or even slightly before, the software it will verify. All too commonly, unit testing is skipped entirely or performed far too late to have any beneficial effect on the software.

There are many different kinds of unit tests that can be applied to software in general and embedded software in particular. These include:

  • Functional – tests the behavior or functionality of a system or system element

  • Quality of Service – tests the “performance” of a system or system element, often to measure the performance of the system or element against its performance requirements

  • Precondition tests – tests that the behavior of the system or system element is correct in the case that the preconditional invariants are met and in the case that the preconditional invariants are violated

  • Range – tests values within a data range

  • Statistical – tests values within a range by selecting them stochastically from a probability density function (PDF)

  • Boundary – tests values just at the edges of, just inside, and just outside a data range

  • Coverage – tests that all execution paths are executed during a test suite

  • Stress – tests data that exceeds the expected bandwidth of a system or system element

  • Volume – also known as “load testing” – tests the system with large amounts of data that meet or exceed its design load

  • Fault Seeding – tests in which a fault is intentionally introduced to the system to ensure the system handles it properly

  • Regression tests – normally a subset of previously passed tests to ensure that modification to a system did not introduce errors into previously correctly functioning systems

In a desktop system, unit testing software with test cases that explore all of these concerns is rare (and difficult) enough. When you embedded that software on a less-accessible target platform it becomes even more difficult. A number of different strategies can be employed to execute test cases, such as

  • “printf” testing – tests the system by writing to a file or to stdout

  • “Test buddies” – writing test fixtures that embed the test cases in their own functionality

  • Testing on host – performing most of the tests on the host platforms using a host native complier and a critical subset on the target platform using a cross compiler

  • Simulating on host – simulating the target platform on the host with cross-compiled software and retesting a critical subset on the target with the same object code

  • Commercial software testing tools – using software testing tools, such as TestRT™, LDRA™, or VectorCAST™

  • Commercial hardware-software integrated tools – this includes tools such as logic analyzers, in-circuit emulators, JTAG-compliant testing tools, and ROM emulators

Of the various kinds of tests, performance tests are usually the most difficult to adequately perform. This is because most of the common methods for executing test cases involve running addition software on the same target platform, which affects the timing and performance of the software under test. Where possible, it is clearly best to use hardware-assisted debugging with in-circuit emulators or ROM emulators for performance testing.

1.2 OO or Structured – It’s Your Choice

Structured programming is a disciplined form of software development that emphasizes two separate and distinct aspects.

On one hand, functions or procedures form the foundation of behavioral programming: a procedure is a collection of primitive actions with a single entry point that performs a coherence behavioral goal; a function is simply a procedure that returns a value. Procedures can be broken up into call trees by one procedure invoking another, permitting algorithmic decomposition. Procedures are usually synchronously invoked (i.e., called) but by adding additional means, asynchronous invocations can be invoked as well.

The other side of structured programming is the notion of data structuring. All third-generation computer languages have the notion of building complex data structures from more primitive elements, ultimately basing them on the basic types provided by the computer language. These may be homogeneous collections, such as arrays, or heterogeneous ones, as with C structs.

Object-oriented programming is based on an orthogonal paradigm. Rather than have two separate taxonomies, object-oriented programming has a single one based on the notion of a class. A class combines together both data (stored in attributes) and procedures (known as operations) that operate on that data, because they tend to be inherently tightly bound anyway. An object is an instance of a class. This makes an object equivalent to, but more powerful than, a variable in a structured language because the object provides both the values held in its attributes and the operations that manipulate them.

A structured language is a language that directly supports structured programming. In the context of this book, C is clearly a structured language. C is by far the most prevalent language for creating embedded systems and C is an inherently “structured” language. It has all the earmarks – functions, variables, and so forth. However, an interesting question arises: can object-oriented programming be done in a structured language, such as C? And even if it can be done, should it be done?

I am reminded of a similar discussion back when the most common programming languages were assembly languages. Was it possible to write structured programs in assembly code? After all, assembly languages do not provide structured language features intrinsically. As it happens, the answer is clearly, “Yes!” It is not only possible but profitable to write structured assembly language programs. It requires some discipline on the part of the programmer and decisions about the mapping of assembly language instructions to structured concepts (such as passing parameters on the stack), but it is easy enough to do.

The same is true of object-oriented programming in structured languages such as C. In this book, we will take a primarily object-based perspective in the programming (object-oriented without subclassing) because I believe there are benefits to doing so. However, the creation of object-based or object-oriented programs in C is pretty straightforward. Let’s briefly discuss the important aspects of object-oriented programming and how to implement those concepts in C.

1.2.1 Classes

A class is really nothing more than a C struct, but what is special about it is that it contains two different kinds of features: data (attributes) and behavioral (operations).

The simplest way to implement classes is simply to use the file as the encapsulating boundary; public variables and functions can be made visible in the header file, while the implementation file holds the function bodies and private variables and functions. Multiple files can be linked with «Usage» dependencies to support the call tree, such as in Figure 1-1. In this case, a “class” is represented as a pair of files and its implementation uses some features (variables or functions) of the Display class (file), in this case, the displayMsg() function.

Figure 1.1 Classes represented as files in UML

A somewhat more flexible approach uses structs within the files to represent the classes. The operations of the class are defined as functions located within the same file as the struct. To make sure that the function has access to the correct object data, we need to pass in a me pointer. This allows us to have multiple instances (objects) of the same class and make sure that the member functions work on the correct copies of the data. In addition, classes are endowed with “special” operations. A constructor creates an object of the class. An initializer (optionally) initializes the object and its attributes. A destructor destroys the class and releases the memory used.

Consider a simple class Sensor with data elements value, updateFrequency, and filterFrequency and operations, getValue(), setValue(v: int), setUpdateFreq(r:int), getUpdateFreq(), setFilterFreq(ff: int), and getFilterFreq(). The header file would look something like Code Listing 1.

Code Listing 1-1 Sensor Header File

#ifndef Sensor_H

#define Sensor_H

/*## class Sensor */

typedef struct Sensor Sensor;

struct Sensor {

int filterFrequency;

int updateFrequency;

int value;

};

int Sensor_getFilterFrequency(const Sensor* const me);

void Sensor_setFilterFrequency(Sensor* const me, int p_filterFrequency);

int Sensor_getUpdateFrequency(const Sensor* const me);

void Sensor_setUpdateFrequency(Sensor* const me, int p_updateFrequency);

int Sensor_getValue(const Sensor* const me);

Sensor * Sensor_Create(void);

void Sensor_Destroy(Sensor* const me);

The associated implement file is shown in Code Listing 2.

Code Listing 2 Sensor Implementation File

#include "Sensor.h"

void Sensor_Init(Sensor* const me) {

}

void Sensor_Cleanup(Sensor* const me) {

}

int Sensor_getFilterFrequency(const Sensor* const me) {

return me->filterFrequency;

}

void Sensor_setFilterFrequency(Sensor* const me, int p_filterFrequency) {

me->filterFrequency = p_filterFrequency;

}

int Sensor_getUpdateFrequency(const Sensor* const me) {

return me->updateFrequency;

}

void Sensor_setUpdateFrequency(Sensor* const me, int p_updateFrequency) {

me->updateFrequency = p_updateFrequency;

}

int Sensor_getValue(const Sensor* const me) {

return me->value;

}

Sensor * Sensor_Create(void) {

Sensor* me = (Sensor *) malloc(sizeof(Sensor));

if(me! = NULL)

{

Sensor_Init(me);

}

return me;

}

void Sensor_Destroy(Sensor* const me) {

if(me! = NULL)

{

Sensor_Cleanup(me);

}

free(me);

}

In this case, the file serves as the encapsulation boundary, and stylistically two files (a header .h file and an implementation .c file) group together the elements within a single class. This approach supports object-based programming very well, but doesn’t implement virtual functions in a fashion that can be easily overridden in subclasses.

An alternative approach that supports object-oriented programming is to embed function pointers within the struct itself. This will be discussed shortly in Section 1.2.3.

1.2.2 Objects

Objects are instances of classes, in the same way that a specific variable x (of type int) is an instance of its type (int). The same set of operators (such as arithmetic operators) apply to all variables of that type, but the specific values of one instance differ from another instance (say, y).

In standard C programming, complex algorithms can still be embedded in classes, but it is common that these classes are singletons, meaning that there is exactly one instance of the class in the application. A singleton Printer class, for example, may have variables such as currentPrinter and operations like print(), but the application would have only a single instance. There are still benefits to encapsulating the data used by a class with the operations that act on that data, even if there is only one instance ever running. In other cases, usually more data-centric (as opposed to algorithm- or service-centric) classes will have multiple instances.

To create an instance of a class, simply create an instance of the struct. Consider one possible main() that creates and uses two instances of the previous Sensor class, shown in Code Listing 3:

Code Listing 3 Sensor main()

#include "Sensor.h"

#include <stdlib.h>

#include <stdio.h>

int main(int argc, char* argv[]) {

Sensor * p_Sensor0, * p_Sensor1;

p_Sensor0 = Sensor_Create();

p_Sensor1 = Sensor_Create();

/* do stuff with the sensors ere */

p_Sensor0->value = 99;

p_Sensor1->value = -1;

printf("The current value from Sensor 0 is %d\n",

Sensor_getValue(p_Sensor0));

printf("The current value from Sensor 1 is %d\n",

Sensor_getValue(p_Sensor1));

/* done with sensors */

Sensor_Destroy(p_Sensor0);

Sensor_Destroy(p_Sensor1);

return 0;

}

1.2.3 Polymorphism and Virtual Functions

Polymorphism is a valuable feature of object-oriented languages. It allows for the same function name to represent one function in one context and another function in a different context. In practice, this means that when either the static or dynamic context of an element changes, the appropriate operation can be called.

Of course, the standard way to do this in C is to use conditional statements such as if or switch. The problem with the standard approach is that it quickly becomes unwieldy when many different contexts are available. Additionally, the approach requires that all possible contexts must be known when the original function is written, or at least the function must be modified to allow a new context. With polymorphic functions, prescience isn’t required. When the new context is discovered, the polymorphic function may be created and added without requiring changes to the original function.

Consider a sensor in which the actual interface may differ. The acquireValue function in one context may require accessing a memory-mapped sensor, waiting for a while and then reading a different address to acquire the value. A different context may use a port-mapped sensor. We would definitely prefer that the client of the sensor doesn’t know how the interface to the sensor works – requiring such knowledge makes the application far more fragile and difficult to maintain. With the standard C approach, we’d write code something like Code Listing 4 with the attributes whatKindOfInterface added to the Sensor struct.

Code Listing 4 Polymorphism the hard way

int acquireValue(Sensor *me) {

int *r, *w; /* read and write addresses */

int j;

switch(me->whatKindOfInterface) {

case MEMORYMAPPED:

w = (int*)WRITEADDR; /* address to write to sensor */

*w = WRITEMASK; /* sensor command to force a read */

for (j = 0;j<100;j++) { /* wait loop */ };

r = (int *)READADDR; /* address of returned value */

me->value = *r;

break;

case PORTMAPPED:

me->value = inp(SENSORPORT);

/* inp() is a compiler-specific port function */

break;

}; /* end switch */

return me->value;

};

1.2.4 Subclassing

Subclassing (known as generalization in the Unified Modeling Language [UML] or as inheritance) is a way of representing classes that are a “specialized kind of” another class. The basic rule is that the specialized class inherits the features of the more general class. That is, all of the features of the more general class (also known as the base or parent class) are also features of the specialized class (also known as the derived or child class), but the latter is allowed to both specialize and extend the more general class.

One of the key advantages of subclassing is the ability to reuse design and code. If I design a class for a specific context and want to reuse it later in a different context, I can just design and code the specializations and extensions without touching the base class. Further, in usage, I can write algorithms that manipulate an instance of a type, without regard to which subclass it might be, when that is appropriate and modify the behavior to be semantically correct with subclass when that is necessary.

By “specialize,” I mean that the specialized class can redefine operations provided by the base class. Because a subclass is a specialized form of a base class, any operations that make sense for an instance of the base class should also make sense with an instance of the derived class. However, it often happens that the implementation of that operation must be slightly different. For example, if I write the code for queuing data from a sensor, it is likely to have operations such as insert(int value), int remove(), int getSize(), and so on. If I create a specialized kind of a queue that can store large amounts of data out to a mass storage device such as a flash drive, those operations still make sense but their implementation will be different. Note that specialization refers exclusively to changing the implementation of operations and not at all to the redefinition of data.

Specialization can be easily done with the switch-case statement approach mentioned in the previous section, but it is much more versatile if function pointers are used instead. Then the designer can simply write the specialized functions and create a new constructor that points to the new functions instead of the old. That is, subclasses can override the operations by inserting pointers to different functions that provide specialized behaviors. The downside is that function pointers are a bit tricky and pointers are the leading cause of programmer-induced defects in C programs.

By “extend,” I mean that the child class will have new features, such as new attributes or operations. In the case of the data queue, this means that I can add new attributes such as the name of the data file used to store cached data to the flash drive as well as new operations such as flush() and load() operations for writing out the in-memory buffer to flash or reading data from the flash drive into the in-memory buffer, respectively. The UML class diagram in Figure 1-2 shows both the Queue and CachedQueue classes. To simplify the problem, we assume the queue is never misused (so we don’t have to handle over and underflow conditions) and we want to store a maximum of 100 integers in the queue11 .

Figure 1.2 Queue and Cached Queue

The CachedQueue is conceptually straightforward. If there is room in the insert buffer, then insert the data as with the normal queue. However, if you fill up the internal buffer, then the insert() operation will call flush() to write the insert buffer out to disk, then the internal buffer will be reset. For removing data, it is only slightly more complicated. If the internal buffer is empty, then remove() calls the load() function to read the oldest data off the disk.

Reading the UML Class diagram

The class diagram shown has a number of classic UML class diagram features. The boxes represent the classes while the lines represent relations between the classes. The class box is (optionally) divided into three segments. The upper-most segment holds the name of the class. The middle segment lists the attributes (data members), along with their types. The bottom-most segment shows the operations within the class.

Two different relations are shown in the figure. The line with the closed arrowhead is the generalization relation; the line points to the more base class. The other line is known as composition. The composition is relation that implies strong ownership, and the responsibility for creation and destruction of the instance of the class at the other end of the relation. The open arrowhead depicts navigation (the owner has a pointer to the part in this case, but not the other way around). The name near the arrowhead is the name of the pointer within the CachedQueue class. The number near the arrowhead is the multiplicity – the number of instances needed to play the role (in this case, a single instance is needed).

This book will show patterns and examples with class diagrams, even though the primary emphasis will be code representations. Appendix A has a brief overview of UML. If you’d like more detail, see any of a number of books on UML, such as my Real-Time UML 3rd Edition: Advances in the UML for Real-Time Systems (Addison-Wesley, 2004).

There is certainly more than one way to implement subclassing in C. In the approach used here, to handle specialization, we will employ the member function pointers described in the previous section. For extension, we will simply embed the base class as a struct within the child class.

For example, let’s look at the code for the queue example of Figure 1-2. Code Listing 5 is the header file for the Queue class, the structure of which should be pretty much as you expect.

Code Listing 5 Queue Header

#ifndef QUEUE_H_

#define QUEUE_H_

#define QUEUE_SIZE 10

/* class Queue */

typedef struct Queue Queue;

struct Queue {

int buffer[QUEUE_SIZE]; /* where the data things are */

int head;

int size;

int tail;

int (*isFull)(Queue* const me);

int (*isEmpty)(Queue* const me);

int (*getSize)(Queue* const me);

void (*insert)(Queue* const me, int k);

int (*remove)(Queue* const me);

};

/* Constructors and destructors:*/

void Queue_Init(Queue* const me,int (*isFullfunction)(Queue* const me),

int (*isEmptyfunction)(Queue* const me),

int (*getSizefunction)(Queue* const me),

void (*insertfunction)(Queue* const me, int k),

int (*removefunction)(Queue* const me) );

void Queue_Cleanup(Queue* const me);

/* Operations */

int Queue_isFull(Queue* const me);

int Queue_isEmpty(Queue* const me);

int Queue_getSize(Queue* const me);

void Queue_insert(Queue* const me, int k);

int Queue_remove(Queue* const me);

Queue * Queue_Create(void);

void Queue_Destroy(Queue* const me);

#endif /*QUEUE_H_*/

Code Listing 6 shows the (simple) implementation of the Queue operations and Code Listing 7 provides a simple little test program that shows elements inserted and removed into and from the queue.

Code Listing 6 Queue Implementation

#include <stdio.h>

#include <stdlib.h>

#include "queue.h"

void Queue_Init(Queue* const me,int (*isFullfunction)(Queue* const me),

int (*isEmptyfunction)(Queue* const me),

int (*getSizefunction)(Queue* const me),

void (*insertfunction)(Queue* const me, int k),

int (*removefunction)(Queue* const me) ){

/* initialize attributes */

me->head = 0;

me->size = 0;

me->tail = 0;

/* initialize member function pointers */

me->isFull = isFullfunction;

me->isEmpty = isEmptyfunction;

me->getSize = getSizefunction;

me->insert = insertfunction;

me->remove = removefunction;

}

/* operation Cleanup() */

void Queue_Cleanup(Queue* const me) {

}

/* operation isFull() */

int Queue_isFull(Queue* const me){

return (me->head+1) % QUEUE_SIZE = = me->tail;

}

/* operation isEmpty() */

int Queue_isEmpty(Queue* const me){

return (me->head = = me->tail);

}

/* operation getSize() */

int Queue_getSize(Queue* const me) {

return me->size;

}

/* operation insert(int) */

void Queue_insert(Queue* const me, int k) {

if (!me->isFull(me)) {

me->buffer[me->head] = k;

me->head = (me->head+1) % QUEUE_SIZE;

++me->size;

}

}

/* operation remove */

int Queue_remove(Queue* const me) {

int value = -9999; /* sentinel value */

if (!me->isEmpty(me)) {

value = me->buffer[me->tail];

me->tail = (me->tail+1) % QUEUE_SIZE;

--me->size;

}

return value;

}

Queue * Queue_Create(void) {

Queue* me = (Queue *) malloc(sizeof(Queue));

if(me! = NULL)

{

Queue_Init(me, Queue_isFull, Queue_isEmpty, Queue_getSize,

Queue_insert, Queue_remove);

}

return me;

}

void Queue_Destroy(Queue* const me) {

if(me! = NULL)

{

Queue_Cleanup(me);

}

free(me);

}

Code Listing 7 Queue Test Program

#include <stdio.h>

#include <stdlib.h>

#include "queue.h"

int main(void) {

int j,k, h, t;

/* test normal queue */

Queue * myQ;

myQ = Queue_Create();

k = 1000;

for (j = 0;j<QUEUE_SIZE;j++) {

h = myQ->head;

myQ->insert(myQ,k);

printf("inserting %d at position %d, size = %d\n",k--,h, myQ->getSize(myQ));

};

printf("Inserted %d elements\n",myQ->getSize(myQ));

for (j = 0;j<QUEUE_SIZE;j++) {

t = myQ->tail;

k = myQ->remove(myQ);

printf("REMOVING %d at position %d, size = %d\n",k,t, myQ->getSize(myQ);

};

printf("Last item removed = %d\n", k);

printf("Current queue size %d\n", myQ->getSize(myQ));

puts("Queue test program");

return EXIT_SUCCESS;

}

Now let us suppose that memory is very tight and so we need to reduce the size of the in-memory buffer and store most elements to disk. Our CachedQueue fits the bill.

Code Listing 8 shows the header file for the class. You can see some important aspects of the definition of the class. First, note that the base class is retained in the subclass as an attribute named queue. This brings in the operations and attributes from the base class within the subclass. Secondly, note that the operations defined for the base class are replicated as member function pointers in the subclass. In that way, they can be directly invoked on the subclass instance. Each such virtual function can then either deal entirely with the request or deal with some aspect of it and delegate the rest to the original function defined on the Queue class (still held within its virtual functions). Lastly, note that the aggregate outputQueue is also an attribute of the subclass. This allows the CachedQueue to manage two in-memory buffers. The “normal” one (represented by the base class) is where new data gets inserted. The other one (represented by the aggregated outputQueue) is where the data comes from for a remove.

Code Listing 8 CachedQueue header file

#ifndef CACHEDQUEUE_H_

#define CACHEDQUEUE_H_

#include "queue.h"

typedef struct CachedQueue CachedQueue;

struct CachedQueue {

Queue* queue; /* base class */

/* new attributes */

char filename[80];

int numberElementsOnDisk;

/* aggregation in subclass */

Queue* outputQueue;

/* inherited virtual functions */

int (*isFull)(CachedQueue* const me);

int (*isEmpty)(CachedQueue* const me);

int (*getSize)(CachedQueue* const me);

void (*insert)(CachedQueue* const me, int k);

int (*remove)(CachedQueue* const me);

/* new virtual functions */

void (*flush)(CachedQueue* const me);

void (*load)(CachedQueue* const me);

};

/* Constructors and destructors:*/

void CachedQueue_Init(CachedQueue* const me, char* fName,

int (*isFullfunction)(CachedQueue* const me),

int (*isEmptyfunction)(CachedQueue* const me),

int (*getSizefunction)(CachedQueue* const me),

void (*insertfunction)(CachedQueue* const me, int k),

int (*removefunction)(CachedQueue* const me),

void (*flushfunction)(CachedQueue* const me),

void (*loadfunction)(CachedQueue* const me));

void CachedQueue_Cleanup(CachedQueue* const me);

/* Operations */

int CachedQueue_isFull(CachedQueue* const me);

int CachedQueue_isEmpty(CachedQueue* const me);

int CachedQueue_getSize(CachedQueue* const me);

void CachedQueue_insert(CachedQueue* const me, int k);

int CachedQueue_remove(CachedQueue* const me);

void CachedQueue_flush(CachedQueue* const me);

void CachedQueue_load(CachedQueue* const me);

CachedQueue * CachedQueue_Create(void);

void CachedQueue_Destroy(CachedQueue* const me);

#endif /*CACHEDQUEUE_H_*/

Code Listing 9 shows the implementation (sans file I/O, just to simplify the example a bit). You can see how the CachedQueue_Init() function constructs the subclass instance (with a call to Queue_Create()) and then sets up its own attributes and virtual member functions. You can also see how the CachedQueue_getSize() function computes the number of elements held: it is the sum of the data held in the queue, the outputQueue, and the number of elements stored on disk. While the implementation is not quite up to shippable standards (I’d want to add error handling, for example), it does illustrate one way to create classes and instances of those classes in the C language.

Code Listing 9 CachedQueue Implementation

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include "cachedQueue.h"

void CachedQueue_Init(CachedQueue* const me, char* fName,

int (*isFullfunction)(CachedQueue* const me),

int (*isEmptyfunction)(CachedQueue* const me),

int (*getSizefunction)(CachedQueue* const me),

void (*insertfunction)(CachedQueue* const me, int k),

int (*removefunction)(CachedQueue* const me),

void (*flushfunction)(CachedQueue* const me),

void (*loadfunction)(CachedQueue* const me)) {

/* initialize base class */

me->queue = Queue_Create(); /* queue member must use its original functions */

/* initialize subclass attributes */

me->numberElementsOnDisk = 0;

strcpy(me->filename, fName);

/* initialize aggregates */

me->outputQueue = Queue_Create();

/* initialize subclass virtual operations ptrs */

me->isFull = isFullfunction;

me->isEmpty = isEmptyfunction;

me->getSize = getSizefunction;

me->insert = insertfunction;

me->remove = removefunction;

me->flush = flushfunction;

me->load = loadfunction;

}

/* operation Cleanup() */

void CachedQueue_Cleanup(CachedQueue* const me) {

Queue_Cleanup(me->queue);

}

/* operation isFull() */

int CachedQueue_isFull(CachedQueue* const me){

return me->queue->isFull(me->queue) &&

me->outputQueue->isFull(me->outputQueue);

}

/* operation isEmpty() */

int CachedQueue_isEmpty(CachedQueue* const me){

return me->queue->isEmpty(me->queue) &&

me->outputQueue->isEmpty(me->outputQueue) &&

(me->numberElementsOnDisk = = 0);

}

/* operation getSize() */

int CachedQueue_getSize(CachedQueue* const me) {

return me->queue->getSize(me->queue) +

me->outputQueue->getSize(me->outputQueue) +

me->numberElementsOnDisk;

}

/* operation insert(int) */

// Imsert Algorithm:

// if the queue is full,

// call flush to write out the queue to disk and reset the queue

// end if

// insert the data into the queue

void CachedQueue_insert(CachedQueue* const me, int k) {

if (me->queue->isFull(me->queue)) me->flush(me);

me->queue->insert(me->queue, k);

}

/* operation remove */

// remove algorithm

// if there is data in the outputQueue,

// remove it from the outputQueue

// else if there is data on disk

// call load to bring it into the outputQueue

// remove it from the outputQueue

// else if there is data in the queue

// remove it from there

// (if there is no data to remove then return sentinel value)

int CachedQueue_remove(CachedQueue* const me) {

if (!me->outputQueue->isEmpty(me->outputQueue))

return me->outputQueue->remove(me->outputQueue);

else if (me->numberElementsOnDisk>0) {

me->load(me);

return me->queue->remove(me->queue);

}

else

return me->queue->remove(me->queue);

}

/* operation flush */

// Precondition: this is called only when queue is full

// and filename is valid

// flush algorithm

// if file is not open, then open file

// while not queue->isEmpty()

// queue->remove()

// write data to disk

// numberElementsOnDisk++

// end while

void CachedQueue_flush(CachedQueue* const me){

// write file I/O statements here …

}

/* operation load */

// Precondition: this is called only when outputQueue is empty

// and filename is valid

// load algorithm

// while (!outputQueue->isFull() && (numberElementsOnDisk>0)

// read from start of file (i.e., oldest datum)

// numberElementsOnDisk-- --;

// outputQueue->insert()

// end while

void CachedQueue_load(CachedQueue* const me) {

// write file I/O statements here …

}

CachedQueue * CachedQueue_Create(void) {

CachedQueue* me = (CachedQueue *) malloc(sizeof(CachedQueue));

if(me! = NULL)

{

CachedQueue_Init(me, "C:\\queuebuffer.dat",

CachedQueue_isFull, CachedQueue_isEmpty,

CachedQueue_getSize, CachedQueue_insert, CachedQueue_remove,

CachedQueue_flush, CachedQueue_load);

}

return me;

}

void CachedQueue_Destroy(CachedQueue* const me) {

if(me! = NULL)

{

CachedQueue_Cleanup(me);

}

free(me);

}

The capability to use inheritance (subclassing) will be useful in some of the design patterns I will discuss later in this book, besides being of value all on its own.

1.2.5 Finite State Machines

A finite state machine (FSM) is a machine specified by a finite set of conditions of existence (called “states”) and a likewise finite set of transitions among the states triggered by events. An FSM differs from an activity diagram or flow chart in that the transitions are triggered by events (primarily) rather than being triggered when the work done in the previous state is complete. Statecharts are primarily used to model the behavior of reactive elements, such as classes and use cases, that wait in a state until an event of interest occurs. At that point, the event is processed, actions are performed, and the element transitions to a new state.

Actions, such as the execution of a primitive C language statement or the invocation of an operation, may be specified to be executed when a state is entered or exited, or when a transition is taken. The order of execution of actions is exit actions of the predecessor state, followed by the transition actions, followed by the entry actions of the subsequent state.

The UML uses statecharts as their formal FSM representation, which are significantly more expressive and scalable than “classical” Mealy-Moore FSMs. UML state machines, based on Dr. David Harel’s statechart semantics and notation, have a number of extensions beyond Mealy-Moore state machines, including:

  • Nested states for specifying hierarchical state membership

  • AND-states for specifying logical independence and concurrency

  • Pseudostates for annotating commonly-needed specific dynamic semantics

Figure 1-3 shows some of the basic elements of a statechart for the SecuritySupervisor class shown previously in Figure 1-1. It includes basic OR-states and transitions, as well as a few less-elementary concepts, including nested states and conditional and initial pseudostates.

Figure 1-3 Basic State Machine

Transitions are arrowed lines coming from a predecessor state and terminating on a subsequent state. Transitions usually have the optional event signature and action list. The basic form of an event signature is

event-name ‘(‘parameter-list‘)’ ‘[‘guard’]’ ‘/’ action-list

The event-name is simply the logical name of the event class that may be sent to an instance of the Classifier at run-time, such as keypress in Figure 1-3. The UML defines four distinct kinds of events that may be passed or handled:

  • SignalEvent – an asynchronously sent event

  • CallEvent – a synchronously sent event

  • TimeEvent – an event due to the passage of an interval of time (most common) or arrival of an epoch

  • ChangeEvent – a change in a state variable or attribute of the Classifier

Asynchronous event transfer is always implemented via queuing of the event until the element is ready to process it. That is, the sender “sends and forgets” the event and goes on about its business, ignorant of whether or not the event has been processed. Synchronous event transfer executes the state processing of the event in the thread of the sender, with the sender blocked from continuing until that state processing is complete. This is commonly implemented by invoking a class method called an event handler that executes the relevant part of the state machine, returning control to the sender only when the event processing is complete. Such “triggered operations” don’t have a standard method body; their method body is the action list on the state machine.

Events may have parameters – typed values accepted by the state machine which may then be used in the guard and actions in the processing of the event. The statechart specifies the formal parameter list, while the object that sends the event must provide the necessary actual parameters to bind to the formal parameter list. We will use a slightly peculiar syntax for passing events. We will create a struct named params that contains the named parameters for every event that carries data. If an event e has two parameters, x and y, to use these in a guard, for example, you would dereference the params struct to access their value. So a transition triggered by event e with a guard that specified that x must be greater than 0 and y must be less than or equal to 10 for the transition to be taken would look like:

e[params->x>0 && params->y< = 10]

Time events are almost always relative to the entry to a state. A common way to name such an event (and what we will use here) is “tm(interval),” where “interval” is the time interval parameter for the timeout event12 . If the timeout occurs before another specified event occurs, then the transition triggered by the timeout event will be taken; if another event is sent to the object prior to the triggering of the timeout, then the time is discarded; if the state is reentered, the timeout interval is started over from the beginning.

If a transition does not provide a named event trigger, then this is activated by the “completion” or “null” event. This event occurs either as soon as the state is entered (which includes the execution of entry actions for the state) or when the state activities complete.

A guard is a Boolean expression contained within square brackets that follows the event trigger. The guard should return only TRUE or FALSE and not have side effects. If a guard is specified for a transition and the event trigger (if any) occurs, then the transition will be taken if and only if the guard evaluates to TRUE. If the guard evaluates to FALSE, then the triggering event is quietly discarded and no actions are executed.

The action list for the transition is executed if and only if the transition is taken; that is, the named event is received by the object while it is in the predecessor state and the guard, if any, evaluates to TRUE. The entire set of exit actions–transition actions–entry actions is executed in that order and is executed using run-to-completion semantics. Figure 1-3 shows actions on a number of different transitions as well as on entry into some of the states.

A run of the three “classes” from Figure 1-1 using test1 from the TestDriver (which sends ‘1’, ‘2’, ‘3’, ‘4’, ‘e’ (the code for the ENTER key), followed by ‘r’ (the code for the RESET key) is shown in Figure 1-4 in a UML sequence diagram.

Figure 1-4 Running the state behavior

While I will later discuss design patterns for implementation of state machines, the most common implementation is to generate simple if-then or switch-case statements. For example, a common and easy implementation scheme is to

  • Add a state variable (e.g., activeState)

  • For each event to the “class,” add an event receptor function and pass in any data that it needs as parameters

  • Create an event dispatcher function, called by each event receptor; this processes the incoming event

The structure of the event dispatcher can be as simple as

Code Listing 10 SecuritySupervisor Event Dispatcher

switch(activeState) {

/* for each state */

case state1:

/* for each event */

Switch (eventID) {

event1:

/* check each guard */

If (guard1()) {

action1();

] else if (guard2()) {

action2();

};

break;

event2:

if (guard3()) {

action3();

} else if (guard4()) {

action5();

};

break;

};

break;

case state2:

// etc

}

For example, given the SecuritySupervisor class, you might implement the state machine event dispatcher as shown in Code Listing 10. Adding asynchronous events requires adding the queuing of the events and their data. Note: the Null_id represents so-called “null events” – transitions that are not triggered by explicit events, but simply “fire” when the actions in the previous state complete.

static eventStatus dispatchEvent(short id) {

eventStatus res = eventNotConsumed;

switch (activeState) {

/* are we in the Idle state? */

case SecuritySupervisor_Idle:

{

if(id = = Null_id) /* null triggered event? */

{

if(retries> = 3)

{

activeState = SecuritySupervisor_ErrorState;

displayMsg("ERROR: Max retries Exceeded");

res = eventConsumed;

}

else

{

++retries;

activeState = SecuritySupervisor_Accepting;

res = eventConsumed;

}

}

}

break;

/* are we in the Accepting State? */

case SecuritySupervisor_Accepting:

{

if(id = = keypress_SecuritySupervisor_Event_id)

{

/* params struct has the data in the attribute 'key' */

/* transition 1 */

if(isCANCEL(params->key))

{

retries = 0;

displayMsg("Cancelled");

activeState = SecuritySupervisor_Idle;

{

/* state ROOT.Idle.(Entry) */

strcpy(pin, "");

}

res = eventConsumed;

}

else

{

/* transition 3 */

if(isDigit(params->key))

{

/* transition 3 */

addKey(params->key);

activeState = SecuritySupervisor_Accepting;

res = eventConsumed;

}

else

{

/* transition 5 */

if(isENTER(params->key))

{

activeState =

SecuritySupervisor_CheckingLength;

res = eventConsumed;

}

}

}

}

}

break;

case SecuritySupervisor_CheckingLength:

{

if(id = = Null_id)

{

/* transition 10 */

if(strlen(pin) = = 4)

{

activeState = SecuritySupervisor_ValidatingPIN;

res = eventConsumed;

}

else

{

{

/* transition 9 */

displayMsg("ERROR: PIN wrong length");

}

activeState = SecuritySupervisor_Idle;

{

/* state ROOT.Idle.(Entry) */

strcpy(pin, "");

}

res = eventConsumed;

}

}

}

break;

case SecuritySupervisor_ValidatingPIN:

{

if(id = = Null_id)

{

/* transition 13 */

if(isValid(pin))

{

{

/* transition 13 */

unlockDoor();

displayMsg("Door unlocked");

}

activeState = SecuritySupervisor_SecurityOpen;

res = eventConsumed;

}

else

{

{

/* transition 12 */

displayMsg("ERROR:: Invalid PIN");

}

activeState = SecuritySupervisor_Idle;

{

/* state ROOT.Idle.(Entry) */

strcpy(pin, "");

}

res = eventConsumed;

}

}

}

break;

case SecuritySupervisor_SecurityOpen:

{

if(id = = keypress_SecuritySupervisor_Event_id)

{

/* params-key has the data passed with the event */

/* transition 14 */

if(isRESET(params->key))

{

{

/* transition 14 */

lockDoor(); retries = 0;

displayMsg("Door locked.");

}

activeState = SecuritySupervisor_Idle;

{

/* state ROOT.Idle.(Entry) */

strcpy(pin, "");

}

res = eventConsumed;

}

}

}

break;

default:

break;

}

return res;

}

There is certainly more to state machines than we’ve introduced so far, but those are discussed later in Chapter 5. For a more detailed discussion of UML state machines, refer to Chapter 3 of Real-Time UML 3rd Edition 13 and Chapters 7 and 12 in Doing Hard Time 14 .

1.3 What Did We Learn?

This chapter discussed the special characteristics of embedded systems – working within tight design constraints while meeting functional and quality-of-service constraints. To do this, embedded developers use tools from the embedded tool chain to develop on a more powerful host computer while targeting an often smaller and less capable target computing environment. Target environments run the gamut from small eight-bit processors with a few kilobytes of memory to networked collections of 64-bit systems. At the small scale, many systems are too tightly constrained in memory, performance, or cost to permit the use of a commercial RTOS, while at the large scale, the systems may include a heterogeneous set of operating systems, middleware, and databases. Often, embedded software must be developed simultaneously with the hardware, making it difficult to test and debug since the target environments may not exist when the software is being developed.

By far, the most common language for developing embedded systems is C. C has the advantages of high availability of compilers for a wide range of target processors and a well-deserved reputation for run-time efficiency. Nevertheless, the pressures to add capabilities while simultaneously reducing costs mean that embedded developers must be continually looking for ways to improve their designs, and their ability to design efficiently. Structured methods organize the software into parallel taxonomies, one for data and the other for behavior. Object-oriented methods combine the two to improve cohesion for inherently tightly-coupled elements and encapsulation of content when loose coupling is appropriate. While C is not an object-oriented language, it can be (and has been) used to develop object-based and object-oriented embedded systems.

The next chapter will discuss the role of process and developer workflows in development. These workflows will determine when and where developers employ design and design patterns in the course of creation of embedded software. It will go on to define what design patterns are, how they will be discussed and organized within this book, and how they can be applied into your designs.

Subsequent chapters will list a variety of design patterns that can be applied to optimize your designs. Chapter 3 provides a number of patterns that address access of hardware, such as keyboards, timers, memory, sensors, and actuators. Chapter 4 discusses concurrency patterns both for managing and executing concurrent threads as well as sharing resources among those threads. The last two chapters provide a number of patterns for the implementation and use of state machines in embedded systems: Chapter 5 provides basic state machine implementation-focused patterns, while Chapter 6 addresses the concerns of safety and high-reliability software.

1 Although, as the saying goes, “Hard real-time systems are hard. Soft real-time systems are harder.”

2 See www.gnu.org for releases and supported targets.

3 Joint Test Architecture Group, an electronic and software standard interface used by many developers for testing on embedded targets. See www.freelabs.com/%26whitis/electronics/jtag/ or www.embedded.com/story/OEG20021028S0049 for more information.

4 www.eclipse.org

5 See my book Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems (Addison-Wesley, 2002).

6 http://homepages.cs.ncl.ac.uk/brian.randell/NATO/nato1968.PDF

7 www.omg.org

8 I was naïve back then. 😉

9 See my book Real-Time Agility (Addison-Wesley, 2009) for a thorough discussion of the topic.

10 von Clausewitz, C., 1976. On War. Howard, M., Paret, P. (Eds, tr.), Princeton University Press; published originally in 1832 as Vom Kriege.

11 If you’re feeling a little ambitious, feel free to elaborate the example to handle under flow and overflow as well as use C preprocessor macros to be able to change the type and number of elements for the data being queued. Don’t worry – I’ll wait until you’re done.

12 Another common way is to use the term “after(interval).”

13 Douglass, B.P., 2004. Real-Time UML: Advances in the UML for Real-Time Systems, third ed. Addison-Wesley.

14 Douglass, B.P., 1999. Doing Hard Time: Developing Real-Time Systems with UML, Objects, Frameworks, and Patterns. Addison-Wesley.