Skip to content

About

A/B dual-page flash storage for small MCUs. Zero dependencies. Stateless. | A/B 双页 Flash 存储,面向小型 MCU。零依赖,无状态。

Topics

Resources

Stars

14 stars

Watchers

0 watching

Forks

Latest commit

 

History

19 Commits

Folders and files

Repository files navigation

中文

FlashStore A/B Partition

A/B dual-page flash storage for small MCUs. Zero dependencies. Stateless.

https://github.com/createskyblue/flashstore-ab-partition

Which to choose?

FlashStore FlashDB LittleFS
Purpose Single-blob reliable storage Embedded key-value DB Embedded file system
Data model Binary blob Key-value pairs Files
I/O Save/Load whole blob Set/Get by key Open/Read/Write
Wear leveling ❌ ✅ ✅
Power-loss safe ✅ Dual-page ✅ Transaction log ✅
Best for Config data, infrequent writes Multiple KVs, independent updates Files, logs, large data

Resource usage (Cortex-M0 -Os measured)

Module Flash Static RAM Stack
flash_store 500 bytes 0 ~20 bytes
chacha20 (optional) 700 bytes 0 ~200 bytes
xxtea (optional) 737 bytes 0 ~0

flash_store breakdown: Load 158B / load_page 108B / save_page 100B / Save 66B / CRC32 52B / MaxDataSize 16B. Link what you use.

Quick start

  1. Copy include/flash_store.h and src/flash_store.c into your project. Add chacha20 or xxtea if you need encryption.
  2. Implement 3 callbacks:
bool my_read(void *ctx, uint32_t addr, uint8_t *out, size_t len) {
    memcpy(out, (void *)(uintptr_t)addr, len);  // or HAL read
    return true;
}
bool my_erase(void *ctx, uint32_t addr, size_t len) {
    HAL_FLASH_ErasePage(addr);                  // full page erase
    return true;
}
bool my_program(void *ctx, uint32_t addr, const uint8_t *data, size_t len) {
    // word-by-word program, or memcpy if byte-addressable flash
    return true;
}
  1. Init and go:
FlashStore_IO io = {
    .read = my_read, .erase = my_erase, .program = my_program,
};
FlashStore_Config cfg = {
    .io = &io, .page_a_address = 0x0800FE00, .page_b_address = 0x0800FE80,
    .page_size = 128,
};
FlashStore_Save(&cfg, data, len);   // write
FlashStore_Load(&cfg, data, len);   // read

Need encryption? Use ChaCha20 — RFC 8439, stream cipher, no alignment requirements. Cortex-M0 -Os: 700B Flash + ~200B stack, 0 static RAM. For extreme resource savings, XXTEA — obfuscation only, requires 4-byte alignment and ≥8 bytes. Encrypt externally: encrypt → FlashStore_Save / FlashStore_Load → decrypt.

Why data won't get lost

A is written before B. At any power-loss moment, at least one valid copy exists in flash.

Save:  A first → B second     Load: read both → CRC check → return good → repair bad
Power loss at… A B Load result
Idle v2 v2 Returns v2
Writing A bad v1 Returns v1, repairs A
A done, before B v2 (CRC=c2) v1 (CRC=c1) CRC differ → A is newer → returns v2, repairs B
Writing B v2 bad Returns v2, repairs B next time
Both pages bad bad bad ERROR_NO_VALID_DATA

WARN_REPAIR_FAILED? Means erase/program failed during repair. Should not happen in normal operation — check your hardware or IO.

Header layout

12-byte header + payload per page:

Offset Field Description
0 magic (4B) 0x46534142
4 length (4B) Payload size
8 crc32 (4B) Payload CRC32

Bigger logical pages (span several erase sectors)

page_size is a logical size — a page may span several physical erase sectors, so a single blob can be larger than one sector without changing the library at all. Your erase/program callbacks just handle the whole region:

#define FLASH_BASE   0x08000000u
#define SECTOR_SIZE  0x800u   /* 2KB erase sector */
#define A_SECTOR     8        /* A = sectors 8..11 */
#define SECTORS      4        /* logical page = 4 sectors */

FlashStore_Config cfg = {
    .io              = &io,
    .page_a_address  = FLASH_STORE_SECTOR_ADDR(FLASH_BASE, SECTOR_SIZE, A_SECTOR),
    .page_b_address  = FLASH_STORE_SECTOR_ADDR(FLASH_BASE, SECTOR_SIZE, A_SECTOR + SECTORS),
    .page_size       = FLASH_STORE_PAGE_SIZE(SECTORS, SECTOR_SIZE),
};

The helpers FLASH_STORE_SECTOR_ADDR / FLASH_STORE_PAGE_SIZE / FLASH_STORE_PAGE_B (see flash_store.h) are constant expressions, so they work in static initializers and _Static_assert.

Three rules:

  1. A and B must not share a physical erase sector. Erasing A wipes every sector it covers; if B overlaps any of them, the redundancy is destroyed. ConfigCheck verifies the logical pages don't overlap, but it cannot know the physical sector map — that part is on you.
  2. Erase the whole region. Your erase callback receives page_size bytes; if that spans N physical sectors, erase all N (or use a multi-sector erase).
  3. Wear scales with page size. Every commit erases and rewrites both pages in full, so a 4KB page wears ~4x faster than a 1KB page — keep commits infrequent.

Status codes

Return Meaning
OK Success
ERROR_ARGUMENT Bad parameter
ERROR_WRITE Erase or program failed
ERROR_NO_VALID_DATA Both pages are corrupt
WARN_REPAIR_FAILED Data OK but redundancy not restored

Tests

cmake -S . -B build && cmake --build build && ./build/flash_store_tests.exe

10 tests: round-trip, A corrupt→B, B corrupt→repair, A-write-fail, CRC-staleness, both corrupt, MaxDataSize, Save/Load arg check, ConfigCheck, repair-failed.

License

MIT

About

A/B dual-page flash storage for small MCUs. Zero dependencies. Stateless. | A/B 双页 Flash 存储,面向小型 MCU。零依赖,无状态。

Topics

Resources

Stars

14 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages