M0AGX / LB9MG

Amateur radio and embedded systems

Reliable storage of settings in EEPROM

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
typedef enum {filter_lpf, filter_none} radio_filter_t;
typedef enum {rf_power_low, rf_power_high} radio_power_t;

typedef struct {
    uint32_t frequency;
    radio_filter_t filter;
    uint16_t ctcss;
    uint8_t soft_volume;
    uint8_t squelch;
    radio_power_t power;
} radio_configuration_t;

typedef struct {
    uint16_t crc;
    uint32_t revision;
    radio_configuration_t vhf;
    radio_configuration_t uhf;
} repeater_configuration_t;

/* --------- public  data --------------- */
extern repeater_configuration_t repeater;

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.

Easy. Here is the code for an Xmega.

Address declarations

1
2
3
4
5
6
7
//settings struct is stored in 3 eeprom banks for wear levelling
#define BANK_COUNT 3
static const uint16_t settings_bank[BANK_COUNT] = { 0,
    sizeof(repeater_configuration_t),
    2*sizeof(repeater_configuration_t)
};
static uint8_t current_settings_bank;

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).

Checking CRC of a struct in RAM

1
2
3
4
5
6
7
8
static bool settings_check_crc(repeater_configuration_t *p){
    uint16_t computed_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){
        return true;
    }
    return false;
}

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.

Reading settings at startup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void eeprom_init(void){
    debug("Init");
    for (uint8_t retries = 0; retries < 5; retries++){
        uint32_t best_revision = 0;
        uint8_t  best_bank = 0;
        bool valid_found = false;
        for (uint8_t i = 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);
            }//end if valid crc
        } //end of bank scan loop

        if (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(); //this function sets safe values in settings struct
            settings_write();
            return;
        }
    } //end of eeprom read retry loop

    //if control reaches here it means that eeprom read was unsuccessful more than 5 times
    repeater_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.

Saving new settings

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void settings_write(void){
    repeater.revision++;
    //calculate CRC of current settings
    repeater.crc = crc16( (uint8_t*)&repeater+sizeof(repeater.crc) /*skip the CRC field itself*/, sizeof(repeater)-sizeof(repeater.crc));

    //find oldest eeprom sector
    uint32_t oldest_revision = repeater.revision;
    uint8_t  oldest_bank = 0;
    for (uint8_t i = 0; i < BANK_COUNT; i++){
            repeater_configuration_t tmp;
            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{ //crc of this bank is invalid, use it immediately
                debug("Bank %d has invalid crc, will use this one", i);
                for (uint8_t j = 0; j<sizeof(tmp); j++){
                    printf("%02X", ((uint8_t*)&tmp)[j]  );
                }
                printf("\n\r");
                oldest_bank = i;
                break;
            }
        } //end of bank scan loop
    debug("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.

Determining if a write operation is needed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void eeprom_update_task(void){ //execute every 5s
    uint8_t* p1 = (uint8_t*)&repeater; //pointer to current configuration in RAM

    repeater_configuration_t config_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_t i = 0; i < sizeof(repeater); i++){ //compare byte-by-byte the struct in RAM
        if (p1[i] != p2[i]){                        //and the one in EEPROM
            debug("EEPROM requires update, bank=%d i=%d", current_settings_bank, i);
            settings_write();
            return;
        }
    }
}

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 :)

Complete code

1
2
3
4
5
6
7
8
9
#ifndef EEPROM_H_
#define EEPROM_H_

extern void eeprom_init(void);
extern void eeprom_update_task(void);

extern void settings_write(void);

#endif
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#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>

#define debug(M, ...) printf("EEPROM %d:%s: " M "\n\r", __LINE__, __func__, ##__VA_ARGS__) 
//#define debug(...)

//settings struct is stored in 3 eeprom banks for wear levelling
#define BANK_COUNT 3
static const uint16_t settings_bank[BANK_COUNT] = { 0,
                                          sizeof(repeater_configuration_t),
                                          2*sizeof(repeater_configuration_t)
                                        };
static uint8_t current_settings_bank;

/* --------- private prototypes --------- */
static bool settings_check_crc(repeater_configuration_t *p);


/* ----------- implementation ----------- */
void eeprom_init(void){
    debug("Init");
    for (uint8_t retries = 0; retries < 5; retries++){
        uint32_t best_revision = 0;
        uint8_t  best_bank = 0;
        bool valid_found = false;
        for (uint8_t i = 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);
            }//end if valid crc
        } //end of bank scan loop

        if (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(); //this function sets safe values in settings struct
            settings_write();
            return;
        }
    } //end of eeprom read retry loop

    //if control reaches here it means that eeprom read was unsuccessful more than 5 times
    repeater_reset_to_defaults();
    settings_write();
}

void settings_write(void){
    repeater.revision++;
    //calculate CRC of current settings
    repeater.crc = crc16( (uint8_t*)&repeater+sizeof(repeater.crc) /*skip the CRC field itself*/, sizeof(repeater)-sizeof(repeater.crc));

    //find oldest eeprom sector
    uint32_t oldest_revision = repeater.revision;
    uint8_t  oldest_bank = 0;
    for (uint8_t i = 0; i < BANK_COUNT; i++){
            repeater_configuration_t tmp;
            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{ //crc of this bank is invalid, use it immediately
                debug("Bank %d has invalid crc, will use this one", i);
                for (uint8_t j = 0; j<sizeof(tmp); j++){
                    printf("%02X", ((uint8_t*)&tmp)[j]  );
                }
                printf("\n\r");
                oldest_bank = i;
                break;
            }
        } //end of bank scan loop
    debug("Writing to bank %d", oldest_bank);
    current_settings_bank = oldest_bank;
    eeprom_update_block(&repeater, (void*)settings_bank[oldest_bank], sizeof(repeater));
}

void eeprom_update_task(void){ //execute every 5s
    uint8_t* p1 = (uint8_t*)&repeater; //pointer to current configuration in RAM

    repeater_configuration_t config_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_t i = 0; i < sizeof(repeater); i++){ //compare byte-by-byte the struct in RAM
        if (p1[i] != p2[i]){                        //and the one in EEPROM
            debug("EEPROM requires update, bank=%d i=%d", current_settings_bank, i);
            settings_write();
            return;
        }
    }

}

static bool settings_check_crc(repeater_configuration_t *p){
    uint16_t computed_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){
        return true;
    }
    return false;
}

I release the code into public domain.