Skip to content

Instantly share code, notes, and snippets.

@aaronjamt
Last active January 12, 2025 10:19

Revisions

  1. aaronjamt revised this gist Feb 29, 2024. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions rp2040_persistent_flash_storage.c
    Original file line number Diff line number Diff line change
    @@ -53,8 +53,8 @@ namespace PersistentStorage {

    // Forward declarations
    uint8_t findCurrentChunk();
    bool read(uint8_t *output, uint8_t size);
    void write(uint8_t *data, uint8_t size);
    bool read(void *output, uint8_t size);
    void write(void *data, uint8_t size);
    void _writePage(uint32_t pageAddr, uint8_t *chunk);
    void _eraseSector(uint32_t sectorAddr);
    inline bool isFirstRun();
    @@ -79,7 +79,7 @@ namespace PersistentStorage {
    return lastValidChunk;
    }

    bool read(uint8_t *output, uint8_t size) {
    bool read(void *output, uint8_t size) {
    uint8_t chunkIdx = findCurrentChunk();
    if (chunkIdx == INVALID_CHUNK) {
    return false;
    @@ -89,7 +89,7 @@ namespace PersistentStorage {
    return true;
    }

    void write(uint8_t *data, uint8_t size) {
    void write(void *data, uint8_t size) {
    // Will get the next available chunk
    uint8_t chunkIdx = findCurrentChunk() + 1;
    // If we've already written all chunks (or ), erase the flash
  2. aaronjamt revised this gist Feb 18, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion rp2040_persistent_flash_storage.c
    Original file line number Diff line number Diff line change
    @@ -170,7 +170,8 @@ namespace PersistentStorage {
    // are erased, however, they are left at 0xFF instead. This could potentially be used to do a "first run" check,
    // might be something to look into in the future (edit: now implemented).
    // This means that, for each flash erase, there can be multiple chunks written (up to CHUNKS_PER_SECTOR)
    // This also means that you can undo a write by going to the 2nd or 3rd used chunk, provided there wasn't an erase
    // This also means that you can theoretically undo a write by going to the 2nd or 3rd used chunk, provided there
    // wasn't an erase, although there are no guarantees about this and it's not really supported.
    // It's up to userland code to handle what the data actually means, and each chunk has 255 bytes available (since
    // the first byte is reserved for "valid chunk" flag). This means that you can, for example, create a struct
    // to store different parameters, and pass a pointer to the struct to the read/write functions to automatically
  3. aaronjamt created this gist Feb 18, 2024.
    178 changes: 178 additions & 0 deletions rp2040_persistent_flash_storage.c
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,178 @@
    #include "hardware/flash.h"

    // The two primary functions that you would use are:
    // bool read(uint8_t *output, uint8_t size);
    // void write(uint8_t *data, uint8_t size);

    // The read(...) function will either fill the *output buffer
    // with size bytes (and return true), or will return false if
    // there is no saved data available to be read.

    // The write(...) function will write the provided buffer to
    // flash memory.

    // There is also a function:
    // bool isFirstRun()
    // which can be called to check if this is the first time
    // this program has run since it was flashed to the MCU.
    // Technically, this checks for whether this persistent
    // storage module has ever performed a write operation,
    // but in most cases that will likely mean the same thing.

    // NOTE:
    // The data being read or written can be, at most, 255 bytes in
    // length, due to the way the flash memory is arranged (see the
    // end of this file for a detailed explanation of how it works,
    // including information about why this limitation exists).

    namespace PersistentStorage {
    // How many sectors to use? Each sector contains CHUNKS_PER_SECTOR
    // chunks, which is 16 chunks per sector on my RP2040 board.
    const uint8_t NUMBER_OF_SECTORS = 1;

    // The number of chunks in each flash sector
    const uint8_t CHUNKS_PER_SECTOR = FLASH_SECTOR_SIZE / FLASH_PAGE_SIZE;

    // This buffer contains one extra flash sector, as a sector is the minimum size we can erase, and we
    // don't know where this variable will end up in memory. By giving it one extra sector, we guarantee
    // that at least NUMBER_OF_SECTORS flash sector(s) will fit entirely within it, making those sector(s)
    // safe to erase and reprogram, without risking accidentally overwriting the program itself.
    const uint8_t _PERSISTENT_STORAGE_BUFFER[FLASH_SECTOR_SIZE * (NUMBER_OF_SECTORS+1)] = {};

    // This pointer points to the beginning of the "usable" chunk of the persistent storage, which
    // is the flash sector that entirely fits within the persistent storage buffer (as explained above)
    uint8_t *PERSISTENT_STORAGE = (
    // If the persistent storage buffer is already aligned to the flash sector boundary, use it as the pointer
    ((unsigned long)_PERSISTENT_STORAGE_BUFFER % FLASH_SECTOR_SIZE == 0) ? (uint8_t*)_PERSISTENT_STORAGE_BUFFER : (uint8_t*)(
    // Otherwise, calculate the offset for the first full sector within the bounds of the buffer
    ((unsigned long)_PERSISTENT_STORAGE_BUFFER - ((unsigned long)_PERSISTENT_STORAGE_BUFFER % FLASH_SECTOR_SIZE) + FLASH_SECTOR_SIZE)
    )
    );

    uint32_t PERSISTENT_STORAGE_FLASH_OFFSET = ((uint32_t)PERSISTENT_STORAGE - XIP_BASE);

    // Forward declarations
    uint8_t findCurrentChunk();
    bool read(uint8_t *output, uint8_t size);
    void write(uint8_t *data, uint8_t size);
    void _writePage(uint32_t pageAddr, uint8_t *chunk);
    void _eraseSector(uint32_t sectorAddr);
    inline bool isFirstRun();

    // Preproc definitions and macros
    #define INVALID_CHUNK 0xFF
    #define CHUNK_VALID_FLAG 0x55
    #define ChunkAddr(chunkIdx) (PERSISTENT_STORAGE + (FLASH_PAGE_SIZE*chunkIdx))

    uint8_t findCurrentChunk() {
    uint8_t lastValidChunk = INVALID_CHUNK;
    // Iterate over chunks until we find the last valid chunk
    for (uint8_t chunkIdx = 0; chunkIdx < NUMBER_OF_SECTORS*CHUNKS_PER_SECTOR; chunkIdx++) {
    if (ChunkAddr(chunkIdx)[0] == CHUNK_VALID_FLAG) {
    // Valid chunk!
    lastValidChunk = chunkIdx;
    } else {
    // No more valid chunks
    break;
    }
    }
    return lastValidChunk;
    }

    bool read(uint8_t *output, uint8_t size) {
    uint8_t chunkIdx = findCurrentChunk();
    if (chunkIdx == INVALID_CHUNK) {
    return false;
    }
    // Add one to the chunk address to get the actual data part of the chunk (first byte is a flag)
    memcpy(output, ChunkAddr(chunkIdx) + 1, size);
    return true;
    }

    void write(uint8_t *data, uint8_t size) {
    // Will get the next available chunk
    uint8_t chunkIdx = findCurrentChunk() + 1;
    // If we've already written all chunks (or ), erase the flash
    if (chunkIdx >= NUMBER_OF_SECTORS*CHUNKS_PER_SECTOR || chunkIdx == 0) {
    for (uint8_t sectorIdx = 0; sectorIdx < NUMBER_OF_SECTORS; sectorIdx++)
    _eraseSector(sectorIdx);

    // Since we've erased the flash sector(s), write to chunk 0
    chunkIdx = 0;
    }

    // Prepare the chunk by creating a 256 byte buffer to write
    // The first byte should be set to indicate a valid chunk
    uint8_t chunk[256] = {CHUNK_VALID_FLAG};
    memcpy(chunk+1, data, size);

    // Write the actual chunk to the corresponding flash page
    _writePage(chunkIdx * FLASH_PAGE_SIZE, chunk);
    }

    // Write a single FLASH_PAGE_SIZE-byte flash page
    // Wrapper that takes care of disabling interrupts, locking the 2nd core, etc
    void _writePage(uint32_t pageAddr, uint8_t *chunk) {
    // The address that was passed is the address within the persistent
    // storage region, add the offset to find the true flash address
    pageAddr += PERSISTENT_STORAGE_FLASH_OFFSET;
    // We can't have interrupts or the 2nd core running
    rp2040.idleOtherCore();
    uint32_t ints = save_and_disable_interrupts();
    // Do the actual write
    flash_range_program(pageAddr, chunk, FLASH_PAGE_SIZE);
    // Set things back
    restore_interrupts(ints);
    rp2040.resumeOtherCore();
    }

    // Erase a single FLASH_SECTOR_SIZE-byte flash sector
    // Wrapper that takes care of disabling interrupts, locking the 2nd core, etc
    void _eraseSector(uint32_t sectorAddr) {
    // The address that was passed is the address within the persistent
    // storage region, add the offset to find the true flash address
    sectorAddr += PERSISTENT_STORAGE_FLASH_OFFSET;
    // We can't have interrupts or the 2nd core running
    rp2040.idleOtherCore();
    uint32_t ints = save_and_disable_interrupts();
    // Do the actual erase
    flash_range_erase(sectorAddr, FLASH_SECTOR_SIZE);
    // Set things back
    restore_interrupts(ints);
    rp2040.resumeOtherCore();
    }

    // Determines if this is the first time this program has been run.
    // It takes advantage of the fact that the program, when compiled, fills persistent storage
    // with 0x00s by default, but when the flash chunks are erased, they are filled with 0xFF,
    // and when we write data, we write a 3rd, different value. This means that the only time
    // the chunk validity flag(s) would ever be set to 0x00 is after a program flash, before
    // the first erase or write operation(s). As a result, we can check just the first chunk's
    // validity flag and, if it's 0x00, we can assume the rest are, and that we haven't yet
    // written any data.
    inline bool isFirstRun() {
    return PERSISTENT_STORAGE[0] == 0x00;
    }

    // Here's how it all works:
    // * Use flash page size as the size of a "chunk" of data
    // * Search through each "chunk" looking for a specific byte flag to indicate there's valid data
    // * The last valid chunk is the current one, ignore any previous chunks
    // * If it doesn't find a valid chunk, there's no save data
    // * By using a value other than 0x00 and 0xFF, a chunk is considered invalid both after a flash sector erase,
    // and after initial programming with 0x00s
    // * When writing, find the most recent valid chunk and save to the slot after it
    // * That way, any future reads will consider it the "current" chunk and ignore the rest
    // * If there's no more free slots, erase the flash sector and start from first chunk
    // * Also erase if findCurrentChunk() returns INVALID_CHUNK, meaning something went wrong, which usually indicats
    // that all of the chunks are full of 0x00. This happens because, when the program is compiled, that data is
    // populated with 0x00s by default, so after the program is flashed, it's all 0x00. After the flash sector(s)
    // are erased, however, they are left at 0xFF instead. This could potentially be used to do a "first run" check,
    // might be something to look into in the future (edit: now implemented).
    // This means that, for each flash erase, there can be multiple chunks written (up to CHUNKS_PER_SECTOR)
    // This also means that you can undo a write by going to the 2nd or 3rd used chunk, provided there wasn't an erase
    // It's up to userland code to handle what the data actually means, and each chunk has 255 bytes available (since
    // the first byte is reserved for "valid chunk" flag). This means that you can, for example, create a struct
    // to store different parameters, and pass a pointer to the struct to the read/write functions to automatically
    // store different values with different data types, without having to do any special (de)serialization.
    }