Thursday, July 12, 2018

AVR EEPROM Library

Sometimes some data needs to be saved after the microcontroller is powered off. Say you have a rotary encoder to set audio volume. When the user sets the volume, the value is stored in SRAM but when the power goes off, the memory content is lost. In this cases you would use the EEPROM memory to store data. AVR devices have three types of memory:
  • Flash memory - for the program code
  • SRAM memory - also referred as data memory (volatile memory)
  • EEPROM memory


EEPROM stands for Electronically Erasable Read-Only Memory and is a non-volatile memory, meaning that unlike RAM, it will hold the stored information even after power is removed, much like an USB flash drive. Here can be stored settings and data that can not be hard coded, usually taken by the interaction with a user. Keep in mind that the EEPROM has a lifespan of 100,000 writes - reads are unlimited - so is not a good idea to write to this memory every second or even every few seconds.

Normally, to make use of the EEPROM memory, you would have to read the datasheet and implement some read/write functions using some registers. But fear not - the AVR-GCC compiler comes with prebuilt routines making things much easier.

How to use the AVR EEPROM Library

Include the library

#include <avr/eeprom.h>

There are five main types of EEPROM access: byte, word, dword, float and block. Each type has three types of functions: write, update, and read.
In AVR-GCC, a word is two bytes long and a double word (dword) is 4 bytes, while a block is an arbitrary number of bytes which you supply.

uint8_t eeprom_read_byte ( const uint8_t * addr )
void eeprom_write_byte ( uint8_t *addr, uint8_t value )
void eeprom_update_byte ( uint8_t *addr, uint8_t value )

uint16_t eeprom_read_word ( const uint16_t * addr )
void eeprom_write_word ( uint16_t *addr, uint16_t value )
void eeprom_update_word ( uint16_t *addr, uint16_t value )

uint32_t eeprom_read_dword ( const uint32_t * addr )
void eeprom_write_dword ( uint32_t *addr, uint32_t value )
void eeprom_update_dword ( uint32_t *addr, uint32_t value )

float eeprom_read_float ( const float * addr )
void eeprom_write_float ( float *addr, float value )
void eeprom_update_float ( float *addr, float value )

void eeprom_read_block ( void * pointer_ram, const void * pointer_eeprom, size_t n)
void eeprom_write_block ( const void * pointer_ram, void * pointer_eeprom, size_t n)
void eeprom_update_block ( const void * pointer_ram, void * pointer_eeprom, size_t n)

It is highly recommended that update functions should be used instead of the write functions. Update functions will first check if the written data differs from the one already in EEPROM and only then it writes, and so increasing the EEPROM lifetime. It's a bit slower than the write function, because it executes read and then write, but is not like you write to EEPROM every few milliseconds, so it shouldn't matter.



Reading data from the EEPROM


Reading a byte


uint8_t byteFromEEPROM;
byteFromEEPROM = eeprom_read_byte((uint8_t*)10);

This will read out location 10 of the EEPROM, and put it into the variable byteFromEEPROM. The function expects an pointer to an address, and because 10 is a constant we typecast it. The address can be from 0 to maximum EEPROM size.

Reading a word


uint16_t wordFromEEPROM;
wordFromEEPROM = eeprom_read_word((uint16_t*)10);

Same as before, except the data type is of two bytes long now.
For dword and float functions you would use a pointer to a uint32_t or float variable.

Reading a block from EEPROM


uint8_t stringOfData[10];
eeprom_read_block((void*)&stringOfData, (const void*)12, 10)

The block command are useful when a larger number of bytes need to be read/write.

eeprom_read_block functions don't return anything; instead they modify the provided buffer stringOfData. In our case the data type for stringOfData array is uint8_t but the function expects a void pointer. A void pointer allows the function to have flexibility meaning it can work with any data types. But you must be sure that the type of data in EEPROM is the same as the provided array buffer; you can't put a uint16_t in a uint8_t variable.

(void*)&stringOfData
 
Typecast our array buffer to a void pointer. Notice the & operator. This means that is passing the address of the array not the data that it stores. In case of arrays the & operator it's optional because an array is always a pointer to it's first index (e.g stringOfData[0]), but in case of a regular variable, is not optional.

(const void*)12

Read the content starting at address 12. Because nothing is modified here the constant address number is typecast to a constant void pointer.

10

How many bytes to read. In our case, read 10 bytes starting at address 12 and put them in the stringOfData array buffer. Make sure the array is big enough to fit all the bytes. This parameter can also be a variable instead of a constant.

Writing data to the EEPROM


Writing a byte


uint8_t byteToEEPROM;
byteToEEPROM = 100;
eeprom_update_byte((uint8_t*)10, byteToEEPROM);

As in the case of read command, the first argument is the EEPROM address, typecast to a uint8_t pointer, except that now it takes a second argument - the data to be written to EEPROM.

Writing a word


uint16_t wordToEEPROM;
wordToEEPROM = 2600;
eeprom_update_word((uint16_t*)10, wordToEEPROM);

And double words and floats can be written using the eeprom_update_dword() and eeprom_update_float() functions and a uint32_t or float variable.
 

Writing a block to EEPROM


uint8_t stringOfData[10] = "Hello";
eeprom_update_block((const void*)&stringOfData, (void*)12, sizeof(stringOfData));

The first parameter is the data to be written to the EEPROM and it can be an array or even a struct, and is of the type const since the array is not being modified as in the case of read command.
Second parameter is the EEPROM address from where the string of data will start to be written.
Third and last argument is the length of the array in bytes obtained using sizeof function because if we decide to modify the size of stringOfData we wouldn't need to remember to change the size argument of the function.



Writing variables to EEPROM using the EEMEM atribute

Keeping track of all those addresses is hard and messy. Wouldn't be better to use variable names instead of addresses to refer to EEPROM locations? You can do this using EEMEM attribute placed before the variable name, and the compiler will take care of where in EEPROM memory should place them.

Bellow is an example on how to use the EEMEM attribute. First some variable are declared in the global scope with the EEMEM attribute and are prefixed with "eeprom_" to distinguish them from their counterpart variables located in RAM.
At startup the firmware loads the values from EEPROM into RAM.
In the while loop the code could verify if any variables have changed and then update the EEPROM with the new values.
This is the same as explained above, except instead of using addresses we are using the addresses of variables. Notice the & operator before the variable name; we are not passing the value of the variable but the address of it.

/*************************************************************
 INCLUDES
**************************************************************/
#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay.h>
#include <string.h>


// variables stored on EEPROM using EEMEM attribute. Must be global.
uint8_t EEMEM eeprom_phoneVolume = 100;
uint16_t EEMEM eeprom_receivedMessages = 0;
float EEMEM eeprom_temperature = 0.0;
char EEMEM eeprom_lastSMS[40] = "Pizza order received";
   

/*************************************************************
 MAIN FUNCTION
**************************************************************/
int main(void){
   // variables stored on SRAM
   uint8_t phoneVolume = 75;
   uint16_t receivedMessages;
   float temperature;
   char lastSMS[40];
   
   // Update variable located in RAM, using last saved values inside EEPROM
   phoneVolume = eeprom_read_byte(&eeprom_phoneVolume);
   receivedMessages = eeprom_read_word(&eeprom_receivedMessages);
   temperature = eeprom_read_float(&eeprom_temperature);
   eeprom_read_block((void*)lastSMS, (const void*)eeprom_lastSMS, sizeof(lastSMS));
 
   while(1){
      phoneVolume = 45;
      receivedMessages = 1;
      temperature = 27.5;
      strcpy(lastSMS, "Pizza delivered");
      
      //_delay_ms(1000);
      
      // if volume changed
      eeprom_update_byte(&eeprom_phoneVolume, phoneVolume);
      
      // if number of received messages changed
      eeprom_update_word(&eeprom_receivedMessages, receivedMessages);
      
      // update temperature
      eeprom_update_float(&eeprom_temperature, temperature);

      // if new message is received
      eeprom_update_block((const void*)lastSMS, (void*)eeprom_lastSMS, sizeof(lastSMS));
   }
}

You could also update only a part of an array. In the following example, the array is updated starting from index 2 with the size argument as 3; so the array will be modified starting from index 2 to index 4 ((2 + 3) - 1).

uint8_t stringOfData[10] = "Hello";

eeprom_update_block((const void*)&stringOfData[2], (void*)&eeprom_stringOfData[2], 3);

Setting EEPROM variables with initial/default values

If, when declaring a variable with the EEMEM attribute you assign a value to it, the compiler will generate an .eep file. This file can be manually uploaded to the EEPROM or depending on the programmer, it will be automagically uploaded. Initially all EEPROM memory defaults to 0xFF.

Writing structures on EEPROM using EEMEM attribute

To keep the code more organized, struct data types can also be written to EEPROM as a whole or individual variables.

/*************************************************************
 INCLUDES
**************************************************************/
#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay.h>
#include <string.h>

// structure stored in EEPROM. Must be declared in global space
typedef struct {
 uint8_t eeprom_phoneVolume;
 uint16_t eeprom_receivedMessages;
 float eeprom_temperature;
 char eeprom_lastSMS[40];
}eeprom_struct;

eeprom_struct EEMEM eeprom_phoneSettings;

/*************************************************************
 MAIN FUNCTION
**************************************************************/
int main(void){ 
   // structure stored in RAM
   struct {
    uint8_t phoneVolume;
    uint16_t receivedMessages;
    float temperature;
    char lastSMS[40];
   }phoneSettings;
   
   // Update variable located in RAM, using last saved values inside EEPROM
   phoneSettings.phoneVolume = eeprom_read_byte(&eeprom_phoneSettings.eeprom_phoneVolume);
   phoneSettings.receivedMessages = eeprom_read_word(&eeprom_phoneSettings.eeprom_receivedMessages);
   phoneSettings.temperature = eeprom_read_float(&eeprom_phoneSettings.eeprom_temperature);
   eeprom_read_block((void*)phoneSettings.lastSMS, (const void*)eeprom_phoneSettings.eeprom_lastSMS, sizeof(phoneSettings.lastSMS));
 
   while(1){
      phoneSettings.phoneVolume = 45;
      phoneSettings.receivedMessages = 1;
      phoneSettings. temperature = 27.5;
      strcpy(phoneSettings.lastSMS, "Pizza delivered");
   
 //_delay_ms(1000);
      
      // if volume changed
      eeprom_update_byte(&eeprom_phoneSettings.eeprom_phoneVolume, phoneSettings.phoneVolume);
      
      // if number of received messages changed
      eeprom_update_word(&eeprom_phoneSettings.eeprom_receivedMessages, phoneSettings.receivedMessages);
      
      // update temperature
      eeprom_update_float(&eeprom_phoneSettings.eeprom_temperature, phoneSettings.temperature);

      // if new message is received
      eeprom_update_block((const void*)&phoneSettings.lastSMS, (void*)&eeprom_phoneSettings.eeprom_lastSMS, sizeof(phoneSettings.lastSMS));
   
 // or... put whole structure on EEPROM
 eeprom_update_block((const void*)&phoneSettings, (void*)&eeprom_phoneSettings, sizeof(phoneSettings));
   }
}



Other things to consider

  • When using a bootloader to program the MCU via USB, you must first check if SELFPRGEN in SPMCSR register is zero before attempting to write to EEPROM. For more info about this check the datasheet.
  •  The EEPROM functions should not be used in an ISR function and outside at the same time because if a function in the main is interrupted by an interrupt, and another function inside the ISR attempts to access the EEPROM, both are working with the same EEPROM registers and will result in a mess of corrupted data. In this scenario, the interrupts should be disabled while accessing the EEPROM and then re-enabled.
  • All of the read/write functions first make sure the EEPROM is ready to be accessed using the eeprom__busy_wait() loop function. So for time critical applications, the state of the eeprom should be polled using eeprom_is_ready() function and if the return value is 1 then the eeprom read/write functions can be executed.
  • If the power supply voltage drops to low while data is written to EEPROM, the data may get corrupted. To mitigate this risk is recommended the use of BOD (Brown Out Detector) which can be enabled using the AVR's fuse bits.

Source: https://www.nongnu.org/avr-libc/user-manual/group__avr__eeprom.html


2 comments: