EEPROM Part 2 – external I2C EEPROMs

About this post

In my last post I had explained the use of the internal EEPROM of the AVR microcontrollers or the AVR based Arduino boards. In this article I will focus on the external I2C controlled EEPROMs. With suitable libraries, such as the one from Sparkfun, you can write and read the EEPROMs very conveniently. For a better understanding, however, it is also worthwhile to learn how this works without a library.

The article is structured as follows:

In a separate post I introduce the external SPI based EEPROMs. They are so different from the I2C based EEPROMs that I couldn’t fit both in a single post.

Introduction

What is an EEPROM?

The abbreviation EEPROM stands for “Electrically Erasable Programmable Read Only Memory”. This somewhat contradictory name has evolved historically. I discussed this in my last post. In short, an EEPROM serves as a memory for data that should not be lost even after the power supply has been switched off.

What are the characteristics of EEPROMs?

One of the advantages of EEPROMs is their compact design. In addition, the data on an EEPROM is stored securely for a comparatively long time. In some cases, manufacturers guarantee data retention of more than 200 years. I will check this in 200 years and complain if necessary ;-).

A disadvantage, however, is the comparatively slow write speed of the EEPROMs. It is in the range of milliseconds for a single byte. Through “Page Writing”, the models discussed here increase their effective write speed considerably compared to the internal EEPROMs. However, they are still much slower than flash memory.

An EEPROM has a limited lifetime in terms of the number of write cycles. However, this disadvantage is of limited relevance, as the specified limit set is usually higher than one million. If the same memory cell were overwritten every second, its lifetime would be reached after ~11.5 days. If you overwrite it every 5 minutes, the lifetime would be exceeded after almost 20 years.

The EEPROMs I will discuss

In this article, I will focus on EEPROMs of the 24 series. These EEPROMs are labeled according to the scheme “24xxyy”.

The “xx” part encodes different voltage ranges and transmission speeds. Often you will find “LC”, “C” and “A” types. The “yy” usually indicates the storage capacity in kilobits. At the bottom left you can see, for example, the “24LC64“, which has a capacity of 64 kbit = 8 kilobytes.

You control the 24 series via I2C. There are also other series. The 25 series, for example, is controlled by SPI, the 11, 21 and 28 series communicate via one-wire techniques.

Further abbreviations define design types (PDIP, SIOC, etc), temperature ranges and others.

EEPROM 24LC64
24LC64 EEPROM
EEPROM 24C256
24C256 EEPROM Module

Power

The EEPROMs are quite undemanding as far as their power requirements are concerned. While writing, they usually consume 0.1 -1.0 milliamps. When reading, the value is even lower. In standby, the maximum current is a few microamps.

Pinout and connection to the microcontroller

Pinout of the 24 EEPROM series

The pinout of the EEPROMs of the 24 series usually looks like this:

EEPROM Pinout
EEPROM Pinout (24 series)
  • A0 / A1 / (A2): Address pins, 4 to 8 addresses can usually be set.
  • VCC / GND: Power supply, for example 2.5 – 5.5 Volt for the 24LCxx series (check the data sheet!).
  • WP: Write Protection;
    • Inactive when connected to GND.
    • Active when connected to VCC.
  • SDA/SCL: I2C connectors, max. 400 kHz for the 24LCxx series (check the data sheet!).
    • Address scheme: 1 0 1 0 A2 A1 A0 with GND = 0 and VCC = 1.
    • Example: A1/A2/A3 to GND → address = 0b1010000 = 80 = 0x50.

Connection to an Arduino Nano

Example circuit of a 24LC256 on an Arduino Nano:

EEPROM circuit with an Arduino Nano
EEPROM circuit with an Arduino Nano

Often you can do without the pull-up resistors. Just try it.

Write to and reading from the EEPROM

In a first, simple example, we write three byte values to the EEPROM and then read them from the EEPROM. You initiate the writing process with Wire.beginTransmission().

In the next step, you use Wire.write() to pass the memory address at which you want to store the value. For EEPROMs with a storage capacity up to 2kbit (=256 bytes) you can pass the address as a single byte. But usually you will use bigger ones. In this case, up to 512 kbit (= 64 kByte), the address can be an unsigned integer variable. You split the addresses into the MSB (Most Significant Byte) and LSB (Least Significant Byte).

You finalize the writing process with Wire.endTransmission(). However, the EEPROM needs some additional time to complete the write cycle. Before that, you can’t write additional data to the EEPROM. The “Write Cycle Time” can be found in the data sheet of your EEPROM. Typically, it is 5 milliseconds.

You must count the memory used when writing to calculate the next free address. There is no function that warns you that an address has already been written to. Strictly speaking, there is no distinction between “full” and “empty” memory. There can’t be no value at all at an address. New EEPROMS usually have 0xFF at all addresses. So, the data can only be overwritten to be deleted.

For reading, you use the Wire.requestFrom() function. You don’t have to add any waiting time between the read operations.

#include <Wire.h>
#define I2C_ADDRESS 0x50

void setup(){
  Wire.begin();
  Serial.begin(9600);
  
  unsigned int address = 0;
  byte byteVal_1 = 42;
  byte byteVal_2 = 123;
  byte byteVal_3 = 255;
  
  eepromByteWrite(address,byteVal_1);
  address++;
  eepromByteWrite(address,byteVal_2);
  address++;
  eepromByteWrite(address,byteVal_3);
  
  for(address=0; address<3; address++){
    Serial.print("Byte at address ");
    Serial.print(address);
    Serial.print(": ");
    Serial.println(eepromByteRead(address));
  }
}
void loop(){}

void eepromByteWrite(unsigned int addr, byte byteToWrite){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5); // important!
}

int eepromByteRead(unsigned int addr){
  int byteToRead;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 1);
  byteToRead = Wire.read();
  return byteToRead;
}

 

Here’s what the output looks like:

Output of ext_eeprom_byte_write_read.ino
Output of ext_eeprom_byte_write_read.ino

Write larger data sets to the EEPROM

In the next example, we first create an array of one hundred integers. The value of the i-th element of the array is 10 · i (arbitrarily chosen). We write the array to the EEPROM, read it from EEPROM and output it to the serial monitor.

Here is the sketch:

#include <Wire.h>
#define I2C_ADDRESS 0x50

void setup(){
  Wire.begin();
  //Wire.setClock(400000);
  Serial.begin(9600);
  unsigned int address = 0;
  unsigned long writeStart = 0;
  unsigned long writeDuration = 0;
  
  unsigned int arraySize = 100;
  int intArray[arraySize];
  
  for(unsigned int i=0; i<arraySize; i++){
    intArray[i] = i*10;
  }
  writeStart = millis();
  writeIntArrayToEEPROM(address, intArray, arraySize);
  writeDuration = millis() - writeStart; 

  address = 0;
  for(unsigned int i=0; i<arraySize; i++){
    Serial.print("intArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(readIntFromEEPROM(address + 2*i)); 
  }
  Serial.print("Time needed for writing [ms]: ");
  Serial.println(writeDuration);
}

void loop(){}

void writeIntArrayToEEPROM(unsigned int addr, int *iArr, unsigned int arrSize){  
  for(unsigned int i = 0; i<arrSize; i++){
    Wire.beginTransmission(I2C_ADDRESS);
    Wire.write((byte)((2*i+addr)>>8));
    Wire.write((byte)((2*i+addr)&0xFF));
    Wire.write((byte)(iArr[i]>>8));
    Wire.write((byte)(iArr[i])&0xFF);
    Wire.endTransmission();
    delay(5);
//    while(isBusy()){   // alternativ to delay(5).
//      delayMicroseconds(50);
//    }
  }
}
  
unsigned int readIntFromEEPROM(unsigned int addr){
  int intToRead;
  byte msByte;
  byte lsByte;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 2);
  msByte = Wire.read();
  lsByte = Wire.read();
  intToRead = msByte<<8 | lsByte;
  return intToRead;
}

bool isBusy(){
  Wire.beginTransmission(I2C_ADDRESS);
  return Wire.endTransmission();
}

Just a few explanations:

  • The array is passed to the write function writeIntArrayToEEPROM() as a pointer. I discussed this in my last post.
  • An integer is two bytes (at least on an Arduino). Therefore, not only the address, but also the value to be written must be split into MSB and LSB.
Output of ext_eeprom_int_array_write_read.ino
Output of ext_eeprom_int_array_write_read.ino

Writing the array took 552 milliseconds. 500 milliseconds of this time is “write cycle time”. If your EEPROM supports Fast I2C, you can reduce the other 52 milliseconds by uncommenting line 6: Wire.setClock(400000). This increases the I2C frequency from 100 to 400 kHz. At 400 kHz I was able to reduce the write process to 519 milliseconds.

The data sheet specifies the “write cycle Ttme” to be 5 milliseconds. In reality it’s shorter. As long as the EEPROM is busy with writing, it will not “listen” to I2C commands. This means that it will acknowledge during a write cycle. This property is used by the function isBusy() to check if the EEPROM is available.

Comment line 43, delay(5), and uncomment the following three lines. With this measure and the switch to 400 kHz, the time required to write the array dropped to 380 milliseconds. The real “write cycle time” is therefore about 3.5 milliseconds.

EEPROM Page Write

As you have just seen, it is possible to write several bytes to the EEPROM “in one go” (Page Write), i.e. without intermediate Wire.endTransmission() and delay(). Otherwise, writing to the integer array would have taken at least 1000 milliseconds.

This works because the EEPROM is segmented into memory sections (Pages) and has a buffer for these sections. The data to be written is quickly transferred to the buffer. And from there the data is written to the memory. The advantage is that writing the entire buffer to the memory takes no longer than the write cycle time. The page size can be found in the data sheet. For the 24LC64 the page size is 32 bytes, for a 24LC256 it is 64 bytes.

The pages start and end at fixed memory addresses. Page writing beyond the end of a page does not work.

Limitation by the Wire.write() buffer

For page writing, there is still a limiting factor related to the Arduino. And that’s the buffer for Wire.write(). The following sketch demonstrates the issue:

#include <Wire.h>
#define I2C_ADDRESS 0x50

void setup(){
  Wire.begin();
  Serial.begin(9600);
  unsigned int address = 0;
   
  for(unsigned int i=address; i<64; i++){
    eepromByteWrite(i,(byte)i);
  }

  byte byteArray[64];
  for(byte i=0; i<64; i++){
    byteArray[i] = i*2;
  }
  eepromBytePageWrite(address, byteArray, sizeof(byteArray));
  
  address = 0;
  for(unsigned int i=address; i<sizeof(byteArray); i++){
    Serial.print("byteArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(readEEPROM(i)); 
  }
}
void loop(){}

void eepromByteWrite(unsigned int addr, byte byteToWrite){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5);
}

void eepromBytePageWrite(unsigned int addr, byte *byteArrayToWrite, unsigned int sizeOfArray){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  for(unsigned int i=addr; i<sizeOfArray; i++){
    Wire.write(byteArrayToWrite[i]);
  }
  Wire.endTransmission();
  delay(5);
}

byte readEEPROM(unsigned int addr){
  byte byteToRead;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 1);
  byteToRead = Wire.read();
  return byteToRead;
}

 

The sketch writes values to the first 64 EEPROM memory addresses, namely the address itself. The values are written individually and with a 5 millisecond pause. With this, we have a defined initial state.

Then the sketch writes (or at least tries to write) 64 new values (address multiplied by 2)  to the EEPROM. This time it uses the page write method. Here is the unexpected result:

Output of ext_eeprom_wire_limit_test.ino
Output of ext_eeprom_wire_limit_test.ino

The first 30 values (0-29) have been replaced by the Page Write procedure, but after that get the old values. The reason is as follows: For successive Wire.write() commands (i.e. without intermediate Wire.endTransmission()), the data to be written is written to a buffer which is limited to 32 bytes. This happens on the Arduino side, and it has nothing to do with the EEPROM. The transmission of the address requires 2 bytes, so there are still 30 bytes left for the data. All additional Wire.write() calls end up in Nirvana.

Correct page writing

When page writing, three aspects have to be considered:

  1. The page size,
  2. the wire.write() buffer, and
  3. you must not write beyond a page end.

In the next example, we write an array of 100 integer values (= 200 bytes) to an EEPROM with a page size of 64 bytes using the page write method correctly. If we start at the address 0, then we have 64 bytes which we can write to the page. The limiting factor is the Wire.write() buffer. So, we first write the addresses 0-29, then 30-59. After that, we have to consider page end after address 63 and can only another 4 bytes at a time. The same happens in the following two pages. Finally, we write the remaining 8 bytes to the last page. Schematically, it looks like this:

Writing 100 integers to an EEPROM with a page size of 64 bytes
Writing 100 integers to an EEPROM with a page size of 64 bytes

A sketch could look like this:

#include <Wire.h>
#define I2C_ADDRESS 0x50
#define PAGE_SIZE 64
#define WRITE_LIMIT 30 

void setup(){
  Wire.begin();
  Serial.begin(9600);
  unsigned int address = 0;
  unsigned long writeStart = 0;
  unsigned long writeDuration = 0;
  
  unsigned int arraySize = 100;
  int intArray[arraySize];
  
  for(unsigned int i=0; i<arraySize; i++){
    intArray[i] = i*1;
  }
  writeStart = millis();
  writeIntArrayToEEPROM(address, intArray, arraySize);
  writeDuration = millis() - writeStart; 
  
  address = 0;
  for(unsigned int i=0; i<arraySize; i++){
    Serial.print("intArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(readIntFromEEPROM(address + 2*i)); 
  }
  Serial.print("Time needed for writing [ms]: ");
  Serial.println(writeDuration);
}

void loop(){}

void writeIntArrayToEEPROM(unsigned int addr, int *iArr, unsigned int arrSize){  
  unsigned int noOfIntsStillToWrite = arrSize;
  unsigned int arrayIndex = 0;
  
  while((noOfIntsStillToWrite != 0)){
    unsigned int chunk = (WRITE_LIMIT / sizeof(int));  // max chunk in number of ints
    unsigned int positionInPage = (addr % PAGE_SIZE);  // current position in page
    unsigned int spaceLeftInPage = (PAGE_SIZE - positionInPage) / sizeof(int); // available storage space 
    if(spaceLeftInPage < chunk){
      chunk = spaceLeftInPage;
    }
    if(noOfIntsStillToWrite < chunk){
      chunk = noOfIntsStillToWrite;
    }
    writeEEPROM(addr, iArr, chunk, arrayIndex);
    noOfIntsStillToWrite -= chunk;
    addr += (chunk * 2);
    arrayIndex += chunk;
  } 
}

void writeEEPROM(unsigned int addr, int *iArr, unsigned int chunkSize, unsigned int arrIdx){  
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));

  for(unsigned int i=0; i<chunkSize; i++){
    Wire.write((byte)(iArr[i+arrIdx]>>8));
    Wire.write((byte)(iArr[i+arrIdx])&0xFF);
  }
  Wire.endTransmission();
  delay(5); 
}

unsigned int readIntFromEEPROM(unsigned int addr){
  int intToRead;
  byte msByte;
  byte lsByte;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((byte)(addr>>8));
  Wire.write((byte)(addr&0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 2);
  msByte = Wire.read();
  lsByte = Wire.read();
  intToRead = msByte<<8 | lsByte;
  return intToRead;
}

 

You – or your sketch – have to calculate again and again the number of bytes you can write to the EEPROM in one step. As expected, the speed gain through the page write method is considerable:

Output of ext_eeprom_int_array_write_read.ino
Output of ext_eeprom_int_array_write_read.ino

Again, you can further increase the speed by using the isBusy() function and by switching to 400 kHz. The latter, of course, only if the EEPROM masters it.

Sparkfun Library

You can make your life much easier by using a library. I tested the SparkFun_External_EEPROM_Arduino_Library. It is easy to use and quite comfortable. You can install the library via the library manager of the Arduino IDE or you can download it directly from GitHub (link). The functions get() and put() are similar to the corresponding functions from EEPROM.h for the internal EEPROMs of the AVR boards. Their use is similar.

If you want to write strings to or read strings from the EEPROM, use the functions putString() and getString(). The rest should actually be self-explanatory. Here is an example sketch:

#include <Wire.h>

#include "SparkFun_External_EEPROM.h" // Click here to get the library: http://librarymanager/All#SparkFun_External_EEPROM
ExternalEEPROM myMem;

void setup()
{
  Serial.begin(115200);
  Serial.println("Qwiic EEPROM example");

  Wire.begin();

  if (myMem.begin() == false)
  {
    Serial.println("No memory detected. Freezing.");
    while (1)
      ;
  }
  Serial.println("Memory detected!");

  Serial.print("Mem size in bytes: ");
  Serial.println(myMem.length());

  //Yes you can read and write bytes, but you shouldn't!
  byte myValue1 = 200;
  myMem.write(0, myValue1); //(location, data)

  byte myRead1 = myMem.read(0);
  Serial.print("I read: ");
  Serial.println(myRead1);

  //You should use gets and puts. This will automatically and correctly arrange
  //the bytes for larger variable types.
  int myValue2 = -366;
  myMem.put(10, myValue2); //(location, data)
  int myRead2;
  myMem.get(10, myRead2); //location to read, thing to put data into
  Serial.print("I read: ");
  Serial.println(myRead2);

  float myValue3 = -7.35;
  myMem.put(20, myValue3); //(location, data)
  float myRead3;
  myMem.get(20, myRead3); //location to read, thing to put data into
  Serial.print("I read: ");
  Serial.println(myRead3);

  String myString = "Hi, I am just a simple test string";
  unsigned long nextEEPROMLocation = myMem.putString(30, myString);
  String myRead4 = "";
  myMem.getString(30, myRead4);
  Serial.print("I read: ");
  Serial.println(myRead4);
  Serial.print("Next available EEPROM location: ");
  Serial.println(nextEEPROMLocation);  
}

void loop()
{
}

 

Here’s what the output looks like:

Ausgabe von Example1_BasicReadWrite.ino
Ausgabe von Example1_BasicReadWrite.ino

I recommend to also try out the other example sketches of the library, in particular Example2_Settings.ino.

5 thoughts on “EEPROM Part 2 – external I2C EEPROMs

  1. Great work. Keep it up.
    Sehr komplexe HO Modeleisenbahn Ardu Mega Sketches schwirren herum. Ich habe nun alte Ardu Uno R3 (60) und Mega (12).
    Maeklin durchschnittliche Zuglaenge ist 90-180 cm. Wollte Unos fuer jeweils 3 Weichen mit je 2 inputs, 2 outpouts fuer Weichen LED Anzeige im Gleissteuerpult verwenden. Habe bisher 555 clock flip flops seit 1985 verbaut, die nach Spannungsabschaltung nichts mehr wissen. Mit neuer 14m x 8m habe ich jede Menge drei Ebenen verdeckte Gleise. Nur mal kucken passee’. Mit 24C256 externem Eeprom moechte ich den STellungszustand der Weichen speichern und nur das Eeprom beschreiben wenn eine Eingangsaenderung vorliegt. Meine Steuerpultkippschalter steuern die Weichenspulen mit 16V AV direkt an. Der zweite Kontaktsatz ist fuer die Anzeigenlogic.
    Hatte die Idee die Unos in den 150 cm langen Streckenabschnitte als inputs einzusetzen und per EThernet mit dem Steuerpult Mega als LED output zu verwinden. Signale und Blocksteuerung mit anderen Unos und Pult Megas. Sie brauchen nicht bei Adam und Eva anfangen. Ich habe in 1970 Rdf FernsehTechn gelernt, Meister und spaeter FH. Auf Rente nach USA, CN 20 Jahren MAN Roland Kundendienst. SPS, HMI, HP, Siemens, Fuji, Mitsu, A&B, Besten Dank aus Amiland.

    1. Danke für das Feedback! Klingt nach einem schönen Projekt. Bei Fragen helfe ich gerne.

Leave a Reply

Your email address will not be published. Required fields are marked *