Chapter 6: Safety and Reliability Patterns – Design Patterns for Embedded Systems in C

Chapter 6 Safety and Reliability Patterns

Chapter Outline

  1. A Little Bit About Safety and Reliability 359

    1. Safety and Reliability Related Faults 360

    2. Achieving Safety and Reliability 361

  2. One’s Complement Pattern 362

    1. Abstract 362

    2. Problem 362

    3. Pattern Structure 362

    4. Collaboration Roles 362

      1. DataClient 362

      2. OnesCompProtectedDataElement 362

    5. Consequences 363

    6. Implementation Strategies 363

    7. Related Patterns 363

    8. Example 363

  3. CRC Pattern 367

    1. Abstract 367

    2. Problem 367

    3. Pattern Structure 368

    4. Collaboration Roles 368

      1. computeCRCData Function 368

      2. CRCProtectedData 368

      3. DataClient 368

      4. DataType 368

    5. Consequences 369

    6. Implementation Strategies 369

    7. Related Patterns 369

    8. Example 369

  4. Smart Data Pattern 380

    1. Abstract 380

    2. Problem 381

    3. Pattern Structure 381

    4. Collaboration Roles 382

      1. ErrorCodeType 382

      2. ErrorManager 382

      3. ServerClass 382

      4. SmartDataType 382

    5. Consequences 382

    6. Implementation Strategies 383

    7. Related Patterns 383

    8. Example 383

  5. Channel Pattern 395

    1. Abstract 395

    2. Problem 395

    3. Pattern Structure 395

    4. Collaboration Roles 395

      1. AbstractDataTransform 395

      2. ActuatorDeviceDriver 396

      3. ConcreteDataTransform 396

      4. SensorDeviceDriver 396

    5. Consequences 396

    6. Implementation Strategies 397

    7. Related Patterns 397

    8. Example 397

  6. Protected Single Channel Pattern 402

    1. Abstract 402

    2. Problem 402

    3. Pattern Structure 403

    4. Collaboration Roles 404

      1. AbstractDataTransform 404

      2. AbstractTransformChecker 404

      3. ActuatorDeviceDriver 404

      4. ConcreteDataTransform 404

      5. ConcreteTransformChecker 404

      6. SensorDeviceDriver 404

    5. Implementation Strategies 405

    6. Related Patterns 405

    7. Example 405

  7. Dual Channel Pattern 413

    1. Abstract 413

    2. Problem 414

    3. Pattern Structure 414

    4. Collaboration Roles 414

      1. AbstractDataTransform 414

      2. AbstractTransformChecker 414

      3. ActuatorDeviceDriver 415

      4. Channel 416

      5. ConcreteDataTransform 416

      6. ConcreteTransformChecker 417

      7. SensorDeviceDriver 417

    5. Consequences 417

    6. Implementation Strategies 417

    7. Related Patterns 417

      1. Homogeneous Redundancy Pattern 417

      2. Heterogeneous Redundancy Pattern 418

      3. Triple Modular Redundancy (TMR) Pattern 418

      4. Sanity-Check Pattern 419

      5. Monitor-Actuator Pattern 420

    8. Example 421

  8. Summary 422

Patterns in this chapter

  • One’s Complement Pattern – Adds a bitwise inverted copy of primitive data elements to identify when data is corrupted in vivo

  • CRC Pattern – Adds a cyclic redundancy check to identify when bits of the data have been corrupted in vivo

  • Smart Data Pattern – Adds behavior to the data to ensure that the data’s preconditions and constraints are adhered to

  • Channel Pattern – Arranges the processing of sensory data as a series of transformational steps to provide a unit of large-scale redundancy

  • Protected Single Channel Pattern – Elaborates the channel pattern by adding data and processing validation checks at various points within the single channel

  • Dual Channel Pattern – Creates multiple channels to aid in the identification of errors and failures and, optionally, allows continuation of service in the presence of such as fault

Reliability is a measure of the “up-time” or “availability” of a system – specifically, it is the probability that a computation will successfully complete before the system fails. Put another way, it is a stochastic measure of the probability that the system can deliver a service. It is normally estimated with Mean Time Between Failure (MTBF) or a probability of successful service delivery (e.g., 0.9999). Reliability analysis is normally managed within Fault Means and Effect Analysis (FMEA) such as shown in Figure 6.1.

6.1 A Little Bit About Safety and Reliability

Reliability is a measure of the “up-time” or “availability” of a system – specifically, it is the probability that a computation will successfully complete before the system fails. Put another way, it is a stochastic measure of the probability that the system can deliver a service. It is normally estimated with Mean Time Between Failure (MTBF) or a probability of successful service delivery (e.g., 0.9999). Reliability analysis is normally managed within Fault Means and Effect Analysis (FMEA) such as shown in Figure 6.1.

Figure 6-1 FMEA example

Safety is distinct from reliability. A safe system is one that does not incur too much risk to persons or equipment. A hazard is an event or condition that leads to an accident or mishap – a loss of some kind, such as injury or death. The risk associated with a hazard is the product of the severity of the hazard and its likelihood.

Hazards arise in five fundamental ways:

  • Release of energy

  • Release of toxins

  • Interference of life support functions

  • Supplying misleading information to safety personnel or control systems

  • Failure to alarm when hazardous conditions arise

6.1.1 Safety and Reliability Related Faults

Accidents can occur either because the system contains errors or because some aspect of the system fails. An error is a systematic fault and may be due to incorrect requirements, poor design or poor implementation. An error is always present in the system even though it may not be visible. When an error is visible, it is said to be manifest. Failures are different in that they occur at some point in time and result in a failed state. That is, the system worked properly before but something changed, such as a metal rod breaking in the drive train of your car (a persistent failure) or a bit was flipped in your car’s engine control computer but is repaired as the system runs (a volatile failure). A fault is either an error or a failure.

I make this distinction about the different kinds of faults because the kinds of mitigation behavior for the two kinds is different. Specifically, failures can be addressed by homogeneous redundancy – multiple replicas of the processing channel (a set of software elements). If you assume the system is safe enough if it remains safe in the case of a single point failure, then having an identical backup channel can address such a failure. However, if the fault is an error, then all identical copies of the channel will exhibit the same fault. In this case, you must have heterogeneous redundancy (also known as n-way programming) – multiple channels that provide the service using different code and/or designs-in order to continue functioning safely.

Faults have relations to various elements in your system. Behavior around faults may be specified in requirements. An element can be the source of the fault. Another element may be the identifier of the fault. Yet another element may mitigate or extenuate the fault.

I believe that there is no such thing as “safe software” because it is the composite system – the electronics, mechanical, and software aspects working together in their operational environment – that is safe or unsafe. Hardware faults can be mitigated with software and vice versa. The system as a whole is safe or unsafe. Similarly, the reliability of a system is a function of all its constituent parts, since other parts can be designed to continue to provide services normally provided by failed parts. There is extensive literature on how to assess safety and reliability and how to construct safe and reliable systems, but a detailed discussion of those topics is beyond the scope of this book. More information can be had in my Real-Time UML Workshop for Embedded Systems or my Real-Time Agility books1 as well as many other sources.

6.1.2 Achieving Safety and Reliability

In general, all safety-critical systems and high-reliability systems must contain and properly manage redundancy to achieve their safety and reliability requirements. How these elements are managed is different depending on the particular mix of safety and reliability concerns. This redundancy can be “redundancy-in-the-large” (architectural redundancy) or “redundancy-in-the-small” (detailed design redundancy). In this chapter we will consider a few patterns of both varieties. More safety-related architectural patterns can be found in my book Real-Time Design Patterns 2 .

In the safety and reliability patterns in this chapter, there are three kinds of related elements. Fault Manifestors are elements that can, if they contain a fault, lead to a safety or reliability concern. Fault Identifiers are elements that can identify when a failure has occurred or when an error has become manifest. Fault Extenuators are elements that extenuate (reduce) a fault by either making the fault-related hazard less severe or by making the manifestation of the fault at the system level less likely. The patterns in this chapter differ in how these elements are organized and managed. Feedforward error correction patterns address the concern by providing enough redundancy to identify and deal with the fault and continue processing. Feedback error correction patterns address the faults by repeating one or more computational steps. Fault-safe state patterns address safety by going to a fault-safe state, but this reduces the reliability of the system3 .

And now, let’s get to the patterns. First we’ll start with some detailed design-level patterns, which some people call idioms.

6.2 One’s Complement Pattern

The One’s Complement Pattern is useful to detect when memory is corrupted by outside influences or hardware faults.

6.2.1 Abstract

The One’s Complement Pattern provides a detailed design pattern for the identification of single or multiple memory bit corruption. This can occur due to EMI (Electromagnetic interference), heat, hardware faults, software faults, or other external causes. The pattern works by storing critical data twice – once in normal form and once in one’s complement (bit-wise inverted) form. When the data are read, the one’s complement form is reinverted and then compared to the value of the normal form. If the values match, then that value is returned, otherwise error processing ensues.

6.2.2 Problem

This pattern addresses the problem that variables may be corrupted by a variety of causes such as environmental factors (e.g., EMI, heat, radiation), hardware faults (e.g., power fluctuation, memory cell faults, address line shorts), or software faults (other software erroneously modifying memory). This pattern addresses the problem of identifying data corruption for small sets of critical data values.

6.2.3 Pattern Structure

The basic pattern structure is shown in Figure 6-2. Since this is a detailed design pattern, its scope is primarily within a single class.

Figure 6-2 One's Complement Pattern

6.2.4 Collaboration Roles

This section describes the roles for this pattern.

6.2.4.1 DataClient

The DataClient is an element that either sets or retrieves the value of the datum.

6.2.4.2 OnesCompProtectedDataElement

The OnesCompProtectedDataElement is the class that combines the storage of the data with services that can detect bit-level data corruption. This class stores the values of the data twice; once in regular form and once as a bit-wise complement. When data are retrieved, inverted data are reinverted and compared to the value stored normally. If the values match, that value is returned; if not, the class provides a function to handle the error. The actual behavior of the error handler is system- and context-specific.

6.2.5 Consequences

This pattern uses twice the memory for storage of critical data; in addition, there is some performance hit on both data storage and retrieval. On the plus side, the pattern can identify errors due to EMI or due to stuck or shorted memory or address bits.

6.2.6 Implementation Strategies

This pattern is very easy to implement in C. For primitive types, the ∼ operator computes the bit inversion. For structured types, you must iterate over the primitive parts of the data (see the example below).

6.2.7 Related Patterns

This pattern provides a very reliable way to identify faults that affect a single memory location. For very large data structures, the memory impact of complete data replication may be too large; in such cases, the Cyclic Redundancy Check (CRC) Pattern provides a reasonably reliable alternative.

6.2.8 Example

Figure 6-3 shows a simple application of this pattern. OwnShipAttitude stores the attitude (orientation, as specified in its attributes roll, pitch, and yaw) using the One’s Complement Pattern. When the getAttitude() function is called, it reinverts the inverted copy and compares it to normally stored values. If there is a difference, then the errorHandler() function is invoked which, in this case, calls the addAlarm() function of the AlarmHandler. If the values are the same, the value is returned to the client.

Figure 6-3 One’s Complement example

The code for the OwnShipAttitude (fulfilling the role of the OnesCompProtectedDataElement in the pattern) is shown in Code Listing 6-1 and Code Listing 6-2. Note that the error code passed to the alarm manager ATTITUDE_MEMORY_FAULT is simply a #define-d value.

Code Listing 6-1 OwnShipAttitude.h

#ifndef OwnShipAttitude_H#define OwnShipAttitude_H

#include "AttitudeDataType.h"

struct AlarmManager;

typedef struct OwnShipAttitude OwnShipAttitude;

struct OwnShipAttitude {

struct AttitudeDataType attitude;

struct AttitudeDataType invertedAttitude;

struct AlarmManager* itsAlarmManager;

};

void OwnShipAttitude_Init(OwnShipAttitude* const me);

void OwnShipAttitude_Cleanup(OwnShipAttitude* const me);

/* Operations */

void OwnShipAttitude_errorHandler(OwnShipAttitude* const me);

int OwnShipAttitude_getAttitude(OwnShipAttitude* const me, AttitudeDataType * aPtr);

AttitudeDataType OwnShipAttitude_invert(OwnShipAttitude* const me, AttitudeDataType a);

void OwnShipAttitude_setAttitude(OwnShipAttitude* const me, AttitudeDataType a);

struct AlarmManager* OwnShipAttitude_getItsAlarmManager(const OwnShipAttitude* const me);

void OwnShipAttitude_setItsAlarmManager(OwnShipAttitude* const me, struct AlarmManager* p_AlarmManager);

OwnShipAttitude * OwnShipAttitude_Create(void);

void OwnShipAttitude_Destroy(OwnShipAttitude* const me);

#endif

Code Listing 6-2 OwnShipAttitude.c

#include "OwnShipAttitude.h"

#include "AlarmManager.h"

static void cleanUpRelations(OwnShipAttitude* const me);

void OwnShipAttitude_Init(OwnShipAttitude* const me) {

AttitudeDataType_Init(&(me->attitude));

AttitudeDataType_Init(&(me->invertedAttitude));

me->itsAlarmManager = NULL;

}

void OwnShipAttitude_Cleanup(OwnShipAttitude* const me) {

cleanUpRelations(me);

}

void OwnShipAttitude_errorHandler(OwnShipAttitude* const me) {

AlarmManager_addAlarm(me->itsAlarmManager, ATTITUDE_MEMORY_FAULT);

}

int OwnShipAttitude_getAttitude(OwnShipAttitude* const me, AttitudeDataType * aPtr) {

AttitudeDataType ia = OwnShipAttitude_invert(me, me- >invertedAttitude);

if (me->attitude.roll == ia.roll && me->attitude.yaw == ia.yaw &&

me->attitude.pitch == ia.pitch ) {

*aPtr = me->attitude;

return 1;

}

else {

OwnShipAttitude_errorHandler(me);

return 0;

};

}

AttitudeDataType OwnShipAttitude_invert(OwnShipAttitude* const me, AttitudeDataType a) {

a.roll = ∼a.roll;

a.yaw = ∼a.yaw;

a.pitch = ∼a.pitch;

return a;

}

void OwnShipAttitude_setAttitude(OwnShipAttitude* const me, AttitudeDataType a) {

me->attitude = a;

me->invertedAttitude = OwnShipAttitude_invert(me, a);

}

struct AlarmManager* OwnShipAttitude_getItsAlarmManager(const OwnShipAttitude* const me) {

return (struct AlarmManager*)me->itsAlarmManager;

}

void OwnShipAttitude_setItsAlarmManager(OwnShipAttitude* const me, struct AlarmManager* p_AlarmManager) {

me->itsAlarmManager = p_AlarmManager;

}

OwnShipAttitude * OwnShipAttitude_Create(void) {

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

if(me!=NULL)

OwnShipAttitude_Init(me);

return me;

}

void OwnShipAttitude_Destroy(OwnShipAttitude* const me) {

if(me!=NULL)

OwnShipAttitude_Cleanup(me);

free(me);

}

static void cleanUpRelations(OwnShipAttitude* const me) {

if(me->itsAlarmManager != NULL)

me->itsAlarmManager = NULL;

}

6.3 CRC Pattern

The Cyclic Redundancy Check (CRC) Pattern calculates a fixed-size error-detection code based on a cyclic polynomial that can detect corruption in data sets far larger than the size of the code.

6.3.1 Abstract

The CRC Pattern computes a fixed-length binary code, called a CRC value, on your data to detect whether or not they have been corrupted. This code is stored in addition to the data values and is set when data are updated and checked when data are read.

A detailed explanation of the mathematics is beyond the scope of this book4 but CRCs are both common and useful in practice. CRCs are characterized by the bit-length of the polynomials used to compute them. While algorithmic computation can be complex and time consuming, table-driven algorithms are quite time efficient. CRCs provide good detection of single and multiple bit errors for arbitrary length data fields and so are good for large data structures.

6.3.2 Problem

This pattern addresses the problem that variables may be corrupted by a variety of causes such as environmental factors (e.g., EMI, heat, radiation), hardware faults (e.g., power fluctuation, memory cell faults, address line shorts), or software faults (other software erroneously modifying memory). This pattern addresses the problem of data corruption in large data sets.

6.3.3 Pattern Structure

The CRC Pattern structure is shown in Figure 6-4. The CRCProtectedData class uses a generic computeCRC() function to calculate the CRC over its data. While data are shown as an array, it can be any contiguous block of data.

Figure 6-4 CRC Pattern

6.3.4 Collaboration Roles

This section describes the roles for this pattern.

6.3.4.1 computeCRCData Function

This is a general function for the computation of a CRC of a specific polynomial bit-length. Common bit lengths include 12, 16, and 32. In practice, this is usually implemented with a table-driven algorithm for computational efficiency.

6.3.4.2 CRCProtectedData

This is the class that owns and protects the data. When data are stored, the CRC is calculated. When data are read, the CRC is recalculated and compared to the stored CRC; if the CRCs differ, then the errorHandler() is called, otherwise the retrieved data are returned to the client.

6.3.4.3 DataClient

The DataClient is the element that stores and/or retrieves data from the CRCProtectedData class.

6.3.4.4 DataType

The is the base data type of the data structure owned by the CRCProtectedDataClass. This might be a primitive type, such as int or double, or it may be a complex data type such as an array or struct.

6.3.5 Consequences

With a little bit of memory used by the CRC data table and a relatively small amount of computation time to compute the polynomial, this pattern provides good detection of single (and small number of) bit errors. It is used often in communications messaging because such connections tend to be extremely unreliable. In addition, it can be used for in-memory error detection for harsh EMI environments or for mission-critical data. It is excellent for large data sets with relatively few bit errors.

6.3.6 Implementation Strategies

As mentioned, the table-driven algorithm is almost always used because it provides good performance with only a minor memory space penalty. If memory is tight, the polynomial can be computed algorithmically but this has a significant impact on run-time performance.

6.3.7 Related Patterns

The One’s Complement Pattern replicates the data in a bit-inverted form, and so is useful for small data sets. This pattern is better for large data sets because it has a smaller memory impact.

6.3.8 Example

Figure 6-5 shows a medical application of this pattern. In this case, several clients set or get patient data, which is defined by the PatientDataType. The PatientData class provides functions to set and get the data, protecting them via calls to the CRCCalculator. A single CRC protects the entire patient data set.

Figure 6-5 CRC example

The code listing in Code Listing 6-3 is adapted from free software written by Jack Klein, and released under the GNU General Public License Agreement5 . It computes a CRC-16 but other polynomial bit-lengths are available for use.

Code Listing 6-3 CRCCalculator.c

/* crcccitt.c - a demonstration of look up table based CRC

*         computation using the non-reversed CCITT_CRC

*         polynomial 0x1021 (truncated)

*

* Copyright (C) 2000 Jack Klein

*         Macmillan Computer Publishing

*

* This program is free software; you can redistribute it

* and/or modify it under the terms of the GNU General

* Public License as published by the Free Software

* Foundation; either version 2 of the License, or

* (at your option) any later version.

*

* This program is distributed in the hope that it will

* be useful, but WITHOUT ANY WARRANTY; without even the

* implied warranty of MERCHANTABILITY or FITNESS FOR A

* PARTICULAR PURPOSE. See the GNU General Public License

* for more details.

*

* You should have received a copy of the GNU General

* Public License along with this program; if not, write

* to the Free Software Foundation, Inc., 675 Mass Ave,

* Cambridge, MA 02139, USA.

*

* Jack Klein may be contacted by email at:

*  The_C_Guru@yahoo.com

*

* Modified slightly by Bruce Powel Douglass, 2010

*/

/*# # dependency stdio */

#include <stdio.h>

/*# # dependency stdlib */

#include <stdlib.h>

/*# # dependency string */

#include <string.h>

static unsigned short crc_table[256] = {

0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5,

0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b,

0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,

0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,

0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,

0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401,

0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,

0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,

0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6,

0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,

0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5,

0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,

0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,

0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,

0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,

0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,

0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03,

0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd,

0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6,

0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,

0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,

0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb,

0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1,

0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,

0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,

0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,

0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb,

0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,

0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447,

0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,

0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2,

0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,

0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9,

0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827,

0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,

0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,

0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0,

0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d,

0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,

0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,

0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba,

0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,

0x2e93, 0x3eb2, 0x0ed1, 0x1ef0

};

unsigned short computeCRC(unsigned char * data, size_t length, unsigned short seed, unsigned short final) {

size_t count;

unsigned int crc = seed;

unsigned int temp;

for (count = 0; count < length; ++count)

{

temp = (*data++ ^ (crc >> 8)) & 0xff;

crc = crc_table[temp] ^ (crc << 8);

}

return (unsigned short)(crc ^ final);

}

The data types used in the example appear in Code Listing 6-4. Note that ErrorCodeType includes other errors, such as data element out of range, as is common in such applications.

Code Listing 6-4 CRCExample.c

#ifndef CRCExample_H

#define CRCExample_H

struct AlarmManager;

struct BloodO2Sensor;

struct CRCBuilderClass;

struct CRCClient2;

struct DrugDeliverySystem;

struct HositalPatientSystem;

struct NIBP;

struct PatientData;

struct Thermometer;

typedef enum ErrorCodeType {

NO_ERRORS,

UNKNOWN_ERROR,

CORRUPT_DATA,

WEIGHT_TOO_LOW,

WEIGHT_TOO_HIGH,

AGE_TOO_HIGH,

GENDER_OUT_OF_RANGE,

SYSTOLIC_TOO_LOW,

SYSTOLIC_TOO_HIGH,

DIASTOLIC_TOO_LOW,

DIASTOLIC_TOO_HIGH,

TEMPERATURE_TOO_LOW,

TEMPERATIURE_TOO_HIGH,

BLOOD_O2_TOO_LOW,

BLOOD_O2_TOO_HIGH

} ErrorCodeType;

typedef enum GenderType {

MALE,

FEMALE,

HERMAPHRODITE

} GenderType;

typedef struct PatientDataType {

unsigned short age;

unsigned short bloodO2Conc;

unsigned short diastolicBP;

GenderType gender;

unsigned short heartRate;

char name[100];

unsigned long patientID;

unsigned short systolicBP;

unsigned short temperature;

double weight;

} PatientDataType;

#endif

Of course the main action is in the PatientData class code, shown in Code Listing 6-5 and Code Listing 6-6.

Code Listing 6-5 PatientData.h

#ifndef PatientData_H

#define PatientData_H

#include "CRCExample.h"

struct AlarmManager;

typedef struct PatientData PatientData;

struct PatientData {

PatientDataType pData;

unsigned short crc;

struct AlarmManager* itsAlarmManager;

};

void PatientData_Init(PatientData* const me);

void PatientData_Cleanup(PatientData* const me);

/* Operations */

void PatientData_errorHandler(PatientData* const me, ErrorCodeType errCode);

unsigned short PatientData_getAge(PatientData* const me);

unsigned short PatientData_getBloodO2Conc(PatientData* const me);

unsigned short PatientData_getDiastolicBP(PatientData* const me);

GenderType PatientData_getGender(PatientData* const me);

char* PatientData_getName(PatientData* const me);

unsigned long PatientData_getPatientID(PatientData* const me);

unsigned short PatientData_getSystolicBP(PatientData* const me);

unsigned short PatientData_getTemperature(PatientData* const me);

double PatientData_getWeight(PatientData* const me);

void PatientData_setAge(PatientData* const me, unsigned short a);

void PatientData_setBloodO2Conc(PatientData* const me, unsigned short o2);

void PatientData_setDiastolicBP(PatientData* const me, unsigned short dBP);

void PatientData_setGender(PatientData* const me, GenderType g);

void PatientData_setName(PatientData* const me, char* n);

void PatientData_setPatientID(PatientData* const me, unsigned long id);

void PatientData_setSystolicBP(PatientData* const me, unsigned short sBP);

void PatientData_setTemperature(PatientData* const me, unsigned short t);

void PatientData_setWeight(PatientData* const me, double w);

int PatientData_checkData(PatientData* const me);

struct AlarmManager* PatientData_getItsAlarmManager(const PatientData* const me);

void PatientData_setItsAlarmManager(PatientData* const me, struct AlarmManager* p_AlarmManager);

PatientData * PatientData_Create(void);

void PatientData_Destroy(PatientData* const me);

#endif

Code Listing 6-6 PatientData.c

#include "PatientData.h"

#include "AlarmManager.h"

#include <string.h>

#include <stdio.h>

static void cleanUpRelations(PatientData* const me);

void PatientData_Init(PatientData* const me) {

me->itsAlarmManager = NULL;

strcpy(me->pData.name, " ");

me->pData.patientID = 0;

me->pData.weight = 0;

me->pData.age = 0;

me->pData.gender = HERMAPHRODITE;

me->pData.systolicBP = 0;

me->pData.diastolicBP = 0;

me->pData.temperature = 0;

me->pData.heartRate = 0;

me->pData.bloodO2Conc = 0;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

}

}

void PatientData_Cleanup(PatientData* const me) {

cleanUpRelations(me);

}

int PatientData_checkData(PatientData* const me) {

unsigned short result = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

printf("computed CRC = %d, stored CRC = %d\n", result, me->crc);

return result == me->crc;

}

void PatientData_errorHandler(PatientData* const me, ErrorCodeType errCode) {

AlarmManager_addAlarm(me->itsAlarmManager, errCode);

}

unsigned short PatientData_getAge(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.age;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

unsigned short PatientData_getBloodO2Conc(PatientData* const me){

if (PatientData_checkData(me))

return me->pData.bloodO2Conc;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

unsigned short PatientData_getDiastolicBP(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.diastolicBP;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

GenderType PatientData_getGender(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.gender;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

char* PatientData_getName(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.name;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

unsigned long PatientData_getPatientID(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.patientID;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

unsigned short PatientData_getSystolicBP(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.systolicBP;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

unsigned short PatientData_getTemperature(PatientData* const me)

{

if (PatientData_checkData(me))

return me->pData.temperature;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

double PatientData_getWeight(PatientData* const me) {

if (PatientData_checkData(me))

return me->pData.weight;

else {

PatientData_errorHandler(me, CORRUPT_DATA);

return 0;

};

}

void PatientData_setAge(PatientData* const me, unsigned short a) {

if (PatientData_checkData(me)) {

me->pData.age = a;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setBloodO2Conc(PatientData* const me, unsigned short o2) {

if (PatientData_checkData(me)) {

me->pData.bloodO2Conc = o2;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setDiastolicBP(PatientData* const me, unsigned short dBP) {

if (PatientData_checkData(me)) {

me->pData.diastolicBP = dBP;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setGender(PatientData* const me, GenderType g) {

if (PatientData_checkData(me)) {

me->pData.gender = g;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setName(PatientData* const me, char* n) {

if (PatientData_checkData(me)) {

strcpy(me->pData.name, n);

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setPatientID(PatientData* const me, unsigned long id) {

if (PatientData_checkData(me)) {

me->pData.patientID = id;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setSystolicBP(PatientData* const me, unsigned short sBP) {

if (PatientData_checkData(me)) {

me->pData.systolicBP = sBP;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setTemperature(PatientData* const me, unsigned short t) {

if (PatientData_checkData(me)) {

me->pData.temperature = t;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

void PatientData_setWeight(PatientData* const me, double w) {

if (PatientData_checkData(me)) {

me->pData.weight = w;

me->crc = computeCRC((unsigned char *)&me->pData, sizeof(me->pData), 0xffff, 0);

if (!PatientData_checkData(me))

PatientData_errorHandler(me, CORRUPT_DATA);

}

else {

printf("Set failed\n");

PatientData_errorHandler(me, CORRUPT_DATA);

};

}

struct AlarmManager* PatientData_getItsAlarmManager(const PatientData* const me) {

return (struct AlarmManager*)me->itsAlarmManager;

}

void PatientData_setItsAlarmManager(PatientData* const me, struct AlarmManager* p_AlarmManager) {

me->itsAlarmManager = p_AlarmManager;

}

PatientData * PatientData_Create(void) {

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

if(me!=NULL)

PatientData_Init(me);

return me;

}

void PatientData_Destroy(PatientData* const me) {

if(me!=NULL)

PatientData_Cleanup(me);

free(me);

}

static void cleanUpRelations(PatientData* const me) {

if(me->itsAlarmManager != NULL)

me->itsAlarmManager = NULL;

}

6.4 Smart Data Pattern

One of the biggest problems I see with embedded C code in actual application is that functions all have preconditions for their proper execution but these functions don’t explicitly check that those preconditons are actually satisfied. This falls under the heading of what is commonly known as “defensive design” – a development paradigm in which we design with proactive run-time defenses in place to detect problems. The Smart Data Pattern codifies that paradigm for scalar data elements.

6.4.1 Abstract

One of the reasons that languages such as Ada are considered “safer6 ” than C is that they have run-time checking. These checks include array index range checking, definition of subtypes and subranges (such as defining a subtype Color to be not only limited to a set of colors but generate errors if that range is exceeded), and parameter range checks. Indeed although the C++ language is not inherently “safe” by this standard, because data structures may be bound together with operations that provide run-time checking, it can be made “safe” by intention if not by default. This pattern, although landing more within the accepted range of “idiom” rather than “pattern,” addresses those concerns by creating C classes that explicitly perform these checks.

6.4.2 Problem

The most common idiom for error checking of data ranges in C is for functions that have no other return value: return a 0 value for function success and a -1 value for failure and set errno to an error code.

The problem is, of course, that the most common way to “handle” the return values is to ignore them, resulting in difficult-to-debug systems. Some people even put error checking into the system during development but remove such checks in the final release. I would argue that the time when you really want to know that the altitude determination program in your aircraft isn’t producing the right value is when you are in the flying aircraft.

The problem this pattern addresses is to build functions and data types that essentially check themselves and provide error detection means that cannot be easily ignored.

6.4.3 Pattern Structure

As mentioned above, this pattern falls into the idiomatic (tiny pattern) range rather than a collaboration pattern. The key concepts of the pattern are to 1) build self-checking types whenever possible; 2) check incoming parameter values for appropriate range checking; and 3) check consistency and reasonableness among one or a set of parameters. Figure 6-6 shows the structure of the pattern.

Figure 6-6 Smart Data Pattern

6.4.4 Collaboration Roles

This section describes the roles for this pattern.

6.4.4.1 ErrorCodeType

The ErrorCodeType is an enumeration of possible error codes. This can be, and is often, done with ints but enums clarify the intention and usage of the codes more clearly.

6.4.4.2 ErrorManager

The ErrorManager class handles the errors identified within the SubrangeType; the actions are system-, context-, and error-specific.

6.4.4.3 ServerClass

This role in the pattern is an element that uses the SmartDataType.

6.4.4.4 SmartDataType

One of the disadvantages of data structures is that while they all have preconditions and rules for proper usage, they have no intrinsic behavior to enforce the conditions and rules. This class binds together the data (of type PrimitiveType) with operations that ensure they stay in range.

This class provides a variety of functions. The SmartDataType_Init() function takes five parameters – the me pointer to point to the instance data, the initial value being set, the low and high values specifying the valid data subrange, and the address of the ErrorManager. If the entire range of the primitive type is valid, then you can simply use the primitive type or use the low and high values to the min and max for the type (such as SHRT_MIN and SHRT_MAX, or INT_MIN and INT_MAX declared in <limits.h>, for example). To create a new instance on the heap, simply use SmartDataType_Create(), which takes three parameters – the value, the low boundary, and the high boundary and returns a pointer to the newly created instance.

SmartDataTypes can be assigned to each other with the SmartDataType_setValue() and SmartDataType_getValue() functions and compared with the boundary with the SmartDataType_cmp() function and without the boundaries with the SmartDataType_pCmp() function. The held data can be independently set with the SmartDataType_setPrimitive() and SmartDataType_getPrimitive() functions. The low and high boundaries can also be set and read independently.

6.4.5 Consequences

The downside for using smart data types is the performance overhead for executing the operations. The upside is that data are self-protecting and provide automatic checking when data are set. It is also possible for the programmers to avoid using the functions and access the values directly if they are so inclined, defeating the purpose of the smart data type.

6.4.6 Implementation Strategies

The key implementation strategy is to create a struct with a set of defined operations, and then only use those operations to access the values. The pattern can be extended to include an enumerated type of the units of the value when dealing with numbers with units, such as currency (e.g., euro and dollar), mass (such as pounds, kilograms), and distance (such as inches, feet, yards, fathoms, miles, and even meters). This is a useful extension when dealing with data from different clients that may require conversion. The conversion functions can be built into the class so that you both set and get the values in a specified unit.

6.4.7 Related Patterns

The idea of building smart types is extended pointers with the Smart Pointer Pattern.

6.4.8 Example

Figure 6-7 shows a simple medical example of using smart data types. The ServerClass role from the pattern is fulfilled by the PatientDataClass in the figure. It has three SmartInt variables (weight, age, and heart rate) and two SmartColor variables (foreground color and background color). I included the use of an enumerated type ColorType to show how the pattern can be used for types other than built-in types.

Figure 6-7 Smart Data example

Code Listing 6-7 gives the basic use types for the example.

Code Listing 6-7 SmartDataExample.h

#ifndef SmartDataExample_H

#define SmartDataExample_H

struct AlarmManager;

struct PatientDataClass;

struct SmartColor;

struct SmartDataClient;

struct SmartInt;

typedef enum ErrorCodeType {

NO_ERRORS,

BELOW_RANGE,

ABOVE_RANGE,

INCONSISTENT_VALUE,

ILLEGAL_USE_OF_NULL_PTR,

INDEX_OUT_OF_RANGE

} ErrorCodeType;

typedef enum ColorType {

BLACK,

BROWN,

RED,

PINK,

BLUE,

GREEN,

YELLOW,

WHITE

} ColorType;

#endif

The SmartInt data type is used for three variables in the PatientDataClass. It provides the basic functions identified in the SmartData role of the pattern, where int is the primitive type. Its code is shown in Code Listing 6-8 and Code Listing 6-9.

Code Listing 6-8 SmartInt.h

#ifndef SmartInt_H

#define SmartInt_H

#include "SmartDataExample.h"

struct AlarmManager;

typedef struct SmartInt SmartInt;

struct SmartInt {

ErrorCodeType errorCode;

int highRange;

int lowRange;

int value;

struct AlarmManager* itsAlarmManager;

};

/* Constructors and destructors:*/

void SmartInt_Init(SmartInt* const me, int val, int low, int high, struct AlarmManager* errMgr);

void SmartInt_Cleanup(SmartInt* const me);

/* Operations */

ErrorCodeType SmartInt_checkValidity(SmartInt* const me);

int SmartInt_cmp(SmartInt* const me, SmartInt s);

void SmartInt_errorHandler(SmartInt* const me, ErrorCodeType err);

ErrorCodeType SmartInt_getErrorCode(SmartInt* const me);

int SmartInt_getHighBoundary(SmartInt* const me);

int SmartInt_getLowBoundary(SmartInt* const me);

int SmartInt_getPrimitive(SmartInt* const me);

SmartInt SmartInt_getValue(SmartInt* const me);

int SmartInt_pCmp(SmartInt* const me, SmartInt s);

void SmartInt_setHighBoundary(SmartInt* const me, int high);

void SmartInt_setLowBoundary(SmartInt* const me, int low);

ErrorCodeType SmartInt_setPrimitive(SmartInt* const me, int p);

ErrorCodeType SmartInt_setValue(SmartInt* const me, SmartInt s);

struct AlarmManager* SmartInt_getItsAlarmManager(const SmartInt* const me);

void SmartInt_setItsAlarmManager(SmartInt* const me, struct AlarmManager* p_AlarmManager);

SmartInt * SmartInt_Create(int val, int low, int high, struct AlarmManager* errMgr);

void SmartInt_Destroy(SmartInt* const me);

#endif

Code Listing 6-9 SmartInt.c

#include "SmartInt.h"

#include "AlarmManager.h"

static void cleanUpRelations(SmartInt* const me);

void SmartInt_Init(SmartInt* const me, int val, int low, int high, struct AlarmManager* errMgr) {

me->errorCode = NO_ERRORS;

me->itsAlarmManager = NULL;

me->lowRange = low;

me->highRange = high;

me->value = val;

me->itsAlarmManager = errMgr;

}

void SmartInt_Cleanup(SmartInt* const me) {

cleanUpRelations(me);

}

ErrorCodeType SmartInt_checkValidity(SmartInt* const me) {

if (me->value < me->lowRange)

return BELOW_RANGE;

else if (me->value > me->highRange)

return ABOVE_RANGE;

else

return NO_ERRORS;

}

int SmartInt_cmp(SmartInt* const me, SmartInt s) {

return memcmp(me, &s, sizeof(s));

}

void SmartInt_errorHandler(SmartInt* const me, ErrorCodeType err) {

AlarmManager_addAlarm(me->itsAlarmManager, err);

}

ErrorCodeType SmartInt_getErrorCode(SmartInt* const me) {

return me->errorCode;

}

int SmartInt_getHighBoundary(SmartInt* const me) {

return me->highRange;

}

int SmartInt_getLowBoundary(SmartInt* const me) {

return me->lowRange;

}

int SmartInt_getPrimitive(SmartInt* const me) {

return me->value;

}

SmartInt SmartInt_getValue(SmartInt* const me) {

/*#[ operation getValue() */

return *me;

/*#]*/

}

int SmartInt_pCmp(SmartInt* const me, SmartInt s) {

int a = SmartInt_getPrimitive(&s);

return memcmp(&me->value, &a, sizeof(me->value));

}

void SmartInt_setHighBoundary(SmartInt* const me, int high) {

me->highRange = high;

}

void SmartInt_setLowBoundary(SmartInt* const me, int low) {

me->lowRange = low;

}

ErrorCodeType SmartInt_setPrimitive(SmartInt* const me, int p) {

me->errorCode = NO_ERRORS;

if (p < me->lowRange) {

me->errorCode = BELOW_RANGE;

AlarmManager_addAlarm(me->itsAlarmManager, me->errorCode);

}

else if (p > me->highRange) {

me->errorCode = ABOVE_RANGE;

AlarmManager_addAlarm(me->itsAlarmManager, me->errorCode);

}

else

me->value = p;

return me->errorCode;

}

ErrorCodeType SmartInt_setValue(SmartInt* const me, SmartInt s) {

me->errorCode = SmartInt_checkValidity(&s);

if (me->errorCode == NO_ERRORS)

*me = s;

else

SmartInt_errorHandler(me, me->errorCode);

return me->errorCode;

}

struct AlarmManager* SmartInt_getItsAlarmManager(const SmartInt* const me) {

return (struct AlarmManager*)me->itsAlarmManager;

}

void SmartInt_setItsAlarmManager(SmartInt* const me, struct AlarmManager* p_AlarmManager) {

me->itsAlarmManager = p_AlarmManager;

}

SmartInt * SmartInt_Create(int val, int low, int high, struct AlarmManager* errMgr) {

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

if(me!=NULL)

SmartInt_Init(me, val, low, high, errMgr);

return me;

}

void SmartInt_Destroy(SmartInt* const me) {

if(me!=NULL)

SmartInt_Cleanup(me);

free(me);

}

static void cleanUpRelations(SmartInt* const me) {

if(me->itsAlarmManager != NULL)

me->itsAlarmManager = NULL;

}

Similarly, the code for the SmartColor class is given in Code Listing 6-10 and Code Listing 6-11.

Code Listing 6-10 SmartColor.h

#ifndef SmartColor_H

#define SmartColor_H

#include "SmartDataExample.h"

#include "SmartInt.h"

struct AlarmManager;

typedef struct SmartColor SmartColor;

struct SmartColor {

ErrorCodeType errorCode;

ColorType highRange;

ColorType lowRange;

ColorType value;

struct AlarmManager* itsAlarmManager;

};

/* Constructors and destructors:*/

void SmartColor_Init(SmartColor* const me, ColorType val, ColorType low, ColorType high, struct AlarmManager* errMgr);

void SmartColor_Cleanup(SmartColor* const me);

/* Operations */

ErrorCodeType SmartColor_checkValidity(SmartColor* const me);

int SmartColor_cmp(SmartColor* const me, SmartColor s);

void SmartColor_errorHandler(SmartColor* const me, ErrorCodeType err);

ErrorCodeType SmartColor_getErrorCode(SmartColor* const me);

ColorType SmartColor_getHighBoundary(SmartColor* const me);

ColorType SmartColor_getLowBoundary(SmartColor* const me);

ColorType SmartColor_getPrimitive(SmartColor* const me);

SmartColor SmartColor_getValue(SmartColor* const me);

int SmartColor_pCmp(SmartColor* const me, SmartInt s);

void SmartColor_setHighBoundary(SmartColor* const me, ColorType high);

void SmartColor_setLowBoundary(SmartColor* const me, ColorType low);

ErrorCodeType SmartColor_setPrimitive(SmartColor* const me, ColorType p);

ErrorCodeType SmartColor_setValue(SmartColor* const me, SmartColor s);

struct AlarmManager* SmartColor_getItsAlarmManager(const SmartColor* const me);

void SmartColor_setItsAlarmManager(SmartColor* const me, struct AlarmManager* p_AlarmManager);

SmartColor * SmartColor_Create(ColorType val, ColorType low, ColorType high, struct AlarmManager* errMgr);

void SmartColor_Destroy(SmartColor* const me);

#endif

Code Listing 6-11 SmartColor.c

#include "SmartColor.h"

#include "AlarmManager.h"

static void cleanUpRelations(SmartColor* const me);

void SmartColor_Init(SmartColor* const me, ColorType val, ColorType low, ColorType high, struct AlarmManager* errMgr) {

me->errorCode = NO_ERRORS;

me->itsAlarmManager = NULL;

me->lowRange = low;

me->highRange = high;

me->value = val;

me->itsAlarmManager = errMgr;

}

void SmartColor_Cleanup(SmartColor* const me) {

cleanUpRelations(me);

}

ErrorCodeType SmartColor_checkValidity(SmartColor* const me) {

if (me->value < me->lowRange)

return BELOW_RANGE;

else if (me->value > me->highRange)

return ABOVE_RANGE;

else

return NO_ERRORS;

}

int SmartColor_cmp(SmartColor* const me, SmartColor s) {

return memcmp(me, &s, sizeof(s));

}

void SmartColor_errorHandler(SmartColor* const me, ErrorCodeType err) {

AlarmManager_addAlarm(me->itsAlarmManager, err);

}

ErrorCodeType SmartColor_getErrorCode(SmartColor* const me) {

return me->errorCode;

}

ColorType SmartColor_getHighBoundary(SmartColor* const me) {

return me->highRange;

}

ColorType SmartColor_getLowBoundary(SmartColor* const me) {

return me->lowRange;

}

ColorType SmartColor_getPrimitive(SmartColor* const me) {

return me->value;

}

SmartColor SmartColor_getValue(SmartColor* const me) {

return *me;

}

int SmartColor_pCmp(SmartColor* const me, SmartInt s) {

ColorType a = SmartInt_getPrimitive(&s);

return memcmp(&me->value, &a, sizeof(me->value));

}

void SmartColor_setHighBoundary(SmartColor* const me, ColorType high) {

me->highRange = high;

}

void SmartColor_setLowBoundary(SmartColor* const me, ColorType low) {

me->lowRange = low;

}

ErrorCodeType SmartColor_setPrimitive(SmartColor* const me, ColorType p) {

me->errorCode = NO_ERRORS;

if (p < me->lowRange) {

me->errorCode = BELOW_RANGE;

AlarmManager_addAlarm(me->itsAlarmManager, me->errorCode);

}

else if (p > me->highRange) {

me->errorCode = ABOVE_RANGE;

AlarmManager_addAlarm(me->itsAlarmManager, me->errorCode);

}

else

me->value = p;

return me->errorCode;

}

ErrorCodeType SmartColor_setValue(SmartColor* const me, SmartColor s) {

me->errorCode = SmartColor_checkValidity(&s);

if (me->errorCode == NO_ERRORS)

*me = s;

else

SmartColor_errorHandler(me, me->errorCode);

return me->errorCode;

}

struct AlarmManager* SmartColor_getItsAlarmManager(const SmartColor* const me) {

return (struct AlarmManager*)me->itsAlarmManager;

}

void SmartColor_setItsAlarmManager(SmartColor* const me, struct AlarmManager* p_AlarmManager) {

me->itsAlarmManager = p_AlarmManager;

}

SmartColor * SmartColor_Create(ColorType val, ColorType low, ColorType high, struct AlarmManager* errMgr) {

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

if(me!=NULL)

SmartColor_Init(me, val, low, high, errMgr);

return me;

}

void SmartColor_Destroy(SmartColor* const me) {

if(me!=NULL)

SmartColor_Cleanup(me);

free(me);

}

static void cleanUpRelations(SmartColor* const me) {

if(me->itsAlarmManager != NULL)

me->itsAlarmManager = NULL;

}

Finally, the PatientDataClass uses these types to store values. In this case, it provides the access functions for the primitive types to its clients but uses smart data types for itself.

Code Listing 6-12 PatientDataClass.h

#ifndef PatientDataClass_H

#define PatientDataClass_H

#include "SmartDataExample.h"

#include "AlarmManager.h"

#include "SmartColor.h"

#include "SmartInt.h"

typedef struct PatientDataClass PatientDataClass;

struct PatientDataClass {

SmartInt age;

SmartColor backgroundColor;

SmartColor foregroundColor;

SmartInt heartRate;

char name[100];

long patientID;

SmartInt weight;

};

/* Constructors and destructors:*/

void PatientDataClass_Init(PatientDataClass* const me, AlarmManager* errMgr);

void PatientDataClass_Cleanup(PatientDataClass* const me);

/* Operations */

ErrorCodeType PatientDataClass_checkAllData(PatientDataClass* const me);

int PatientDataClass_getAge(PatientDataClass* const me);

ColorType PatientDataClass_getBColor(PatientDataClass* const me);

ColorType PatientDataClass_getFColor(PatientDataClass* const me);

int PatientDataClass_getHeartRate(PatientDataClass* const me);

char* PatientDataClass_getName(PatientDataClass* const me);

int PatientDataClass_getWeight(PatientDataClass* const me);

void PatientDataClass_setAge(PatientDataClass* const me, int a);

void PatientDataClass_setBColor(PatientDataClass* const me, ColorType bc);

void PatientDataClass_setFColor(PatientDataClass* const me, ColorType fc);

void PatientDataClass_setHeartRate(PatientDataClass* const me, int hr);

void PatientDataClass_setName(PatientDataClass* const me, char* n);

void PatientDataClass_setWeight(PatientDataClass* const me, int w);

PatientDataClass * PatientDataClass_Create(AlarmManager* errMgr);

void PatientDataClass_Destroy(PatientDataClass* const me);

#endif

Code Listing 6-13 PatientDataClass.c

#include "PatientDataClass.h"

void PatientDataClass_Init(PatientDataClass* const me, AlarmManager* errMgr) {

strcpy(me->name, " ");

me->patientID = 0;

/* initialize smart variables */

SmartInt_Init(&me->weight, 0, 0, 500, errMgr);

SmartInt_Init(&me->age, 0, 0, 130, errMgr);

SmartInt_Init(&me->heartRate, 0, 0, 400, errMgr);

SmartColor_Init(&me->foregroundColor, WHITE, BLACK, WHITE, errMgr);

SmartColor_Init(&me->backgroundColor, BLACK, BLACK, WHITE, errMgr);

}

void PatientDataClass_Cleanup(PatientDataClass* const me) {

}

ErrorCodeType PatientDataClass_checkAllData(PatientDataClass* const me) {

ErrorCodeType res;

res = SmartInt_checkValidity(&me->weight);

if (res != NO_ERRORS)

return res;

res = SmartInt_checkValidity(&me->age);

if (res != NO_ERRORS)

return res;

res = SmartInt_checkValidity(&me->heartRate);

if (res != NO_ERRORS)

return res;

res = SmartColor_checkValidity(&me->foregroundColor);

if (res != NO_ERRORS)

return res;

res = SmartColor_checkValidity(&me->backgroundColor);

if (res != NO_ERRORS)

return res;

}

int PatientDataClass_getAge(PatientDataClass* const me) {

return SmartInt_getPrimitive(&me->age);

}

ColorType PatientDataClass_getBColor(PatientDataClass* const me) {

return SmartColor_getPrimitive(&me->backgroundColor);

}

ColorType PatientDataClass_getFColor(PatientDataClass* const me) {

return SmartColor_getPrimitive(&me->foregroundColor);

}

int PatientDataClass_getHeartRate(PatientDataClass* const me) {

return SmartInt_getPrimitive(&me->heartRate);

}

char* PatientDataClass_getName(PatientDataClass* const me) {

return me->name;

}

int PatientDataClass_getWeight(PatientDataClass* const me) {

return SmartInt_getPrimitive(&me->weight);

}

void PatientDataClass_setAge(PatientDataClass* const me, int a) {

SmartInt_setPrimitive(&me->age, a);

}

void PatientDataClass_setBColor(PatientDataClass* const me, ColorType bc) {

SmartColor_setPrimitive(&me->backgroundColor, bc);

}

void PatientDataClass_setFColor(PatientDataClass* const me, ColorType fc) {

SmartColor_setPrimitive(&me->foregroundColor, fc);

}

void PatientDataClass_setHeartRate(PatientDataClass* const me, int hr) {

SmartInt_setPrimitive(&me->heartRate, hr);

}

void PatientDataClass_setName(PatientDataClass* const me, char* n) {

strcpy(me->name, n);

}

void PatientDataClass_setWeight(PatientDataClass* const me, int w) {

SmartInt_setPrimitive(&me->weight, w);

}

PatientDataClass * PatientDataClass_Create(AlarmManager* errMgr) {

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

if(me!=NULL)

PatientDataClass_Init(me, errMgr);

return me;

}

void PatientDataClass_Destroy(PatientDataClass* const me) {

if(me!=NULL)

PatientDataClass_Cleanup(me);

free(me);

}

6.5 Channel Pattern

The Channel Pattern is a larger-scale pattern than the previous patterns we’ve discussed. The Channel Pattern is used to support medium- to large-scale redundancy to help identify when run-time faults occur and possibly (depending on how it’s used) continue to provide services in the presence of such a fault.

6.5.1 Abstract

A channel is an architectural structure that contains software (and possibly hardware as well) that performs end-to-end processing; that is, it processes data from raw acquisition, through a series of data processing steps, to physical actuation in the real world. The advantage of a channel is that it provides an independent, self-contained unit of functionality that can be replicated in different ways to address different safety and reliability concerns.

6.5.2 Problem

This pattern provides a basic element of large-scale redundancy that can be used in different ways to address safety and reliability concerns.

6.5.3 Pattern Structure

The basic structure of the pattern is shown in Figure 6-8. The generalization shown in the figure can be notional only for implementations without true inheritance in C. The basic idea is that the software objects are arranged through a series of links so that data are acquired and processed through a series of data transformational steps until it results in an output that controls one or more actuators. The data types may change (for example, it may be acquired as a double but result in an int output to the actuator) or they may remain the same throughout the processing.

Figure 6-8 Channel Pattern

6.5.4 Collaboration Roles

This section describes the roles for this pattern.

6.5.4.1 AbstractDataTransform

This class is a placeholder for one of several possible ConcreteDataTransforms. In an object-oriented language or implementation, true generalization can be used but in C, it is more common to use this as a notional concept to give the intent rather than drive the implementation. It has two optional7 associations. One is to itself – that is, the next transformation in the list. The other is to the output ActuationDeviceDriver – this is in lieu of the former association. Put another way, the AbstractDataTransform has a link to the next processing step in the chain or to the output device driver, but not both.

6.5.4.2 ActuatorDeviceDriver

This class uses the computed output values to drive one or more actuators, such as a motor, light, or heating unit.

6.5.4.3 ConcreteDataTransform

This class performs a mathematical transformation on the incoming data value (from either the sensor or a previous ConcreteDataTransform) and produces an output (sent to the next ConcreteDataTransform in the sequence or the ActuatorDeviceDriver). In simple cases, there may be only a single ConcreteDataTransform class, but in more complex situations there may be dozens.

6.5.4.4 SensorDeviceDriver

This class acquires physical data from one or more sensors to put into the channel for processing.

6.5.5 Consequences

The channel in this simple form provides only a little value. Its true value lies in the use of multiple instances of the channel. There are a number of different ways to combine the channels, as well as the data transformations, to address safety and reliability concerns.

In general, the benefits are providing a clear means for identifying faults and, depending on the pattern, providing redundant channels to continue to provide services or to go to a “fault-safe state.” The drawbacks of using the pattern are the extra memory (both code and data), processing time, and hardware required, depending on the specific usage of the channel.

6.5.6 Implementation Strategies

As mentioned, the pattern may be implemented with true generalization but it is more common in C to think of the AbstractDataTransformation as a placeholder for some particular ConcreteDataTransform class; the links are from different types of ConcereteDataTransform classes that each perform some special step in the overall data transformation function.

Data are passed through using one of two primary strategies. The simplest is to acquire a datum and pass it through the series of transformations and out to the actuator before acquiring the next. In higher bandwidth applications multiple instances of the data may be present in the channel at the same time, one data instance per transform, with data passed from one transform to the next at the same time as all the other data instances.

6.5.7 Related Patterns

This pattern provides medium- to large-scale redundancy. It is the basis for many other patterns including the Protected Single-Channel Pattern and Homogeneous Redundancy Pattern discussed later in this chapter. Other patterns include Triple Modular Redundancy (TMR) Pattern, in which three channels run in parallel and vote on the outcome, and the Sanity Check Pattern in which a primary channel is checked for reasonableness by a lighter-weight secondary channel. A number of such patterns are discussed in my Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems (Addison-Wesley, 2002).

6.5.8 Example

I will provide a relatively simple example here of the use of the pattern. The classes shown in Figure 6-9 implement a simple electromyography biofeedback device. It works by measuring the voltage across two electrodes placed on the skin over a muscle. As the muscle tension changes, the voltage potential between the electrodes changes. This is gathered (as an int) by the EMGSensorDeviceDriver. Then it is converted into a frequency by the first data transformation class; this is done by taking a set of samples and computing the fundamental frequency of those data. This results in a computed frequency that is then filtered with a moving average filter; this is a filter that smoothes out rapid frequency changes by averaging the change over several (frequency) data samples measured sequentially in time. Lastly, the filtered frequency is converted into a pleasing color (specified via RGB values) and this color is sent to the light device driver.

Figure 6-9 Channel Pattern example

Figure 6-10 shows the same pattern but with the channel objects as a part of a larger assembly8 in which the elements of the channel are parts. The boxes inside the EMG Channel are part objects, and this is indicated with an underlined name.

Figure 6-10 Channel Pattern (object view)

The code is very straightforward for the pattern. For initialization, the key is that the EMG channel instantiates the objects and then initializes the links (see the initRelations() function in Code Listing 6-15) between them. For operation, the EMG Channel provides the services of the start of the chain (that is, the EMG Device Driver); when acquireData() is called on the EMG Channel, it calls EMGSensorDeviceDriver_acquireData() which gets the data and passes them on to the ConvertToFrequency_computeFreq() function. This function does the conversion and then calls the MovingAverageFilter_filter() function which smooths out high frequency artifacts and then passes the output of that onto CalculateColor_freqToColor(). This function computes the RGB values associated with the frequency and calls LightDeviceDriver_setLightColor() to turn the light to the correct color. In this case, the processing is all synchronous. It is possible to pass the data among the ConcreteDataTransformations asynchronously as well, by putting queues between them and having them pend on data input to those queues.

The next two code listings, Code Listing 6-14 and Code Listing 6-15 give the code for the EMGChannel class header and implementation, respectively.

Code Listing 6-14 EMGChannel.h

#ifndef EMGChannel_H

#define EMGChannel_H

#include "CalculateColor.h"

#include "ConvertToFrequency.h"

#include "EMGSensorDeviceDriver.h"

#include "LightDeviceDriver.h"

#include "movingAverageFilter.h"

typedef struct EMGChannel EMGChannel;

struct EMGChannel {

struct CalculateColor itsCalculateColor;

struct ConvertToFrequency itsConvertToFrequency;

struct EMGSensorDeviceDriver itsEMGSensorDeviceDriver;

struct LightDeviceDriver itsLightDeviceDriver;

struct movingAverageFilter itsMovingAverageFilter;

};

/* Constructors and destructors:*/

void EMGChannel_Init(EMGChannel* const me);

void EMGChannel_Cleanup(EMGChannel* const me);

/* Operations */

void EMGChannel_acquireData(EMGChannel* const me);

double EMGChannel_getFrequency(EMGChannel* const me);

long EMGChannel_getLightColor(EMGChannel* const me);

int EMGChannel_getVoltage(EMGChannel* const me);

void EMGChannel_setSensitivity(EMGChannel* const me, int sen);

struct CalculateColor* EMGChannel_getItsCalculateColor(const EMGChannel* const me);

struct ConvertToFrequency* EMGChannel_getItsConvertToFrequency(const EMGChannel* const me);

struct EMGSensorDeviceDriver* EMGChannel_getItsEMGSensorDeviceDriver(const EMGChannel* const me);

struct LightDeviceDriver* EMGChannel_getItsLightDeviceDriver(const EMGChannel* const me);

struct movingAverageFilter* EMGChannel_getItsMovingAverageFilter(const EMGChannel* const me);

EMGChannel * EMGChannel_Create(void);

void EMGChannel_Destroy(EMGChannel* const me);

#endif

Code Listing 6-15 EMGChannel.c

#include "EMGChannel.h"

static void initRelations(EMGChannel* const me);

static void cleanUpRelations(EMGChannel* const me);

void EMGChannel_Init(EMGChannel* const me) {

initRelations(me);

}

void EMGChannel_Cleanup(EMGChannel* const me) {

cleanUpRelations(me);

}

void EMGChannel_acquireData(EMGChannel* const me) {

/* delegate to the appropriate part */

EMGSensorDeviceDriver_acquireData(&me->itsEMGSensorDeviceDriver);

}

double EMGChannel_getFrequency(EMGChannel* const me) {

return me->itsMovingAverageFilter.computedFreq;

}

long EMGChannel_getLightColor(EMGChannel* const me) {

return me->itsCalculateColor.red<<16 + me->itsCalculateColor.green<<8 + me->itsCalculateColor.blue;

}

int EMGChannel_getVoltage(EMGChannel* const me) {

return me->itsEMGSensorDeviceDriver.voltage;

}

void EMGChannel_setSensitivity(EMGChannel* const me, int sen) {

EMGSensorDeviceDriver_setSensitivity(&me->itsEMGSensorDeviceDriver, sen);

}

struct CalculateColor* EMGChannel_getItsCalculateColor(const EMGChannel* const me) {

return (struct CalculateColor*)&(me->itsCalculateColor);

}

struct ConvertToFrequency* EMGChannel_getItsConvertToFrequency(const EMGChannel* const me) {

return (struct ConvertToFrequency*)&(me->itsConvertToFrequency);

}

struct EMGSensorDeviceDriver* EMGChannel_getItsEMGSensorDeviceDriver(const EMGChannel* const me) {

return (struct EMGSensorDeviceDriver*)&(me->itsEMGSensorDeviceDriver);

}

struct LightDeviceDriver* EMGChannel_getItsLightDeviceDriver(const EMGChannel* const me) {

return (struct LightDeviceDriver*)&(me->itsLightDeviceDriver);

}

struct movingAverageFilter* EMGChannel_getItsMovingAverageFilter(const EMGChannel* const me) {

return (struct movingAverageFilter*)&(me->itsMovingAverageFilter);

}

EMGChannel * EMGChannel_Create(void) {

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

if(me!=NULL)

EMGChannel_Init(me);

return me;

}

void EMGChannel_Destroy(EMGChannel* const me) {

if(me!=NULL)

EMGChannel_Cleanup(me);

free(me);

}

static void initRelations(EMGChannel* const me) {

CalculateColor_Init(&(me->itsCalculateColor));

ConvertToFrequency_Init(&(me->itsConvertToFrequency));

EMGSensorDeviceDriver_Init(&(me->itsEMGSensorDeviceDriver));

LightDeviceDriver_Init(&(me->itsLightDeviceDriver));

movingAverageFilter_Init(&(me->itsMovingAverageFilter));

EMGSensorDeviceDriver_setItsConvertToFrequency(&(me->itsEMGSensorDeviceDriver),&(me->itsConvertToFrequency));

ConvertToFrequency_setItsMovingAverageFilter(&(me->itsConvertToFrequency),&(me->itsMovingAverageFilter));

movingAverageFilter_setItsCalculateColor(&(me->itsMovingAverageFilter),&(me->itsCalculateColor));

CalculateColor_setItsLightDeviceDriver(&(me->itsCalculateColor),&(me->itsLightDeviceDriver));

}

static void cleanUpRelations(EMGChannel* const me) {

movingAverageFilter_Cleanup(&(me->itsMovingAverageFilter));

LightDeviceDriver_Cleanup(&(me->itsLightDeviceDriver));

EMGSensorDeviceDriver_Cleanup(&(me->itsEMGSensorDeviceDriver));

ConvertToFrequency_Cleanup(&(me->itsConvertToFrequency));

CalculateColor_Cleanup(&(me->itsCalculateColor));

}

6.6 Protected Single Channel Pattern

The Protected Single Channel Pattern is a simple elaboration of the Channel pattern in which data checks are added at one or more ConcreteDataTransformations. It provides lightweight redundancy but typically cannot continue to provide service if a fault is discovered.

6.6.1 Abstract

The Protected Single Channel Pattern uses a single channel to handle the processing of sensory data up to and including actuation based on that data. Safety and reliability are enhanced through the addition of checks at key points in the channel, which may require some additional hardware. Because there is only a single channel, this pattern will not be able to continue to function in the presence of persistent faults, but it detects and may be able to handle transient faults.

6.6.2 Problem

Redundancy is expensive, not only in terms of development effort but also in terms of replicated hardware. In some cases, fully redundant channels may not be necessary. For example, if there is a fault-safe state (a state known to always be safe), the system must only be able to detect a fault and enter that state. This lowers the reliability of the system, but it may still meet the user needs.

6.6.3 Pattern Structure

The pattern, shown in Figure 6-11, is a simple elaboration of the Channel Pattern (see Figure 6-8). All we’ve added here are some checks on some of the transformation steps. In fact, these checks need not be implemented in separate classes, but can be implemented internal to the ConcreteTransformations themselves. These checks can be any kind of checks, such as:

  • Range checking (are data in the required range?)

  • Reasonableness checks (are data in the expected range for the given situation?)

  • Consistency checks (is the value consistent with other data being computed at the same time?)

  • Backwards transformational checks (can we recompute the input values from the output values?)

  • Data validity checks, such as we’ve seen before with the One’s Complement and CRC Patterns

and so on.

Figure 6-11 Protected Single Channel Pattern

6.6.4 Collaboration Roles

This section describes the roles for this pattern.

6.6.4.1 AbstractDataTransform

This class is a placeholder for one of several possible ConcreteDataTransforms. In an object-oriented language or implementation, true generalization can be used but in C, it is more common to use this as a notional concept to give the intent rather than drive the implementation. It has two optional9 associations. One is to itself – that is, the next transformation in the list. The other is to the output ActuationDeviceDriver – this is in lieu of the former association. Put another way, the AbstractDataTransform has a link to the next processing step in the chain or to the output device driver, but not both.

This class has also an optional link to AbstractTransformChecker for points in the transformation chain where checking is appropriate.

6.6.4.2 AbstractTransformChecker

This is a placeholder for an element that can check on the validity of the processing or data in their current state.

6.6.4.3 ActuatorDeviceDriver

This class uses the computed output values to drive one or more actuators, such as a motor, light, or heating unit.

6.6.4.4 ConcreteDataTransform

This class performs a mathematical transformation on the incoming data value (from either the sensor or a previous ConcreteDataTransform) and produces an output (sent to the next ConcreteDataTransform in the sequence or the ActuatorDeviceDriver). In simple cases, there may be only a single ConcreteDataTransform class, but in more complex situations there may be dozens.

Certain of these classes form checkpoints – points in the transformation chain at which the validality of the transformations will be checked. For those cases, the optional link to the specific ConcreteTransformChecker will be implemented.

6.6.4.5 ConcreteTransformChecker

This is a specific transformation checking class that checks the transformation at a single point in the chain.

6.6.4.6 SensorDeviceDriver

This class acquires physical data from one or more sensors to put into the channel for processing.

6.6.5 Implementation Strategies

In general, the benefits are providing a clear means for identifying faults and, depending on the pattern, providing redundant channels to continue to provide services or to go to a “fault-safe state.” The drawbacks of using the pattern are the extra memory (both code and data), processing time, and hardware required, depending on the specific usage of the channel.

As mentioned, the pattern may be implemented with true generalization, but it is more common in C to think of the AbstractDataTransformation as a placeholder for some particular ConcreteDataTransform class; the links are from different subtypes of ConcereteDataTransform classes that each perform some special step in the overall data transformation function.

Data are passed through using one of two primary strategies. The simplest is to acquire a datum and pass it through the series of transformations and out to the actuator before acquiring the next. In higher bandwidth applications multiple instances of the data may be present in the channel at the same time, one data instance per transform, with the data passed from one transform to the next at the same time as all the other data instances.

In addition, the data checks may be put “in place” in some of the transformation steps rather than implement them as a separate class. That is, in fact, the specialization of this pattern from the more generic Channel Pattern.

6.6.6 Related Patterns

This pattern provides medium- to large-scale redundancy. It is one of many patterns based on the Channel Pattern discussed earlier in this chapter. A number of such patterns are discussed in my Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems (Addison-Wesley, 2002).

The transformation checks can themselves be based on patterns such as the One’s Complement and CRC Patterns discussed earlier.

6.6.7 Example

In Figure 6-12, I purposely chose a situation that demanded a bit more creative application of the pattern. This is a heating system with sensor sets for multiple rooms, each having sensors for fan speed, temperature, and thermostat settings. Because the example isn’t laid out exactly as the pattern, I indicated which pattern role each class plays by adding a stereotype10 to each. I further modified the pattern by making the first ConcreteDataTransform pull the data from its SensorDeviceDrivers (the thermometer and room fan) associated with each room. The next transform, ComputeRequiredHeatFlow, not only gets the actual heat flow from the previous transform but reads each room’s thermostat to get the desired temperature and computes the desired output heat flow from the furnace. The difference between actual and desired temperatures results in a desired heat flow. This is passed on to the CalculateFurnaceParams, which computes the necessary furnace gas flow, furnace blower speeds, and commands the FurnaceController.

Figure 6-12 Protected Single Channel example

In terms of safety, there are several checks in this protected single channel. The Thermometers are checked for actual temperature, and if the temperature is out of bounds, this leads to either emergency restart of the furnace (if below 0º F) or shutting the furnace down (if over 130º F)11 . The actual heat flow and (computed) desired heat flow are checked to ensure that they don’t exceed 14,000 BTUs. If this limit is exceeded, the furnace is shut down. In addition, both the Thermometer and Thermostat use the One’s Complement Pattern (described earlier in this chapter) to detect memory corruption. In the case of the Thermometer, the behavior on corruption is to shut the furnace down, while in the case of the Thermostat, the behavior is to use the default temperature setting of 70º F. If the default is corrupted, then it calls FurnaceController_shutDown().

I don’t want to give all the code here, but we can look at a few relevant classes. The Thermostat class implements a One’s Complement Pattern to ensure its data isn’t corrupted. Its code is shown in Code Listing 6-16 and Code Listing 6-17.

Code Listing 6-16 Thermostat.h

#ifndef Thermostat_H

#define Thermostat_H

struct FurnaceController;

typedef struct Thermostat Thermostat;

struct Thermostat {

int defaultTempSetting;

int desiredTemp;

int invertedDefaultTemp;

int invertedDesiredTemp;

struct FurnaceController* itsFurnaceController;

};

/* Constructors and destructors:*/

void Thermostat_Init(Thermostat* const me);

void Thermostat_Cleanup(Thermostat* const me);

/* Operations */

int Thermostat_getDesiredTemperature(Thermostat* const me);

void Thermostat_setDesiredTemperature(Thermostat* const me, int temp);

struct FurnaceController* Thermostat_getItsFurnaceController(const Thermostat* const me);

void Thermostat_setItsFurnaceController(Thermostat* const me, struct FurnaceController* p_FurnaceController);

Thermostat * Thermostat_Create(void);

void Thermostat_Destroy(Thermostat* const me);

#endif

Code Listing 6-17 Thermostat.c

#include "Thermostat.h"

#include "FurnaceController.h"

static void cleanUpRelations(Thermostat* const me);

void Thermostat_Init(Thermostat* const me) {

me->defaultTempSetting = 70;

me->invertedDefaultTemp = ~70;

me->itsFurnaceController = NULL;

}

void Thermostat_Cleanup(Thermostat* const me) {

cleanUpRelations(me);

}

int Thermostat_getDesiredTemperature(Thermostat* const me) {

if (me->desiredTemp == ~me->invertedDesiredTemp)

return me->desiredTemp;

else

if (me->defaultTempSetting == ~me->invertedDefaultTemp) {

Thermostat_setDesiredTemperature(me, me->defaultTempSetting);

return me->defaultTempSetting;

}

else {

FurnaceController_shutDown(me->itsFurnaceController);

return me->defaultTempSetting;

};

}

void Thermostat_setDesiredTemperature(Thermostat* const me, int temp) {

if (me->desiredTemp == ~me->invertedDesiredTemp) {

me->desiredTemp = temp;

me->invertedDesiredTemp = ~temp;

if (me->desiredTemp == ~me->invertedDesiredTemp)

return;

}

else

FurnaceController_shutDown(me->itsFurnaceController);

}

struct FurnaceController* Thermostat_getItsFurnaceController(const Thermostat* const me) {

return (struct FurnaceController*)me->itsFurnaceController;

}

void Thermostat_setItsFurnaceController(Thermostat* const me, struct FurnaceController* p_FurnaceController) {

me->itsFurnaceController = p_FurnaceController;

}

Thermostat * Thermostat_Create(void) {

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

if(me!=NULL)

Thermostat_Init(me);

return me;

}

void Thermostat_Destroy(Thermostat* const me) {

if(me!=NULL)

Thermostat_Cleanup(me);

free(me);

}

static void cleanUpRelations(Thermostat* const me) {

if(me->itsFurnaceController != NULL)

me->itsFurnaceController = NULL;

}

The CheckTemperature class provides a reasonableness check on the temperature. Code Listing 6-18 and Code Listing 6-19 give the header and implementation.

Code Listing 6-18 CheckTemperature.h

#ifndef CheckTemperature_H

#define CheckTemperature_H

struct FurnaceController;

typedef struct CheckTemperature CheckTemperature;

struct CheckTemperature {

int maximumTemp;

int minimumTemp;

struct FurnaceController* itsFurnaceController;

};

void CheckTemperature_Init(CheckTemperature* const me);

void CheckTemperature_Cleanup(CheckTemperature* const me);

void CheckTemperature_checkTemperature(CheckTemperature* const me, int temp);

struct FurnaceController* CheckTemperature_getItsFurnaceController(const CheckTemperature* const me);

void CheckTemperature_setItsFurnaceController(CheckTemperature* const me, struct FurnaceController* p_FurnaceController);

CheckTemperature * CheckTemperature_Create(void);

void CheckTemperature_Destroy(CheckTemperature* const me);

#endif

Code Listing 6-19 CheckTemperature.c

#include "CheckTemperature.h"

#include "FurnaceController.h"

static void cleanUpRelations(CheckTemperature* const me);

void CheckTemperature_Init(CheckTemperature* const me) {

me->maximumTemp = 130;

me->minimumTemp = 0;

me->itsFurnaceController = NULL;

}

void CheckTemperature_Cleanup(CheckTemperature* const me) {

cleanUpRelations(me);

}

void CheckTemperature_checkTemperature(CheckTemperature* const me, int temp) {

if (temp < me->minimumTemp)

FurnaceController_emergencyRestart(me->itsFurnaceController);

if (temp > me->maximumTemp)

FurnaceController_shutDown(me->itsFurnaceController);

}

struct FurnaceController* CheckTemperature_getItsFurnaceController(const CheckTemperature* const me) {

return (struct FurnaceController*)me->itsFurnaceController;

}

void CheckTemperature_setItsFurnaceController(CheckTemperature* const me, struct FurnaceController* p_FurnaceController) {

me->itsFurnaceController = p_FurnaceController;

}

CheckTemperature * CheckTemperature_Create(void) {

CheckTemperature* me = (CheckTemperature *)

malloc(sizeof(CheckTemperature));

if(me!=NULL)

CheckTemperature_Init(me);

return me;

}

void CheckTemperature_Destroy(CheckTemperature* const me) {

if(me!=NULL)

CheckTemperature_Cleanup(me);

free(me);

}

static void cleanUpRelations(CheckTemperature* const me) {

if(me->itsFurnaceController != NULL)

me->itsFurnaceController = NULL;

}

The CheckTemperature class is used by the Thermometer. The last code listings, Code Listing 6-20 and Code Listing 6-21 , give the header and implementation for this class.

Code Listing 6-20 Thermometer.h

#ifndef Thermometer_H

#define Thermometer_H

struct CheckTemperature;

struct FurnaceController;

typedef struct Thermometer Thermometer;

struct Thermometer {

int invertedMeasuredTemp;

int measuredTemp;

struct CheckTemperature* itsCheckTemperature;

struct FurnaceController* itsFurnaceController;

};

/* Constructors and destructors:*/

void Thermometer_Init(Thermometer* const me);

void Thermometer_Cleanup(Thermometer* const me);

/* Operations */

void Thermometer_acquireMeasuredTemperature(Thermometer* const me);

int Thermometer_getMeasuredTemperature(Thermometer* const me);

struct CheckTemperature* Thermometer_getItsCheckTemperature(const Thermometer* const me);

void Thermometer_setItsCheckTemperature(Thermometer* const me, struct CheckTemperature* p_CheckTemperature);

struct FurnaceController* Thermometer_getItsFurnaceController(const Thermometer* const me);

void Thermometer_setItsFurnaceController(Thermometer* const me, struct FurnaceController* p_FurnaceController);

Thermometer * Thermometer_Create(void);

void Thermometer_Destroy(Thermometer* const me);

struct CheckTemperature* Thermometer_getItsCheckTemperature_1(const Thermometer* const me);

void Thermometer_setItsCheckTemperature_1(Thermometer* const me, struct CheckTemperature* p_CheckTemperature);

#endif

Code Listing 6-21 Thermometer.c

#include "Thermometer.h"

#include "CheckTemperature.h"

#include "FurnaceController.h"

static void cleanUpRelations(Thermometer* const me);

void Thermometer_Init(Thermometer* const me) {

me->itsCheckTemperature = NULL;

me->itsFurnaceController = NULL;

}

void Thermometer_Cleanup(Thermometer* const me) {

cleanUpRelations(me);

}

void Thermometer_acquireMeasuredTemperature(Thermometer* const me) {

/* access the hardware here */

}

int Thermometer_getMeasuredTemperature(Thermometer* const me) {

if (me->measuredTemp == ~me->invertedMeasuredTemp) {

CheckTemperature_checkTemperature(me->itsCheckTemperature, me->measuredTemp);

return me->measuredTemp;

}

else {

FurnaceController_shutDown(me->itsFurnaceController);

return 0;

}

}

struct CheckTemperature* Thermometer_getItsCheckTemperature(const Thermometer* const me) {

return (struct CheckTemperature*)me->itsCheckTemperature;

}

void Thermometer_setItsCheckTemperature(Thermometer* const me, struct CheckTemperature* p_CheckTemperature) {

me->itsCheckTemperature = p_CheckTemperature;

}

struct FurnaceController* Thermometer_getItsFurnaceController(const Thermometer* const me) {

return (struct FurnaceController*)me->itsFurnaceController;

}

void Thermometer_setItsFurnaceController(Thermometer* const me, struct FurnaceController* p_FurnaceController) {

me->itsFurnaceController = p_FurnaceController;

}

Thermometer * Thermometer_Create(void) {

Thermometer* me = (Thermometer *)

malloc(sizeof(Thermometer));

if(me!=NULL)

Thermometer_Init(me);

return me;

}

void Thermometer_Destroy(Thermometer* const me) {

if(me!=NULL)

Thermometer_Cleanup(me);

free(me);

}

static void cleanUpRelations(Thermometer* const me) {

if(me->itsCheckTemperature != NULL)

me->itsCheckTemperature = NULL;

if(me->itsFurnaceController != NULL)

me->itsFurnaceController = NULL;

}

6.7 Dual Channel Pattern

The Dual Channel Pattern is primarily a pattern to improve reliability by offering multiple channels, thereby addressing redundancy concerns at the architectural level. If the channels are identical (called the Homogeneous Redundancy Channel), the pattern can address random faults (failures) but not systematic faults (errors). If the channels use a different design or implementation, the pattern is called the Heterogeneous Redundancy Pattern (also known as Diverse Design Pattern) and can address both random and systematic faults.

6.7.1 Abstract

The Dual Channel Pattern provides architectural redundancy to address safety and reliability concerns. It does this by replicating channels and embedding logic that manages them and determines when the channels will be “active.”

6.7.2 Problem

This pattern provides protection against single-point faults (either failures or both failures and errors, depending on the specific subpattern selected). Depending on which pattern, the system may detect a fault in one channel by comparing it to the other and then transition to the fault-safe state OR it may use other means to detect the fault in one channel and switch to the other when a fault occurs.

6.7.3 Pattern Structure

Figure 6-13 shows the basic structure for the pattern.

Figure 6-13 Dual Channel Pattern

Figure 6-14 shows the detailed internal structure within the pattern. It is almost exactly what appears in the Protected Single Channel Pattern, with the exception of the association from the ConcreteTransformChecker to the current and alternative channel. Note that the itsChannel association points to the current channel that owns the ConcreteTransformChecker instance while the itsOtherChannel points to the alternative channel. This allows the checker to disable the current channel and enable the alternative channel.

Figure 6-14 Dual Channel Pattern detailed class view

6.7.4 Collaboration Roles

This section describes the roles for this pattern. Note that the roles that are internal to the pattern can be seen in the previous section on the Protected Single Channel Pattern.

6.7.4.1 AbstractDataTransform

This class is a placeholder for one of several possible ConcreteDataTransforms. In an object-oriented language or implementation, true generalization can be used but in C, it is more common to use this as a notional concept to give the intent rather than drive the implementation. It has two optional12 associations. One is to itself – that is, the next transformation in the list. The other is to the output ActuationDeviceDriver – this is in lieu of the former association. Put another way, the AbstractDataTransform has a link to the next processing step in the chain or to the output device driver, but not both.

This class also an optional link to AbstractTransformChecker for points in the transformation chain where checking is appropriate.

6.7.4.2 AbstractTransformChecker

This is a placeholder for an element that can check on the validity of the processing or data in its current state.

6.7.4.3 ActuatorDeviceDriver

This class uses the computed output values to drive one or more actuator, such as a motor, light, or heating unit.

6.7.4.4 Channel

This class is the container for the instances of the others. It is important in this context because it provides a “unit of redundancy.” It provides two services of itself – enable() and disable().

6.7.4.5 ConcreteDataTransform

This class performs a mathematical transformation on the incoming data value (from either the sensor or a previous ConcreteDataTransform) and produces an output (sent to the next ConcreteDataTransform in the sequence or the ActuatorDeviceDriver). In simple cases, there may be only a single ConcreteDataTransform class, but in more complex situations there may be dozens.

Certain of these classes form checkpoints – points in the transformation chain at which the validality of the transformations will be checked. For those cases, the optional link to the specific ConcreteTransformChecker will be implemented.

6.7.4.6 ConcreteTransformChecker

This is a specific transformation checking class that checks the transformation at a single point in the chain.

6.7.4.7 SensorDeviceDriver

This class acquires physical data from one or more sensors to put into the channel for processing.

6.7.5 Consequences

This pattern addresses safety- or reliability-related faults by replicating the channel; this usually requires replication of some amount of hardware as well, giving it a rather high recurring (production) cost. If the channels are identical, then all replicants contain the same errors and so will manifest the error under the same circumstances.

6.7.6 Implementation Strategies

The large-scale implementation strategies are commonly identified as different subpatterns of this one. These are discussed in the next section. In addition, the management of the two channels can be implemented differently. Both channels can be run simultaneously and checked against each other – if the outputs differ above some threshold, the system transitions to a fault-safe state. Alternatively, one channel can run until a fault is detected and then the other channel can be enabled, allowing continuation of services in the presence of the fault.

6.7.7 Related Patterns

There are a number of variants on this pattern. Each gives a different cost/benefit ratio.

6.7.7.1 Homogeneous Redundancy Pattern

Description

This pattern variant uses identical designs and implementations for the different channels. It can effectively address single-point faults – provided that the faults are isolated within a single channel. The system can continue functionality in the presence of a failure within a channel.

Consequences

This pattern variant has a relatively high recurring cost (cost per shipped item) but a relatively low design cost (since each channel must only be designed once). Typically sensors and actuators are replicated to provide fault isolation, and often processors, memory, and other hardware must be replicated.

6.7.7.2 Heterogeneous Redundancy Pattern

Description

This pattern variant uses dual channels of different designs or different implementations to address both random and systematic faults. The system can continue to deliver services in the presence of a fault, provided that the fault is properly isolated within a single channel. Typically, sensors and actuators are replicated to provide fault isolation, and often processors, memory, and other hardware must be replicated as well as providing multiple (different) implementations of the same software functionality.

Consequences

This pattern variant has a relatively high recurring cost (cost per shipped item) and a relatively high design cost (since each channel must be designed twice). Typically, sensors and actuators are replicated to provide fault isolation, and often processors, memory, and other hardware must be replicated in addition to the software functionality. Note that this pattern variant addresses both random and systematic faults, although Nancy Leveson notes that there is usually some correlation between errors even in diverse implementations13 .

6.7.7.3 Triple Modular Redundancy (TMR) Pattern

Description

This pattern variant uses three channels of usually identical designs to address both faults. The theory of use is that if there is a single-point fault then one of the channels will disagree with the other two and the outlier is discarded. The voting mechanism (the ChannelArbiter) is designed to “fail on” so that at least one channel (it doesn’t matter which) will result in an output if the voting mechanism fails. The system can continue to deliver services in the presence of a fault, provided that the fault is properly isolated within a single channel. Typically, sensors and actuators are replicated to provide fault isolation, and often processors, memory, and other hardware must be replicated as well as providing multiple (different) implementations of the same software functionality.

Consequences

This pattern has a very high recurring cost because the channel must be replicated three times. If all the channels have the same design (most common), then the design cost is relatively low; the only additional cost is the ChannelArbiter used to reject the outlier output. If the channels are of different designs, then the cost of this pattern variant is very high since each channel must be designed three times. This is a very common pattern in avionics.

Figure 6-15 Triple Modular Redundancy Pattern

6.7.7.4 Sanity-Check Pattern

Description

This pattern variant uses two heterogeneous channels; one is the primary actuation channel and the second is a lightweight channel that checks on the output using lower fidelity computations and hardware. If the primary channel has a fault that can be detected by the lower fidelity checking channel, then the system enters its fault-safe state.

Consequences

This pattern has a low recurring cost and a medium design cost since it requires additional design work but lower fidelity redundancy. It cannot continue in the presence of a single fault.

Figure 6-16 Sanity Check Pattern

6.7.7.5 Monitor-Actuator Pattern

Description

This pattern variant uses two extremely heterogeneous channels. The first, just as in the Sanity Check Pattern, is the actuator channel. This channel provides the system service. The second channel monitors the physical result of the actuation channel using one or more independent sensors. The concept of the pattern is that if the actuator channel has a fault and performs incorrectly, the monitoring channel identifies it and can command the system to a fault-safe state. If the monitoring channel has a fault, then the actuation channel is still behaving correctly. This pattern assumes there is a fault-safe state and that the sensor(s) used by the monitoring channel are not used by the actuation channel.

The primary differences between the Monitor-Actuator Pattern and the Sanity Check Pattern are:

  1. The Sanity Check Pattern is a low-fidelity check on the output of the computation of the actuation channel; this does not require an independent sensor.

  2. The Monitor-Actuator is a high-fidelity check on the physical result of the actuator channel and therefore requires an independent sensor.

Consequences

This is a medium-weight pattern. It requires some additional recurring cost because of the additional sensor(s) and processing capability for the monitoring channel. The extra monitoring channel requires a modest amount of additional design work as well. As mentioned, this channel provides single-point fault safety for systems in which there is a fault-safe state.

Figure 6-17 Monitor-Actuator Pattern

6.7.8 Example

The example shown in Figure 6-18 is a heterogeneous dual channel pattern for a train speed control system. One channel uses a light shining on the inside of a train wheel. By knowing the circumference of the wheel and by measuring the frequency of the mark appearance (measured by the optical sensor), one can compute the speed of the train. These data are then filtered, validated, and used to compute a desired engine output. This engine output is checked for reasonableness and, if acceptable, then sent to the engine. This is the OpticalWheelSpeedChannel shown in Figure 6-19.

Figure 6-18 Dual Channel example

Figure 6-19 Details of Optical Wheel Speed Channel

The other channel uses a GPS to determine train speed. This channel receives ephemeris data from some set of (at least three) satellites and using the time sent (and comparing it to the time within the system), it computes the distance to the satellite. Through a process called 3D triangulation, the train’s position can be calculated. This location is validated and then used to compute the speed by looking at the rate of change of positions. This speed is then validated and, if valid, is used to compute the desired engine output to maintain current speed. This is the GPSSpeedChannel shown in Figure 6-20.

Figure 6-20 Details of GPS Speed Channel

In either channel, if data are identified as invalid, that channel does not command the engine speed. Because there is another channel to take over the task of determining desired engine output, the system can continue to operate even if a single point fault results in a channel shutting down. Further, because this is an extremely heterogeneous design, an error in one channel will not be replicated in the other. This design provides protection against both random and systematic faults.

There is nothing profound about the code that is produced to implement this example beyond what has been described earlier in this chapter. The interesting part is how the channels are connected to provide the ability of the system to detect a fault and continue safe operation in the presence of the fault.

6.8 Summary

This chapter has presented a number of different patterns that can be used to improve the safety and reliability of a design executing in its operational context. The first few patterns were small in scope (often referred to as “design idioms” instead of “design patterns”). The One’s Complement Pattern replicated primitive data to identify run-time corruption – a problem common to harsh environments including medical operating rooms, industrial factory settings, and space. The CRC Pattern is useful in protecting larger volumes of data. The Smart Data Pattern adds behavior to data (that is, creates classes) to ensure that the data’s preconditions and constraints are met.

The other patterns in this chapter have a larger scope. The fundamental pattern for the rest of the chapter is the Channel Pattern, in which the processing of sensory data into actionable commands is set up as a series of data transform steps. This pattern is elaborated into the Protected Single Channel Pattern in which checks are added at various steps to validate the processing within the channel. The Dual Channel Pattern adds two channels so that even if one fails, the other can continue providing the required service.

1 Douglass, B.P., 2009. Real-Time UML Workshop for Embedded Systems. Elsevier. Douglass, B.P., 2009. Real-Time Agility. Addison-Wesley.

2 Douglass, B.P., 2002. Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems. Addison-Wesley.

3 In general, safety and reliability are opposing concerns when there is a fault-safe state and they are cooperating concerns when there is not.

4 See for example, “A Painless Guide to CRC Error Detection Algorithms” at http://www.ross.net/crc/download/crc%26v3.txt for more information.

5 For information concerning usage of this code, contact the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA

6 Using the term loosely and not in the precise sense defined in this chapter.

7 Common implementation is a pointer, set to NULL if the link isn’t active.

8 In UML, such an assembly is known as a structured class.

9 Common implementation is a pointer, set to NULL if the link isn’t active.

10 A stereotype is “a special kind of” marking that appears inside «» marks. In this case, the stereotypes are the names of the pattern roles.

11 That’s -18º C and 54º C for you metric-speaking types.

12 Common implementation is a pointer, set to NULL if the link isn’t active.

13 See Leveson, N., 1995. Safeware: System Safety and Computers. Addison-Wesley.