Basic operations with EEPROM using Arduino and ConnDuino

A temperature controller should remember the desired temperature level after powering it off and on. An alarm clock, the next time it should ring. Several projects should remember some user preferences. Other projects need more space to store their strings. To these requirements, EEPROM seems a convenient and economical solution. This article makes an introduction to EEPROM modules, how to wire them, understand them and effectively take advantage of them in our programs with an Arduino or any compatible board, like the ConnDuino that is discussed through this site. To be more specific, this article is about EEPROM modules that use the I2C (also called TWI) interface, although a section about the Atmega328 built-in EEPROM is also included. This will be the first in a series of articles, dealing with EEPROM.

From the Wikipedia we get a nice definition for EEPROM:

EEPROM (also written E2PROM and pronounced "e-e-prom", "double-e prom", "e-squared", or simply "e-prom") stands for Electrically Erasable Programmable Read-Only Memory and is a type of non-volatile memory used in computers and other electronic devices to store small amounts of data that must be saved when power is removed, e.g., calibration tables or device configuration.

As you can see, this memory type is defined as read-only. This doesn’t mean that we cannot ever write anything to them. This would make them useless for any purpose. We can write data to them, but each individual memory address may be rewritten up to a specified number of times. Typically specifications call for a 100 000 write count.

With this limitation, it seems ineffective to use EEPROM for high frequency write operations. For example, if you need to write at the same memory address once per second, you could destroy this overused memory cell after: 100 000 / 3600  = 28 hours! However, most application don’t require that frequent updates. Examples of perfectly suitable uses are:

  • Storing configuration data, that the device needs to read when it is powered on, and that remain unmodified for its lifecycle.
  • Storing user preferences that may change during the devices lifecycle without ever reaching the 100 000 limit.
  • Storing a daily log of data. The 100 000 limit would be reached after 274 years. This is considered safe. 

The EEPROM modules we are going to use for this article are the “24LC256” chips. These come in 8-pin PDIP configuration, and are capable to store up to 256 000 bits of data which is equal to 32KB. They use the I2C interface and their datasheet indicates the number of erase-write cycles to be more the one million.

They may be powered by voltages down to 2,5V and up to 5,5V. Thus, they are usable with both the Arduino Uno, Mega and the other 5V boards, as well as with the Due and Lilypad, that operate at 3.3V. Similarly, referring to our board, ConnDuino, they may be used in either 5V or 3.3V configuration.

Pinout and Wiring

The pinout for the “24LC256” EEPROM module is shown in the figure below. Descriptions for each pin are given in the following table.

24LC256 EEPROM pin description

Pin 

Description 

Vcc

Connect to positive voltage, in the range 2.5V~5.5V

Vss

To ground

SDA

To the SDA bus of the I2C interface. In Arduino UNO this is A4.

SCL

To the SCL bus of the I2C interface. In Arduino UNO this is A5.

A0, A1, A2

These three pins define the address of the device in the I2C interface. All three define the 3-bit value [A2][A1][A0], which may represent a number A between 0 and 7. When any of these pins is grounded, its bit is assumed to be zero. Practically, the final I2C address of the device should be 0x50 + A (in hex format). When all of them are grounded, the address is 0x50. This config is used in ConnDuino.

WP

Write protect when pulled up. To be able to write any data, this pin should be connected to ground. 

 

Wiring an “24LC256” EEPROM module with an Arduino Uno may be accomplished as shown in the next figure:

Connecting an “24LC256” EEPROM module to ConnDuino requires just to plug the module to the existing socket on the board:

Atmega microprocessor built-in EEPROM

As indicated in the datasheet, the Atmega328p microprocessor is equipped with a built-in EEPROM. Its size is 1KB, and the erase/write count is specified at 100 000.

The size is rather small, but for simple projects that need to store some configuration options this should be adequate. Extra care should be taken however, not to come close to the specified erase/write cycles, since a failure of the built-in EEPROM could mean an entire Atmega328p replacement.

To use the built-in EEPROM, you have to include the EEPROM library in your sketch. In this library two functions are implemented:

byte EEPROM.read( int address)
void EEPROM.write(int address, byte value)

The first function returns a byte from the built-in EEPROM while the second writes a byte to it. Argument ‘address’ is the location where the byte should be read from or written to. The first address is 0 and the final one should be 1023, since this is a 1KB EEPROM.

The following sketch writes an array of 10 bytes to the built-in EEPROM, during setup and then reads the stored values back, summing them up every 3 seconds. The data are written sequentially, at addresses starting from 800 and beyond:

#include <EEPROM.h>
const int START_ADDRESS = 800;
long  sum;                    //the sum of read values

void setup(){
      Serial.begin(9600);
      sum = 0;
      byte  values[10] = {1, 3, 5, 12, 43, 23, 5, 124, 99, 55};  
      for (int i=0; i<10; i++){
            EEPROM.write(i+START_ADDRESS, values[i]);
      }
}

void loop(){
      delay(3000);
      for (int i=0; i<10; i++){
            byte readByte = EEPROM.read(i+START_ADDRESS);
            sum += readByte;
      }
      Serial.println(sum);
}

A more advanced library that supports transparent writing and reading of any data type, not only single bytes, is EEPROMex.Details can be found here.

 

Using external I2C EEPROM in a sketch program

EEPROMs organize their space in a per byte base. This means you can write or read single bytes. You should be in position to interpret these bytes to meaningful data types, like char, int, float etc. But, many data types consist of more than one byte. Typically, the common data types in C/C++ have the following lengths in the Arduino environment:

  • char:                1 byte
  • bool:                1 byte
  • int:                   2 bytes
  • long:                4 bytes
  • float:                4 bytes
  • double:            4 bytes on UNO/Mega, 8 bytes on Due!

In the Arduino environment the following data types exist also, with the specified lengths:

  • byte:                1 byte
  • uint8_t :           1 byte
  • uint16_t:          2 bytes

Custom data types, like structs and classes or compound data like unions and arrays have a variable size that could be as low as 1 byte, and up to thousands, restricted only by the available memory.

Any multi-byte object should be written or read as a sequence of bytes. Of course decomposing variables to bytes and recomposing them, any time we want to write or read something in EEPROM sounds more than laborious. Hopefully, for simple objects, it can be accomplished in an easy and almost transparent way, using the two routines shown below. They use the Wire library for handling the I2C bus communication.

template <class T>
uint16_t writeObjectSimple(uint8_t i2cAddr, uint16_t addr, const T& value){

      const uint8_t* p = (const uint8_t*)(const void*)&value;
      uint16_t i;
      for (i = 0; i < sizeof(value); i++){
            Wire.beginTransmission(i2cAddr);
                  Wire.write((uint16_t)(addr >> 8));  // MSB
                  Wire.write((uint16_t)(addr & 0xFF));// LSB
                  Wire.write(*p++);
            Wire.endTransmission();
            addr++;
            delay(5);
      }
      return i;
}
template <class T>
uint16_t readObjectSimple(uint8_t i2cAddr, uint16_t addr, T& value){

      uint8_t* p = (uint8_t*)(void*)&value;
      uint8_t objSize = sizeof(value);
      uint16_t i;     
      for (i = 0; i < objSize; i++){
            Wire.beginTransmission (i2cAddr);
                  Wire.write((uint16_t)(addr >> 8));  // MSB
                  Wire.write((uint16_t)(addr & 0xFF));// LSB
            Wire.endTransmission();
            Wire.requestFrom(i2cAddr, (uint8_t) 1);        
            if(Wire.available()){
                  *p++ = Wire.read();
            }
            addr++;
      }
      return i;
}

As you can see, these are template functions. When the sketch is compiled, they are realized, replacing their T argument with the correct data type, the user calls them with. If called with many different data types, many instances of these functions will be created. Using templates, the replication of the same code for many different data types, that should be handled the same way, is avoided. However, the compiled sketch is not reduced in size. Inside the processor different versions of the required functions are created, as if we have written them for any special data-type we need. An inconvenience though is that template functions should be defined in a separate header file.

The writeObjectSimple function writes a value of any type, to the specified memory address. The function finds the size of the data type and sends it byte-by-byte to the Wire object, that is responsible for the I2C interface communication. The I2C address is specified with the first argument. Using the function is straightforward. In the following code snippet, two float values are written to the I2C EEPROM (with I2C address 0x50), consecutively, starting at memory address 24000:

float f1 = 25.5;        //the 1st variable to be written
float f2 = 45.05;       //the 2nd one     

uint16_t addr = 24000;
addr += writeObjectSimple (0x50, addr, f1);
addr += writeObjectSimple (0x50, addr, f2);

The readObjectSimple function reads a value of any type, from the specified memory address. The function finds the size of the data type and requests it byte-by-byte from the Wire object, that is responsible for the I2C interface communication. The I2C address is specified with the first argument. Using the function is again simple. In the following code snippet the two float values that written before, are read back from EEPROM:

float f1=0;       //the 1st variable to be read
float f2=0;       //the 2nd one

uint16_t addr = 24000;
addr += readObjectSimple (0x50, addr, f1);
addr += readObjectSimple (0x50, addr, f2);

Serial.println(f1);
Serial.println(f2);

What will happen when someone tries to read back a value using different data type than the one used for writing it? For example, if a stored float is attempted to be read as long? The code will run without problems at during reading, but the result will be meaningless. The read long will get some arbitrary value (not the truncated float) because different data-types have completely different byte representations. If we are lucky this value may cause a crash to the program or result in a profound error in our output. If not, we may find ourselves in the unfortunate position, to treat meaningless output as correct one. With a few words: you should know where and with what data type each object is stored.   

Concluding, these functions are simple and safe. You can use them with any data type, having any size, and the result will be as expected. No worries about crossing the EEPROM page borders or about the Wire library buffer limit, since only one byte is written or read each time. They are great for any simple or complex operations and they have a small footprint to the sketch size. The only drawback is that they are slow. For simple uses they probably are fine. But, EEPROM support the so called block read/write operations, that enable the transfer of multiple bytes in one pass. This subject will be covered in a following article.

Example

The following sketch writes an array of 10 int’s to the I2C EEPROM, during setup and then reads the stored values back, summing them up every 3 seconds. The data are written sequentially, at addresses starting from 831 and beyond. Using this address, the first int crosses a page border, but the data are written correctly, because single bytes are send to EEPROM each time.  Output to the serial port should like this:

2000
4000
6000
8000
…

The sketch file should contain the following:

#include "eeprom_routines.h"
#include <Wire.h>
#define START_ADDRESS 831//memory address of the 1st stored byte 
long  sum;               //the sum of read values, updated in loop

//--------------------------------------------------------------

void setup(){

      Serial.begin(9600);
      Wire.begin();
      sum = 0;    //initialize sum

      //define the values
      int values[10] = {1026, 3, 5, 22, 43, 23, 500, 224, 99, 55};

      int  addr = START_ADDRESS; //this will increment after each write
      for (int i=0; i<10; i++){
            addr += writeObjectSimple(0x50, addr, values[i]);
      }    
}
//--------------------------------------------------------------

void loop(){

      delay(3000);
      int addr = START_ADDRESS;     //this will increment after each read
      int readInt;                  //the read value

      for (int i=0; i<10; i++){
            addr += readObjectSimple(0x50, addr, readInt);
            sum  += readInt;
      }
      Serial.println(sum);
}

The eeprom_routines.h header file should contain the template functions mentioned above. It should contain the following:

#ifndef  EEPROM_ROUTINES
#define  EEPROM_ROUTINES

#if defined(ARDUINO) && ARDUINO >= 100
  #include "Arduino.h"
#else
  #include "WProgram.h"
#endif
#include <Wire.h>

template <class T>
uint16_t writeObjectSimple(uint8_t i2cAddr, uint16_t addr, const T& value){

      const uint8_t* p = (const uint8_t*)(const void*)&value;    
      uint16_t i;
      for (i = 0; i < sizeof(value); i++){
            Wire.beginTransmission(i2cAddr);
                  Wire.write((uint16_t)(addr >> 8));  // MSB
                  Wire.write((uint16_t)(addr & 0xFF));// LSB
                  Wire.write(*p++);
            Wire.endTransmission();
            addr++;
            delay(5);  //max time for writing in 24LC256
      }
      return i;
}

     

template <class T>
uint16_t readObjectSimple(uint8_t i2cAddr, uint16_t addr, T& value){

            uint8_t* p = (uint8_t*)(void*)&value;
            uint8_t objSize = sizeof(value);
            uint16_t i;      
            for (i = 0; i < objSize; i++){
                  Wire.beginTransmission (i2cAddr);
                        Wire.write((uint16_t)(addr >> 8));  // MSB
                        Wire.write((uint16_t)(addr & 0xFF));// LSB
                  Wire.endTransmission();
                  Wire.requestFrom(i2cAddr, (uint8_t)1);         
                  if(Wire.available()){
                        *p++ = Wire.read();
                  }
                  addr++;
            }
            return i;
}

#endif

In a following article, the page write and read operations will be covered. This will enable sending and getting multiple bytes to EEPROM, making things considerably faster. Applications to character string and array storage will be shown.

Useful links

EEPROM Library 

Arduino EEPROMex library

Adding External I2C EEPROM to Arduino (24LC256)

24LC256 product page 

 

Comments

Erik (not verified)

What a great explanation of using an EEPROM ! By far the best I looked at, thank you!

Also I am looking forward to your weather station and watering system projects. I have a long interest in weather/weather info gathering. Just build an arduino system to capture the serial data from the Peet Bros weather station so I can nog do with that data whatever I want, build my own 'weather picture' for example. great stuff.

Greetings from The Netherlands!

Erik

connadmin

Thank you Erik. Weather data is my thing too.

For more powerful uses of EEPROM i have built a library that i will publish as soon as i find some extra time. It is capable to write/read arrays, bitmap pictures, fonts and of course strings. It is quite impressive, particularly when coupled with a TFT screen for rendering text using a calligraphic font.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.