The ESP32 does not have physical EEPROM. When beginners search for "esp32 dev module eeprom", they are usually trying to use the Arduino EEPROM.h library, which on the ESP32 merely emulates EEPROM by carving out a 4KB sector of the main SPI Flash. I have seen countless projects brick their flash sectors because the developer wrote a boot counter every 10 seconds, not realizing that every EEPROM.commit() erases and rewrites that entire 4KB block, burning through the flash's 100,000 write-cycle limit in days.
If you need to store WiFi credentials or a one-time configuration, use the ESP32's internal NVS (Non-Volatile Storage). If you are logging sensor data, tracking runtime, or writing data more than once an hour, you must wire an external I2C EEPROM. This guide gives you the exact decision framework, hardware pinouts, and fail-proof C++ code to implement an external AT24C256 I2C EEPROM on your ESP32 Dev Module.
Difficulty: Intermediate (Requires I2C bus understanding)
Time to Complete: 25 minutes
Estimated Cost: $8.50 (ESP32 DevKit + AT24C256 breakout + jumpers)
The Decision Path: Internal Emulation vs. External I2C Chip
Do not guess which storage method to use. Follow this decision tree to select the right memory architecture for your specific write-frequency and data-retention needs.
| Your Use Case | Write Frequency | Recommended Solution | Concrete Part / Library |
|---|---|---|---|
| WiFi passwords, device tokens, boot config | Once per boot / Rarely | Internal NVS Flash | Preferences.h library |
| Daily max/min temperature logging | Once per day | Internal Emulated EEPROM | EEPROM.h (with caution) |
| Sensor logging, state machines, runtime tracking | Every 5 mins to 1 hour | External I2C EEPROM | AT24C256 (32KB) |
| High-speed data buffering (10Hz+) | Multiple times per second | External I2C FRAM | MB85RC256V (Ferroelectric RAM) |
Parts List and Hardware Pin Mapping
The code and wiring below target the ESP32-DevKitC V4 (specifically the ESP32-WROOM-32E module variant). This board operates at 3.3V logic. While the AT24C256 can tolerate 5V on its VCC pin, the I2C data lines must not exceed 3.3V to avoid frying the ESP32's GPIO pins. Power the EEPROM from the ESP32's 3V3 pin.
Bill of Materials
- Microcontroller: ESP32-DevKitC V4 (ESP32-WROOM-32E, 30-pin or 38-pin variant)
- Memory: AT24C256 I2C EEPROM Breakout Board (DIP-8 package if wiring raw, but breakout is preferred for built-in pull-ups)
- Resistors: 2x 4.7kΩ pull-up resistors (Only required if your breakout board lacks them)
- Wiring: 22 AWG solid-core jumper wires
Pin Mapping Table
| ESP32-WROOM-32E Pin | GPIO Number | AT24C256 Pin | Notes / Constraints |
|---|---|---|---|
| SDA (Default I2C Data) | GPIO 21 | SDA (Pin 5) | Requires 4.7kΩ pull-up to 3.3V if not on breakout |
| SCL (Default I2C Clock) | GPIO 22 | SCL (Pin 6) | Requires 4.7kΩ pull-up to 3.3V if not on breakout |
| 3V3 | N/A (Power) | VCC (Pin 8) | Do NOT use 5V/VIN; ESP32 GPIOs are not 5V tolerant |
| GND | N/A (Ground) | GND (Pin 4) | Common ground is mandatory for I2C reference |
| N/C | N/A | A0, A1, A2 (Pins 1-3) | Tie all three to GND for base I2C address 0x50 |
| N/C | N/A | WP (Pin 7) | Tie to GND to enable writes; tie to VCC for write-protection |
Complete Compilable Code for AT24C256
This sketch uses the native Arduino Wire library to communicate with the AT24C256. It avoids third-party EEPROM libraries to eliminate version-dependency errors. It includes explicit error handling for I2C NACKs (Not Acknowledged) and respects the AT24C256's 64-byte page write boundary.
Target Board: ESP32 Dev Module (ESP32-WROOM-32E). Tested on Arduino IDE 2.3.x and ESP32 Core v2.0.14+.
#include <Wire.h>
// Hardware I2C pins for ESP32-DevKitC V4
#define I2C_SDA 21
#define I2C_SCL 22
// AT24C256 base address (A0, A1, A2 tied to GND)
#define EEPROM_ADDR 0x50
// AT24C256 has a 64-byte page buffer
#define PAGE_SIZE 64
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("ESP32 External EEPROM (AT24C256) Initialization...");
// Initialize I2C bus with explicit GPIO mapping and 400kHz Fast Mode
Wire.begin(I2C_SDA, I2C_SCL, 400000);
// Verify EEPROM is present on the bus
Wire.beginTransmission(EEPROM_ADDR);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.println("SUCCESS: AT24C256 found at 0x50.");
} else {
Serial.print("FATAL: EEPROM not found. Wire.endTransmission() error code: ");
Serial.println(error);
while(1); // Halt execution to prevent bus spam
}
// Example 1: Write and read a single byte
writeEEPROMByte(0, 42);
byte val = readEEPROMByte(0);
Serial.printf("Read from addr 0: %d\n", val);
// Example 2: Write a string safely across page boundaries
String testData = "ElectricalFlux ESP32 EEPROM Test Data 2026";
writeEEPROMString(100, testData);
String readBack = readEEPROMString(100, testData.length());
Serial.printf("Read back string: %s\n", readBack.c_str());
}
void loop() {
// Nothing to do in loop for this demonstration
delay(10000);
}
// --- Function Definitions ---
void writeEEPROMByte(unsigned int eeaddress, byte data) {
Wire.beginTransmission(EEPROM_ADDR);
// AT24C256 uses 16-bit addressing (2 bytes for memory address)
Wire.write((int)(eeaddress >> 8)); // MSB
Wire.write((int)(eeaddress & 0xFF)); // LSB
Wire.write(data);
byte status = Wire.endTransmission();
if (status != 0) {
Serial.printf("ERROR: Write failed at addr %d. Status: %d\n", eeaddress, status);
}
delay(5); // AT24C256 requires up to 5ms for internal write cycle
}
byte readEEPROMByte(unsigned int eeaddress) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((int)(eeaddress >> 8)); // MSB
Wire.write((int)(eeaddress & 0xFF)); // LSB
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
if (Wire.available()) {
return Wire.read();
}
return 0xFF; // Return 0xFF on failure
}
void writeEEPROMString(unsigned int eeaddress, String data) {
unsigned int len = data.length();
unsigned int currentAddr = eeaddress;
for (unsigned int i = 0; i < len; i++) {
writeEEPROMByte(currentAddr, data[i]);
currentAddr++;
// Handle 64-byte page boundary wrap-around
if (currentAddr % PAGE_SIZE == 0) {
delay(5); // Mandatory pause at page boundary to let the chip commit
}
}
}
String readEEPROMString(unsigned int eeaddress, unsigned int len) {
String result = "";
for (unsigned int i = 0; i < len; i++) {
char c = readEEPROMByte(eeaddress + i);
result += c;
}
return result;
}
Debugging: First 3 Checks and Exact Error Strings
When your ESP32 fails to communicate with the EEPROM, do not immediately rewrite your code. Hardware I2C failures follow a strict hierarchy of probability. Here are the first three things to check on your bench.
- Check the I2C Address (The 0x50 vs 0x57 Trap): The AT24C256 base address is
0x50when pins A0, A1, and A2 are grounded. However, some cheap breakout boards route these pins to VCC by default, shifting the address to0x57. Run an I2C Scanner sketch. If you see0x57, update#define EEPROM_ADDR 0x57in the code above. - Verify Pull-Up Resistors: I2C is an open-drain bus. It physically cannot pull the signal high without resistors. If your breakout board does not have 4.7kΩ surface-mount resistors on SDA and SCL, the signals will float, resulting in random NACK errors. Measure resistance from SDA to 3.3V; it should read ~4.7kΩ.
- Check the Write Protect (WP) Pin: Pin 7 on the raw AT24C256 chip is the Write Protect pin. If it is left floating, it can pick up EMI and lock the memory. Hardwire Pin 7 to GND to guarantee write access.
Decoding Exact Error Strings
If the Serial Monitor outputs an error, match it to this ranked cause list:
| Exact Error String / Code | Meaning | Fix |
|---|---|---|
Wire.endTransmission() == 2 |
NACK on Address. The ESP32 sent the address, but no chip replied. | Check wiring, verify 3.3V power, run I2C scanner to find correct hex address. |
Wire.endTransmission() == 3 |
NACK on Data. The chip acknowledged the address but rejected the payload. | Check WP pin (ensure it is GND). Verify you aren't writing past the 32KB limit (addr > 32767). |
E (123) i2c: i2c_master_cmd_begin: I2C_MASTER_NACK |
Underlying ESP-IDF hardware timeout. The bus is locked up or missing pull-ups. | Add 4.7kΩ pull-up resistors. Check for loose jumper wires on the breadboard. |
| Corrupted Data / Garbage Characters | You crossed a 64-byte page boundary during a burst write without pausing. | Use the writeEEPROMString function provided above, which enforces a 5ms delay at page boundaries. |
Extending and Simplifying Your Build
Once your baseline AT24C256 circuit is verified on the bench, you will inevitably need to adapt it for production or scale. Here is how to pivot based on your final constraints.
How to Simplify (If You Only Need to Store Configs)
If you realize you only need to store a WiFi SSID, a device ID, or a relay state that changes once a day, rip out the external EEPROM. Use the ESP32's native NVS (Non-Volatile Storage) via the Preferences.h library. NVS handles wear-leveling automatically across the flash chip, meaning you don't have to manually track memory addresses or worry about page boundaries. It is vastly simpler for key-value pair storage.
How to Extend (For High-Speed or High-Endurance Logging)
If you are building a solar charge controller or a motor diagnostics logger that needs to write a 10-byte telemetry packet every 100 milliseconds, the AT24C256 will fail. Its 5ms internal write cycle time will bottleneck your I2C bus, and you will burn through its 1,000,000 write cycles in under two days.
The Upgrade: Swap the AT24C256 for an MB85RC256V FRAM (Ferroelectric RAM) module. FRAM uses the exact same I2C protocol and pinout, but it has zero write delay (it writes at bus speed) and a 10^14 (100 trillion) write cycle endurance. The C++ code provided above will work with the FRAM module with only one modification: you can delete the delay(5) commands, as FRAM does not require a write-cycle pause.






