Real-Time Systems Inc.

Specializing in Embedded Controller Design


A Flexible EEPROM Manager in C++

Many embedded controllers contain EEPROM to support field configuration. Designers favor EEPROM because it can be erased and programmed in-circuit, yet it retains its contents when power fails. Like RAM (and unlike Flash ROM), EEPROM can be both erased and programmed byte-by-byte.

We can simply store field configuration variables directly in EEPROM, but adding a bit of record structure yields some advantages:

This article shows how to use C++ to create a simple EEPROM manager with these features:

Included is a completely worked-out example of an EEPROM manager in about 300 lines of code along with some suggestions for extensions. It shows real-world use of several major C++ techniques, such as abstract base classes, operator overloading, and templates.

Design goals

We have one main goal: to create variables that persist over power failure -- in other words, nonvolatile variables. We'll make these nearly identical in use to normal variables, confining the differences to declarations and initialization. The code actually reading and writing the variables will look the same as the code for normal variables.

We'll use C++ templates to build a type-safe client interface. We'll have the compiler ensure (without manual intervention) that only the correct data types are stored in a given variable. We'll make the client interface as simple as possible to discourage copy-paste coding and to avoid obscuring the business logic with lots of housekeeping code.

We'll assign ASCII names to each variable in EEPROM and automatically allocate space in EEPROM for them. We'll thereby allow program upgrades without worrying about how to reallocate EEPROM when adding new variables or when abandoning old ones. (We won't, however, support changing the size or interpretation of an existing variable, but only its contents. To redefine a variable, we can simply create a new variable and abandon the old one, converting the contents as required.)

Finally, we'll support choosing a new EEPROM hardware driver without modifying the storage layout engine, client interface, or client code in any way.

Traditional Approach

The traditional approach to EEPROM storage uses a set of routines like

    readEEPROM(unsigned eeaddr, void* memaddr, size_t len);
    writeEEPROM(unsigned eeaddr, const void* memaddr, size_t len);

that are invoked explicitly when needed. EEPROM addresses are often #define constants, calculated by hand during manual EEPROM space allocation.

Some problems with this approach:

We can solve all of these problems in a simple and straightforward way if we use C++ intelligently.

Overview of a C++ solution

We first separate the solution into three layers:

We'll make some simplifying assumptions for the sake of brevity and execution speed:

We can make these simplifications because not every design must be completely general, especially in small systems. Though it's always possible to imagine a system needing more features, it's often better to wait until they're actually needed and provide them then.

EEPROM drivers

The drivers for various EEPROM devices will be encapsulated in classes with write() and read() methods, with one class for each device type.

Since we shouldn't constrain our solution to a single device type, we'll derive all of our EEPROM drivers from a single base class called Eeprom. The main public methods of this class will be virtual, so they can be redefined for each new EEPROM device type. The driver for a given device type will inherit from Eeprom and override the write() and read() methods.

Since the write() and read() methods in the base class have no reasonable default implementation, we'll mark them pure virtual by ending their declarations with "= 0". Having at least one pure virtual function makes Eeprom an abstract base class. Abstract base classes can't be instantiated; they can only be used as a base for derivation of other abstract or concrete classes. In this situation, making Eeprom abstract matches reality, for without actual write() and read() methods we can`t talk to an EEPROM device.

By making write() and read() virtual, we ensure that the layout manager need not know about the specific EEPROM driver, but can instead work entirely in terms of the base class Eeprom. The actual function run by a call to, say, Eeprom::read() will be determined at run-time by indirection through the derived class's virtual table. In this way, the layout manager will perform a read operation while remaining ignorant of the actual device type.

The Eeprom class begins like this:

    typedef unsigned int EeAddr;    // EEPROM address type

    class Eeprom
    {
    public:
      virtual ~Eeprom() { }

      virtual bool erase() { return false; }
      virtual bool write(EeAddr addr, const void* buf, size_t len) = 0;
      virtual bool read(EeAddr addr, void* buf, size_t len) = 0;

      // continued below...

Since the class has at least one virtual method, we provide a virtual destructor. Though not strictly required, this is good practice so that if someone tries to delete an Eeprom-derived object through a pointer to its base class, Eeprom, the derived-class destructor (if any) will be run.

We also provide an erase() method to completely erase the device. It's possible that not all devices will support this feature, so we provide a default implementation that returns false to indicate the erase was not done.

Now while the "void*, size_t" description of the memory block to read or write is completely general, it's also somewhat error-prone or at least tedious to use. If we happen to know the data type of the object we're reading or writing, then we already know its size, so there's no need to specify it explicitly. Though we could write separate methods for each data type, that would be redundant and would still not cover all the possible data types.

Instead, we can write templates to cover all cases. These templates can be members of Eeprom:

      template <typename T>
      bool write(EeAddr addr, const T& t) { return write(addr, &t, sizeof(t)); }

      template <typename T>
      bool read(EeAddr addr, T& t) { return read(addr, &t, sizeof(t)); }
    };

These have no run-time penalty at all since they reduce to calls to the virtual read() and write() methods at compile time. They do, however, simplify our use of Eeprom and eliminate any possibility of mismatch between the object we're passing and its size.

The storage layout manager

The nonvolatile layout manager lives between the EEPROM driver, below, and the nonvolatile variables, above, organizing the storage of the variables in the EEPROM device.

The simplest possible layout manager would allocate EEPROM space sequentially as the nonvolatile variables were created, returning the device-based address of each variable for later use.

This simple approach has some drawbacks, though. First, we have no mechanism to detect errors, such as those that can occur if power fails while writing the device. We can reduce susceptibility to these errors by adding a checksum to each variable's record in EEPROM.

The second problem is more subtle. In a real system, we would have several classes, each creating its own nonvolatile variables. For example:

    class SerialPort
    {
      // ...

      // Nonvolatile variable for baud rate, stop bits, etc.

      // ...
    };

    class CanBusXcvr
    {
      // ...

      // Nonvolatile variable for bit rate, etc.

      // ...
    };

and so forth. Objects of these classes would be created during system initialization, perhaps in main():

    int main()
    {
      // ...

      MyEepromDriver eeprom( ... );       // Eeprom-derived device driver
      NvStore nvStore(eeprom);            // storage layout manager

      SerialPort com0(nvStore);           // serial port
      CanBusXcvr can0(nvStore);           // CANBus port

      // ...
    };

As each client object is created, it asks the layout manager to allocate space in the EEPROM for its nonvolatile variables. Every time the system starts, this process repeats, with the same EEPROM addresses being assigned each time.

But what if we decide to reverse the order of initializations or add another serial port between com0 and can0? This would work fine for a brand-new system with an empty EEPROM, but if we loaded the new code into an existing system, the old EEPROM contents wouldn't be where the new program expected them, and chaos would ensue.

We could arrange to wipe the EEPROM on program upgrades, but that would cause loss of any previous configuration. In some applications, losing configuration would be inconvenient, in others, dangerous.

To avoid this problem, we'll give each nonvolatile variable a unique name and put that name in the EEPROM record. At system startup we'll search for records by name; thus, the actual order in EEPROM won't matter.

Using named records solves another problem: how to detect that the system is being powered-up for the first time so we can set default values. With named records, we'll take the absence of a record to indicate its variable was newly defined by the current program version. In this first-time-up case, we'll write default values to EEPROM. In any later startup, we'll instead fetch the values from EEPROM and write them to the RAM variable.

Class overview

The layout manager class looks like this:

    class NvStore
    {
      Eeprom& eeprom;       // EEPROM driver
      EeAddr base;          // start address of first record in EEPROM
      EeAddr end;           // end of available EEPROM

      static const size_t maxNameLen = 16;  // maximum record name length

    public:
      NvStore(Eeprom& ee, EeAddr b, EeAddr e)
      : eeprom(ee), base(b), end(e) { }

      EeAddr open(const char* name, void* buf, size_t len);
      void update(EeAddr addr, const void* buf, size_t len);
    };

NvStore holds a reference to the abstract base class Eeprom, which allows it to read and write an actual EEPROM device. Since we might share the device with other uses, NvStore also includes the boundaries base and end, which delimit the part of the EEPROM to manage.

The open() method will be called at system startup to find a nonvolatile variable and get its contents or to create and initialize it if necessary. The update() method will be called to back up the variable to EEPROM whenever the application code changes the variable`s contents.

Record structure

Each nonvolatile variable will have its own record in EEPROM, with this structure:

    size_t length;      // length of data[] array in bytes
    char name[];        // zero-terminated record name string
    uint8_t hdrsum;     // checksum of length and name[] array

    uint8_t data[];     // record payload
    uint8_t checksum;   // checksum of data[] array

The length of the name and data members are not fixed, and will potentially be different for each record.

Checksumming and allocation

A record consists of two checksummed fields: the header and the payload. The NvField class manages these fields, performing checksumming and assisting with space allocation.

    typedef uint8_t NvSum;  // ones-complement checksum

    class NvField
    {
      Eeprom& eeprom;       // the backing EEPROM device
      EeAddr addr;          // current read/write address in device
      NvSum sum;            // checksum accumulator

      // continued...

Each NvField holds a reference to the Eeprom device providing its backing store. To support space allocation in the backing store, its addr member is advanced as the field is read or written. Finally, NvField contains a checksum accumulator, also updated as the field is accessed.

We protect each field with an 8-bit ones-complement checksum. This type of checksum incorporates all bits of the data with equal weight, unlike a twos-complement sum which under-emphasizes the high-order bits. To form a ones-complement sum, we first perform a common twos-complement sum then add any carry-out back into the least-significant bit of the sum.

Given a block of field bytes, the note() method incorporates the bytes into the ones-complement checksum and advances the EEPROM read/write address:

      void note(const void* buf, size_t len)
      {
        const uint8_t* p = static_cast<const uint8_t*>(buf);
        
        for (size_t i = 0; i < len; i++)
        {
          NvSum t = sum;
          
          if ((sum += *p++) < t)
            sum += 1;
        }

        addr += len;
      }

      // continued...

If sum appears smaller after an addition, we obviously had a carry-out. We know, however, that a single addition can never generate a carry of more than one, so that's the most we ever need to add.

The public interface of NvField allows either writing or reading its subfields in sequence followed by setting or testing the field checksum:

The constructor simply captures the Eeprom reference and the starting address of the record and initializes the checksum. Both the address and checksum will be updated as we go along and can be accessed after writing or reading the entire record:

    public:
      NvField(Eeprom& e, EeAddr a) : eeprom(e), addr(a), sum(0) { }

The write() and read() methods support writing or reading the separate parts of a field to/from the EEPROM device:

      void write(const void* buf, size_t len)
      {
        eeprom.write(addr, buf, len);
        note(buf, len);
      }
       
      bool read(void* buf, size_t len)
      {
        bool retval = eeprom.read(addr, buf, len);
        note(buf, len);
        return retval;   
      }
       
      template <typename T> void write(const T& t) { write(&t, sizeof(t)); }
      template <typename T> bool read(T& t) { return read(&t, sizeof(t)); } 

After each EEPROM access, we update the checksum accumulator and EEPROM address in preparation for the next access. Template forms of write() and read() offer type-safe access as shown previously in the EEPROM driver layer.

The writeSum() method writes the checksum accumulator to the end of the field in EEPROM. The corresponding testSum() method reads and tests the checksum in the EEPROM against the checksum accumulator. These methods are called after a sequence of writes or reads, respectively.

      void writeSum() { write(NvSum(~sum)); }
      bool testSum() { NvSum s;  return read(s) && NvSum(~sum) == 0; }

By writing the complement of the checksum accumulator, we ensure that the checksum of the entire field will be 0xFF. The compiler promotes this complement to a full-width unsigned integer, so we convert it back to an NvSum (an 8-bit unsigned value) before writing or testing it.

Finally, the next() method simply returns the EEPROM address, which, after reading or writing an entire field, is the address of the next field in EEPROM:

      EeAddr next() const { return addr; }
    };

Initializing a nonvolatile variable

At system startup, we'll arrange to call NvStore::open() for each nonvolatile variable. This method will search for the record; if found, will copy its contents to the RAM image. If either we can't find the record or we can't read the payload successfully, we'll assume this is the first time up for this variable and we'll create the record and initialize it from the RAM image. (Doing so assumes the image is initialized with default values before calling open().)

The open() method is a bit involved, so we'll look at it bit by bit.

The arguments are the name of the record to look up and the address and length of the RAM image:

    EeAddr NvStore::open(const char* name, void* buf, size_t len)
    {

First we'll declare an EEPROM address variable and initialize it to the start of our part of the EEPROM. We'll iterate over all of the records, advancing this address as we parse each record. If we reach the end of our part of the EEPROM, we'll quit unconditionally.

      EeAddr addr = base;

      while (addr < end)
      {

Each record starts with a header, which we'll parse with an NvField:

        NvField hdr(eeprom, addr);

First, get the length of the payload from the header. If the low-level EEPROM read fails, abort and return zero as a flag. (This flag value will cause update() to abort early as well; that's what we want since the EEPROM device isn't working.)

        size_t dataLen;
        if (!hdr.read(dataLen))
          return 0;

EEPROM's generally hold all ones after erasure. If the data length is all ones, we can assume there's no record here at all. If the length is not all ones, continue parsing.

        if (dataLen != ~0u)
        {

Read the record name from the EEPROM, character by character, and compare with the desired name. If they don't match, note that fact, but keep fetching name characters from the EEPROM until we find the zero byte that terminates the name.

          bool match = true;
          char c;
          const char* n = name;
          for (size_t i = 0;
               i < maxNameLen && hdr.read(c) && c != '\0';
               i++)
          {
            if (c != *n++)
              match = false;
          }

Recall that as we've been fetching from the record in EEPROM, the NvField object has been accumulating the checksum of that data. We can now ask the NvField object to fetch the checksum byte and test it.

          if (hdr.testSum())
          {

If we matched the name in the loop above, we've found the desired record, and the address in the NvField points to it. Now we need to read the payload to test its checksum. We don't want to read it into the RAM variable yet because if the checksum fails, we'll need the default values there. We use a temporary NvField to do the reading and checksumming, and we read the bytes into a dummy variable:

            if (match)
            {
              NvField pay(eeprom, hdr.next());
              uint8_t t;
              for (size_t i = 0; i < len; i++)
                pay.read(t);

If the payload checksum is good, copy the payload to the RAM variable. Otherwise, copy the defaults from the RAM variable to the payload in EEPROM. Either way we're done, so return the address of the payload to the client to use later when updating the payload.

We use an anonymous NvField to read the payload into RAM. Since we only need to run a single method, read(), we don't even need to name the NvField. If instead we're writing the RAM to the payload, we call update(), just as the application would.

              if (pay.testSum())
                NvField(eeprom, hdr.next()).read(buf, len);
              else
                update(hdr.next(), buf, len);

              return hdr.next();
            }

If we get to this point, we've scanned the record name, but it didn't match. Advance the address past the payload of this record and continue the search.

            else
            {   
              addr = hdr.next() + dataLen + sizeof(NvSum);
              continue;
            }
          }  
        }    

If we arrive here, we either found all ones in the payload length word, or the header checksum failed. In either case, we have to assume there are no records beyond this point in the EEPROM, so break out of the search loop. Create the desired record, and then initialize its payload to the default values from the RAM variable. Test first, however, to ensure the record will fit inside our part of the EEPROM.

        break;
      }
      
      size_t recLen = sizeof(size_t)                    // payload length
                    + min(strlen(name) + 1, maxNameLen) // record name
                    + sizeof(NvSum)                     // header checksum
                    + len                               // payload length
                    + sizeof(NvSum);                    // payload checksum

      if (addr >= end - recLen)
        return 0;

      NvField nhdr(eeprom, addr);
      nhdr.write(len);
      nhdr.write(name, min(strlen(name), maxNameLen - 1));
      nhdr.write('\0');
      nhdr.writeSum(); 

      update(nhdr.next(), buf, len);
      return nhdr.next();
    }

Updating a nonvolatile variable

As the application runs, it may change the value of the RAM image of a variable. Whenever this happens, we'll arrange to call NvStore::update() to back up the changes to EEPROM. The application will supply the EEPROM address of the payload of the nonvolatile record (it receives this address from open()).

After first checking for a valid address, update() simply creates an NvField to accumulate the payload checksum, uses it to copy the RAM image to the payload, and then writes out the new checksum.

    void NvStore::update(EeAddr addr, const void* buf, size_t len)
    {
      if (addr)
      {
        NvField payload(eeprom, addr);
        payload.write(buf, len);
        payload.writeSum();
      }
    }  

Declaring and using nonvolatile variables

In the application code, our nonvolatile variables should look and work like regular variables but they should also call NvStore::update() when they`re written.

We can use C++ operator overloading to "hook" the reading and writing of our variable. The client code will remain unchanged except for variable declarations and initialization. As an example, let's make a class that looks like an int while in use, but backs itself up to EEPROM whenever it's written to.

    class NonVolInt
    {
      int value;            // RAM image of the nonvolatile variable
      NvStore& store;       // EEPROM storage layout manager
      EeAddr addr;          // EEPROM address of the record payload

    public:
      NonVolInt(int v, NvStore& nvs, const char* name) 
      : value(v), store(nvs), addr(store.open(name, &value, sizeof(value))) 
      { } 

      operator const int&() const { return value; }

      const int& operator = (int v) 
      { 
        value = v; 
        store.update(addr, &value, sizeof(value));
        return value;
      }
    };

The constructor's v argument provides a default value for the nonvolatile variable. The constructor records this default value, saves a reference to the nonvolatile layout manager, and then calls NvStore::open() to either fetch a new value from EEPROM or to create a new record and initialize it with the default value. In either case, NvStore::open() returns the EEPROM address of the variable for later use.

The conversion operator, operator const int&(), simply returns a reference to the value field of the NonVolInt. It's a const reference, meaning the caller can't modify value through the reference; it can only read the value. (The second const is a promise that the operator won't modify the state of the NonVolInt object. It allows the compiler to generate better code in some situations.)

The conversion operator comes into play in code like this:

    class SerialPort
    {
      // ...

      NonVolInt baudRate;

    public:
      int baud() { return baudRate; }

      // constructor, etc...
    };

When the compiler sees us trying to read an integer from baudRate, it looks for a way to convert baudRate to an int. For a function return value we need only read the variable; thus, the const reference returned by the conversion operator is good enough. The compiler generates code to "call" the conversion operator and read the value through it.

Since the conversion operator is defined inline, it's reduced at compile-time to a simple fetch of value. The operator is still necessary, however, because the const nature of its returned reference ensures that client code can't write directly to the value field.

So how do we modify value? The assignment operator, const int& operator = (int), is invoked whenever we try to assign to a NonVolInt:

    class SerialPort
    {
      // ...

    public:
      baud(int b) { baudRate = b; }

     // ...
    };

Here, the assignment operator is called with b as its argument; thus, baudRate = b effectively becomes baudRate.operator = (b).

In order to preserve the usual assignment semantics, the operator first updates the value field from its argument, and at the end it returns a const reference to the value field, so that expressions like myBaud = baudRate = 9600 will still work. (The assignment to myBaud uses the result of the expression baudRate = 9600 as its source, but this result is precisely the reference returned from the assignment operator.)

In between these two operations, our assignment operator can do anything else it wishes. In the NonVolInt case, it writes the new value out to the EEPROM so the baud rate setting will persist through power cycles.

Generalizing to nonvolatile variables of any type

We'd like to be able to make any type of variable non-volatile, which we could do by creating more NonVolXXX classes like NonVolInt. This sort of copy-paste coding is tedious and error-prone, but we can easily avoid it with a template. Not only will a template allow a single piece of code to cover all possible data types, it will also correctly match the variable type and its length automatically.

The Nv template takes the variable type as an argument, but otherwise looks remarkably like the single-type class NonVolInt:

    template <typename T>
    class Nv
    {
      T t;                  // RAM image of the nonvolatile variable
      NvStore& store;       // EEPROM storage layout manager
      EeAddr addr;          // EEPROM address of the record payload

    public:
      Nv(const T& _t, NvStore& s, const char* name)
      : t(_t), store(s), addr(store.open(name, &t, sizeof(t)))
      { }

      operator const T& () const { return t; }

      const T& operator = (const T& _t)
      { 
        t = _t;
        store.update(addr, &t, sizeof(t));
        return t; 
      }
    };  

Note that we've replaced each reference to int in NonVolInt with the more general T in the Nv template. Now we can use any type for T and make it nonvolatile. For example:

    class SerialPort
    {
      // ...

      Nv<int> baudRate;

      public:
        SerialPort(NvStore& nvs)
        : baudRate(9600, nvs, "baudrate")
        { /* initialize hardware */ }

        int baud() const { return baudRate; }

        void baud(int b) { baudRate = b; }

        // ...
      };

The declaration "Nv<int> baudRate" creates an Nv object with int for the type argument T. When its methods are expanded, sizeof(t) will be sizeof(int) and so forth.

The SerialPort constructor uses an initial value of 9600, its NvStore argument, and a record name "baudrate" to initialize its nonvolatile baudRate member. This initialization calls the constructor of Nv<int>, which calls NvStore::open(), which either creates a new record with default values or reads an existing record into the int inside baudRate.

The two baud() accessor methods read and write, respectively, the int inside baudRate. When writing, the Nv<int> assignment operator also backs up the new value to EEPROM.

Larger nonvolatile variables

In actual practice, a class like SerialPort would need several nonvolatile variables rather than just the one shown above. As such, it may make more sense to store them together in a single record. For the SerialPort class, we might have:

    class SerialPort
    {
      struct Config
      {
        int baudRate;
        enum Parity { none, odd, even } parity;
        int dataBits;
        int stopBits;
      
        Config(int b = 9600, Parity p = none, int d = 8, int s = 1)
        : baudRate(b), parity(p), dataBits(d), stopBits(s)
        { }
      };

      Nv<Config> config;

      // continued below...

Two points are worth noting about this example:

In SerialPort's constructor we need to initialize its config member. Recall that the Nv template constructor needs a default value for the nonvolatile variable; this is where Config's constructor comes in handy:

    public:
      SerialPort(NvStore& nvs, int baud)
      : config(Config(baud), nvs, "serialport")
      { /* set up hardware */ }

The expression Config(baud) creates an anonymous, temporary Config object, passing the baud argument to its constructor and leaving the other arguments to take on their default values. This temporary object exists just long enough to be copied into the Nv<Config> object, and then the temporary object is destroyed.

Though this may seem a bit ponderous, the compiler actually just does the obvious: it initializes the Config object inside config with the specified values. So long as we define Config's constructor inline, the "temporary object" will be optimized away.

If we wish to reconfigure the serial port after system startup, we'll need some support from SerialPort. First, let's provide a method to show the current settings in the traditional "9600,n,8,1"-type format:

      void show(ostream& os) const
      {
        Config c(config);       // local copy of configuration

        os << c.baudRate << ","
           << (c.parity == Config::none ? "n" :
               c.parity == Config::odd  ? "o" :
                                          "e") << ","
           << c.dataBits << ","
           << c.stopBits;
      }

Note that we create c, a temporary Config object, because the Config object inside the nonvolatile variable config is private, preventing SerialPort::show() from accessing its members directly. The config object does, however, allow us to extract a const reference to it, which we use to initialize c. Since show() doesn't actually modify c, the compiler can optimize it away, in fact reaching directly inside config for its contents.

Next, to modify the serial port configuration, we'll need to make a local copy of config's contents, modify that copy, and then store it back into config. Again, we require the local copy because the Config object inside config is private.

      bool parse(istream& is)
      {
        Config c(config);       // local copy of configuration
        char p;                 // parity character
        char comma;             // dummy for commas

        if (is >> c.baudRate >> comma 
               >> p >> comma 
               >> c.dataBits >> comma 
               >> c.stopBits)
        {
          switch (tolower(p))
          {
            case 'n':  c.parity = Config::none;  break;
            case 'o':  c.parity = Config::odd;   break;
            case 'e':  c.parity = Config::even;  break;
            default:  return false;
          }
 
          config = c;
          return true;
        }
        return false;
      }
    };

While perhaps a bit rigid, this parser will work well enough for properly-formed strings. It first makes a temporary copy of config, and then it attempts to parse the string into its fields. If all fields parse cleanly, it then assigns the temporary copy back to config and returns true to indicate success. The write to config, of course, also causes a write to EEPROM of config's contents. If, on the other hand, one or more fields fail to parse, it simply skips the write-back to config; thus, config and its EEPROM backup remain unchanged.

Enhancements

The EEPROM manager in this article easily handles all power failures except those when the EEPROM is actually being written. Consider the following two cases:

  1. If power fails during system startup, one or more records might not be fully written. On the next power-up, however, the checksums on those records should fail, causing re-creation of the records in the usual way.

  2. If power fails during a later call to update(), the checksum of that record's payload might fail on the next power-up. This will cause a reversion to the first-time-up default values.

In the first case, we can handle any foreseeable corruption during power-up, except if a multi-byte error in a record leaves a corrupted record whose checksum just happens to come out correct. If this is a concern, the 8-bit ones-complement sum could be replaced with a wider sum, or even with a CRC.

In the second case, if the EEPROM writes result from human interaction with the system, we might reasonably expect the operator to notice the power failure during the operation and to take corrective action. If for a particular application this assumption isn't true, we may need to add redundancy, say by duplicating each record as a protection against one of the copies being corrupted.

If do we provide another storage layout, we'll want to make NvStore an abstract base class and derive all of the different layout managers from it. Doing so will allow choosing the layout manager to suit the application while preserving the Eeprom drivers and Nv template unchanged.

Conclusion

This article demonstrates several C++ techniques and how to use them in a real-world system. We've demonstrated:

As used in this article, these techniques incur little or no run-time penalty, but they make the code shorter, clearer, more secure, and more robust under long-term maintenance.

The full source code is available here: eeprom.h, nonvol.h and nonvol.cpp. If you've found this article useful, we'd appreciate hearing from you. Please email the author.


Copyright 2014 Real-Time Systems Inc. All Rights Reserved.