Last active
June 11, 2021 15:57
-
-
Save rikka0w0/99278e44eb0244c354fc97f408ae605e to your computer and use it in GitHub Desktop.
Reliable config data storage on the built-in flash of STM32. Tolerate to power loss and flash damage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Resource used: | |
* Flash, CRC | |
*/ | |
/** | |
* A utility which reliably stores config data on the built-in flash of the MCU. | |
* Data integrity is guaranteed by CRC32. If a power loss happens during a flash | |
* write operation, upon next start-up, the last valid configuration will be used. | |
*/ | |
#include "config.h" | |
#include <stdint.h> | |
#include <stddef.h> | |
#include <string.h> | |
#include "crc.h" | |
#include "stm32f0xx_ll_bus.h" | |
#define CONFIG_ENTER_CRITICAL() (__disable_irq()) | |
#define CONFIG_EXIT_CRITICAL() (__enable_irq()) | |
#if ((SNAPSHOT_PAYLOAD_SIZE >> 2) << 2) != SNAPSHOT_PAYLOAD_SIZE | |
#error "SNAPSHOT_PAYLOAD_SIZE is not a multiple of 4!" | |
#endif | |
#if ((FLASH_PAGE_SIZE >> 4) << 4) != FLASH_PAGE_SIZE | |
#error "FLASH_PAGE_SIZE is not a multiple of 16!" | |
#endif | |
/** | |
* These are defined in the linker script (xxxxxx_FLASH.ld). | |
*/ | |
extern uint32_t _user_config_start; | |
extern uint32_t _user_config_break; | |
#define FLASH_PAGE_A (&_user_config_start) | |
#define FLASH_PAGE_B (&_user_config_break) | |
#define INDEX_IS_NEWER(input, ref) ((uint32_t)((int8_t)input) - (uint32_t)((int8_t)ref) < 0x7FFFFFFF) | |
typedef struct config_snapshot_t { | |
uint32_t crc32; | |
uint8_t index; | |
uint8_t reserved1; | |
uint16_t reserved2; | |
uint8_t payload[SNAPSHOT_PAYLOAD_SIZE]; | |
} cs; | |
enum config_active_page { | |
PAGE_NONE, | |
PAGE_A, | |
PAGE_B | |
}; | |
/** | |
* Indicates the flash page of the current valid snapshot. | |
*/ | |
static enum config_active_page config_page; | |
/** | |
* The address for the next possible snapshot. | |
* Set to NULL if the current flash page don't have sufficient space. | |
*/ | |
static void* config_next_snapshot; | |
static cs config_wip_snapshot; | |
static uint32_t config_snapshot_crc32(const cs* snapshot_ptr) { | |
return crc32_calc(&snapshot_ptr->index, sizeof(cs) - sizeof(uint32_t)); | |
} | |
static int config_snapshot_validate(const cs* snapshot_ptr) { | |
return snapshot_ptr->crc32 == config_snapshot_crc32(snapshot_ptr); | |
} | |
/** | |
* Search for the latest snapshot in the current page | |
*/ | |
static cs* config_locate_latest_snapshot(void* page_addr) { | |
cs* cur_addr = (cs*) page_addr; | |
void* end_addr = (void*) ((char*)page_addr + FLASH_PAGE_SIZE); | |
while ((void*)(cur_addr + 1) <= end_addr) { | |
cur_addr++; | |
} | |
cur_addr = cur_addr - 1; | |
// Now cur_addr contains the address of the latest possible snapshot in this page. | |
while ((void*)cur_addr >= page_addr) { | |
if (config_snapshot_validate(cur_addr)) { | |
return cur_addr; | |
} | |
cur_addr--; | |
} | |
// Unable to find a valid snapshot in this page. | |
return NULL; | |
} | |
/** | |
* Return the pointer to next vacant space if a new snapshot can be inserted into the page, | |
* otherwise return NULL. | |
*/ | |
static cs* config_fit_next_snapshot(void* page_addr, cs* latest_snapshot) { | |
cs* ret = latest_snapshot + 1; | |
void* end_addr = (void*) ((char*)page_addr + FLASH_PAGE_SIZE); | |
return (void*)(ret+1) > end_addr ? NULL : ret; | |
} | |
static void config_load_defaults() { | |
config_page = PAGE_NONE; | |
config_next_snapshot = NULL; | |
memset(&config_wip_snapshot, 0, sizeof(cs)); | |
config_wip_snapshot.index = 254; // Test the index roll-over | |
config_cb_load_default(); | |
} | |
void config_init(void) { | |
// Look for the latest snapshot | |
cs* latest_snapshot_a = config_locate_latest_snapshot(FLASH_PAGE_A); | |
cs* latest_snapshot_b = config_locate_latest_snapshot(FLASH_PAGE_B); | |
if (latest_snapshot_a == NULL) { | |
if (latest_snapshot_b == NULL) { | |
// Unable to find valid snapshot in both flash page | |
config_page = PAGE_NONE; | |
} else { | |
config_page = PAGE_B; | |
} | |
} else { | |
if (latest_snapshot_b == NULL) { | |
config_page = PAGE_A; | |
} else { | |
config_page = INDEX_IS_NEWER(latest_snapshot_a->index, latest_snapshot_b->index) ? PAGE_A : PAGE_B; | |
} | |
} | |
// Copy the active snapshot into memory | |
switch (config_page) { | |
case PAGE_A: | |
memcpy(&config_wip_snapshot, latest_snapshot_a, sizeof(cs)); | |
config_next_snapshot = config_fit_next_snapshot(FLASH_PAGE_A, latest_snapshot_a); | |
break; | |
case PAGE_B: | |
memcpy(&config_wip_snapshot, latest_snapshot_b, sizeof(cs)); | |
config_next_snapshot = config_fit_next_snapshot(FLASH_PAGE_B, latest_snapshot_b); | |
break; | |
default: | |
config_load_defaults(); | |
} | |
config_cb_on_load(); | |
} | |
static void flash_erase_page(void* page_addr) { | |
CONFIG_ENTER_CRITICAL(); | |
// Wait until flash is not in use | |
while (FLASH->SR & FLASH_SR_BSY); | |
// Unlock | |
FLASH->KEYR = 0x45670123; | |
FLASH->KEYR = 0xCDEF89AB; | |
// Erase page | |
FLASH->CR |= FLASH_CR_PER; // (1) Set the PER bit in the FLASH_CR register to enable page erasing | |
FLASH->AR = (uint32_t) page_addr; // (2) Program the FLASH_AR register to select a page to erase | |
FLASH->CR |= FLASH_CR_STRT; // (3) Set the STRT bit in the FLASH_CR register to start the erasing | |
// (4) Wait until the BSY bit is reset in the FLASH_SR register | |
while ((FLASH->SR & FLASH_SR_BSY) != 0); | |
if (FLASH->SR & FLASH_SR_EOP) { // (5) Check the EOP flag in the FLASH_SR register | |
FLASH->SR = FLASH_SR_EOP; // (6) Clear EOP flag by software by writing EOP at 1 | |
} else { | |
// Error, trigger a hard fault by writing to an invalid address | |
*((uint32_t*) 0xDEADBEEF) = 0xDEADBEEF; | |
} | |
FLASH->CR &= ~FLASH_CR_PER; // (7) Reset the PER Bit to disable the page erase | |
// Flash RW operation done, re-lock the flash | |
FLASH->CR = FLASH_CR_LOCK; | |
} | |
static void flash_program_page(void* dst_addr) { | |
uint16_t* src_addr = (uint16_t*) &config_wip_snapshot; | |
void* end_addr = (void*) ((char*)dst_addr + sizeof(cs)); | |
CONFIG_ENTER_CRITICAL(); | |
// Wait until flash is not in use | |
while (FLASH->SR & FLASH_SR_BSY); | |
// Unlock | |
FLASH->KEYR = 0x45670123; | |
FLASH->KEYR = 0xCDEF89AB; | |
// Program page | |
while (FLASH->SR & FLASH_SR_BSY); // Wait until flash is not in use | |
FLASH->CR |= FLASH_CR_PG; | |
while (dst_addr < end_addr) { | |
*((__IO uint16_t*) dst_addr) = *src_addr; | |
while (FLASH->SR & FLASH_SR_BSY); // Wait until flash is not in use | |
src_addr++; | |
dst_addr = (void*) ((uint16_t*)dst_addr + 1); | |
} | |
if (FLASH->SR & FLASH_SR_EOP) { | |
FLASH->SR = FLASH_SR_EOP; | |
} else { | |
// Error, trigger a hard fault by writing to an invalid address | |
*((uint32_t*) 0xDEADBEEF) = 0xDEADBEEF; | |
} | |
FLASH->CR &= ~FLASH_CR_PG; | |
// Flash RW operation done, re-lock the flash | |
FLASH->CR = FLASH_CR_LOCK; | |
CONFIG_EXIT_CRITICAL(); | |
} | |
void config_wipe() { | |
config_load_defaults(); | |
flash_erase_page(FLASH_PAGE_A); | |
flash_erase_page(FLASH_PAGE_B); | |
config_cb_on_load(); | |
} | |
void config_commit() { | |
// Increase the index | |
config_wip_snapshot.index++; | |
// Recalc. the CRC32 checksum | |
config_wip_snapshot.crc32 = config_snapshot_crc32(&config_wip_snapshot); | |
if (config_next_snapshot) { | |
// Current flash page still have space. | |
flash_program_page(config_next_snapshot); | |
// Update global state | |
config_next_snapshot = config_fit_next_snapshot( | |
config_page == PAGE_B ? FLASH_PAGE_B : FLASH_PAGE_A, | |
config_next_snapshot); | |
} else { | |
// Current flash page is full or uninitialized. | |
// If both pages are uninitialized or invalid, default to PAGE_A. | |
if (config_page == PAGE_A) { | |
config_next_snapshot = FLASH_PAGE_B; | |
config_page = PAGE_B; | |
} else { | |
config_next_snapshot = FLASH_PAGE_A; | |
config_page = PAGE_A; | |
} | |
// Commit the current snapshot to the beginning of the other flash page | |
flash_erase_page(config_next_snapshot); | |
flash_program_page(config_next_snapshot); | |
if (config_snapshot_crc32((cs*)config_next_snapshot) != config_wip_snapshot.crc32) { | |
// Error, trigger a hard fault by writing to an invalid address | |
*((uint32_t*) 0xDEADBEEF) = 0xDEADBEEF; | |
} | |
// Update global state | |
config_next_snapshot = config_fit_next_snapshot(config_next_snapshot, config_next_snapshot); | |
} | |
} | |
void* config_get_data_ptr() { | |
return (void*) &config_wip_snapshot.payload; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef CONFIG_H_ | |
#define CONFIG_H_ | |
/** | |
* The maximum size of the config data. | |
* This size should be multiple of 4. | |
*/ | |
#define SNAPSHOT_PAYLOAD_SIZE 492 | |
/** | |
* See datasheet. | |
*/ | |
#define FLASH_PAGE_SIZE 1024 | |
/** | |
* Read the stored config data from flash | |
*/ | |
void config_init(void); | |
/** | |
* Returns a pointer to the config data in the memory, | |
* 4-byte aligned. Write changes to this memory area, | |
* then commit the changes by invoking config_commit(). | |
* If config_commit() is not called, any changes will | |
* be discarded after power lost. | |
*/ | |
void* config_get_data_ptr(void); | |
/** | |
* Erase the config flash, and then load the default content. | |
*/ | |
void config_wipe(void); | |
/** | |
* Write the changes into flash. | |
*/ | |
void config_commit(void); | |
/** | |
* Callback, load the default content. | |
*/ | |
void config_cb_load_default(void); | |
/** | |
* Callback, when the config is loaded. | |
*/ | |
void config_cb_on_load(void); | |
#endif /* CONFIG_H_ */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment