Exercise 4 — EEPROM: Persistent Memory
Every variable you have written so far lives in SRAM — the fast working memory of the microcontroller. When power goes off, SRAM forgets everything instantly. The moment you unplug the board your counter resets to zero, your settings vanish.
But what if your device needs to remember something across a power cut? A thermostat needs to remember the target temperature the user set. A door lock needs to remember the access code. A vending machine needs to remember how many cans have been dispensed. This is the problem EEPROM solves.
"EEPROM is the MCU's notebook. RAM is a whiteboard that gets wiped every morning. EEPROM is a notebook that survives the night."
Learning Objectives
By the end of this exercise, you should be able to:
- explain why SRAM, Flash, and EEPROM serve different jobs in an MCU;
- read and write one byte of ATmega328P EEPROM through
EEAR,EEDR, andEECR; - apply the required
EEMPEtoEEPEwrite sequence safely; - handle erased EEPROM values such as
0xFF; - avoid premature EEPROM wear by writing only when persistent data changes.
What is EEPROM?
EEPROM stands for Electrically Erasable Programmable Read-Only Memory.
Each word in the name tells you part of the story:
| Word | What it means |
|---|---|
| Read-Only Memory | Originally, ROM was burned at the factory and could never be changed |
| Programmable | PROM could be written once by the user (burn the fuses) |
| Erasable | EPROM could be erased by shining UV light through a quartz window for 20 minutes |
| Electrically Erasable | EEPROM can be erased and rewritten byte-by-byte using ordinary voltage signals — no UV lamp needed |
The ATmega328P has 1024 bytes (1 KB) of EEPROM built into the same chip as the CPU. No external component is needed. It is always there.
How EEPROM Works Internally
EEPROM stores bits as electric charge trapped inside a floating-gate transistor. This is the same fundamental technology used in USB flash drives and SSDs — EEPROM is just the slower, byte-addressable, small-capacity version found inside microcontrollers.
SRAM vs EEPROM vs Flash
┌────────────────────────────────────────────────────────┐
│ Memory type │ Survives poweroff │ Write speed │ Size │
│──────────────│───────────────────│─────────────│────────│
│ SRAM │ NO │ 1 cycle │ 2 KB │
│ EEPROM │ YES │ ~3.3 ms │ 1 KB │
│ Flash │ YES │ ~4.5 ms │ 32 KB │
└────────────────────────────────────────────────────────┘
(ATmega328P figures)
The write penalty
Writing to EEPROM is 3.3 milliseconds per byte — about 30,000 times slower than writing to SRAM. This is not a bug; it is the physics of forcing charge into a floating gate. The consequence:
- Never write EEPROM inside a fast loop.
- Never write EEPROM on every measurement (once per second or faster is too often).
- Only write when the value actually changes.
Write endurance
EEPROM cells wear out. The ATmega328P datasheet guarantees 100,000 erase/write cycles per byte. At one write per day that is 273 years. At one write per second that is 27 hours. Write only when something changes — not continuously.
The Three Memories Side by Side
It helps to see all three memories in context of where your code lives:
EEPROM Registers on the ATmega328P
You access EEPROM through three hardware registers. No library is needed — these registers are always there.
Read subroutine (5 steps)
; ─────────────────────────────────────────────────────────
; eeprom_read — read one byte from EEPROM
;
; In: r25:r24 = 10-bit EEPROM address (high byte in r25)
; Out: r24 = byte read back
; Clobbers: r18
; ─────────────────────────────────────────────────────────
eeprom_read:
; Step 1 — wait: poll EEPE; it stays 1 while a write is running
eerd_wait:
in r18, EECR
sbrc r18, EEPE ; skip next if EEPE = 0 (no write in progress)
rjmp eerd_wait
; Step 2 — load address high byte into EEARH
out EEARH, r25
; Step 3 — load address low byte into EEARL
out EEARL, r24
; Step 4 — set EERE bit to trigger the read
sbi EECR, EERE
; Step 5 — data is ready instantly in EEDR
in r24, EEDR
ret
Write subroutine (6 steps)
; ─────────────────────────────────────────────────────────
; eeprom_write — write one byte to EEPROM
;
; In: r25:r24 = 10-bit EEPROM address
; r22 = data byte to write
; Clobbers: r18
; ─────────────────────────────────────────────────────────
eeprom_write:
; Step 1 — wait for any previous write to finish
eewr_wait:
in r18, EECR
sbrc r18, EEPE
rjmp eewr_wait
; Step 2 — set address high byte
out EEARH, r25
; Step 3 — set address low byte
out EEARL, r24
; Step 4 — load data byte into EEDR
out EEDR, r22
; Step 5 — master write enable (opens the 4-cycle write window)
sbi EECR, EEMPE
; Step 6 — start write ← must happen within 4 clock cycles of step 5
sbi EECR, EEPE ; sbi = 2 cycles, so gap = 2 cycles ✓
ret
Why the two-step write enable? Steps 5 and 6 must happen within 4 clock cycles of each other. In assembly this is natural — `sbi` takes 2 cycles, so the two back-to-back `sbi` instructions leave only a 2-cycle gap, well inside the window. This hardware interlock stops a runaway program or electrical noise from accidentally erasing your stored data.
Prerequisites — What to Buy
This exercise requires no new hardware. Everything needed was already used in Exercise 3.
Components list
| # | Component | Qty | Approx. cost | Notes |
|---|---|---|---|---|
| 1 | Arduino Uno R3 | 1 | ₹350–₹500 | Carry over from Exercise 1. Has 1 KB EEPROM built into the ATmega328P. |
| 2 | USB cable (Type A to Type B) | 1 | ₹50–₹100 | Carry over from Exercise 1. Needed to program the board and watch serial output. |
| 3 | Breadboard + jumper wires | — | — | Optional. Not needed for the core exercise. |
Total new cost: ₹0
Software you need
| Software | Purpose |
|---|---|
| AVR-GCC toolchain | Assembles and links .S files — comes with Arduino IDE or install avr-gcc separately |
| avrdude | Uploads the .hex file to the board — bundled with Arduino IDE |
| PuTTY / screen / minicom | Serial terminal to watch the boot counter output (same as Exercise 3) |
Exercise: The Boot Counter
What you will build
Every time the Arduino powers on or resets, it reads a counter stored in EEPROM, adds 1, prints the new count over UART, and writes the updated count back. You will unplug and replug the USB cable several times and watch the count climb — proving the data survives power-off.
First power-on : Boot count: 1
Unplug, replug : Boot count: 2
Unplug, replug : Boot count: 3
Reset button : Boot count: 4
This demonstrates the core EEPROM use case: keeping state across resets.
The Circuit
No new wiring. The UART output goes through the USB cable just like Exercise 3.
If you want to add a visual indicator, wire an LED + 220 Ω resistor to PB5 (Arduino pin 13 — the built-in LED). The program will blink it once for each boot count (1 blink on first boot, 2 blinks on second, etc.), capped at 10.
Building the Program — Snippet by Snippet
Snippet 1 — Register and Constant Definitions
Name every register address and bit position. The rule: never use a raw number in assembly code — give it a name with .equ. This is the assembly equivalent of #define.
; I/O registers (address < 0x20 → use in/out/sbi/cbi)
.equ EECR, 0x1F ; EEPROM Control Register
.equ EEDR, 0x20 ; EEPROM Data Register
.equ EEARL, 0x21 ; EEPROM Address — low byte
.equ EEARH, 0x22 ; EEPROM Address — high byte
.equ EERE, 0 ; bit 0: trigger read
.equ EEPE, 1 ; bit 1: write in progress / start write
.equ EEMPE, 2 ; bit 2: master write enable
; UART registers (address > 0x5F → use lds/sts)
.equ UCSR0A, 0xC0
.equ UCSR0B, 0xC1
.equ UCSR0C, 0xC2
.equ UBRR0L, 0xC4
.equ UBRR0H, 0xC5
.equ UDR0, 0xC6
.equ UDRE0, 5
.equ TXEN0, 3
; Application constants
.equ BOOT_ADDR, 0 ; EEPROM byte 0 = boot counter
.equ LED_PIN, 5 ; PB5 = Arduino pin 13
Notice two address ranges: EECR at 0x1F is in low I/O space (0x00–0x1F) so sbi/cbi/in/out all work. UART registers are above 0x5F (extended I/O) — only lds/sts reach them.
Snippet 2 — Stack Pointer and UART Init
Same pattern as every exercise: set SP first, then configure UART.
main:
LDI R16, HIGH(RAMEND)
OUT SPH, R16
LDI R16, LOW(RAMEND)
OUT SPL, R16
; UART: 9600 baud @ 16 MHz → UBRR = 103
CLR R16
STS UBRR0H, R16
LDI R16, 103
STS UBRR0L, R16
LDI R16, (1<<TXEN0) ; TX only (we don't receive in this exercise)
STS UCSR0B, R16
LDI R16, 0x06 ; 8-bit, 1 stop, no parity
STS UCSR0C, R16
SBI DDRB, LED_PIN ; PB5 as output for the LED
Snippet 3 — Reading from EEPROM
The eeprom_read subroutine follows the 5-step hardware sequence. The key thing to remember: EEPROM is shared hardware — if a previous write is still running (EEPE = 1), you must wait before touching any register.
eeprom_read:
; Step 1 — poll until any previous write is done
eerd_wait:
IN R18, EECR
SBRC R18, EEPE ; EEPE clear = no write running → skip rjmp
RJMP eerd_wait
; Steps 2+3 — write the 10-bit address into EEARH:EEARL
OUT EEARH, R25
OUT EEARL, R24
; Step 4 — set EERE bit to trigger the read
SBI EECR, EERE
; Step 5 — result is in EEDR immediately
IN R24, EEDR
RET
Call it like this: load r25 = address high byte, r24 = address low byte, RCALL eeprom_read. The byte comes back in r24.
Snippet 4 — Writing to EEPROM
The write sequence has a critical timing requirement: EEMPE and EEPE must be set within 4 clock cycles of each other. In assembly, two back-to-back sbi instructions each take 2 cycles — so the gap is exactly 2 cycles, safely inside the window. A C compiler cannot guarantee this timing; in assembly it is automatic.
eeprom_write:
; Step 1 — wait for previous write to finish
eewr_wait:
IN R18, EECR
SBRC R18, EEPE
RJMP eewr_wait
; Steps 2+3 — set address
OUT EEARH, R25
OUT EEARL, R24
; Step 4 — load data byte into EEDR
OUT EEDR, R22
; Steps 5+6 — two-step write enable (4-cycle window)
SBI EECR, EEMPE ; ① open the window (2 cycles)
SBI EECR, EEPE ; ② start write (2 cycles after ①)
RET
Snippet 5 — The 0xFF First-Boot Check and Counter Logic
Fresh EEPROM reads 0xFF (all bits = erased). We treat 0xFF as "never been written" and start the counter at 0. Then we increment and save.
; Read boot count from address 0
LDI R25, 0x00
LDI R24, BOOT_ADDR
RCALL eeprom_read ; result in r24
; If 0xFF, it is the first boot — start at 0
CPI R24, 0xFF
BRNE not_fresh
CLR R24
not_fresh:
INC R24 ; count++
MOV R19, R24 ; save count in r19 for later use
; Write updated count back
LDI R25, 0x00
LDI R24, BOOT_ADDR
MOV R22, R19
RCALL eeprom_write
MOV R19, R24 saves the count before RCALL eeprom_write can clobber R24. When eeprom_write returns, R19 still holds the original count — we use it for printing and blinking.
Snippet 6 — Printing and Blinking
Print a Flash string with LPM Z+, then blink the LED N times capped at 10.
; Print "=== ATmega328P boot ===\r\nBoot count: " from Flash
LDI ZH, HIGH(msg_header<<1)
LDI ZL, LOW(msg_header<<1)
RCALL print_flash_str
MOV R24, R19
RCALL print_decimal ; print the number
LDI R24, '\r'
RCALL uart_tx
LDI R24, '\n'
RCALL uart_tx
; Blink LED: min(count, 10) times
MOV R20, R19
CPI R20, 10
BRLO blink_loop
LDI R20, 10 ; cap at 10
blink_loop:
TST R20
BREQ idle
SBI PORTB, LED_PIN ; on
LDI R16, 150
RCALL delay_ms
CBI PORTB, LED_PIN ; off
LDI R16, 200
RCALL delay_ms
DEC R20
RJMP blink_loop
idle:
RJMP idle
Complete Program
Save this as boot_count.S (capital S = assembly with preprocessor).
; ═══════════════════════════════════════════════════════════════════
; boot_count.S — EEPROM Boot Counter
; ATmega328P @ 16 MHz
;
; On every power-on or reset:
; 1. Read boot count from EEPROM address 0
; 2. Increment and write back
; 3. Print "Boot count: N" over UART at 9600 baud
; 4. Blink the built-in LED (PB5) N times, capped at 10
; ═══════════════════════════════════════════════════════════════════
; ── I/O register addresses (use in / out / sbi / cbi) ─────────────
.equ DDRB, 0x04
.equ PORTB, 0x05
.equ EECR, 0x1F ; EEPROM Control Register
.equ EEDR, 0x20 ; EEPROM Data Register
.equ EEARL, 0x21 ; EEPROM Address Register — low byte
.equ EEARH, 0x22 ; EEPROM Address Register — high byte
; Bit positions inside EECR
.equ EERE, 0
.equ EEPE, 1
.equ EEMPE, 2
; ── UART registers (extended I/O — use lds / sts) ─────────────────
.equ UCSR0A, 0xC0
.equ UCSR0B, 0xC1
.equ UCSR0C, 0xC2
.equ UBRR0L, 0xC4
.equ UBRR0H, 0xC5
.equ UDR0, 0xC6
.equ UDRE0, 5 ; bit 5 of UCSR0A — 1 = UDR0 ready to accept byte
.equ TXEN0, 3 ; bit 3 of UCSR0B — enables TX pin
; ── Constants ─────────────────────────────────────────────────────
.equ BOOT_ADDR, 0 ; EEPROM byte 0 holds the boot counter
.equ LED_PIN, 5 ; PB5 = Arduino Uno built-in LED (pin 13)
; ══════════════════════════════════════════════════════════════════
; Reset vector — must be at 0x0000
; ══════════════════════════════════════════════════════════════════
.org 0x0000
rjmp main
; ══════════════════════════════════════════════════════════════════
; main — runs once after every power-on or reset
; ══════════════════════════════════════════════════════════════════
.org 0x0034 ; skip over interrupt vector table
main:
; ── Stack pointer: point to top of SRAM (0x08FF on ATmega328P) ──
ldi r16, hi(RAMEND)
out SPH, r16
ldi r16, lo(RAMEND)
out SPL, r16
; ── UART init: 9600 baud @ 16 MHz ────────────────────────────
; Formula: UBRR = (F_CPU / (16 × baud)) - 1 = 103
clr r16
sts UBRR0H, r16 ; high byte = 0
ldi r16, 103
sts UBRR0L, r16 ; low byte = 103
ldi r16, (1<<TXEN0)
sts UCSR0B, r16 ; enable transmitter only
ldi r16, 0x06 ; UCSZ01|UCSZ00 = 8-bit, 1 stop, no parity
sts UCSR0C, r16
; ── LED: set PB5 as output ────────────────────────────────────
sbi DDRB, LED_PIN
; ── Read boot counter from EEPROM ────────────────────────────
ldi r25, 0x00 ; address high byte (always 0 for addr < 256)
ldi r24, BOOT_ADDR ; address low byte = 0
rcall eeprom_read ; returns byte in r24
; Fresh (erased) EEPROM reads 0xFF — treat as "first ever boot"
cpi r24, 0xFF
brne not_fresh
clr r24 ; start count at 0
not_fresh:
inc r24 ; count++
mov r19, r24 ; r19 = current boot count (keep safe)
; ── Write updated count back to EEPROM ───────────────────────
ldi r25, 0x00
ldi r24, BOOT_ADDR
mov r22, r19 ; data argument
rcall eeprom_write
; ── Print message over UART ───────────────────────────────────
; Uses Z register (r31:r30) to point into Flash string table
ldi ZH, hi(msg_header << 1)
ldi ZL, lo(msg_header << 1)
rcall print_flash_str
mov r24, r19 ; print the count
rcall print_decimal
ldi r24, '\r'
rcall uart_tx
ldi r24, '\n'
rcall uart_tx
; ── Blink LED: once per boot, max 10 ─────────────────────────
ldi r16, 200
rcall delay_ms
mov r20, r19 ; blink counter = boot count
cpi r20, 10
brlo blink_loop ; if count <= 10, use it directly
ldi r20, 10 ; else cap at 10
blink_loop:
tst r20
breq idle
sbi PORTB, LED_PIN ; LED on
ldi r16, 150
rcall delay_ms
cbi PORTB, LED_PIN ; LED off
ldi r16, 200
rcall delay_ms
dec r20
rjmp blink_loop
idle:
rjmp idle ; spin forever — work is done at startup
; ══════════════════════════════════════════════════════════════════
; eeprom_read
; Read one byte from EEPROM
; In: r25:r24 = address Out: r24 = byte Clobbers: r18
; ══════════════════════════════════════════════════════════════════
eeprom_read:
eerd_wait:
in r18, EECR
sbrc r18, EEPE ; loop while previous write is still running
rjmp eerd_wait
out EEARH, r25 ; address high byte
out EEARL, r24 ; address low byte
sbi EECR, EERE ; trigger read
in r24, EEDR ; result is ready in the same cycle
ret
; ══════════════════════════════════════════════════════════════════
; eeprom_write
; Write one byte to EEPROM (hardware takes ~3.3 ms to complete)
; In: r25:r24 = address, r22 = data byte Clobbers: r18
; ══════════════════════════════════════════════════════════════════
eeprom_write:
eewr_wait:
in r18, EECR
sbrc r18, EEPE
rjmp eewr_wait
out EEARH, r25
out EEARL, r24
out EEDR, r22
sbi EECR, EEMPE ; ① master write enable — opens 4-cycle window
sbi EECR, EEPE ; ② start write — 2 cycles after ①, within window
ret
; ══════════════════════════════════════════════════════════════════
; uart_tx
; Send one byte. In: r24 = character Clobbers: r18
; ══════════════════════════════════════════════════════════════════
uart_tx:
lds r18, UCSR0A
sbrs r18, UDRE0 ; loop until transmit buffer is empty
rjmp uart_tx
sts UDR0, r24
ret
; ══════════════════════════════════════════════════════════════════
; print_flash_str
; Print null-terminated string stored in Flash.
; In: Z = byte address of string (label << 1)
; ══════════════════════════════════════════════════════════════════
print_flash_str:
lpm r24, Z+ ; load byte from Flash, auto-increment Z
tst r24 ; null terminator?
breq pfs_done
rcall uart_tx
rjmp print_flash_str
pfs_done:
ret
; ══════════════════════════════════════════════════════════════════
; print_decimal
; Print r24 as unsigned decimal 0–255, no leading zeros
; Clobbers: r18, r20, r21, r22, r23, r24
; ══════════════════════════════════════════════════════════════════
print_decimal:
mov r23, r24 ; r23 = working copy
clr r21 ; r21 = "a digit was already printed" flag
; ── hundreds ──────────────────────────────────────────────
ldi r20, 0
pd_h:
cpi r23, 100
brlo pd_h_emit
inc r20
subi r23, 100
rjmp pd_h
pd_h_emit:
tst r20
breq pd_tens ; skip leading zero
ldi r24, '0'
add r24, r20
rcall uart_tx
ldi r21, 1
; ── tens ──────────────────────────────────────────────────
pd_tens:
ldi r20, 0
pd_t:
cpi r23, 10
brlo pd_t_emit
inc r20
subi r23, 10
rjmp pd_t
pd_t_emit:
tst r20
brne pd_t_print
tst r21
breq pd_ones ; skip leading zero
pd_t_print:
ldi r24, '0'
add r24, r20
rcall uart_tx
; ── ones (always print) ───────────────────────────────────
pd_ones:
ldi r24, '0'
add r24, r23
rcall uart_tx
ret
; ══════════════════════════════════════════════════════════════════
; delay_ms
; Busy-wait. In: r16 = milliseconds (1–255)
; At 16 MHz: inner loop = (sbiw 2 + brne 2) × 4000 ≈ 1 ms
; Clobbers: r16, r26, r27
; ══════════════════════════════════════════════════════════════════
delay_ms:
dm_outer:
ldi r26, lo(4000) ; r27:r26 = inner loop counter
ldi r27, hi(4000)
dm_inner:
sbiw r26, 1 ; 2 cycles — decrement 16-bit counter
brne dm_inner ; 2 cycles if branch taken
dec r16
brne dm_outer
ret
; ══════════════════════════════════════════════════════════════════
; Strings stored in Flash
; ══════════════════════════════════════════════════════════════════
msg_header:
.ascii "\r\n=== ATmega328P boot ===\r\nBoot count: "
.byte 0 ; null terminator
Assemble and Upload
avr-gcc automatically calls the assembler when the file extension is .S (capital S):
# Step 1 — assemble and link into ELF
avr-gcc -mmcu=atmega328p -o boot_count.elf boot_count.S
# Step 2 — convert ELF to Intel HEX (what avrdude needs)
avr-objcopy -O ihex boot_count.elf boot_count.hex
# Step 3 — upload to the board
avrdude -c arduino -p m328p -P COM3 -b 115200 -U flash:w:boot_count.hex
Replace COM3 with your actual port (Device Manager on Windows → Ports; /dev/ttyUSB0 or /dev/ttyACM0 on Linux).
[!TIP] Lowercase
.svs uppercase.S:.Sfiles are passed through the C preprocessor first, which lets you use#define,#include, and macros..sfiles go straight to the assembler. Always use.Sfor AVR projects so you can use register name constants if needed.
Open PuTTY: Serial, COM3, 9600 baud, 8N1. Press the reset button on the Arduino — you will see:
=== ATmega328P boot ===
Boot count: 1
Unplug the USB cable, wait 3 seconds, replug. PuTTY shows:
=== ATmega328P boot ===
Boot count: 2
What is Happening Step by Step
Why 0xFF Means "Never Written"
Fresh EEPROM cells (factory-erased state) read back as 0xFF — all eight bits set to 1. This is the natural resting state of a floating-gate transistor with no charge stored. Writing a byte actually means pulling selected bits down to 0.
This has a practical consequence: if you store a value that legitimately could be 255, you cannot use 0xFF as a sentinel. You have two options:
- Use address 0x00 for a "has been written" flag byte (write 0xAA on first run, then check it).
- Use two bytes — one for the value, one for "valid" flag.
For this exercise the boot counter starts at 1 and will almost certainly never reach 255, so 0xFF = "unwritten" works fine.
EEPROM Address Map — Thinking Ahead
1 KB of EEPROM is 1024 bytes at addresses 0x000–0x3FF. As your projects grow you will store multiple things. Plan an address map so they do not overlap:
Address │ Size │ Contents
─────────┼──────┼──────────────────────────────────
0x000 │ 1 B │ Boot counter (this exercise)
0x001 │ 1 B │ Reserved / valid flag
0x002 │ 2 B │ (future) Target temperature × 10
0x004 │ 4 B │ (future) Total runtime seconds
0x008 │ 8 B │ (future) Last error code + timestamp
... │ ... │ ...
0x3FF │ — B │ End of EEPROM
Define your addresses as named constants using .equ — never scatter raw numbers through your code:
.equ BOOT_COUNTER, 0x000
.equ VALID_FLAG, 0x001
.equ TARGET_TEMP_L, 0x002
.equ TARGET_TEMP_H, 0x003
.equ RUNTIME_0, 0x004 ; total runtime seconds, 4 bytes
.equ RUNTIME_1, 0x005
.equ RUNTIME_2, 0x006
.equ RUNTIME_3, 0x007
Then in code you write ldi r24, BOOT_COUNTER instead of ldi r24, 0 — the name makes the intent obvious and a single .equ change updates every use.
Extension Challenges
Challenge 1 — Reset the counter
Add a button on PD2. If it is held down at startup, write 0 back to EEPROM and print "Counter reset!" This teaches the "press-and-hold at boot to wipe settings" pattern used in almost every consumer device.
Challenge 2 — Store a 16-bit value
Extend the counter to two bytes (0–65535). Use EEPROM addresses 0x00 (low byte) and 0x01 (high byte). Call eeprom_write twice — once for each byte. On read, eeprom_read each byte separately and combine them: mov r25, result_high / mov r24, result_low. Practice big-endian vs little-endian decisions in assembly.
Challenge 3 — Store a device name
Write a 16-character name string to EEPROM addresses 0x10–0x1F using a loop — load each character from Flash with lpm, write it with rcall eeprom_write, increment the EEPROM address. On boot, read the 16 bytes back and print them over UART. Shows how to loop over EEPROM with a pointer in a register pair.
Challenge 4 — Wear levelling concept
Write a program that writes the same byte in a tight loop using a 32-bit counter in four SRAM registers (r16–r19). Increment the counter on every write. After reaching 100,000 writes (0x000186A0), send "EEPROM wear warning!" over UART. This teaches why wear levelling exists in USB drives and SSDs — and how to handle multi-byte counters in assembly.
Verification Checklist
Before you call the EEPROM boot counter complete, verify these items:
- Fresh or erased EEPROM at address
0x000reads as0xFFand is treated as first boot. - The first reset prints
Boot count: 1. - Pressing reset increments the count by exactly one.
- Unplugging USB power for a few seconds does not erase the count.
- The LED blink count matches the printed count up to the cap of ten blinks.
- EEPROM writes are not repeated inside the idle loop.
Common failure symptoms:
- Always prints 1: the write sequence is wrong, address registers are wrong, or power is removed before the write completes.
- Prints 255 or 0 on first run: erased
0xFFwas not handled as an unwritten sentinel. - UART output is garbled: baud-rate settings or CPU clock assumption are wrong.
- EEPROM wears quickly: firmware writes every loop instead of only at startup or on value change.
Explained Solution
The program first initializes the stack, UART transmitter, and PB5 LED pin. It then reads EEPROM byte 0x000. If the byte is 0xFF, the code treats the cell as never written and starts from zero before incrementing. The updated count is written back using the required EEPROM write sequence: wait for EEPE to clear, load EEARH:EEARL, load EEDR, set EEMPE, then set EEPE within four cycles. The CPU can continue after starting the write, but this exercise prints and blinks only after the update is requested. The idle loop does not write again, so the stored byte changes once per reset.
Summary
Previous Exercise: ← Exercise 3 — UART Serial Communication
Next Exercise: Exercise 5 — External Interrupts →
Further Reading
- Microchip ATmega328P Datasheet - EEPROM memory, control registers, timing, and endurance.
- AVR Instruction Set Manual -
SBI,SBRC,IN,OUT, and branch instructions. - AVR Libc EEPROM API - C library functions that implement the same hardware rules.