AVR Internal EEPROM: Saving Settings with EEMEM and avr/eeprom.h
Variables in SRAM are lost the moment power is removed. Many projects need to remember something across power cycles: a calibration value, a user's preferred brightness, a total count of products on a conveyor, or the last state of a relay. Every ATmega has a small built-in EEPROM for exactly this purpose: 1 KB on the ATmega328P and ATmega32, and 512 bytes on the ATmega16.
This tutorial shows how the EEPROM is accessed at register level, how to use the simpler avr-libc functions, how to place variables in EEPROM with EEMEM, and how to protect stored data against wear and corruption. The example is a power-on counter that also stores a settings structure with a validity check.
What You Need to Know About AVR EEPROM
| Property | ATmega328P | Notes |
|---|---|---|
| Size | 1024 bytes | Addresses 0 to 1023 |
| Endurance | 100,000 write/erase cycles | Per cell, as specified in the datasheet |
| Write time | About 3.3 ms per byte | Timed by an internal oscillator, independent of F_CPU |
| Erased value | 0xFF | A fresh or chip-erased EEPROM reads 0xFF |
| Read time | A few CPU cycles | The CPU is halted for 4 cycles during a read |
The 3.3 ms write time is long in microcontroller terms. Writing 100 bytes takes a third of a second, so do not write EEPROM in a fast loop. The endurance limit matters too: writing the same byte once per second would exhaust 100,000 cycles in a little over a day.
Register-Level Access
The EEPROM is accessed through three registers: EEAR (address, EEARH:EEARL), EEDR (data) and EECR (control). In EECR, EERE triggers a read, EEPE starts a write, and EEMPE is the master write enable. A write must follow a timed sequence: set EEMPE, then set EEPE within four clock cycles. This protects against accidental writes by runaway code. Because an interrupt between the two steps would break the timing, interrupts must be disabled during the sequence.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
uint8_t eeprom_read_raw(uint16_t addr)
{
while (EECR & (1 << EEPE)) /* wait for previous write */
;
EEAR = addr;
EECR |= (1 << EERE); /* start read */
return EEDR;
}
void eeprom_write_raw(uint16_t addr, uint8_t data)
{
while (EECR & (1 << EEPE)) /* wait for previous write */
;
EEAR = addr;
EEDR = data;
uint8_t sreg = SREG;
cli(); /* timed sequence must not be interrupted */
EECR |= (1 << EEMPE); /* master write enable */
EECR |= (1 << EEPE); /* start write within 4 cycles */
SREG = sreg; /* restore interrupt state */
}
At optimisation level -Os, avr-gcc compiles each EECR |= into a single SBI instruction, which keeps the two writes within the four-cycle window. On the ATmega16/32 the same bits are named EEMWE and EEWE.
Using avr/eeprom.h and EEMEM
In practice you should use the avr-libc functions, which handle the timed sequence and waiting for you. Declaring a variable with the EEMEM attribute places it in the .eeprom section, and the linker assigns its address. You must never access an EEMEM variable directly; always pass its address to the eeprom functions.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay.h>
#include <stdint.h>
#define SETTINGS_MAGIC 0xA5
typedef struct {
uint8_t magic; /* marks the block as valid */
uint8_t brightness; /* 0..255 */
uint16_t threshold; /* e.g. an ADC trip level */
} settings_t;
/* Variables placed in EEPROM; initial values go into main.eep */
uint16_t EEMEM ee_boot_count = 0;
settings_t EEMEM ee_settings = { SETTINGS_MAGIC, 128, 512 };
int main(void)
{
settings_t cfg;
DDRB |= (1 << DDB5);
/* Count how many times the board has powered up */
uint16_t boots = eeprom_read_word(&ee_boot_count);
if (boots == 0xFFFF) /* erased EEPROM */
boots = 0;
boots++;
eeprom_update_word(&ee_boot_count, boots);
/* Load settings, fall back to defaults if not valid */
eeprom_read_block(&cfg, &ee_settings, sizeof(cfg));
if (cfg.magic != SETTINGS_MAGIC) {
cfg.magic = SETTINGS_MAGIC;
cfg.brightness = 128;
cfg.threshold = 512;
eeprom_update_block(&cfg, &ee_settings, sizeof(cfg));
}
/* Show the boot count (low 4 bits) as LED blinks */
for (uint8_t i = 0; i < (boots & 0x0F); i++) {
PORTB |= (1 << PORTB5);
_delay_ms(200);
PORTB &= ~(1 << PORTB5);
_delay_ms(200);
}
while (1) {
/* normal application using cfg.brightness and cfg.threshold */
}
}
The update functions read each byte first and only write it if the value differs. This saves both time and endurance, so prefer eeprom_update_byte(), eeprom_update_word() and eeprom_update_block() over the write versions.
Programming Initial EEPROM Values
The initial values given to EEMEM variables are not part of the flash HEX file. Extract them into a separate .eep file and program it with avrdude if you want the defaults present from the first boot:
avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex main.elf main.eep avrdude -c usbasp -p m328p -U eeprom:w:main.eep:i
By default, programming flash with avrdude performs a chip erase, which also erases the EEPROM to 0xFF. If you want stored data to survive reprogramming, program the EESAVE fuse. The code above handles the erased case anyway by checking for 0xFFFF and the magic byte.
Worked Example: How Long Will the EEPROM Last?
Suppose a production counter is saved every time a product passes a sensor, at a steady rate of one product every 10 seconds, around the clock. A quick calculation shows why the save strategy matters.
Writes per day = 86 400 s / 10 s = 8 640 Life of one 2-byte slot = 100 000 / 8 640 = about 11.6 days Spread over 64 slots (128 bytes) in rotation: Life = 64 x 11.6 days = about 740 days, roughly 2 years Save only once per minute instead (1 440 writes per day): One slot lasts 100 000 / 1 440 = about 69 days
Combining both ideas, rotating the save location and reducing how often you save, easily stretches the life to many years. A simple rotation scheme stores a sequence number with each record; at power-up the program scans all slots and uses the record with the highest sequence number.
Reliability Tips and Common Mistakes
- Enable the brown-out detector (BODLEVEL fuses). If the supply drops during a write, the CPU can run incorrectly and corrupt the EEPROM.
- Do not save on every change. Save when the user confirms a setting, or after the value has been stable for a few seconds.
- For frequently updated counters, spread writes across several locations (wear levelling) instead of hammering one address.
- Add a magic byte or checksum so the program can detect blank or corrupted data and fall back to defaults.
- Remember that EEMEM addresses are chosen by the compiler and linker, not by you. Adding or reordering EEMEM variables in a new firmware version moves them, so old saved data is read from the wrong place. Change the magic value whenever the layout changes.
Summary
The internal EEPROM stores data across power cycles. At register level you set EEAR and EEDR and follow the EEMPE then EEPE sequence with interrupts disabled. In practice, use EEMEM variables with the avr-libc eeprom_read_* and eeprom_update_* functions, validate data with a marker, enable brown-out detection, and limit how often you write.