Reading and writing EEPROM

EEPROM (Electrically Erasable Programmable Read Only Memory) Is non-volatile memory, meaning it persists after power is removed. The ATmega168 microcontroller has 512 bytes of EEPROM which can be used to store system parameters and small amounts of data. This tutorial shows you how to read and write EEPROM.


Our code examples will be very simple. We have an ATmega168 microcontroller connected to an LCD display. In our first example we will:

  • Read an integer value from EEPROM
  • Increment that value
  • Output it to the LCD module
  • Store the value back to EEPROM

This counts how many times the device has been powered up or reset. In later examples we'll be adding other datatypes and interracting with the EEPROM in slightly different ways.

Building the circuit

No need to reinvent the wheel here. We will use the circuit from the Character LCD Displays – Part 2 post we did last year, but build it on a breadboard instead..

Circuit Diagram

First we need to build a basic Atmega168 breadboard circuit. See "Atmega8 breadboard circuit" Part 1 and Part 2 but substitute an Atmega168 for the Atmega8.

Next we add the necessary connections for the LCD module.

Wiring up the LCD Module

Now we solder a single row header to the back of the LCD module and insert it into the breadboard.

Inserting the LCD Module into the breadboard

Using AVRdude Terminal Mode to read EEPROM

AVRdude terminal mode allows you to establish a session with the microcontroller and issue commands to it. To enter terminal mode use the following command

avrdude -p m168 -c usbasp -t

and to dump EEPROM to the standard output

dump eeprom 0 512

as shown in the example below.
AVRdude terminal mode

You will notice that all EEPROM values are "0xFF". This are the default values for an erased EEPROM.

Reading and writing EEPROM Programmatically

To following program demonstrates how to read and write a double byte value to EEPROM.

  1. #include <avr/eeprom.h>
  2. #include "hd44780.h"
  4. #define NUM_EXECUTIONS_ADDRESS 0x00
  6. int main (void)
  7. {
  8.     lcd_init();
  10.     uint16_t num_executions = eeprom_read_word((uint16_t*)NUM_EXECUTIONS_ADDRESS);
  11.     if (num_executions==0xFFFF)
  12.         num_executions=0;
  13.     num_executions++;
  14.     eeprom_write_word((uint16_t*)NUM_EXECUTIONS_ADDRESS,num_executions);
  16.     lcd_clrscr();
  18.     char buffer[16];
  19.     sprintf(buffer,"%d Executions", num_executions);
  20.     lcd_puts(buffer);
  22.     return(0);
  23. }

Before we can use any of the EEPROM functions in the avr-libc library, we need to include the EEPROM header file. This is done at line 1.

At line 10 we define the num_executions variable and load it from EEPROM location 0. This variable is a double byte unsigned integer.

You will recall that the default values for an erase byte is 0xFF. At lines 11 & 12 we set num_executions to 0 if being run against an erase EEPROM (e.g. first execution).

Lastly at line 14 we write away the updated num_executions value.

Reading and writing other datatypes

In the previous example we read and wrote a double byte datatype. In this example we will write:

  • Double byte unsigned integer, as in the previous example
  • Single byte unsigned integer. This will be 0x00 for even instances of the counter and 0x01 for odd instances.
  • A text string. This will be "Even" for even instances of the counter and "Odd" for odd instances
  1. #include <avr/eeprom.h>
  2. #include <string.h>
  3. #include "hd44780.h"
  5. #define NUM_EXECUTIONS_ADDRESS      0x00
  6. #define ODD_OR_EVEN_ADDRESS         0x02
  7. #define ODD_OR_EVEN_TEXT_ADDRESS    0x03
  8. #define ODD_OR_EVEN_TEXT_LENGTH     5
  10. int main (void)
  11. {
  12.     lcd_init();
  14.     uint16_t num_executions = eeprom_read_word((uint16_t*)NUM_EXECUTIONS_ADDRESS);
  15.     if (num_executions==0xFFFF)
  16.         num_executions=0;
  17.     num_executions++;
  18.     eeprom_write_word((uint16_t*)NUM_EXECUTIONS_ADDRESS,num_executions);
  20.     uint8_t odd_or_even = eeprom_read_byte((uint8_t*)ODD_OR_EVEN_ADDRESS);
  21.     if (odd_or_even==0xFF)
  22.         odd_or_even=num_executions % 2;
  23.     else
  24.         odd_or_even = !odd_or_even;
  25.     eeprom_write_byte((uint8_t*)ODD_OR_EVEN_ADDRESS,odd_or_even);
  27.     char odd_or_even_string[ODD_OR_EVEN_TEXT_LENGTH];
  28.     eeprom_read_block((void*)&odd_or_even_string, (const void*)ODD_OR_EVEN_TEXT_ADDRESS, ODD_OR_EVEN_TEXT_LENGTH);
  29.     char ff_string[5]={0xFF,0xFF,0xFF,0xFF,0xFF};
  30.     if (!strncmp(odd_or_even_string,ff_string,ODD_OR_EVEN_TEXT_LENGTH))
  31.         strcpy(odd_or_even_string,"Odd");
  32.     else if (strcmp(odd_or_even_string,"Even"))
  33.         strcpy(odd_or_even_string,"Even");
  34.     else
  35.         strcpy(odd_or_even_string,"Odd");
  36.     eeprom_write_block ((void*)&odd_or_even_string, (const void*)ODD_OR_EVEN_TEXT_ADDRESS, ODD_OR_EVEN_TEXT_LENGTH);
  38.     lcd_clrscr();
  39.     char buffer[16];
  40.     sprintf(buffer,"%d Executions", num_executions);
  41.     lcd_puts(buffer);
  42.     lcd_goto(40);
  43.     sprintf(buffer,"%d %s", odd_or_even, odd_or_even_string);
  44.     lcd_puts(buffer);
  46.     return(0);
  47. }

Reading and writing the 8 bit value is almost identical to reading and writing the 16 bit value. If you look at lines 20-25 you will see different functions and datatypes being used but the differences are minor.

The interesting one I think is where we read and write the string value. This is done between lines 27 and 36. The 2 function we use are:

void  eeprom_read_block (void *__dst, const void *__src, size_t __n)
void  eeprom_write_block (const void *__src, void *__dst, size_t __n)

In c, "void *" is used to denote a generic pointer. When we pass real pointers to the function we need to cast them as "void *". This is shown in lines 28 and 36.

The eeprom_read_block and eeprom_write_block function are very versatile as they can be used with any datatype. For example you could read/write structs or multi dimension arrays using these 2 functions.

Examining EEPROM using AVRdude Terminal Mode

After running the code and pressing reset a bunch of times, let's examine the EEPROM using AVRdude.

AVRdude terminal mode

The values of "07 00 01 45 76 65 6e 00" correspond to

  • num_executions = 7 (notice the reverse byte encoding)
  • odd_or_even = 1
  • odd_or_even_string = "Even" (notice the null termination in the string)

Using the EEMEM attribute

In large projects, it can be tedious to define the addresses for each EEPROM stored variable. The EEMEM attribute is used to auto allocate addresses for the non-volatile variables. Consider the following code example.

  1. #include <avr/eeprom.h>
  2. #include <string.h>
  3. #include "hd44780.h"
  5. #define ODD_OR_EVEN_TEXT_LENGTH 5
  7. uint16_t EEMEM ee_num_executions;
  8. uint8_t EEMEM ee_odd_or_even;
  9. char EEMEM ee_odd_or_even_string[ODD_OR_EVEN_TEXT_LENGTH];
  11. int main (void)
  12. {
  13.     lcd_init();
  15.     uint16_t num_executions = eeprom_read_word(&ee_num_executions);
  16.     if (num_executions==0xFFFF)
  17.         num_executions=0;
  18.     num_executions++;
  19.     eeprom_write_word(&ee_num_executions,num_executions);
  21.     uint8_t odd_or_even = eeprom_read_byte(&ee_odd_or_even);
  22.     if (odd_or_even==0xFF)
  23.         odd_or_even=num_executions % 2;
  24.     else
  25.         odd_or_even = !odd_or_even;
  26.     eeprom_write_byte(&ee_odd_or_even,odd_or_even);
  28.     char odd_or_even_string[ODD_OR_EVEN_TEXT_LENGTH];
  29.     eeprom_read_block((void*)&odd_or_even_string, (const void*)&ee_odd_or_even_string, ODD_OR_EVEN_TEXT_LENGTH);
  30.     char ff_string[5]={0xFF,0xFF,0xFF,0xFF,0xFF};
  31.     if (!strncmp(odd_or_even_string,ff_string,ODD_OR_EVEN_TEXT_LENGTH))
  32.         strcpy(odd_or_even_string,"Odd");
  33.     else if (strcmp(odd_or_even_string,"Even"))
  34.         strcpy(odd_or_even_string,"Even");
  35.     else
  36.         strcpy(odd_or_even_string,"Odd");
  37.     eeprom_write_block ((void*)&odd_or_even_string, (const void*)&ee_odd_or_even_string, ODD_OR_EVEN_TEXT_LENGTH);
  39.     lcd_clrscr();
  40.     char buffer[16];
  41.     sprintf(buffer,"%d Executions", num_executions);
  42.     lcd_puts(buffer);
  43.     lcd_goto(40);
  44.     sprintf(buffer,"%d %s", odd_or_even, odd_or_even_string);
  45.     lcd_puts(buffer);
  47.     return(0);
  48. }

In lines 7 to 9 we define 3 variables that use the EEMEM attribute. The EEMEM attribute tells the compiler that these variables are store in EEPROM.

Even though these variables look like any other variable, we cannot use them directly. We still need to use the EEPROM read and write functions, but this time we use a pointer to the variable to denote the EEPROM address. This can be seen in lines 15, 19, 21, 26, 29 & 37.

Disabling EEPROM erasure

By default when we run AVRdude to upload the firmware a chip erase is performed. This erases FLASH and EEPROM memory.

The AVRdude -D option can disable the erasure, but this is not recommended as FLASH sections that are not being written with the new firmware will remain as they are. A much better approach is to use fuse settings.

By default the Atmega168 ships with fuse settings of 0x62 and 0xDF (low and high). By setting the EESAVE bit, we can prevent an EEPROM erasure during the chip erase cycle. To do this, use the following command.

avrdude -p atmega168 -P usb -c usbasp -U hfuse:w:0xd7:m

and to revert back to EEPROM erasing mode

avrdude -p atmega168 -P usb -c usbasp -U hfuse:w:0xdf:m

Note: Care should always be taken when writing fuse settings. It is very easy to render your microcontroller inoperable by setting incorrect fuse settings.

Writing default EEPROM Values

When the program is compiled, an eep file is generated. This contains an EEPROM map which can be uploaded to the microcontroller if required. The contents of this file can be set by initializing the non-volatile variables as follows.

  1. uint16_t EEMEM ee_num_executions = 100;
  2. uint8_t EEMEM ee_odd_or_even = 0;
  3. char EEMEM ee_odd_or_even_string[ODD_OR_EVEN_TEXT_LENGTH] = "Even";

The eep file can be uploaded to the microcontroller by adding "-U eeprom:w:main.eep" to your AVRdude command string. If you are using the "make program" command to upload your firmware, simply uncomment line 208 of the makefile as follows

  1. AVRDUDE_WRITE_EEPROM = -U eeprom:w:$(TARGET).eep

This will ensure that the eep file is uploaded with every "make program" command.

Share the love

If you like this post, please post it on twitter, Facebook or your blog. Your support is very much appreciated.

Code Downloads