When you develop firmware for a Raspberry Pico, there are cases where you want to store user data in Flash. Data that needs to survive a power-down. E.g: calibration data, user preferences, display Celsius/Fahrenheit … The Pico C SDK has an API for Flash read/write. You can access Flash, read content and change it. But that area (2048k on a Raspberry Pico) is by default used to store firmware. The approach that I'm suggesting here will take care that you have a dedicated block of Flash for persistent storage. Additional bonus; your C code does not have to know the memory layout or Flash addresses. Everything is managed by the linker / loader script. |
What we'll do in this post: steal 4k from the big Flash pool, and set it aside for persistent storage.
Wins:
- the block is reserved at the end of the Flash block. When you try to build firmware that runs into this area, the firmware build will fail.
- your C code does not have to know the memory locations and addresses. All config is done in the linker / loader script. That .ld script creates a symbol for the reserved Flash area; you can use that area in your code.
- rigidity instead of magic addresses. The space used for persistent storage is not defined somewhere in some source or header file. It is pre-allocated. Tools can make it visible. The compiler understands it, the linker will enforce it.
Adapt the default SDK .ld file
Reserve the Memory
The Pico C SDK (as of version 1.5) has four .ld files. A default one, and a few variants (e.g.: one that loads flash code into RAM before executing). Most of our projects use the default script. You can tell CMake to use your own .ld during build:
pico_set_linker_script(PersistentStorage ${CMAKE_SOURCE_DIR}/memmap_custom.ld)
MEMORY { FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k }
I will steal 4k from that Flash for the persistent storage of my firmware:
__PERSISTENT_STORAGE_LEN = 4k ; MEMORY { FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k - __PERSISTENT_STORAGE_LEN FLASH_PERSISTENT(rw) : ORIGIN = 0x10000000 + (2048k - __PERSISTENT_STORAGE_LEN) , LENGTH = __PERSISTENT_STORAGE_LEN
You can see that I take 4k from FLASH, and create a new memory area FLASH_PERSISTENT. The script loads all our firmware in FLASH, so it will never use the new area to store code. It understands that it now has 2044k instead of 2048. The script does not give any instructions on how to load code or data in FLASH_PERSISTENT. So the binary will stay out of it. And debuggers and programmers will stay out of it too.
Make the start address available to C
Our memory layout knows where the persistent block is. It would be a shame if we'd have to also hard-code that address in our C code. Luckily, GCC can turn the address into a symbol that can be used in C.
Wins:
- no two separate definitions that can get out of sync
- if our code uses an unknown symbol, the build will fail. The tools protect us (again)
To make this work, we need to define a memory section, and create a symbol that represents the start of that section. If we take care that this is the first section that uses our FLASH_PERSISTENT memory block, we know that the variable will also represent the start address of our reserved area.
.section_persisitent : { "ADDR_PERSISTENT" = .; } > FLASH_PERSISTENT
The ADDR_PERSISTENT symbol will be available to our C code.
Validate that the C code knows the persistent block (without hardcoding)
If we declare the memory block as an external variable (that we will not declare anywhere in the code), the linker will resolve this to that physical Flash area:
flash_utils.h
Test:
#include <stdio.h> #include "flash_utils.h" int main() { char addr[80]; sprintf(addr, "address = %x", ds_get_address_persistent()); return 0; }
You see that we now have a #define that represents our block's start address. The rest of the code is there as a test case, and to make it visible in a debugger:
The address that we see is exactly the start location of our little protected Flash block. Confirmed by the memory map:
layout:
symbol available in C code:
The Pico Flash API can now be used to store and read data from the area. Data that you save in that block will be available after a power down.
fun fact: this design does not use any runtime resources: no code space, 0 clock ticks, no memory (except the flash area we want to reserve. 0 bits beyond that). The inline helper function does not generate machine code. If your goal is to reserve 4k of Flash, then the net resource use of the mechanism in this blog is: 4k of Flash is reserved. |
Enjoy. Here is the VSCode project: PersistentStorage_20230901.zip
Top Comments