arduino beginner 25 min

Arduino: persist data with EEPROM, the right way

Save a calibration value or a small setting to EEPROM so it survives a power cycle. The 1 KB on the Uno is enough for hundreds of values if you use it carefully.

Code available for: Arduino CESP32 Arduino
Published Aug 26, 2026

I built a soil moisture monitor once and ran it for a few weeks without saving the calibration. Then I unplugged it to move it. The calibration was gone. The new reading on the new soil was 0% or 100% depending on how I held the sensor. I had to re-calibrate.

EEPROM fixes this. EEPROM is a small chunk of non-volatile memory on the Arduino (1 KB on the Uno and Nano, 4 KB on the Mega) that survives power cycles. You write a value, you pull the plug, the value is still there when power comes back.

The “still there” part is the whole point. RAM forgets. Flash (program memory) is read-only at runtime. EEPROM is the only place to put small bits of state that need to survive.

What you need

  • Any Arduino board
  • USB cable
  • A reason to persist something (a calibration value, a counter, a setting)

For a real project, EEPROM matters. For a tutorial project, you might not need it. The use cases that come up most:

  • A calibration value (e.g. a temperature offset for a sensor)
  • A counter (how many times has the device been turned on?)
  • A setting the user picked (a threshold, a mode)
  • A “last state” so the device resumes where it left off

The code

#include <EEPROM.h>

const int CALIBRATION_ADDR = 0;   // byte 0 in EEPROM
const int MAGIC_ADDR = 100;       // byte 100, holds a "magic number"

void setup() {
  Serial.begin(9600);

  // Check if the magic number is present
  byte magic = EEPROM.read(MAGIC_ADDR);
  if (magic == 0xAB) {
    // We have saved data
    int saved = 0;
    EEPROM.get(CALIBRATION_ADDR, saved);
    Serial.print("Loaded calibration: ");
    Serial.println(saved);
  } else {
    // First boot, save a default
    int defaultCal = 250;
    EEPROM.put(CALIBRATION_ADDR, defaultCal);
    EEPROM.update(MAGIC_ADDR, 0xAB);   // update, not write
    Serial.println("First boot, saved default.");
  }
}

void loop() {
  // use the calibration value
}

Two things to notice. First, EEPROM.put() is the modern way to write any type. It uses EEPROM.update() under the hood, which only writes if the value changed. Second, EEPROM.read() returns a byte, and you read multi-byte values with EEPROM.get() (which also takes a type and reads the right number of bytes).

The “end()” gotcha (it is not what you think)

The old EEPROM library (before 1.6 or so) had an EEPROM.end() function that flushed pending writes. The new library does not have that. Each call to EEPROM.write() or EEPROM.update() writes immediately. The “end()” concern is a vestige of older code.

What you do need to know: the underlying write takes a few milliseconds. If you call EEPROM.write() in a tight loop, the chip stalls for each write. For a one-off save, this does not matter. For saving 50 values in a row, it can take a quarter second.

The wear-leveling concern

Every EEPROM cell is rated for about 100,000 writes. That sounds like a lot, until you write every second. A counter that increments once per second will wear out the cell in 27 hours. A counter that increments once per hour is fine for 11 years.

Three things to keep in mind:

  1. Do not write in tight loops. Cache the value in RAM, write it occasionally.
  2. If you need a high-frequency counter, write to RAM and only flush to EEPROM every minute (or on power-down).
  3. If you really need a high-frequency write, use wear-leveling: spread the writes across many cells, and track which cell has the latest value. The EEPROM library does not do this for you.

For the calibration use case (a value that changes once when the user re-calibrates), 100,000 writes is effectively infinite.

When to use EEPROM vs Flash vs SD

This is the part that comes up most when readers ask questions. The short version:

  • EEPROM: small, fast, no external chip, 1 KB on Uno. Use for settings, calibration, last state.
  • Flash (PROGMEM): read-only at runtime, baked into the sketch. Use for lookup tables, strings, fixed data.
  • SD card: large, slow, needs a chip and a file system. Use for logs, images, anything bigger than 4 KB.

If you are tempted to store more than a few hundred bytes, SD. If you are tempted to store data that never changes, PROGMEM. If you have a few bytes that change occasionally, EEPROM.

The “magic number” pattern

The sketch above has MAGIC_ADDR = 100 and a check for 0xAB. Why?

When the EEPROM is fresh (factory new, or after EEPROM.clear()), every byte reads as 0xFF. If you write a calibration value of 0 to address 0, then read it back later, you cannot tell whether the value is 0 because you saved 0 or because the EEPROM is fresh.

The fix: write a known value to a “magic number” address. When you boot, check it. If the magic number is there, your saved data is valid. If not, this is a fresh boot, and you need to write defaults.

const byte MAGIC = 0xAB;
const int MAGIC_ADDR = 100;

if (EEPROM.read(MAGIC_ADDR) == MAGIC) {
  // saved data is valid
} else {
  // first boot
  EEPROM.write(MAGIC_ADDR, MAGIC);
  // write defaults
}

This is the single most important pattern in EEPROM programming. If you only learn one thing from this tutorial, learn this.

Putting structs in EEPROM

EEPROM.put() and EEPROM.get() take any type, including structs. This is the right way to save a small bundle of related values:

struct Settings {
  int calibration;
  byte mode;
  unsigned long bootCount;
};

const int SETTINGS_ADDR = 0;

void saveSettings(const Settings& s) {
  EEPROM.put(SETTINGS_ADDR, s);
}

bool loadSettings(Settings& s) {
  if (EEPROM.read(SETTINGS_ADDR + sizeof(s) - 1) != 0xAB) {
    return false;   // not initialized
  }
  EEPROM.get(SETTINGS_ADDR, s);
  return true;
}

A few gotchas:

  • sizeof(Settings) can change between compiles. If you change the struct definition and the EEPROM has old data, you read garbage. The magic-number check at the end of the struct catches this.
  • On the AVR, sizeof(long) is 4 bytes. On other boards it can be 8. If you ever port the code, check.
  • Always initialize the struct before reading: Settings s = {}; then EEPROM.get(addr, s); This way the fields you did not read are zero instead of uninitialized.

The alternative: PROGMEM for read-only data

If the data never changes (a lookup table, a fixed string), it goes in PROGMEM, not EEPROM. PROGMEM lives in flash (program memory) and survives power cycles, just like EEPROM. The difference is you cannot write to it at runtime.

#include <avr/pgmspace.h>

const char message[] PROGMEM = "Hello from flash";

void setup() {
  Serial.begin(9600);
  char buf[32];
  strcpy_P(buf, message);
  Serial.println(buf);
}

The _P suffix on string functions means “read from PROGMEM.” This is the AVR-specific version. ESP32 and other cores do this differently (or automatically, since flash and RAM are the same memory on those chips).

When something breaks

  • EEPROM.read() always returns 255. The chip is not communicating, or you have the wrong I2C address (if you are using an external EEPROM). Onboard EEPROM on the Uno uses no pins; it just works.
  • Writes appear to fail randomly. The cell is worn out, or you are writing too fast. Add a small delay between writes.
  • The magic number check fails on a fresh chip. You forgot to write the magic number on the first boot. Add the EEPROM.write(MAGIC_ADDR, MAGIC) line in the else branch.
  • You read a value but it is corrupted. A power loss happened mid-write. The cell holds a partial value. Use the magic number as a validity check, and never trust a value that does not have a valid magic.

What to build next

  • A settings menu on a small OLED: cycle through values, save to EEPROM on the press of a button.
  • A weather station that logs min/max temperatures across power cycles.
  • A “first-boot wizard” that prompts the user to calibrate the sensor, then saves the calibration to EEPROM forever.