Embedded systems often require permanent storage of some configuration parameters eg. radio channel, volume in a radio etc. All settings must be saved and read reliably, otherwise the device may become unpredictable. Imagine a variable frequency drive (an "electric motor controller") set to a certain speed, that after a power cut reads bad data from it's memory and overspeeds an expensive piece of moving machinery leading to physical damage.
The issue of reliable configuration data can be broken down into smaller tasks:
At power-on: read data from permanent storage, verify that it is correct (if not - reset to safe defaults).
At runtime: compare current settings with the ones in permanent storage and update if necessary
Do it a way that will guarantee consistency in case of a power cut.
In my projects I group all settings into structs. Sample from my upcoming project:
In this case repeater_configuration_t is the main configuration struct that I want to store in EEPROM.
My approach is to duplicate this struct across several places in EEPROM, each one with an incrementing counter (revision) and a CRC to verify that a particular set of data is valid. I described Xmega's hardware CRC in another post. At power on all "sectors" are scanned to determine if they contain valid data and the one with highest revision counter is copied to RAM. Whenever configuration is changed and needs saving, the oldest set is erased and written with latest data that has revision counter incremented by one.
First I declare an array that holds addresses of structs in EEPROM, so for example settings_bank[1] is the address of a second copy of my settings in EEPROM. Xmega's EEPROM is small enough to be addressed using 16-bits. The addresses can be anywhere inside the EEPROM as long as they obviously don't overlap assuming the size of repeater_configuration_t. I have picked 3 banks, but 2 is enough to implement a reliable storage protocol. Having more of them has the additional benefit of wear leveling (in case the settings change often).
This function just calculates the CRC of whole struct (except the CRC at the beginning itself) and compares the computed one with the one already present in the struct. If they match the data is valid.
voideeprom_init(void){debug("Init");for(uint8_tretries=0;retries<5;retries++){uint32_tbest_revision=0;uint8_tbest_bank=0;boolvalid_found=false;for(uint8_ti=0;i<BANK_COUNT;i++){eeprom_read_block(&repeater,(void*)settings_bank[i],sizeof(repeater));if(settings_check_crc(&repeater)){valid_found=true;if(repeater.revision>best_revision){best_bank=i;best_revision=repeater.revision;debug("Bank %d okay, revision %ld, best so far",i,repeater.revision);}else{debug("Bank %d okay, revision %ld",i,repeater.revision);}}else{debug("Bank %d invalid CRC",i);}//endifvalidcrc}//endofbankscanloopif(valid_found){eeprom_read_block(&repeater,(void*)settings_bank[best_bank],sizeof(repeater));if(settings_check_crc(&repeater)){debug("Loaded settings from bank %d, revision %ld",best_bank,repeater.revision);current_settings_bank=best_bank;repeater_show_configuration();return;}else{debug("Found setting now have invalid crc!");continue;}}else{repeater_reset_to_defaults();//thisfunctionsetssafevaluesinsettingsstructsettings_write();return;}}//endofeepromreadretryloop//ifcontrolreacheshereitmeansthateepromreadwasunsuccessfulmorethan5timesrepeater_reset_to_defaults();settings_write();}
This piece of code has two loops - the inner loops across all banks to find the one that has valid CRC and and has the highest revision number. The outer loop handles eventual EEPROM read failure. To conserve memory I read only one bank to RAM at a time. When the inner loop is done I again read the "best" bank to RAM. The outer loop will retry if the last read detects a CRC error. I think that it would be extremely rare, but not impossible. If the last read is correct the function returns.
voidsettings_write(void){repeater.revision++;//calculateCRCofcurrentsettingsrepeater.crc=crc16((uint8_t*)&repeater+sizeof(repeater.crc)/*skip the CRC field itself*/,sizeof(repeater)-sizeof(repeater.crc));//findoldesteepromsectoruint32_toldest_revision=repeater.revision;uint8_toldest_bank=0;for(uint8_ti=0;i<BANK_COUNT;i++){repeater_configuration_ttmp;eeprom_read_block(&tmp,(void*)settings_bank[i],sizeof(tmp));if(settings_check_crc(&tmp)){if(tmp.revision<oldest_revision){oldest_bank=i;oldest_revision=tmp.revision;debug("Bank %d okay, revision %ld, oldest so far",i,tmp.revision);}else{debug("Bank %d okay, revision %ld",i,tmp.revision);}}else{//crcofthisbankisinvalid,useitimmediatelydebug("Bank %d has invalid crc, will use this one",i);for(uint8_tj=0;j<sizeof(tmp);j++){printf("%02X",((uint8_t*)&tmp)[j]);}printf("\n\r");oldest_bank=i;break;}}//endofbankscanloopdebug("Writing to bank %d",oldest_bank);current_settings_bank=oldest_bank;eeprom_update_block(&repeater,(void*)settings_bank[oldest_bank],sizeof(repeater));}
The write function is similar to read function, but it looks for the memory bank with oldest revision number or one with invalid CRC and simply writes current configuration to that bank. It assumes that a write is successful. If power is cut, then in worst case just a single bank is erased or incompletely written. The device will start using other valid settings.
Every couple of seconds the code reads current settings from EEPROM and compares it with current settings in RAM, if they differ, then settings_write is called and saves the new settings.
Using this code I can achieve reliable storing of configuration data, have very long device lifetime (memory wear leveling) and good usability :)
#include"eeprom.h"#include"radio.h"#include"repeater.h"#include<avr/eeprom.h>#include<stdint.h>#include<stdbool.h>#include"crc.h"#include<stdio.h>#definedebug(M,...)printf("EEPROM %d:%s: "M"\n\r",__LINE__,__func__,##__VA_ARGS__)//#definedebug(...)//settingsstructisstoredin3eeprombanksforwearlevelling#defineBANK_COUNT3staticconstuint16_tsettings_bank[BANK_COUNT]={0,sizeof(repeater_configuration_t),2*sizeof(repeater_configuration_t)};staticuint8_tcurrent_settings_bank;/* --------- private prototypes --------- */staticboolsettings_check_crc(repeater_configuration_t*p);/* ----------- implementation ----------- */voideeprom_init(void){debug("Init");for(uint8_tretries=0;retries<5;retries++){uint32_tbest_revision=0;uint8_tbest_bank=0;boolvalid_found=false;for(uint8_ti=0;i<BANK_COUNT;i++){eeprom_read_block(&repeater,(void*)settings_bank[i],sizeof(repeater));if(settings_check_crc(&repeater)){valid_found=true;if(repeater.revision>best_revision){best_bank=i;best_revision=repeater.revision;debug("Bank %d okay, revision %ld, best so far",i,repeater.revision);}else{debug("Bank %d okay, revision %ld",i,repeater.revision);}}else{debug("Bank %d invalid CRC",i);}//endifvalidcrc}//endofbankscanloopif(valid_found){eeprom_read_block(&repeater,(void*)settings_bank[best_bank],sizeof(repeater));if(settings_check_crc(&repeater)){debug("Loaded settings from bank %d, revision %ld",best_bank,repeater.revision);current_settings_bank=best_bank;repeater_show_configuration();return;}else{debug("Found setting now have invalid crc!");continue;}}else{repeater_reset_to_defaults();//thisfunctionsetssafevaluesinsettingsstructsettings_write();return;}}//endofeepromreadretryloop//ifcontrolreacheshereitmeansthateepromreadwasunsuccessfulmorethan5timesrepeater_reset_to_defaults();settings_write();}voidsettings_write(void){repeater.revision++;//calculateCRCofcurrentsettingsrepeater.crc=crc16((uint8_t*)&repeater+sizeof(repeater.crc)/*skip the CRC field itself*/,sizeof(repeater)-sizeof(repeater.crc));//findoldesteepromsectoruint32_toldest_revision=repeater.revision;uint8_toldest_bank=0;for(uint8_ti=0;i<BANK_COUNT;i++){repeater_configuration_ttmp;eeprom_read_block(&tmp,(void*)settings_bank[i],sizeof(tmp));if(settings_check_crc(&tmp)){if(tmp.revision<oldest_revision){oldest_bank=i;oldest_revision=tmp.revision;debug("Bank %d okay, revision %ld, oldest so far",i,tmp.revision);}else{debug("Bank %d okay, revision %ld",i,tmp.revision);}}else{//crcofthisbankisinvalid,useitimmediatelydebug("Bank %d has invalid crc, will use this one",i);for(uint8_tj=0;j<sizeof(tmp);j++){printf("%02X",((uint8_t*)&tmp)[j]);}printf("\n\r");oldest_bank=i;break;}}//endofbankscanloopdebug("Writing to bank %d",oldest_bank);current_settings_bank=oldest_bank;eeprom_update_block(&repeater,(void*)settings_bank[oldest_bank],sizeof(repeater));}voideeprom_update_task(void){//executeevery5suint8_t*p1=(uint8_t*)&repeater;//pointertocurrentconfigurationinRAMrepeater_configuration_tconfig_from_eeprom;eeprom_read_block(&config_from_eeprom,(void*)settings_bank[current_settings_bank],sizeof(config_from_eeprom));uint8_t*p2=(uint8_t*)&config_from_eeprom;for(uint8_ti=0;i<sizeof(repeater);i++){//comparebyte-by-bytethestructinRAMif(p1[i]!=p2[i]){//andtheoneinEEPROMdebug("EEPROM requires update, bank=%d i=%d",current_settings_bank,i);settings_write();return;}}}staticboolsettings_check_crc(repeater_configuration_t*p){uint16_tcomputed_crc=crc16((uint8_t*)p+sizeof(repeater.crc),sizeof(repeater_configuration_t)-sizeof(uint16_t)/*CRC-16*/);//debug("read=%04X computed=%04X",p->crc,computed_crc);if(computed_crc==p->crc){returntrue;}returnfalse;}