Interfacing with I2C EEPROM


 

A couple weeks ago I was contacted by someone named Stephen for help regarding an Arduino library I wrote for interfacing to an I2C EEPROM chip.  Stephen was having problems with the read method not returning the data he had written using the write method.  Contributing to the unknown, Stephen was using the Atmel ATtiny85 chip, where my library had only ever been successfully tested on the Arduino Uno and Mega boards.

In the end, I was able to correct my library and fully document everything in the process, so I thought it was worth a detailed blog posting generally around the subject of interfacing to the memory (I2C, SPI, etc), and some guidance on how to choose the right memory type and physical interface for a given application.  As a follow-on post, I will discuss different types of interfaces and different types of physical memory as it relates to speed and suitability for various applications.


The library in question was written for the ST Micro M24M01, however, it can also be used with most variants of this chip (i.e. the On Semiconductor CAT24M01).  Admittedly, as it existed in my github repository, it lacked proper code documentation in the comments that otherwise could have helped Stephen work through the issues and possibly debug the problem himself.

In the process of conducting some investigative debugging, I found out the problem was due to a bug in my code which in turn was manifested as a result of a change in the Arduino Wire library in how the Wire library handles a repeated start condition on the I2C bus.  I don’t want this to be a tutorial about I2C communications, but a repeated start condition is a protocol unique to the I2C bus and is commonly used for interfacing with memory devices.

Breaking the problem down, I had to separate two potential issues: 1) was it just a bug in my library alone, 2) was it the fact that Stephen was using an Atmel ATtiny85, or 3) was it a combination of the two.  It actually turned out the be a combination of the two.

As I knew I had used this library on an Arduino Uno before in the past, and it worked, I decided to start from there.  I grabbed an Arduino Uno and started troubleshooting.  It helped that I had a Saleae Logic 8 to hook up to the SDA and SCL lines and see the bits being sent over the I2C bus.

 

General Process for Interfacing to the EEPROM

The generic process for communicating to a M24M01 chip is as follows:

  1. Send the I2C bus address (the address of the chip on the I2C bus)
  2. Optionally send the memory address you want to read/write to. If no memory address sent during a read operation, then data is read starting from the current address pointed to by the address pointer.
  3. Send the data to write to the chip, or clock out the data you want to read from the chip

Now, there are a few caveats with the above process depending on whether you are reading or writing from the chip and, if writing, the type of write operation being performed.

 

Details of Writing Data to the EEPROM

To write data to the EEPROM chip, follow this sequence

  1. Send the Device Select command with the R/W bit = 0 to indicate a write operation (don’t forget any memory address bits, if applicable, that get stuffed here)
  2. Send the MSB of the memory address to be written
  3. Send the LSB of the memory address to be written
  4. Send the data byte(s) to write to memory
  5. As shown below, the EEPROM follows every byte with an ACK bit.

For writing data, there are two methods that can be used and I’ll explain them here:

Byte Write

  • Writes only one byte of data
  • Memory address must be sent to set the memory address pointer
  • Uses the most I2C bus communication overhead due to the Device Select command, two memory address bytes, followed by the data bytes to be written

Page Write

  • Called ‘Page Write’ because it only performs a write operation within the given boundary of one page, usually 256 bytes (i.e. address 0x00 – 0xFF, 0x100 – 0x1FF, etc)
  • Writes N-number of bytes up to a maximum of 256.  This is usually a limitation of the EEPROM chip itself.
  • Memory address must be sent to set the memory address pointer
  • Uses least amount of I2C bus communication overhead since the Device Select command and memory address bytes must only be sent once followed by N-number of bytes.

Details of Reading Data from the EEPROM

As with writing, there are also a few caveats with regard to reading data from EEPROM.  For reading data, there are four methods that can be used and I’ll explain them below.  Reading data from EEPROM can be a little more confusing because a read sequence actually starts with a I2C write operation (i.e. to write the memory address to the address pointer in the EEPROM chip).  That is, if you are using any of the ‘random’ read operations described below:

  1. Current address read (Single Byte Read – Method 1)
    • Reads only one byte of data
    • Memory address pointer must already be pointing to the desired address
    • Uses more I2C bus communication overhead due to the Device Select command that must be sent each time
  2. Random Address read (Single Byte Read – Method 2)
    • Reads only one byte of data
    • Memory address must be sent to set the memory address pointer
    • Uses the most I2C bus communication overhead due to the Device Select command, two memory address bytes, followed by a 2nd Device Select command before reading back one byte of data
  3. Sequential Current read (Block or Bulk Read – Method 1)
    • Reads N-number of bytes, no maximum.  Reading past the end of the last address of the entire chip will wrap around and start reading from address 0x00 again.
    • Memory address pointer must already be pointing to the desired address
    • Uses the least amount of I2C bus communication overhead
  4. Sequential Random Read (Block or Bulk Read – Method 2)
    • Reads N-number of bytes, no maximum.  Reading past the end of the last address of the entire chip will wrap around and start reading from address 0x00 again.
    • Memory address must be sent to set the memory address pointer
    • Uses next to least amount of I2C bus communication overhead

As noted in the datasheet, a read or write operation is terminated by sending a stop condition on the I2C bus.  Therefore, a repeated start condition is needed to keep the bus active after the memory address bytes have been sent before proceeding to send the data to write or clocking out data being read.

Write Timing of the EEPROM

As defined in the datasheet, it takes a finite amount of time to commit the data to the physical EEPROM memory cells.  This can be a few or several milliseconds for each write, regardless of whether you are writing one byte or a full page of 256 bytes.  As you are probably realizing by now, it is most inefficient to perform single byte writes to the EEPROM whenever your firmware calls for more than one byte to be committed to EEPROM at a time.  Not only do you have the I2C bus communication overhead associated with each write operation, but you must wait for N-milliseconds after each write to ensure the EEPROM chip is ready to accept the next write operation.

Methods of Addressing Write Timing

There are at least a couple of popular methods of addressing write timing and can marginally improve write access speeds.

Simple Timer Delay

The simple timer delay method is just as it sounds, whereby a simple dead timer delay can be used to wait N-milliseconds, plus some room for margin, for the write operation to complete before continuing with another write operation.

Device Polling

The device polling method is the most efficient, as it simply repeatedly polls the EEPROM chip until the chip stops responding with a busy status.  To poll the EEPROM chip for the busy status, simply follow this sequence:

  1. Send the Device Select command with the R/W bit = 0 to indicate a write operation (memory address bits are “don’t care” here).
  2. The EEPROM will respond with a NO ACK to indicate it is busy, or ACK to indicate it is ready for the next command.

EEPROM is Busy

EEPROM is Ready

Note: regarding read operations, the read access time is typically insignificant in comparison to the write access time, so I won’t be discussing it here.

Conclusion

With the changes made to the Arduino’s core Wire library regarding how they handle a repeated start condition it is important to utilize the overloaded, optional, parameters in the Wire.endTransmission() and Wire.requestFrom() class methods.  You can find more information on these methods and their optional parameters here.  For convenience, I’ve listed some standard routines from my CAT24M01 github library below that show how the EEPROM access routines described above actually look like.  I hope you’ve enjoyed the read and thank you for checking it out!

Write Byte:

uint32_t CAT24M01::write(uint32_t memAddress, uint8_t data)
{
    uint8_t retVal;
        
    // First transmission byte (beginTransmission) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    Wire.beginTransmission((uint8_t)((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)));
    Wire.write((uint8_t)((memAddress >> 8) & 0xFF));    // Send memory address bits [15:8]
    Wire.write((uint8_t)(memAddress & 0xFF));            // Send memory address bits [7:0]
    Wire.write(data);                                   // Send data byte to write
    retVal = Wire.endTransmission();                    // End I2C transmission
        
    return retVal;                                      // Return number 1 for success; 0 for error
}

Write Page:

uint32_t CAT24M01::write(uint32_t memAddress, uint8_t * buffer, uint8_t numBytes) 
{
    uint8_t retVal;
        
    // First transmission byte (beginTransmission) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    Wire.beginTransmission((uint8_t)((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)));
    Wire.write((uint8_t)((memAddress >> 8) & 0xFF));    // Send memory address bits [15:8]
    Wire.write((uint8_t)(memAddress & 0xFF));           // Send memory address bits [7:0]
    
    uint8_t bytesWritten;
    bytesWritten = Wire.write(buffer, numBytes);        // Send buffer of data bytes to write
    retVal = Wire.endTransmission();                    // End I2C transmission
    
    return (uint32_t)bytesWritten;                      // Return number of bytes written
}

Read Byte:

Note: the optional parameter ‘0’ in Wire.endTransmission(0)
uint32_t CAT24M01::read(uint32_t memAddress, uint8_t * buffer)
{
    uint8_t retVal;
        
    // First transmission byte (beginTransmission) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    Wire.beginTransmission((uint8_t)((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)));
    Wire.write((uint8_t)((memAddress >> 8) & 0xFF)); // Send memory address bits [15:8]
    Wire.write((uint8_t)(memAddress & 0xFF));        // Send memory address bits [7:0]
    retVal = Wire.endTransmission(0);                // Send restart condition (false)
    
    // First transmission byte (requestFrom) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    // requestFrom() puts the received bytes in a receive buffer.
    retVal = Wire.requestFrom(((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)), (uint8_t)numBytes);
    
    if (Wire.available()) 
    {
        *buffer = Wire.read();                       // Read bytes from I2C receive buffer
    }
    
    return 1;                                        // Return number of bytes read (always 1 here)
}

Sequential Random Read:

Note: the optional parameter ‘0’ in Wire.endTransmission(0)
uint32_t CAT24M01::read(uint32_t memAddress, uint8_t * buffer)
{
    uint8_t retVal;
        
    // First transmission byte (beginTransmission) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    Wire.beginTransmission((uint8_t)((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)));
    Wire.write((uint8_t)((memAddress >> 8) & 0xFF)); // Send memory address bits [15:8]
    Wire.write((uint8_t)(memAddress & 0xFF));        // Send memory address bits [7:0]
    retVal = Wire.endTransmission(0);                // Send restart condition (false)
    
    // First transmission byte (requestFrom) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    // requestFrom() puts the received bytes in a receive buffer.
    retVal = Wire.requestFrom(((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1) | ((memAddress >> 16) & 0x01)), (uint8_t)numBytes);
    
    uint32_t index;
    for (index = 0; index < numBytes; index++ )
    {
        if (Wire.available())
        {
            buffer[index] = Wire.read();             // Read bytes from I2C receive buffer
        }
    }
    
    return index;                                    // Return number of bytes read
}

Get Busy Status:

uint8_t CAT24M01::getStatus(void)
{
    uint8_t retVal = 0;
        
    // First transmission byte (beginTransmission) needs bits aligned per 24M01 datasheet
    // Wire library adds the R/W bit, so don't left shift the bits for the R/W bit here
    // 16th bit of memory address gets stuffed in here, then send next two octets of memory address bits
    Wire.beginTransmission((uint8_t)((DEFAULT_ADDRESS << 3) | ((this->busAddress) << 1)));
    retVal = Wire.endTransmission();
        
    return retVal;                                  // Return busy status
}

 

Leave a comment

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