Sunday, April 7, 2013

Object-Oriented EEPROM device driver

After reading a fair number of postings on the Arduino forum about using the Arduino EEPROM class it is time to do something about this area. Some improvements are required. So far I have recommended the use of the basic support in AVR GCC, <avr/eeprom.h>, but after looking in to the code and problems using the Arduino EEPROM library I have come to the conclusion that there is a need for an abstraction of internal and external EEPROM access.

Out of the box AVR GCC supports the usage of EEPROM with the variable attribute EEMEM.

#define EEMEM __attribute__((section(".eeprom")))

Just as program memory (PROGMEM), variable/data may be declared to be in the EEPROM address space. The short form for this is the above macro EEMEM.

int x EEMEM;
int y EEMEM;

In the snippet the two variables, x and y, are declared with the attribute EEMEM. This puts them in the EEPROM address space instead of the SRAM. Assuming that the variable x and y are the first EEMEM variables they are given the addresses 0 and 2, that is &x == 0, and &y == 2 (or address of previous plus sizeof(previous)). The AVR GCC eeprom library provides functions to read and write data in the EEPROM address space. All access is done with the address of the variable.

int t = (int) eeprom_read_word(&x);
t += 1;
eeprom_write_word(&x, t);

Using EEPROM requires writing code for reading and writing data to and from the different memory address spaces. Normally the compiler does this for us for variables in SRAM (global/stack). The Arduino EEPROM library gives almost the same functionality but only byte level access.

External EEPROM may be connected to the Arduino in several ways. The more popular are devices use either the I2C/TWI or SPI bus, such as AT24CXX. The AT24C32 (8 Kbyte EEPROM) is often packaged together on modules with the I2C/TWI Real-time Clock chip DS1307.

Fig.1: Cosa EEPROM member functions

The object-oriented approach of Cosa is to construct an interface for EEPROM devices, both internal and external, and provide an application programmer's interface that allows easy usage with the base data types. The Cosa EEPROM class contains functions to read and write C/C++ base data types. The fundamental driver functions are block read and write, and a function to check if the device is ready (not busy writing data).

To achieve this Cosa uses a delegation design pattern with an EEPROM class and a EEPROM::Device class to be implemented per device type. This allows programs to simply change a variable to move from internal to external EEPROM and/or change size of EEPROM, etc. Also the EEPROM::Device only requires the implementation of the three fundamental functions, and all the data type read/write functions are reused between implementations. The EEMEM attributes may be used together with the Cosa EEPROM class. Please note that the compiler only allows a single EEPROM section/address space.

Fig. 2: AT24CXX Class Hiearchy

Devices such as the AT24C32 allows page write to improve speed. EEPROM are very slow devices when it comes to erase/write operations. The internal EEPROM may take as much as 3.3 ms per byte (approx. 50,000 clock cycles@16 MHz). For the AT24C32 EEPROM the write cycle is 10 ms per page of max 32 bytes, or byte, this does not include the I2C/TWI transfer time. 

The Cosa AT24CXX class takes advantage of the page write mode and handles all the details of reading and writing any size blocks of data. There is no hidden 32-byte max size. Applications must be aware of the delay caused by the write operation. Typical usage of the EEPROM is to store configuration data such as network addresses, sensor thresholds, etc. It is also common to use large EEPROMs for sensor data logs.

EEPROM eeprom;
const int DATA_MAX = 8;
long data[DATA_MAX] EEMEM;


void setup()
{
  ...
  for (uint8_t i = 0; i < DATA_MAX; i++) {
    eeprom.write(&data[i], (long) i);
  }
}


void loop()
{
  static int i = 0;
  long x;
  TRACE(eeprom.read(&x, &data[i]));
  trace << i << ':' << x << endl;
  x += 5;
  TRACE(eeprom.write(&data[i], x));
  i += 1;
  if (i == membersof(data)) i = 0;
  SLEEP(2);

}
Above is a snippet from the CosaEEPROM example sketch. The variable eeprom is used to access the internal EEPROM of the Arduino micro-controller. This is the default EEPROM::Device. The variable data[] is given the attribute EEMEM and the compiler provides the address to the variable (a symbol). Please note that the address of the data element is used for all EEPROM read/write functions.

The setup() function performs a simple initialization of the EEPROM variable data. Each element in the vector is given the initial value corresponding to it's index in the vector. The loop() function is performed every 2 seconds. It reads the data element using and index variable, i, and increments and writes the value back to the EEPROM. 

// The serial eeprom (sub-address 0b000) with binding to eeprom
AT24C32 at24c32(0);
EEPROM eeprom(&at24c32);


// Symbols for data stored in AT24CXX EEPROM memory address space
int x[6] EEMEM;
uint8_t y[300] EEMEM;
float z EEMEM;

void init_eeprom()
{
  int x0[membersof(x)];
  for (uint8_t i = 0; i < membersof(x0); i++) x0[i] = i;
  trace.print(x0, sizeof(x0), 16);
  TRACE(eeprom.write(x, x0, sizeof(x)));
 
  uint8_t y0[sizeof(y)];
  memset(y0, 0, sizeof(y));
  trace.print(y0, sizeof(y0), 16);
  TRACE(eeprom.write(y, y0, sizeof(y)));
 
  float z0 = 1.0;
  trace.print(&z0, sizeof(z), 16);
  TRACE(eeprom.write(&z, z0));
}

void setup()
{
  ...
  init_eeprom();
}

void loop()
{
  // Wait for 2 seconds; we don't want to burn too many write cycles
  SLEEP(2);
  ledPin.toggle();

  // Read the eeprom variables into memory
  uint8_t buffer[sizeof(y)];
  memset(buffer, 0, sizeof(buffer));
 
  TRACE(eeprom.read(buffer, &x, sizeof(x)));
  trace.print(buffer, sizeof(x), 16);

  TRACE(eeprom.read(buffer, &y, sizeof(y)));
  trace.print(buffer, sizeof(y), 16);

  float z1;
  TRACE(eeprom.read(&z1, &z));
  trace.print(&z1, sizeof(z1), 16);

  // Update the floating point number and write back
  z1 += 0.5;
  TRACE(eeprom.write(&z, z1));
 
  // Update the eeprom (y => y+1)
  for (size_t i = 0; i < sizeof(buffer); i++)
    buffer[i]++;
  TRACE(eeprom.write(&y, buffer, sizeof(buffer)));

  // Is the write completed?
  TRACE(eeprom.is_ready());
  eeprom.write_await();
  TRACE(eeprom.is_ready());
 
  // Read back and check
  TRACE(eeprom.read(buffer, &y, sizeof(y)));
  trace.print(buffer, sizeof(y), 16);
  ledPin.toggle();
}

The above CosaAT24CXX example sketch is larger and contains several EEPROM variables that are updated. Both vectors of integers of different sizes and floating point number are stored in the external EEPROM.

Compared to the previous example sketch the only difference is the binding of the eeprom variable to the AT24C32 instance. Otherwise the read and write code style is the same. By changing the instance binding the program will use the internal EEPROM or an other device.

Reading/writing data structures such as a C/C++ struct is done in the same fashion as above. Below is a snippet from the EEPROM example sketch.

EEPROM eeprom;
...
static const int NAME_MAX = 16;
struct config_t {
  int mode;
  int speed;
  char name[NAME_MAX];
};
config_t config EEMEM;

void setup()
{
  ...
  config_t init;
  init.mode = 17;
  init.speed = 9600;
  strcpy_P(init.name, PSTR(".EEPROM"));
  eeprom.write(&config, &init, sizeof(config));
}

void loop()
{
  ...
    config_t init;
    eeprom.read(&init, &config, sizeof(init));
    trace << PSTR("init(mode = ") << init.mode
          << PSTR(", speed = ")   << init.speed
          << PSTR(", name = \"")  << init.name
          << PSTR("\")\n");
  ...
}


First a "configuration" struct is defined as a data type (config_t) and a variable, config, in EEPROM address space. Second, in setup(), a local variable, init, is inititiated and written to the EEPROM. And last, in loop(), the data is read back. The snippet also shows how to write a string to the EEPROM. Note that the name field is initiated with a string in program memory.

1 comment: