Persisting Application Parameters in EEPROM

When an application starts, any data was was part of a previous execution is reset to the initialised values of the variables. Often, however, it is desirable to maintain configuration and state values between processor resets. EEPROM is a good option to store these values.

This article explores ways to make this task easy.

What is EEPROM

EEPROM is an acronym for Electrically Erasable Programmable Read-Only Memory. The name does not quite describe this type of memory because as it is not ‘Read-Only’ but can be changed by the application.

However, unlike RAM, EEPROM has a finite life. This is generally quoted in the microprocessor specification sheet and is in the order of 100,000 write/erase cycles for each byte position. The number of writes therefore needs to be properly managed to avoid premature failures. The number of reads, on the other hand, are unlimited, allowing you to read from the EEPROM as many times as you want without compromising its life expectancy.

As the EEPROM has to be specifically changed by the application, the variables stored in the EEPROM persist when you reset or power off the microprocessor. The EEPROM can therefore be used as permanent storage.

Each EEPROM address store one byte (8 bits) and the number of bytes you can store depends on the microcontroller on the Arduino boards. Common examples are listed in the table below.

MicrocontrollerEEPROM
ATmega328 (Arduino Uno, Nano, Mini)1024 bytes
ATmega168 (Arduino Nano)512 bytes
ATmega2560 (Arduino Mega)4096 bytes

Accessing EEPROM

In the Arduino environment, you can read and write from the EEPROM easily using the standard EEPROM library.

The library provides simple methods for reading and writing single bytes to the EEPROM (EEPROM.read(), EEPROM.write() and EEPROM.update()) or whole objects at a time (EEPROM.get(), EEPROM.put()).

EEPROM can also be treated as a array of bytes using the EEPROM[] access syntax. The specific EEPROM byte is accessed by referencing memory at byte addresses between 0 and the maximum address given by EEPROM.length().

Using Simple Read/Write

The easiest way to manage application data EEPROM is to read and write data from specific byte addresses. While simple, this has limitations:

  • It does not resolve the ‘initial’ configuration problem. When an application runs for the first time EEPROM is not initialised to correct values. Reading the data can result in invalid values with no easy way to determine if this is the case.
  • Reading and writing more than one byte requires some additional code to pack/unpack the bytes into the larger data structures. An example would be packing/unpacking a uint32_t into 4 uint8_t elements.
  • Handling more than a few bytes of configuration requires mapping variables to EEPROM addresses. This can result in complexity that can introduce bugs when code revisions are required.

A Configuration Management Class

Application parameters usually need to be treated as adata consistent set (ie, a set of internally consistent configuration parameters). This attribute and the EEPROM.get() and EEPROM.put() methods can be exploited to create a configuration management class.

The first thing to do is to group all the configuration data into a single data structure. This allows all the config EEPROM data to be consistently read and written in one call to EEPROM.get() and EEPROM.put() respectively.

To get over the ‘initial’ problem, the data structure includes two signature bytes to detect when the application does not have a valid configuration. In this case, the default configuration set is generated and saved to EEPROM, effectively initialising the EEPROM for next time.

If the configuration data set changes (usually as more data is added), the signature bytes can be changed and the default values will be transparently reconstructed. For more complex applications or to persist data between version upgrades, a more graceful way to handle this is to add a version number in the configuration data set. This allows the class fill in the changed/additional detail in the code before ‘upgrading’ the data set in EEPROM.

To round out the class, methods provide access to read and change the configuration data. This allows the class to independently enforce any required data consistency rules.

An example class cAppConfig implementing these ideas is given below. This example class encapsulates two configuration items delaySelect and lastSlotID to keep it manageable in this context, but is easily extensible to more data.

The begin() method should be called in setup() to load the configuration parameters. In the normal manner for such application parameters, all changes are available immediately from the instantiated cAppConfig object, but are lost when the processor resets unless configSave() is invoked.

#include <EEPROM.h>
 // Handle the Application configuration parameters stored in EEPROM
 // Access to these parameters is through the object properties only
 //
 typedef struct configData_t
 {
   uint8_t signature[2];
   uint8_t version;
   // application config data starts below
   uint16_t delaySelect;
   uint16_t lastSlotID;
 };

 class cAppConfig
 {
 public:
   inline uint16_t getSelectDelay() { return(_D.delaySelect); };
   inline void setSelectDelay(uint16_t n) { _D.delaySelect = n; };
   inline uint16_t getLastSlot() { return(_D.lastSlotID); };
   inline void setLastSlot(uint16_t n) { _D.lastSlotID = n; };

   void begin()
   { 
     if (!configLoad())
     {
       configDefault();
       configSave();
     }
   };

   void configDefault(void)
   {
     _D.signature[0] = EEPROM_SIG[0];
     _D.signature[1] = EEPROM_SIG[1];
     _D.version = CONFIG_VERSION;
     _D.delaySelect = SELECT_DELAY_DEFAULT; 
     _D.lastSlotID = LAST_SLOT_DEFAULT; 
   }
 
   bool configLoad(void)
   {
     EEPROM.get(EEPROM_ADDR, _D);
     if (_D.signature[0] != EEPROM_SIG[0] && 
         _D.signature[1] != EEPROM_SIG[1])   
       return(false);
    // handle any version adjustments here
    if (_D.version != CONFIG_VERSION)
    {
      // do something here
    }

    // update version number to current
    _D.version = CONFIG_VERSION;

    return(true);
   }
 
   bool configSave(void)
   {
     EEPROM.put(EEPROM_ADDR, _D);
     return(true);
   }

 private:
   const uint16_t SELECT_DELAY_DEFAULT = 1000; // milliseconds
   const uint16_t LAST_SLOT_DEFAULT = 99;      // number
   const uint16_t EEPROM_ADDR = 0;
   const uint8_t EEPROM_SIG[2] = { 0xee, 0x11 };
   const uint8_t CONFIG_VERSION = 0;
   configData_t _D;
 };

Additional sophistication can be added to this class by cycling the start memory address. This is done to even out the wear on the EEPROM across the whole address range.

In this case, to read the current configuration, the class would need to search through memory until it finds the two signature bytes at the start of a valid configuration block. The next time the data is written, the start address is incremented by 1 before writing. The write address cycles back to 0 once it reaches EEPROM.length() – sizeof(configData_t).

As my applications generally write data with very low frequency once they are set up, I have not found it necessary to implement this additional memory management overhead.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s