Post

ENG | Arduino, EEPROM Memory

This comprehensive guide demonstrates using an external EEPROM chip with Arduino for data storage. It covers I2C communication, Arduino's byte order, and code examples for writing/reading data, including simulated measurements. Crucially, it addresses the issue of writing across EEPROM page boundaries which corrupts data. The article explains the problem, provides relevant datasheets, code examples to illustrate it, and offers a solution for handling page boundary writes correctly to ensure data integrity. A valuable resource for working with external EEPROM and MCUs.

Introduction

This supposed to be short article 🤯🤕

In this article, we’ll explore how to read and write data to an external EEPROM (Electrically Erasable Programmable Read-Only Memory) chip using an Arduino board and the Wire library for I2C communication.

EEPROM is a type of non-volatile memory that can retain data even when power is removed. It’s useful for storing configuration settings, calibration data, or other information that needs to persist across power cycles or reset events.

Before we dive into the code, let’s go over some key points:

  • Arduino Byte Order: The Arduino platform follows the little-endian byte order convention for storing and transmitting multi-byte data types like uint16_t or uint32_t. This means that when transmitting or receiving multi-byte values, the least significant byte (LSB) is sent or received first, followed by the subsequent bytes in order.
  • I2C Communication: EEPROM chips often communicate over the I2C bus, which requires the use of the Wire library on the Arduino platform.
  • EEPROM Capacity: In this example, we’re using a 32-kilobit (4 kilobyte) EEPROM chip with an I2C address of 0x57. Make sure to adjust these values according to your specific EEPROM chip, although maximum capacity is not used in this example.

Don’t miss troubleshooting section

Code Example: Write & Read

Now, let’s dive into the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! \file eeprom_01_read_write.ino
#include "Wire.h"

constexpr uint8_t EEPROM_I2C_ADDR = 0x57;
constexpr uint8_t EEPROM_KILOBITS = 32;
constexpr size_t  EEPROM_BYTES    = EEPROM_KILOBITS/8*1024;

// Function to simulate temperature reading
uint16_t readTemperature() 
{
  static uint16_t temperature = 0xBEEF;
  temperature = (temperature == 0xDEAD) ? 0xBEEF : 0xDEAD;
  return temperature;
}


void printHex(uint8_t value) 
{
  if (value < 16) {
    Serial.print("0");
  }
  Serial.print(value, HEX);
}


void printHex(uint16_t value) 
{
  printHex(static_cast<uint8_t>((value >> 8) & 0xff));
  printHex(static_cast<uint8_t>(value & 0xff));
}


class Eeprom 
{
public:
  Eeprom(uint8_t i2cAddr, size_t capacity)
    : m_i2cAddr(i2cAddr)
    , m_capacity(capacity)
  {    
  }

  void write(uint16_t addr, const void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    const uint8_t* ptr = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      Wire.write(ptr[i]); // Write each data byte
    }
    Wire.endTransmission();
    delay(5); // Optional delay for EEPROM write cycle
  }

  void read(uint16_t addr, void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    Wire.endTransmission();
    delay(5);
    Wire.requestFrom(m_i2cAddr, len);
    uint8_t* ptr = static_cast<uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      ptr[i] = Wire.read(); // Write each data byte
    }
    delay(5); // Optional delay for EEPROM write cycle
  }
private:
  uint8_t m_i2cAddr;  
  size_t  m_capacity;
};


Eeprom eeprom(EEPROM_I2C_ADDR , EEPROM_BYTES);


void setup()
{
  Wire.begin();
  Serial.begin(9600);
  while (!Serial)
  ;
  delay(500);
  Serial.println("\nEEPROM TEST\n==========");

  Serial.println("Will write 5 measurements to EEPROM each seconds ...");
  delay(500);

  uint16_t writeAddr = 0;
  for (int i = 0; i < 5; ++i) {
    // Write temperature to EEPROM
    uint16_t temperature = readTemperature();
    Serial.println();
    eeprom.write(writeAddr, &temperature, sizeof(temperature));    
    Serial.print("Wrote value: ");
    printHex(temperature);
    Serial.print(" to address ");
    printHex(writeAddr);
    Serial.println();
    writeAddr += sizeof(temperature);
    delay(1000);
  }

  Serial.println("EEPROM Content (Arduino is little endian):");  
  for (size_t addr = 0; addr < 32;) { // Assuming 128-byte EEPROM
    uint8_t value;
    eeprom.read(addr, &value, sizeof(value));
    Serial.print(" Address: 0x");
    printHex(addr);
    Serial.print(", Value: 0x");
    printHex(value);
    Serial.println();
    addr += sizeof(value);
  }
}


void loop() 
{
  Serial.println("That's all folks!");
  delay(5000);
}

Breakdown

Here’s a breakdown of what the code does:

  • We define constants for the EEPROM’s I2C address and capacity (in kilobits).
  • The readTemperature function simulates reading a temperature value, alternating between 0xDEAD and 0xBEEF.
  • The printHex functions are used to print byte and word values in hexadecimal format.
  • The Eeprom class encapsulates the functionality for writing and reading data to/from the EEPROM chip using the Wire library.
  • In the setup function, we initialize the I2C communication and the serial port.
  • We then write five simulated temperature values to the EEPROM, printing the addresses and values as we go.
  • After writing the data, we read the beginning of EEPROM content and print the addresses and byte values.

Note that data between beginTransmission and endTransmission are initial address and data written in one go.

When you run this code, you should see output similar to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
EEPROM TEST
==========
Will write 5 measurements to EEPROM each seconds ...
Wrote value: DEAD to address 0000
Wrote value: BEEF to address 0002
Wrote value: DEAD to address 0004
Wrote value: BEEF to address 0006
Wrote value: DEAD to address 0008
EEPROM Content (Arduino is little endian):
 Address: 0000, Value: AD
 Address: 0001, Value: DE
 Address: 0002, Value: EF
 Address: 0003, Value: BE
 Address: 0004, Value: AD
 Address: 0005, Value: DE
 Address: 0006, Value: EF
 Address: 0007, Value: BE
 Address: 0008, Value: AD
 Address: 0009, Value: DE
 Address: 000A, Value: 4B
 Address: 000B, Value: 4C
 Address: 000C, Value: 4D
 Address: 000D, Value: 4E
 Address: 000E, Value: 4F
 Address: 000F, Value: 50
 Address: 0010, Value: 51
 Address: 0011, Value: 52
 Address: 0012, Value: 53
 Address: 0013, Value: 54
 Address: 0014, Value: 55
 Address: 0015, Value: 56
 Address: 0016, Value: 57
 Address: 0017, Value: 58
 Address: 0018, Value: 59
 Address: 0019, Value: 5A
 Address: 001A, Value: FF
 Address: 001B, Value: FF
 Address: 001C, Value: FF
 Address: 001D, Value: FF
 Address: 001E, Value: FF
 Address: 001F, Value: FF
That's all folks

Note how the EEPROM content is printed in little-endian order, with the least significant byte (LSB) of each 16-bit value appearing first.

Interestingly, content of eeprom started by ABCDEFGHIJKLMNOPQRSTUVWXYZ.

Code Example 2: Dump binary data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//! \file   eeprom-02-dump.ino
//! \brief  Simple Arduino EEPROM reader
//! \date   2024-03-23
//! \author Pavel Perina

#include <Wire.h>

constexpr uint8_t eepromI2Caddr  = 0x57;
constexpr uint8_t eepromKilobits = 32;
constexpr size_t  eepromBytes    = eepromKilobits/8*1024;

class Eeprom
{
public:
  Eeprom(uint8_t i2cAddr, size_t capacity)
    : m_i2cAddr(i2cAddr)
    , m_capacity(capacity)
  {    
  }

  //! WARNING: DO NOT USE THIS AND READ THE WHOLE ARTICLE
  void write(uint16_t addr, const void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    const uint8_t* ptr = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      Wire.write(ptr[i]); // Write each data byte
    }
    Wire.endTransmission();
    delay(5); // Optional delay for EEPROM write cycle
  }

  void read(uint16_t addr, void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    Wire.endTransmission();
    delay(5);
    Wire.requestFrom(m_i2cAddr, len);
    uint8_t* ptr = static_cast<uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      ptr[i] = Wire.read(); // Write each data byte
    }
    delay(5); // Optional delay for EEPROM write cycle
  }
private:
  uint8_t m_i2cAddr;  
  size_t  m_capacity;
};

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Wire.begin();
  while (!Serial)
    ;
  Eeprom eeprom(eepromI2Caddr, eepromBytes);
  pinMode(LED_BUILTIN, OUTPUT);    
  digitalWrite(LED_BUILTIN, HIGH);

  uint8_t chunk[16];
  constexpr uint16_t nChunks = eepromBytes / sizeof(chunk);
  for (size_t i = 0; i < nChunks; ++i) {
    const uint16_t addr = static_cast<uint16_t>(i * sizeof(chunk));
    eeprom.read(addr, chunk, sizeof(chunk));
    for (size_t j = 0; j < sizeof(chunk); ++j) {
      Serial.write(chunk[j]);
    }
  }

  digitalWrite(LED_BUILTIN, LOW);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(1000);
}

Establishing serial communication somehow resets Arduino and data are written while LED is on (after perhaps 2s delays).

Getting data via Serial Port on Linux

Sadly usage is not that simple. After trying and failing hard, writing Python script that worked, I found the following:

1
stty -F /dev/ttyUSB0 9600 -xcase -icanon min 0 time 50 && cat /dev/ttyUSB0 > dump.bin
  • -icanon: This disables canonical mode. Canonical mode processes input line by line, which is not ideal for binary data. Disabling it (-icanon) means the system will read data as it arrives without waiting for a newline character, which is exactly what you want for binary data.

  • min 0 time 50: These settings adjust the conditions under which a read() call on the serial port will return. min 0 means that the read() call can return with zero bytes if the conditions specified by time are met. time 50 sets the timeout value for read() operations to 5.0 seconds (since the unit is tenths of a second). This means the read() operation will return either when at least one byte of data is available or when 5 seconds have passed, even if no data is available.

Getting data via Serial Port on Windows

On Windows I had success with CoolTerm.

Addendum: Troubleshooting EEPROM writes across page boundary

Added 2024-03-26

I hoped that nothing can go wrong. After creating first datalogger, and resolving problem with reading of binary data via serial port, I saw partially corrupted data, which were written in chunks of 13 bytes. It seemed that corrupted data followed certain regular pattern, sometimes first bytes were wrong, sometimes last bytes were 0xFF. After consulting problem with friend (thanks David Jež), I was told that it’s not the best idea to write across page boundaries. Then I read different datasheets:

  • Microchip datasheet for 24C32. It’s not very clear what it does, seems like it can up write 64 bytes at once, pages are 8 bytes long, it’s not clear if multibyte writes can start at odd address. With my knowledge of English, it’s pretty confusing.
  • I don’t remember: one datasheet stated 64 bytes cache and did not mentioned any other limitations
  • STMicroelectronics M24C32-WMN6TP datasheet This states 32B pages and states the following:

    5.1.2 Page Write

    The Page Write mode allows up to 32 byte to be written in a single Write cycle, provided that they are all located in the same page in the memory: that is, the most significant memory address bits, b16-b5, are the same. If more bytes are sent than will fit up to the end of the page, a “roll-over” occurs, i.e. the bytes exceeding the page end are written on the same page, from location 0.

I deleted all irrelevant code form datalogger and converted it to test program which simulates writing 13 bytes (index in lower 4 bits) and 16 measuremets (index in upper 4 bits):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! \file   eeprom_03_naive_writes.ino
//! \brief  ~~Simple Arduino Data Logger~~ EEPROM Writes Debugger
//! \date   2024-03-26
//! \author Pavel Perina

#include <Wire.h>

constexpr uint8_t eepromI2Caddr  = 0x57;
constexpr uint8_t eepromKilobits = 32;
constexpr size_t  eepromBytes    = eepromKilobits/8*1024;

//! Auxiliary helpers
namespace aux {

void printByte(uint8_t b) 
{
  static const char table[] = "0123456789ABCDEF";
  Serial.print(table[(b & 0xF0) >> 4]);
  Serial.print(table[ b & 0x0F]);
};

void printArray(const uint8_t* data, uint8_t len)
{
  for (int i = 0; i < len; ++i) {
    printByte(data[i]);
    Serial.print(" ");
  }
}

} // namespace aux


class Eeprom
{
public:
  Eeprom(uint8_t i2cAddr, size_t capacity)
    : m_i2cAddr(i2cAddr)
    , m_capacity(capacity)
  {    
  }

  void write(uint16_t addr, const void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    delay(5);
    const uint8_t* ptr = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      Wire.write(ptr[i]); // Write each data byte
    }
    Wire.endTransmission();
    delay(5); // Optional delay for EEPROM write cycle
  }

  void read(uint16_t addr, void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    Wire.endTransmission();
    delay(5);
    Wire.requestFrom(m_i2cAddr, len);
    uint8_t* ptr = static_cast<uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      ptr[i] = Wire.read(); // Write each data byte
    }
    delay(5); // Optional delay for EEPROM write cycle
  }
private:
  uint8_t m_i2cAddr;  
  size_t  m_capacity;
};

// GLOBALS
Eeprom eeprom(eepromI2Caddr , eepromBytes);

constexpr size_t measurementSize = 13; // Test block size, 1-16
constexpr size_t measurementsMax = 16;
size_t eepromWriteAddr = 0;
uint8_t measurementsCounter = 0;


//! Write 0xFF to first 256 bytes 
void eepromPrepare()
{
  uint8_t buf[16];
  memset(buf, 0xFF, 16);
  for (size_t i = 0; i < 16; ++i) {
    eeprom.write(i*16, buf, 16);
    delay(20);
  }
}

//! Dump EEPROM start to serial
void eepromDump()
{
  uint8_t buf[16];
  for (size_t i = 0; i < 16; ++i) {
    size_t addr = i << 4;
    eeprom.read(addr, buf, 16);
    Serial.print("00");
    aux::printByte((uint8_t)addr);
    Serial.print(": ");
    aux::printArray(buf, 16);
    Serial.println("");
    delay(20);
  }
}


void setup()
{
  // put your setup code here, to run once:
  Wire.begin();    
  Serial.begin(9600);
  while (!Serial)
    ;
 
  Serial.println("=== Initial State");
  eepromPrepare();
  eepromDump();
}


void loop()
{
  uint8_t measurement[16];

  if (measurementsCounter < measurementsMax) {
    delay(50);
    
    Serial.print("=== Measurement #");
    Serial.println(measurementsCounter);

    // fill buffer. First byte is number of measurement second is index
    for (uint8_t i = 0; i < measurementSize; ++i) {
      measurement[i] =  (measurementsCounter << 4) | i;
    }
    // simulate measurement write to EEPROM  
    const size_t addr = measurementsCounter * measurementSize;
    eeprom.write(addr, measurement, measurementSize);

    // print content
    eepromDump();

    measurementsCounter++;
  }
}

Here’s the output:

Animation of writes over EEPROM page boundary Animation of writes over EEPROM page boundary

Side note: animation created from screenshots using the following commands:

1
2
parallel 'magick convert {} -crop 540x376+977+127 -background "#282828" -gravity center -extent 720x480 out-{}' ::: *.png
magick convert -delay 200 out-0.png -delay 100 out-{1..16}.png output.webp

This exactly matches explanation from STMicro datasheet and perfecly explains regular pattern of corrupted data in my first data logger.

Code is left here, cause I’m afraid I (or someone) may need it, as writing to EEPROM differs across manufacturers and chips.

Addendum: Hopefully Correct EEPROM Write

Changes:

  • Original write method renamed to writeUnsafe
  • Delay after write increased to 15ms
  • Added pageSize
  • Added write method that writes data to pageBoundary or remaining length

PageSize, or page row size differs with EEPROM capacity, hopefully like this:

EEPROMPage Row Size
24LC51264
24LC25664
24LC12864
24LC6432
24LC3232
24LC1616
24LC0816
24LC0416
24LC028
24LC018
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Eeprom
{
public:
  Eeprom(uint8_t i2cAddr, size_t capacity, uint8_t pageSize = 16)
    : m_i2cAddr(i2cAddr)
    , m_capacity(capacity)
    , m_pageSize(pageSize)
  {
  }

  void writeUnsafe(uint16_t addr, const void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    delay(5);
    const uint8_t* ptr = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      Wire.write(ptr[i]); // Write each data byte
    }
    Wire.endTransmission();
    delay(15); // Optional delay for EEPROM write cycle
  }

  void write(uint16_t addr, const void* data, size_t len) const
  {
    // Write starting block
    size_t bytesToPageBoundary = m_pageSize - (addr % m_pageSize);
    for(;;) {
      // if all data to write fit before page boundary, write them and return
      if (len < bytesToPageBoundary) {
        if (len != 0) {
          writeUnsafe(addr, data, len);
        }
        return;
      }
      // else write data to next page boundary
      writeUnsafe(addr, data, bytesToPageBoundary);
      // decrement remaining length, increment address and data pointer
      len  -= bytesToPageBoundary;
      addr += bytesToPageBoundary;
      data = static_cast<const uint8_t*>(data) + bytesToPageBoundary;
      // next page boundary is now page size
      bytesToPageBoundary = m_pageSize;
    }
  }


  void read(uint16_t addr, void* data, size_t len) const
  {
    Wire.beginTransmission(m_i2cAddr);
    Wire.write((int)(addr >> 8));   // MSB of address
    Wire.write((int)(addr & 0xFF)); // LSB of address
    Wire.endTransmission();
    delay(5);
    Wire.requestFrom(m_i2cAddr, len);
    uint8_t* ptr = static_cast<uint8_t*>(data);
    for (size_t i = 0; i < len; i++) {
      ptr[i] = Wire.read(); // Write each data byte
    }
    delay(5); // Optional delay for EEPROM write cycle
  }
private:
  uint8_t m_i2cAddr;
  size_t  m_capacity;
  uint8_t m_pageSize;
};

Stuff Learned

Nothing is easy.

  • Reading of binary data using serial port is hard, because various terminal emulators handle control characters differently
  • Overcoming the page boundary write issue demanded considerable troubleshooting

Final Words

This examples demonstrate how to communicate with an external EEPROM chip using the Wire library. You can adapt this code to suit your specific application requirements, such as storing configuration data, sensor calibration values, measurements, or other persistent information.

Happy coding!

This post is licensed under CC BY 4.0 by the author.