An I2C EEPROM (like the ubiquitous Microchip 24LC256) provides non-volatile byte storage using just two shared wires: SDA (data) and SCL (clock). It is the exact right tool for low-speed data logging, configuration retention, or MAC address storage where SPI’s four wires waste valuable GPIO pins, and 1-Wire’s strict microsecond timing is a debugging headache. You wire VCC, GND, SDA, and SCL, add two pull-up resistors, and you can read/write bytes immediately.

The Physical Layer: Wiring I2C EEPROMs and Pull-Up Rules

Unlike UART, I2C is a synchronous, multi-master, multi-slave bus. Before writing a single line of code, you must satisfy the physical layer requirements. The most common bench mistake is assuming the microcontroller's internal pull-ups are sufficient. They are not.

I2C Bus Mechanics & Physical Constraints
Parameter Standard Mode (100kHz) Fast Mode (400kHz) Fast Mode Plus (1MHz)
Wires Required SDA (Bidirectional Data), SCL (Clock), VCC, GND
Max Bus Capacitance 400 pF (limits trace length and number of devices)
Pull-Up Resistor (Rp) 4.7 kΩ to 10 kΩ 2.2 kΩ to 4.7 kΩ 1 kΩ to 2.2 kΩ
Max Practical Distance ~1 meter (unshielded) ~0.5 meter ~0.25 meter

The Pull-Up Resistor Reality

I2C uses open-drain (or open-collector) outputs. Devices can only pull the line LOW; they cannot drive it HIGH. The pull-up resistor provides the HIGH state. The ESP32 has internal pull-ups, but they are typically around 45 kΩ. At 45 kΩ, the RC rise time of the bus exceeds the I2C specification limits, causing data corruption at 400kHz. Always use external resistors. For a standard 100kHz bus with a few devices on a breadboard, 4.7 kΩ is the gold standard. If you push to 400kHz, drop to 2.2 kΩ.

Hardware Addressing (A0, A1, A2)

Most DIP-8 I2C EEPROMs feature three address pins (A0, A1, A2). These dictate the lower three bits of the 7-bit I2C address. The base address for standard EEPROMs is 0x50 (binary 1010000). Tying A0, A1, and A2 to GND yields 0x50. Tying A0 to VCC yields 0x51. This allows up to eight identical EEPROMs on the same bus without address clashes.

I2C EEPROM Spec Sheet: Capacity, Addressing, and Page Sizes

Not all EEPROMs are created equal. The critical metric that trips up embedded developers is the Page Size. I2C EEPROMs do not accept infinite byte streams in a single write command. They buffer incoming bytes in a temporary page register. If you send more bytes than the page size, the internal pointer wraps around to the beginning of that page, overwriting the first bytes you just sent.

Common I2C EEPROM Specifications (Microchip / Atmel)
Part Number Capacity (Bits / Bytes) Page Size (Bytes) Address Pins Max Clock Speed
AT24C02 2 Kbit / 256 Bytes 8 A0, A1, A2 400 kHz
24LC64 64 Kbit / 8,192 Bytes 32 A0, A1, A2 400 kHz
24LC256 256 Kbit / 32,768 Bytes 64 A0, A1, A2 400 kHz
24FC512 512 Kbit / 65,536 Bytes 128 A0, A1, A2 1 MHz (Fc)
Bench Tip: The 16-Bit Address Boundary
EEPROMs larger than 2 Kbit (256 bytes) require a 16-bit memory address (two bytes) to specify where data goes. Smaller ones like the AT24C02 use an 8-bit address. If you use a library designed for a 24LC256 on an AT24C02, the first byte of your memory address will be interpreted as data, and your writes will land in the wrong location.

Debugging the Bus: Sniffing, Clashes, and Classic Failures

When choosing a protocol, I2C wins for device count (up to 127 addresses) and low pin count, but loses on distance (keep it under 1 meter) and speed (max 1-3.4 MHz). If you need to log high-frequency sensor data over long distances, switch to RS-485 or SPI with line drivers. For local configuration storage, I2C is unmatched. When it fails, it is almost always one of three physical issues.

The Classic Failure Modes

  1. Missing or Weak Pull-Ups: The bus floats. SDA and SCL read erratic HIGHs. The microcontroller sends a start condition, but the EEPROM never ACKs (pulls SDA low). Fix: Measure SDA/SCL with a multimeter; they should sit steadily at VCC (3.3V or 5V) when idle. Add 4.7 kΩ external resistors.
  2. Address Clash: You have two sensors and an EEPROM on the bus, and two share the 0x50 address. The bus locks up or returns garbage. Fix: Change the A0-A2 hardware pins on the EEPROM to shift its address to 0x51 or higher.
  3. The Write-Cycle NACK (Baud/Timing Mismatch): After receiving a write command, the EEPROM disconnects from the bus internally to burn the data into its floating gates. This takes up to 5 milliseconds. If your microcontroller immediately tries to read or write again, the EEPROM will NACK (Not Acknowledge). Fix: Implement ACK polling or insert a 5ms delay after every write sequence.

Sniffing and Scanning

Before deploying production firmware, run an I2C scanner. On Linux/Raspberry Pi, use i2cdetect -y 1. On Arduino/ESP32, use the standard Wire scanner sketch. If the EEPROM shows up as 0x50, your physical layer is solid. For deep debugging, a $15 logic analyzer (like a Saleae clone) running PulseView/Sigrok will decode the I2C packets and show you exactly which byte the EEPROM is NACKing.

Minimal Working Exchange: Reading and Writing Bytes

The following code targets an ESP32 DevKit v1 wired to a 24LC256 EEPROM. Wiring: ESP32 GPIO 21 to SDA, GPIO 22 to SCL. VCC to 3.3V, GND to GND. A0, A1, A2 tied to GND. 4.7 kΩ pull-ups on SDA and SCL to 3.3V.

#include <Wire.h>

// 24LC256 base address (A0, A1, A2 tied to GND)
#define EEPROM_I2C_ADDR 0x50 

// ESP32 default I2C pins
#define I2C_SDA 21
#define I2C_SCL 22

void setup() {
  Serial.begin(115200);
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(400000); // Set to 400kHz Fast Mode
  
  // Wait for serial monitor
  while(!Serial) { delay(10); }
  
  Serial.println("Testing 24LC256 I2C EEPROM...");
  
  // 1. Write a byte to memory address 0x0050
  uint16_t memAddr = 0x0050;
  uint8_t dataToWrite = 0xA5;
  writeEEPROMByte(memAddr, dataToWrite);
  
  // 2. Mandatory Write Cycle Delay (Max 5ms per Microchip datasheet)
  // In production, use ACK polling instead of a blind delay.
  delay(10); 
  
  // 3. Read the byte back
  uint8_t readData = readEEPROMByte(memAddr);
  
  Serial.printf("Wrote: 0x%02X | Read: 0x%02X\n", dataToWrite, readData);
}

void loop() {
  // Nothing to do here
}

void writeEEPROMByte(uint16_t memAddr, uint8_t data) {
  Wire.beginTransmission(EEPROM_I2C_ADDR);
  // 24LC256 uses 16-bit addressing (2 bytes)
  Wire.write((uint8_t)(memAddr >> 8));   // MSB
  Wire.write((uint8_t)(memAddr & 0xFF)); // LSB
  Wire.write(data);
  
  uint8_t status = Wire.endTransmission();
  if (status != 0) {
    Serial.printf("Write Failed! I2C Error Code: %d\n", status);
  }
}

uint8_t readEEPROMByte(uint16_t memAddr) {
  Wire.beginTransmission(EEPROM_I2C_ADDR);
  Wire.write((uint8_t)(memAddr >> 8));   // MSB
  Wire.write((uint8_t)(memAddr & 0xFF)); // LSB
  Wire.endTransmission();
  
  Wire.requestFrom(EEPROM_I2C_ADDR, 1);
  if (Wire.available()) {
    return Wire.read();
  }
  return 0xFF; // Return 0xFF on failure
}
Production Upgrade: ACK Polling
Instead of using delay(10), professional firmware uses ACK polling. You repeatedly send a Start condition and the EEPROM control byte. If the EEPROM is still writing, it NACKs. When it finishes, it ACKs. This shaves milliseconds off your write loop and prevents bus timeouts under heavy load. Refer to the Microchip 24LC256 Datasheet for the exact ACK polling flowchart.

For a deeper understanding of the I2C timing specifications, bus capacitance calculations, and clock stretching mechanics, review the NXP I2C Bus Specification (UM10204). When wiring multiple devices, always map out your address space on paper first to avoid the dreaded 0x50 clash, and verify your pull-up resistor values against the total bus capacitance using the guidelines in the SparkFun I2C Tutorial.