M0AGX / LB9MG

Amateur radio and embedded systems

Using the internal EEPROM of STM32L

Most STM32 microcontrollers feature an internal EEPROM. It is useful for storing settings or calibration data. Regular flash (that stores code) can also be used but the EEPROM can be updated byte-by-byte and is independent from regular flash. This may come handy during application updating, as whole flash can be simply erased without affecting the EEPROM.

I wrote a generic driver for keeping settings in the EEPROM based on the standard peripherals library for STM32L, that is easier to understand than the official demos from ST. It was tested on an STM32L151RC.

The interface

Everything starts with a definition of a structure that will hold all the parameters, flags and settings. The structure is verified using CRC32 checksum that is placed at the end. There is also a revision counter that is incremented every time to keep track how many times the EEPROM was written.

The revision counter can also be used to implement a highly reliable two-bank storage protocol that always keeps a single valid copy of all settings, to protect against power failures in the middle of a write operation.

Besides the structure definition the interface exposes two functions (one to load and another to store the settings) and a global pointer to the structure. This is one of the very few occasions where I use global variables. Otherwise I would have to implement setter/getter functions specific to every field. For small and simple configuration structure it is not worth the effort.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#pragma once
#include <stdbool.h>
#include <stdint.h>

typedef struct {
    uint32_t version;
    uint32_t revision;
    uint32_t my_parameter1;
    bool     my_option1;
    //add more fields here....
    uint32_t crc32;
} settings_t;

extern settings_t *GLOBAL_settings_ptr;

void settings_read(void);
uint32_t settings_write(void);

The implementation

The code has to be compiled with -std=gnu11 for _Static_assert to work. The assert statements check if the settings will fit into the EEPROM and if they will align on a 4-byte boundary. This is needed by the hardware CRC generator (its input is a 4-byte word and its output is also a 4-byte word). The size varies across STM32 families so check your datasheet.

Settings are mirrored from EEPROM to RAM at startup so that other modules can update their own parameters at any time and simply call settings_write(). If computed CRC is different than the stored CRC the settings are reset to defaults (here: zeros).

Saving works in a similar way, just the revision counter is incremented, the CRC is recalculated and then the whole setting struct is copied from RAM to the the EEPROM (one word at a time).

 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
#include "settings.h"
#include <stm32l1xx.h>
#include <string.h>

#define DATA_EEPROM_START_ADDR     0x08080000
#define DATA_EEPROM_SIZE_BYTES     8192

_Static_assert(sizeof(settings_t) < DATA_EEPROM_SIZE_BYTES, "EEPROM struct too large!");
_Static_assert(sizeof(settings_t) % 4 == 0, "EEPROM struct has to be multiple of 4 bytes");

static settings_t _settings_in_ram;

settings_t *GLOBAL_settings_ptr = &_settings_in_ram;

static void _settings_reset_to_defaults(void);

void settings_read(void){
    //copy data from EEPROM to RAM
    memcpy(GLOBAL_settings_ptr, (uint32_t*)DATA_EEPROM_START_ADDR, sizeof(settings_t));

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE);

    CRC_ResetDR();

    uint32_t computed_crc = CRC_CalcBlockCRC(
            (uint32_t *)GLOBAL_settings_ptr,
            (sizeof(settings_t)-sizeof(uint32_t))/sizeof(uint32_t)/*size minus the crc32 at the end, IN WORDS*/
    );

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, DISABLE);

    if (computed_crc != GLOBAL_settings_ptr->crc32){
        _settings_reset_to_defaults();
    }
}

uint32_t settings_write(void){
    GLOBAL_settings_ptr->revision++;

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE);

    CRC_ResetDR();

    GLOBAL_settings_ptr->crc32 = CRC_CalcBlockCRC( //calculate new CRC
            (uint32_t *)GLOBAL_settings_ptr,
            (sizeof(settings_t)-sizeof(uint32_t))/sizeof(uint32_t)/*size minus the crc32 at the end, IN WORDS*/
    );

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, DISABLE);

    DATA_EEPROM_Unlock();

    uint32_t *src = (uint32_t*)GLOBAL_settings_ptr;
    uint32_t *dst = (uint32_t*)DATA_EEPROM_START_ADDR;

    //------------------ bug of the day ------------------
    /* Flash option byte user validity error flag caused write to EEPROM operation to fail!
     * The problem did not happen when J-Link was connected. Probably J-Link starts execution
     * right from the application address, skipping the bootloader. The bootloader
     * may have had set the flag.
     */
    FLASH_ClearFlag(FLASH_FLAG_OPTVERR);
    //------------------ bug of the day ------------------

    //write settings word (uint32_t) at a time
    for (uint32_t i = 0; i < sizeof(settings_t)/sizeof(uint32_t); i++){
        if (*dst != *src){ //write only if value has been modified
            FLASH_Status s = DATA_EEPROM_ProgramWord((uint32_t)dst, *src);
            if (s != FLASH_COMPLETE){
                return FLASH->SR;
            }
        }
        src++;
        dst++;
    }

    DATA_EEPROM_Lock();

    settings_read();
    return 0;
}

static void _settings_reset_to_defaults(void){
    memset(GLOBAL_settings_ptr, 0, sizeof(settings_t));
    //TODO: define default values
}