Skip to content

tblanpied/FlashStore

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FlashStore

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.

Table of contents

Overview

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.

Features

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

Project structure

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/
    └── ...

Headers

flashstore/status.hpp

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.

flashstore/flash_adapter.hpp

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.

flashstore/crc32.hpp

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.

flashstore/store.hpp

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.

flashstore/flash_stored.hpp

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.

Usage overview

There are two common ways to use the library.

1. Inheritance-style usage

#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();

2. Composition-style usage

#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);

Storage requirements for T

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.

Defining a flash adapter

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);
};

Adapter guidelines

  • Reserve a flash region that does not overlap code or other persistent data.
  • Match eraseBlockSize to the hardware erase unit.
  • Match programBlockSize to 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 false on any flash access failure.

Porting between MCUs

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.

Integrating into a project

FetchContent

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.

Copy into your project

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
)

Git submodule

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 --recursive

Then include it with CMake:

add_subdirectory(third_party/flashstore)
target_link_libraries(your_firmware PRIVATE flashstore::flashstore)

Build examples

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 build

Flash the STM32 example with:

cmake --build build -j12 --target flash

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

Build and run tests

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-failure

The test setup uses GoogleTest integrated through CMake, and GoogleTest’s CMake quickstart shows the same FetchContent-based integration pattern.

Coverage

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 coverage

Typical outputs:

  • build/coverage/index.html
  • build/coverage/coverage.xml
  • build/coverage/coverage.txt

Use coverage for host-side validation only, not production firmware builds.

Design notes

Record model

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.

Why fixed-size records

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.

Power-loss behavior

The library uses a two-step write:

  1. write header + payload,
  2. write commit block.

If power is lost before the commit block is programmed, that slot is ignored during scanning.

Versioning

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.

License

This project uses the MIT license.

About

Portable C++20 embedded flash persistence library with CRC, versioning, wear-aware storage, and clean platform adapters for MCU firmware.

Topics

Resources

License

Stars

Watchers

Forks

Contributors