How I2C EEPROMs Work
An I2C EEPROM is a small non-volatile memory chip that keeps its data without power. Common families are Microchip 24LC/24AA and Atmel/Microchip AT24C parts, with sizes from 1 kbit to 1 Mbit or more. The microcontroller talks to it over two wires, SDA (data) and SCL (clock), using the standard I2C protocol.
Every operation follows a similar pattern: send the chip’s I2C address, send the memory address you want, then either send data (write) or request data (read).
Wiring and I2C Address
Typical 8-pin chips have the following pins: A0, A1, A2 (address select), VSS (ground), SDA, SCL, WP (write protect) and VCC.
- Connect VCC and GND to the supply. Many 24LC parts accept a wide voltage range, but check your datasheet.
- Connect SDA and SCL to the microcontroller’s I2C pins, with pull-up resistors to VCC (commonly 4.7 kohm; some breakout boards already include them).
- Tie A0 to A2 to GND or VCC to set the address. All grounded usually gives the 7-bit address 0x50, and each address pin set high adds to it, giving addresses 0x50 to 0x57.
- Tie WP to ground to allow writes. If it is high, the chip ignores writes.
On an Arduino Uno, SDA and SCL are on A4 and A5 (also broken out on the header near the USB connector). Other boards use different pins, so check your board’s pinout.
Example 1: Write and Read a Byte (16-bit Address)
Chips of 32 kbit and larger (for example 24LC32, 24LC256) need a two-byte memory address. This sketch assumes a 24LC256 at address 0x50.
#include <Wire.h>
const uint8_t EEPROM_ADDR = 0x50;
void eepromWriteByte(uint16_t memAddr, uint8_t data) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((uint8_t)(memAddr >> 8)); // high byte
Wire.write((uint8_t)(memAddr & 0xFF)); // low byte
Wire.write(data);
Wire.endTransmission();
delay(5); // allow the internal write cycle to finish
}
uint8_t eepromReadByte(uint16_t memAddr) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((uint8_t)(memAddr >> 8));
Wire.write((uint8_t)(memAddr & 0xFF));
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, (uint8_t)1);
if (Wire.available()) return Wire.read();
return 0xFF;
}
void setup() {
Serial.begin(9600);
Wire.begin();
Wire.beginTransmission(EEPROM_ADDR);
if (Wire.endTransmission() != 0) {
Serial.println("EEPROM not found - check wiring and address");
while (true) {}
}
eepromWriteByte(12, 0x37);
Serial.println(eepromReadByte(12), HEX); // expect 37
}
void loop() {}
If the first check fails, nothing acknowledged at that address, so recheck power, pull-ups, the address pins and the SDA/SCL pins.
Smaller Chips with an 8-bit Address
Parts such as 24LC01, 24LC02 and 24LC04 to 24LC16 use a single memory address byte. For 24LC01 and 24LC02 the code above works if you remove the high-byte write. Some larger members of this group (like 24LC04, 08, 16) use part of the I2C address bits as extra memory address bits, so the chip occupies several I2C addresses. Check the datasheet for the exact scheme of your part.
Writing Multiple Bytes and Page Boundaries
EEPROMs write in pages. The page size depends on the chip (for example 8 bytes on small parts, 64 bytes on a 24LC256, but verify in your datasheet). If a page write goes past the page boundary, the address wraps around inside the page and overwrites earlier bytes in that page.
The Arduino Wire library also has an internal buffer, commonly 32 bytes, which includes the address bytes. So a single transmission with a 2-byte address can carry fewer than 30 data bytes on many boards. A safe approach is to write in small chunks that respect both limits:
void eepromWriteBuffer(uint16_t memAddr, const uint8_t *data, uint8_t len) {
const uint8_t PAGE = 16; // chunk size that divides the page size evenly
while (len > 0) {
uint8_t chunk = PAGE - (memAddr % PAGE); // stay inside the page
if (chunk > len) chunk = len;
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((uint8_t)(memAddr >> 8));
Wire.write((uint8_t)(memAddr & 0xFF));
for (uint8_t i = 0; i < chunk; i++) Wire.write(data[i]);
Wire.endTransmission();
delay(5);
memAddr += chunk;
data += chunk;
len -= chunk;
}
}
Sequential reads work across the whole chip, but the Wire buffer limit applies to reads as well, so read in chunks too.
Waiting for the Write Cycle
After a write, the chip is busy internally for a few milliseconds and does not acknowledge. Typical maximum write times are around 5 ms for these parts, but check your datasheet. A fixed delay is easiest. A faster method is acknowledge polling: repeatedly send the chip address and continue when Wire.endTransmission() returns 0.
Common Problems
- Reads return 0xFF. A blank EEPROM reads 0xFF. If you expected data, the write did not happen: check WP, wiring and delays.
- Wrong size address. Using one address byte on a 24LC256 (or two on a 24LC02) gives scrambled data.
- Missing pull-ups. Without them the bus can fail or be unreliable.
- Uno pins. Use A4/A5 or the dedicated SDA/SCL header pins, not other pins.
- Incomplete loops. In forum cases, missing braces in a
forloop or checking the return value of a write function (which may be an error code or byte count depending on the library) led to confusion. - Wear. EEPROM cells have a limited write endurance, commonly in the hundreds of thousands to a million cycles; avoid writing the same cell constantly.
FAQ
Is this the same as the Arduino built-in EEPROM library?
No. The built-in EEPROM library accesses memory inside the microcontroller. An external I2C EEPROM is a separate chip that you reach through Wire.
Can several EEPROMs share one bus?
Yes, by giving each a different address with A0 to A2, up to the chip’s limit.
Where would I use this?
Storing settings, calibration values or sensor logs that must survive power loss, for example in a final year Arduino IoT project.