The internal EEPROM on a standard Arduino Uno or Nano (ATmega328P) holds exactly 1,024 bytes of non-volatile memory. For logging sensor data over time, that fills up in minutes. To scale up, you need an external I2C EEPROM like the Microchip 24LC256, which provides 32,768 bytes (32KB) of storage. This guide walks through wiring the 24LC256 to an Arduino Nano v3, writing robust code that handles the notorious 64-byte page boundary bug, and debugging the exact I2C NACK errors that silently corrupt your data.

Hardware Specs and Parts List

Before wiring, it is critical to understand the physical limits of the silicon you are using. Internal EEPROM and external I2C EEPROM behave differently regarding write cycles, page sizes, and bus capacitance.

Internal vs. External EEPROM Specifications
Feature Internal (ATmega328P) External (Microchip 24LC256)
Capacity 1,024 bytes (1 KB) 32,768 bytes (32 KB)
Interface Direct memory addressing I2C (up to 400 kHz)
Page Write Size 1 byte (no page boundary limits) 64 bytes (strict boundary limits)
Write Cycle Time ~3.4 ms per byte ~5 ms per page (up to 64 bytes)
Endurance 100,000 cycles 1,000,000 cycles

Required Parts

  • Microcontroller: Arduino Nano v3 (ATmega328P variant, 5V/16MHz)
  • EEPROM Chip: Microchip 24LC256-I/P (DIP-8 package) or a pre-wired 24LC256 I2C module
  • Resistors: Two 4.7kΩ pull-up resistors (required if using a bare DIP chip; modules usually include them)
  • Capacitor: One 0.1µF (100nF) ceramic decoupling capacitor
  • Wiring: 22 AWG solid core jumper wires

Wiring the 24LC256 to the Arduino Nano

The 24LC256 uses the I2C protocol. On the Arduino Nano v3, the I2C pins are shared with the analog inputs. The chip requires three address pins (A0, A1, A2) to be tied to either VCC or GND to set its I2C address.

Pin Mapping: Arduino Nano v3 to 24LC256 (DIP-8)
24LC256 Pin Pin Name Arduino Nano v3 Pin Notes
1 A0 GND Sets I2C address to 0x50
2 A1 GND Sets I2C address to 0x50
3 A2 GND Sets I2C address to 0x50
4 VSS (GND) GND Common ground
5 SDA A4 Requires 4.7kΩ pull-up to 5V
6 SCL A5 Requires 4.7kΩ pull-up to 5V
7 WP GND Write Protect OFF (tied to GND)
8 VCC 5V Place 0.1µF cap between VCC and GND
Bench Tip: Never run I2C lines without pull-up resistors on a bare DIP chip. The internal pull-ups on the ATmega328P are roughly 20kΩ to 50kΩ, which are too weak to pull the bus high fast enough at 400 kHz, resulting in corrupted bytes. Always use external 4.7kΩ resistors to 5V.

The Code: Handling Page Boundaries and Write Cycles

The most common reason external EEPROM writes fail silently is the page boundary bug. The 24LC256 writes data in 64-byte pages. If you attempt to write 65 bytes starting at memory address 0, the 65th byte does not roll over to address 64; it wraps around to address 0 and overwrites your first byte. The code below targets the Arduino Nano v3 and includes a custom page-chunking function to prevent this, alongside strict I2C error handling.

#include <Wire.h>

// Target Board: Arduino Nano v3 (ATmega328P)
// Chip: Microchip 24LC256 (32KB I2C EEPROM)
#define EEPROM_I2C_ADDR 0x50
#define PAGE_SIZE 64
#define WRITE_DELAY_MS 6 // Datasheet says 5ms max, we use 6ms for safety

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Wire.setClock(400000); // Set I2C to 400kHz

  Serial.println("Arduino EEPROM I2C Logger Initialized.");
  
  // Example: Write a 100-byte payload across page boundaries
  uint8_t testData[100];
  for (int i = 0; i < 100; i++) {
    testData[i] = i + 10;
  }
  
  writeEEPROMChunked(0, testData, 100);
  
  // Read it back to verify
  Serial.println("Reading back data...");
  for (int i = 0; i < 100; i++) {
    uint8_t val = readEEPROMByte(i);
    if (val != testData[i]) {
      Serial.print("Mismatch at address ");
      Serial.print(i);
      Serial.print(": Expected ");
      Serial.print(testData[i]);
      Serial.print(", Got ");
      Serial.println(val);
    }
  }
  Serial.println("Verification complete.");
}

void loop() {
  // Data logging loop would go here
}

// Handles 64-byte page boundaries automatically
void writeEEPROMChunked(uint16_t memAddr, const uint8_t* data, uint16_t length) {
  uint16_t bytesWritten = 0;
  
  while (bytesWritten < length) {
    uint16_t currentPage = memAddr / PAGE_SIZE;
    uint16_t pageOffset = memAddr % PAGE_SIZE;
    uint16_t bytesToWrite = PAGE_SIZE - pageOffset;
    
    if (bytesToWrite > (length - bytesWritten)) {
      bytesToWrite = length - bytesWritten;
    }

    Wire.beginTransmission(EEPROM_I2C_ADDR);
    Wire.write((uint8_t)(memAddr >> 8));   // MSB of memory address
    Wire.write((uint8_t)(memAddr & 0xFF)); // LSB of memory address
    
    for (uint16_t i = 0; i < bytesToWrite; i++) {
      Wire.write(data[bytesWritten + i]);
    }
    
    uint8_t i2cError = Wire.endTransmission();
    if (i2cError != 0) {
      Serial.print("I2C Write Error Code: ");
      Serial.println(i2cError);
    }
    
    delay(WRITE_DELAY_MS); // Mandatory wait for internal write cycle
    
    memAddr += bytesToWrite;
    bytesWritten += bytesToWrite;
  }
}

uint8_t readEEPROMByte(uint16_t memAddr) {
  Wire.beginTransmission(EEPROM_I2C_ADDR);
  Wire.write((uint8_t)(memAddr >> 8));
  Wire.write((uint8_t)(memAddr & 0xFF));
  Wire.endTransmission();
  
  Wire.requestFrom(EEPROM_I2C_ADDR, (uint8_t)1);
  if (Wire.available()) {
    return Wire.read();
  }
  return 0xFF; // Return 0xFF on read failure
}

Debugging: First Three Things to Check When It Fails

When your serial monitor spits out I2C Write Error Code: 2 or your data reads back as 0xFF, do not immediately assume the chip is dead. I2C EEPROM failures are almost always timing or electrical issues. Here are the first three things to check, ranked by likelihood.

1. Check the Exact Error String from Wire.endTransmission()

The Arduino Wire library returns specific integer codes when Wire.endTransmission() fails. If you see these exact values in your serial output:

  • Returns 2 (Received NACK on transmit of address): The Arduino cannot see the chip. Check your wiring, ensure A0/A1/A2 are tied to GND (yielding address 0x50), and run an I2C Scanner sketch to verify the bus. If your module has a different base address, it might be 0x51 or 0x54.
  • Returns 3 (Received NACK on transmit of data): The chip acknowledged its address but rejected the data. This almost always means you exceeded the chip's internal I2C buffer (usually 32 bytes on some variants, 64 on the 24LC256) in a single Wire.write() burst, or the bus is noisy.

2. Verify the 5ms Write Cycle Delay

According to the Microchip 24LC256 datasheet, after you send a stop condition, the chip disconnects from the I2C bus internally for up to 5ms to burn the data into the floating gate cells. If your code loops back and tries to write or read before this delay expires, the chip will NACK the bus. Ensure delay(6); is present after every page write.

3. Measure I2C Bus Capacitance and Pull-Up Voltage

If you are using long jumper wires (over 12 inches) or have multiple devices on the bus, parasitic capacitance increases. This rounds off the rising edges of the SCL/SDA signals. Use an oscilloscope to check the I2C lines. If the rise time is sluggish, drop your pull-up resistors from 4.7kΩ to 2.2kΩ to charge the capacitance faster.

Extending and Simplifying the Build

Depending on your project requirements, you may not need a full external I2C setup, or you may need vastly more storage.

How to Simplify: If your total data payload is under 1,000 bytes (e.g., saving a single Wi-Fi password, a calibration offset, or a boot counter), ditch the external chip. Use the internal EEPROM via the built-in <EEPROM.h> library. It requires zero wiring, no pull-ups, and no page boundary math. Just remember to implement software wear-leveling if you update the value frequently.

How to Extend:

  • Scale Addressing: The 24LC256 has three address pins (A0, A1, A2). By wiring these to VCC or GND in different combinations, you can put up to eight 24LC256 chips on the same I2C bus, yielding 256KB of total storage.
  • Upgrade to FRAM: If your application requires high-speed logging (e.g., writing every 10ms) and you are burning through the 1,000,000 write cycle limit, swap the EEPROM for a Fujitsu/Ramtron FM24C256 FRAM chip. It uses the exact same I2C commands and pinout, but has zero write delay, no page boundaries, and a 10^14 write cycle endurance.

Frequently Asked Questions

How many write cycles does the Arduino EEPROM actually last?

The internal EEPROM on the ATmega328P is rated for 100,000 write cycles per memory cell. However, this is a conservative baseline tested at room temperature. In real-world bench tests at 25°C, internal AVR EEPROM cells often survive well over 1,000,000 cycles before bit-flipping occurs. External chips like the 24LC256 are officially rated for 1,000,000 cycles. To maximize lifespan, never write to the EEPROM in the main loop() without a state-change check or a time-based interval.

Why is my external EEPROM only saving the first 16 or 64 bytes?

This is the classic page boundary overwrite issue. The 24LC256 has a 64-byte page buffer. If you send 100 bytes in a single I2C transmission starting at address 0, the chip writes the first 64 bytes to addresses 0-63. The remaining 36 bytes do not spill over to address 64; the internal address counter rolls over to 0, and bytes 65-100 overwrite the data you just wrote at addresses 0-35. You must chunk your writes to align with the 64-byte boundaries, exactly as demonstrated in the writeEEPROMChunked() function above.

What is the difference between EEPROM and Flash memory on the Arduino?

Flash memory (32KB on the Nano) is where your compiled sketch code lives. It is optimized for reading and is written in large blocks (pages) by the bootloader. You cannot easily write to Flash memory from within your running sketch without complex bootloader modifications. EEPROM (1KB internal, or 32KB external) is optimized for byte-level reading and writing at runtime. Use Flash for static lookup tables and code; use EEPROM for user settings, sensor logs, and runtime state preservation across power cycles.