To build a reliable Arduino EEPROM programmer for external non-volatile storage, you need an Arduino Uno R3 (ATmega328P), an AT24C256 I2C EEPROM module, and 4.7kΩ pull-up resistors on the SDA/SCL lines if your breakout board lacks them. The internal EEPROM on a standard Uno is only 1KB; the 24C256 gives you 32KB, making it ideal for logging high-frequency sensor data or storing configuration tables that survive power cycles.

Project Difficulty: Intermediate (Requires I2C bus understanding and page-boundary memory management)
Estimated Time: 45 minutes for hardware, 20 minutes for software validation
Target Board: Arduino Uno R3 (5V logic, ATmega328P)

Hardware Spec Sheet & Parts List

Before wiring, verify your exact module variant. Many cheap clone boards omit the required I2C pull-up resistors, which will cause intermittent bus failures at higher clock speeds.

Component Exact Variant / Model Approx. Cost (2026) Critical Notes
Microcontroller Arduino Uno R3 (Rev3) or compatible ATmega328P board $22.00 (Official) / $9.00 (Clone) Must be 5V logic. If using a 3.3V board (like ESP32), you need a logic level shifter.
EEPROM Chip AT24C256 (256Kbit / 32KByte) I2C Breakout Module $4.50 (5-pack) Look for modules with an onboard voltage regulator and pre-soldered 4.7kΩ pull-ups (e.g., Zephyr or HiLetgo variants).
Pull-up Resistors 4.7kΩ 1/4W Metal Film (Only if module lacks them) $0.10 Required between VCC and SDA, and VCC and SCL. Do not use 10kΩ for 400kHz Fast Mode.
Wiring 22 AWG Solid Core Jumper Wires $6.00 (kit) Keep I2C traces under 12 inches (30cm) to minimize parasitic capacitance.

Pin Mapping & Breadboard Wiring

The AT24C256 uses the I2C protocol. The default I2C address is 0x50 (assuming the A0, A1, and A2 address pins on the chip are tied to GND).

Arduino Uno R3 Pin AT24C256 Module Pin Function
5V VCC Power (4.5V to 5.5V nominal)
GND GND Common Ground
A4 (SDA) SDA I2C Data Line
A5 (SCL) SCL I2C Clock Line
Wiring Tip: If your breakout board does not have pull-up resistors, you must physically wire a 4.7kΩ resistor from the SDA line to 5V, and another 4.7kΩ resistor from the SCL line to 5V. Without these, the I2C bus will float, resulting in random Wire.endTransmission() timeouts.
  1. Insert the Arduino Uno and the EEPROM module into your breadboard.
  2. Connect the 5V and GND rails, ensuring the EEPROM VCC reads between 4.8V and 5.2V with your multimeter.
  3. Route the SDA (A4) and SCL (A5) lines. Keep them parallel and avoid routing them near high-current switching nodes to prevent inductive crosstalk.
  4. Verify the A0, A1, and A2 pads on the EEPROM module. If they are unconnected, the chip defaults to address 0x50. If they are bridged to VCC, the address shifts (e.g., 0x57 if all three are high).

The Complete Arduino EEPROM Programmer Code

This code targets the Arduino Uno R3. It includes robust error handling and manages the 24C256's 64-byte page write boundary. A common mistake in basic tutorials is writing a continuous stream of bytes; if you cross a 64-byte page boundary without stopping, the chip's internal address counter wraps around to the start of that same page, overwriting your initial data.

#include <Wire.h>

// --- PIN & CONFIGURATION DEFINITIONS ---
// Arduino Uno R3 uses A4 for SDA and A5 for SCL (hardware I2C)
#define EEPROM_I2C_ADDRESS 0x50 
#define PAGE_SIZE 64            // AT24C256 has 64-byte write pages
#define WRITE_CYCLE_TIME_MS 5   // Max write cycle time per datasheet

void setup() {
  Serial.begin(115200);
  while (!Serial); // Wait for serial port to connect
  
  Wire.begin();
  Wire.setClock(400000); // Set I2C to Fast Mode (400kHz)
  
  Serial.println(F("Arduino EEPROM Programmer Initialized."));
  
  // Test payload: 70 bytes (crosses the 64-byte page boundary)
  uint8_t testData[70];
  for (int i = 0; i < 70; i++) {
    testData[i] = i + 10; 
  }
  
  // Write data starting at memory address 0x0000
  uint16_t startAddr = 0x0000;
  writeEEPROMBlock(startAddr, testData, 70);
  
  delay(100); // Allow final write cycle to complete
  
  // Read back and verify
  uint8_t readData[70];
  readEEPROMBlock(startAddr, readData, 70);
  
  Serial.println(F("\n--- Verification ---"));
  bool success = true;
  for (int i = 0; i < 70; i++) {
    if (readData[i] != testData[i]) {
      Serial.print(F("Mismatch at index ")); Serial.print(i);
      Serial.print(F(": Expected ")); Serial.print(testData[i]);
      Serial.print(F(", Got ")); Serial.println(readData[i]);
      success = false;
    }
  }
  if (success) Serial.println(F("All bytes verified successfully!"));
}

void loop() {
  // Programmer logic runs once in setup for this demonstration
}

// --- I2C WRITE WITH PAGE BOUNDARY HANDLING ---
void writeEEPROMBlock(uint16_t memAddr, const uint8_t* data, uint16_t length) {
  uint16_t bytesWritten = 0;
  
  while (bytesWritten < length) {
    // Calculate bytes remaining in the current 64-byte page
    uint16_t pageOffset = memAddr % PAGE_SIZE;
    uint16_t bytesToWrite = PAGE_SIZE - pageOffset;
    if (bytesToWrite > (length - bytesWritten)) {
      bytesToWrite = length - bytesWritten;
    }
    
    Wire.beginTransmission(EEPROM_I2C_ADDRESS);
    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 error = Wire.endTransmission();
    if (error != 0) {
      Serial.print(F("I2C Write Failed: Error Code ")); 
      Serial.println(error);
      return; // Halt on error
    }
    
    memAddr += bytesToWrite;
    bytesWritten += bytesToWrite;
    delay(WRITE_CYCLE_TIME_MS); // Mandatory delay for internal EEPROM write cycle
  }
}

// --- I2C READ FUNCTION ---
void readEEPROMBlock(uint16_t memAddr, uint8_t* buffer, uint16_t length) {
  // Set the memory address pointer
  Wire.beginTransmission(EEPROM_I2C_ADDRESS);
  Wire.write((uint8_t)(memAddr >> 8));
  Wire.write((uint8_t)(memAddr & 0xFF));
  uint8_t error = Wire.endTransmission();
  
  if (error != 0) {
    Serial.print(F("I2C Address Pointer Failed: Error Code ")); 
    Serial.println(error);
    return;
  }
  
  // Read the data
  uint16_t bytesRead = 0;
  while (bytesRead < length) {
    uint16_t chunk = (length - bytesRead > 32) ? 32 : (length - bytesRead);
    Wire.requestFrom(EEPROM_I2C_ADDRESS, (uint8_t)chunk);
    
    for (uint16_t i = 0; i < chunk; i++) {
      if (Wire.available()) {
        buffer[bytesRead + i] = Wire.read();
      } else {
        Serial.println(F("I2C Read Underflow: Bus locked or device missing."));
        return;
      }
    }
    bytesRead += chunk;
  }
}

Debugging: "I2C Write Failed" and Common Errors

When working with I2C memory, the bus is highly sensitive to capacitance and timing. If your serial monitor outputs an error, do not immediately blame the chip. Follow this diagnostic path.

The First Three Things to Check When It Fails:

  1. Verify Pull-Up Resistors: Measure the voltage on the SDA and SCL lines with a multimeter while the bus is idle. Both should read a steady 5V (or 3.3V if using a 3.3V system). If they read 0V or fluctuate wildly, your pull-ups are missing or the wrong value.
  2. Confirm the I2C Address: Run an I2C Scanner sketch. If the chip does not show up at 0x50 (or 0x51-0x57), check the A0/A1/A2 solder jumpers on the back of the module. A floating address pin can cause the chip to ignore the bus.
  3. Check Wire Length and Capacitance: The I2C specification limits bus capacitance to 400pF. If you are using long, unshielded ribbon cables, the signal edges will degrade, causing the 24C256 to miss clock pulses. Keep wires under 30cm.

Exact Error Strings and Ranked Causes:

1. Exact Error String: I2C Write Failed: Error Code 2

  • Cause A (Most Likely): Address NACK. The Arduino sent 0x50, but no device acknowledged. The chip is unpowered, wired backward, or the address pins are configured for a different hex value.
  • Cause B: The I2C bus is locked. The SDA line is being held low by a previous interrupted transaction. Power cycle both the Arduino and the EEPROM module to reset the bus state.

2. Exact Error String: I2C Write Failed: Error Code 3

  • Cause A (Most Likely): Data NACK. The address was accepted, but the chip rejected a data byte. This almost always happens if you attempt to write more than 64 bytes in a single I2C transmission without breaking it into pages, overflowing the chip's internal volatile buffer.
  • Cause B: Write protection is enabled. Some 24C256 modules have a WP (Write Protect) pin. If this pin is tied to VCC instead of GND, the chip will NACK all write attempts.

Extending and Simplifying the Build

Depending on your end goal, you may want to alter the hardware footprint or add user interfaces.

How to Simplify the Build:
If you want to eliminate the need for external pull-up resistors and 5V logic concerns, switch the microcontroller to an ESP32 DevKit V1. The ESP32 has internal pull-up resistors that can be enabled in software via pinMode(PIN, INPUT_PULLUP) (though external 4.7kΩ is still recommended for reliability). More importantly, you can pair it with a 3.3V AT24C256 module, completely removing the need for logic level shifters if you ever decide to interface with modern 3.3V sensors on the same bus.

How to Extend the Build:
To turn this from a serial-logged programmer into a standalone field tool, add a 0.96" I2C OLED display (SSD1306) and a rotary encoder (KY-040). Because the OLED and the EEPROM share the I2C bus, you only need two extra GPIO pins for the encoder. You can write a menu system that allows you to scroll through the 32KB memory space in 16-byte hex rows, edit individual bytes via the encoder, and save them back to the chip without needing a PC connection. Ensure you add a 100nF decoupling capacitor across the VCC and GND pins of the OLED to prevent display refresh noise from corrupting EEPROM writes.

Frequently Asked Questions

Can I use an Arduino Nano instead of an Uno for this EEPROM programmer?

Yes. The Arduino Nano (ATmega328P variant) shares the exact same hardware I2C pins as the Uno: A4 is SDA and A5 is SCL. The code provided above will compile and run without modification. However, be aware that many modern Nano clones ship with the CH340 USB-serial chip instead of the FT232RL or ATmega16U2. The CH340 is perfectly fine for I2C programming, but ensure you install the correct CH340 drivers on your host PC, otherwise the serial monitor will fail to connect.

Why does my Arduino EEPROM programmer corrupt data after exactly 64 bytes?

This is the classic "page boundary wrap" issue. The AT24C256 organizes its memory into 512 pages of 64 bytes each. When you execute a page write, the internal address counter increments with each byte received. However, if you send a 65th byte without issuing a Stop condition, the lower 6 bits of the address counter roll over to zero, and the 65th byte overwrites the 1st byte of that same page. The code provided in this guide explicitly calculates the remaining bytes in the current page and issues a Stop condition (via Wire.endTransmission()) before crossing the boundary, preventing this corruption.

How do I read a 24C256 chip salvaged from an old motherboard with an Arduino?

Salvaged SOIC-8 chips require an SOIC-to-DIP adapter or a SOIC test clip (like the Pomona 5250). Wire the VCC (Pin 8) to 5V, GND (Pin 4) to GND, SDA (Pin 5) to A4, and SCL (Pin 6) to A5. Crucially, you must tie the WP (Write Protect, Pin 7) to GND if you intend to write to it. For reading, WP can be left floating, but tying it to GND is safer. Note that salvaged chips may have been subjected to high heat during desoldering; if the chip fails to acknowledge its address (Error Code 2) despite correct wiring, the silicon may have been thermally damaged during removal.