Loading header...

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, and EECR;
  • apply the required EEMPE to EEPE write 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:

flowchart TD classDef flash fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef sram fill:#dcfce7,stroke:#16a34a,color:#14532d classDef eeprom fill:#fef9c3,stroke:#ca8a04,color:#713f12 FLASH["🔵 Flash — 32 KB\nProgram storage\n──────────────────\nYour compiled machine code lives here\nLPM reads constant tables from here\nSurvives power-off\nWritten by programmer via ISP/USB"]:::flash SRAM["🟢 SRAM — 2 KB\nWorking memory\n──────────────────\nVariables, stack, heap\nRegisters R0–R31\nFast: 1 clock cycle per access\nLost instantly on power-off"]:::sram EEPROM["🟡 EEPROM — 1 KB\nPersistent storage\n──────────────────\nSettings, counters, calibration data\nSurvives power-off\nSlow: 3.3 ms per byte write\nUp to 100,000 write cycles per byte"]:::eeprom FLASH -->|"CPU fetches instructions into pipeline"| SRAM SRAM <-->|"Read/Write via EEAR + EEDR + EECR registers"| EEPROM

EEPROM Registers on the ATmega328P

You access EEPROM through three hardware registers. No library is needed — these registers are always there.

flowchart TD classDef reg fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef ctrl fill:#ede9fe,stroke:#7c3aed,color:#3b0764 classDef result fill:#dcfce7,stroke:#16a34a,color:#14532d EEAR["EEARH : EEARL\n(EEPROM Address Register)\n──────────────────────\nWrite the target byte address here\nRange: 0x000 – 0x3FF (0 to 1023)\nSet this BEFORE triggering read or write"]:::reg EEDR_IN["EEDR (EEPROM Data Register) — for WRITE\n──────────────────────\nLoad the byte you want to save into EEDR\nThen trigger EECR to start the write"]:::reg EECR["EECR (EEPROM Control Register)\n──────────────────────\nEEMPE = 1 → master write enable (set first)\nEEPE = 1 → start write (set within 4 cycles of EEMPE)\nEERE = 1 → start read (result appears in EEDR instantly)"]:::ctrl EEDR_OUT["EEDR (EEPROM Data Register) — after READ\n──────────────────────\nAfter setting EERE, the byte from EEPROM\nappears here — read it immediately"]:::result EEAR -->|"step 1: set address"| EECR EEDR_IN -->|"step 2: load data (write only)"| EECR EECR -->|"EERE triggers read → result"| EEDR_OUT

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

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 .s vs uppercase .S: .S files are passed through the C preprocessor first, which lets you use #define, #include, and macros. .s files go straight to the assembler. Always use .S for 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

flowchart TD classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef dec fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef first fill:#dcfce7,stroke:#16a34a,color:#14532d subgraph ROW1 ["① Startup — read and increment"] direction LR A["CPU jumps to main\nreset vector 0x0000"]:::step B["Stack pointer setup\nUART init: UBRR=103\nsbi DDRB, LED_PIN"]:::step C["rcall eeprom_read\naddr=0x000\nresult in r24"]:::step D{"r24 == 0xFF?"}:::dec E["First boot:\nclr r24"]:::first F["inc r24\ncount++\nmov r19, r24"]:::step end subgraph ROW2 ["② Save, report, blink"] direction LR G["rcall eeprom_write\naddr=0x000, data=r19\nhardware: 3.3 ms"]:::step H["rcall print_flash_str\nrcall print_decimal\n'Boot count: N'"]:::step I["Blink LED\nN times\n(max 10)"]:::step J["rjmp idle\nwait for\nnext reset"]:::step end A --> B --> C --> D D -->|"Yes"| E --> F D -->|"No"| F F -->|"continue ↓"| G G --> H --> I --> J

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:

  1. Use address 0x00 for a "has been written" flag byte (write 0xAA on first run, then check it).
  2. 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 0x000 reads as 0xFF and 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 0xFF was 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

flowchart TD subgraph KEY ["Key takeaways"] K1["EEPROM survives power-off\nSRAM does not\n1 KB on ATmega328P"] K2["Three registers: EEAR, EEDR, EECR\nSame pattern as every other AVR peripheral"] K3["Write takes 3.3 ms\nOnly write when value changes\n100,000 write cycle limit"] K4["Fresh EEPROM = 0xFF\nAlways handle the 'never written' case"] K5["Plan an address map\nUse named constants, not magic numbers"] end K1 --> K2 --> K3 --> K4 --> K5

Previous Exercise: ← Exercise 3 — UART Serial Communication

Next Exercise: Exercise 5 — External Interrupts →

Further Reading

Mind Map

mindmap root((EEPROM Boot Counter)) Core idea Persistent byte storage Survives power loss Slower than SRAM Limited endurance Memory map SRAM volatile 2 KB Flash program 32 KB EEPROM data 1 KB Address 0x000 stores count Registers EEAR selects address EEDR holds data EECR controls read write EERE starts read EEMPE then EEPE starts write Numbers Write about 3.3 ms Endurance about 100k cycles Fresh cell reads 0xFF Address range 0x000 to 0x3FF Checks Count survives power off Reset increments once No writes in idle loop Blink cap at ten Mistakes Ignoring EEPE busy Missing 4 cycle window Writing too often Treating 0xFF as valid count