A small C++20 embedded library for storing user-defined types in non-volatile flash memory.
flashstore is designed for microcontroller firmware where storage must be:
- deterministic,
- portable,
- easy to integrate,
- explicit about flash geometry,
- resilient to corruption and incomplete writes.
It provides:
- strongly typed persistence for trivially copyable user data,
- CRC-based integrity checking,
- schema versioning,
- corruption detection,
- append-only storage,
- optional wear leveling through sector/page reclamation,
- a clean flash adapter abstraction for platform-specific flash operations.
- Overview
- Features
- Project structure
- Headers
- Usage overview
- Defining a flash adapter
- Integrating into a project
- Build examples
- Build and run tests
- Coverage
- Design notes
- Requirements
- License
flashstore lets you persist a user-defined configuration or state object directly into flash using a small generic storage engine.
The library separates responsibilities cleanly:
- the core handles record layout, CRC, versioning, scanning, and persistence,
- the adapter handles target-specific flash read/program/erase operations,
- the user type stays simple and application-focused.
Typical use cases:
- system configuration,
- calibration data,
- persistent runtime parameters,
- versioned application settings,
- small device state snapshots.
- C++20 API with a small embedded-friendly surface.
- No dynamic allocation in the core library.
- Flash geometry defined explicitly by the platform adapter.
- Fixed-size append-only record storage.
- CRC validation for stored payloads.
- Schema version checks.
- Corruption and incomplete write detection.
- Wear-leveling strategy through region reuse.
- Works with inheritance-style and composition-style integration.
- Suitable for bare-metal and RTOS-based firmware.
flashstore/
├── CMakeLists.txt
├── LICENSE
├── README.md
├── include/
│ └── flashstore/
│ ├── crc32.hpp
│ ├── flash_adapter.hpp
│ ├── flash_stored.hpp
│ ├── status.hpp
│ └── store.hpp
├── cmake/
│ └── flashstore_coverage.cmake
├── tests/
│ ├── CMakeLists.txt
│ ├── crc32_test.cpp
│ ├── flash_adapter_test.cpp
│ ├── store_test.cpp
│ ├── flash_stored_test.cpp
│ └── support/
│ └── test_flash_adapter.hpp
└── examples/
├── pico/
├── stm32/
└── ...
Defines the library status codes returned by load, save, and erase operations.
Typical values include:
- success,
- record not found,
- version mismatch,
- corruption detected,
- no space,
- I/O error.
Use this header whenever you need to inspect persistence results in application code.
Defines the flash geometry model and the FlashAdapter concept.
This is the main platform abstraction layer.
An adapter provides:
geometry(),read(address, span),program(address, span),erase(address, size).
It also provides small helpers such as alignment utilities used by the storage core.
Implements CRC32 calculation for payload integrity checking.
The storage core uses this to verify that persisted payload bytes match the CRC stored in the record header.
Implements the main persistence engine.
This header contains:
- flash record layout,
- scanning logic,
- latest valid record discovery,
- save/load logic,
- record validation,
- erase-all support,
- wear-leveling-friendly slot reuse.
This is the core header of the library.
Provides a CRTP convenience layer for user types that want methods like:
load(),save(),eraseAllStorage().
This is optional.
You can use Store<T, Adapter, Version> directly if you prefer composition.
There are two common ways to use the library.
#include <flashstore/flash_stored.hpp>
struct MyConfig : public flashstore::FlashStored<MyConfig, AppFlash, 1>
{
std::uint32_t baudRate{115200};
float kp{1.0f};
float ki{0.0f};
float kd{0.0f};
};Example:
MyConfig cfg{};
const auto loadStatus = cfg.load();
cfg.kp = 2.0f;
cfg.ki = 0.5f;
const auto saveStatus = cfg.save();#include <flashstore/store.hpp>
struct MyConfig
{
std::uint32_t baudRate{115200};
float kp{1.0f};
float ki{0.0f};
float kd{0.0f};
};
using ConfigStore = flashstore::Store<MyConfig, AppFlash, 1>;Example:
ConfigStore store{};
MyConfig cfg{};
auto st1 = store.load(cfg);
cfg.kp = 2.0f;
auto st2 = store.save(cfg);The stored type must be:
- trivially copyable,
- stable enough in layout for raw-byte persistence,
- versioned explicitly when the schema changes.
For long-lived products, prefer a dedicated storage struct instead of persisting large application objects directly.
A flash adapter is the only platform-specific part required by the core library.
It must provide:
- flash geometry,
- byte reads,
- aligned program operations,
- erase operations over the platform erase granularity.
Minimal example:
#include <flashstore/flash_adapter.hpp>
struct MyFlashAdapter
{
static constexpr flashstore::Geometry geometry() noexcept
{
return flashstore::Geometry{
.baseAddress = 0x08080000u,
.totalSize = 8 * 1024u,
.eraseBlockSize = 2048u,
.programBlockSize = 8u,
.erasedValue = 0xFFu
};
}
static bool read(std::uint32_t address, std::span<std::byte> out);
static bool program(std::uint32_t address, std::span<const std::byte> data);
static bool erase(std::uint32_t address, std::size_t size);
};- Reserve a flash region that does not overlap code or other persistent data.
- Match
eraseBlockSizeto the hardware erase unit. - Match
programBlockSizeto the smallest valid aligned programming unit. - Enforce alignment and range checks inside the adapter.
- Keep all platform-specific IRQ/cache/lock handling inside the adapter.
- Return
falseon any flash access failure.
In most cases, porting flashstore to a new microcontroller means replacing only the adapter.
Typical differences between platforms:
- page erase vs sector erase,
- program granularity,
- XIP read model,
- flash unlock/lock sequences,
- bank selection,
- cache/interrupt restrictions during programming.
The Store logic should usually remain unchanged.
CMake’s FetchContent module populates dependencies during the configure step, which makes it a good fit for small embedded libraries integrated directly into an application build.
Example:
include(FetchContent)
FetchContent_Declare(
flashstore
GIT_REPOSITORY https://github.com/tblanpied/FlashStore.git
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(flashstore)
target_link_libraries(your_firmware PRIVATE flashstore::flashstore)This is the easiest option when:
- you already use CMake,
- you want reproducible dependency setup,
- you want the library version controlled in your build files.
You can also copy the include/flashstore/ folder directly into your repository.
This approach works well when:
- you want a vendored dependency,
- your build system is not CMake-based,
- you want a fully local embedded dependency with no network access.
Typical layout:
your_project/
├── third_party/
│ └── flashstore/
│ └── include/flashstore/...
└── ...
Then add the include path in your build system:
target_include_directories(your_firmware PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/flashstore/include
)A submodule is a good middle ground when you want:
- a clean upstream history,
- pinned revisions,
- no source duplication.
Typical flow:
git submodule add https://github.com/tblanpied/FlashStore.git third_party/flashstore
git submodule update --init --recursiveThen include it with CMake:
add_subdirectory(third_party/flashstore)
target_link_libraries(your_firmware PRIVATE flashstore::flashstore)Examples are intended to demonstrate integration on real platforms.
Build an example tree with the platform-specific top-level CMake entry.
For example, the STM32 example is built with:
cmake -S examples/stm32 -B build
cmake --build buildFlash the STM32 example with:
cmake --build build -j12 --target flashThe STM32 flash target relies on STM32_Programmer_CLI, and if that executable is not found automatically it can be provided through STM32_PROGRAMMER_CLI_DIR or STM32_PROGRAMMER_CLI. STM32CubeProgrammer provides a command-line interface for STM32 programming workflows, which is what this target uses.
Tests are optional and controlled through FLASHSTORE_BUILD_TESTS.
Enable tests:
cmake -S . -B build -DFLASHSTORE_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failureThe test setup uses GoogleTest integrated through CMake, and GoogleTest’s CMake quickstart shows the same FetchContent-based integration pattern.
Coverage is optional and controlled through FLASHSTORE_ENABLE_COVERAGE.
Build with tests and coverage enabled:
cmake -S . -B build \
-DFLASHSTORE_BUILD_TESTS=ON \
-DFLASHSTORE_ENABLE_COVERAGE=ON
cmake --build build
cmake --build build --target coverageTypical outputs:
build/coverage/index.htmlbuild/coverage/coverage.xmlbuild/coverage/coverage.txt
Use coverage for host-side validation only, not production firmware builds.
flashstore stores data as append-only fixed-size records inside a reserved flash region.
Each record contains:
- a header,
- the payload,
- padding to the program alignment if needed,
- a final commit block.
A record is valid only if:
- the header is valid,
- the payload size matches,
- the CRC matches,
- the commit block is present and valid.
Fixed-size slots simplify:
- scanning,
- validation,
- alignment,
- recovery after power loss,
- adapter portability.
The main tradeoff is some wasted flash space due to alignment and fixed slot sizing.
The library uses a two-step write:
- write header + payload,
- write commit block.
If power is lost before the commit block is programmed, that slot is ignored during scanning.
The schema version is part of the store type.
If the stored data schema changes, bump the version and migrate or reset the data explicitly.
This project uses the MIT license.