Using flash memory as non-volatile storage on the Pi Pico microcontroller
The Raspberry Pi Pico is a relatively-new microntroller board based on a chip called the RP2040. This has a more modern, dual-core, ARM-based architecture than AVR-style devices, at a broadly similar price. The board has has 2Mb of flash ROM, 264kB of RAM, and has a rated clock speed of 133MHz. What it doesn't have is separate non-volatile memory like EEPROM although, of course, it's possible to add storage as a peripheral.
An alternative to peripheral storage is to use part of the flash ROM
as storage. Although this mechanism is documented, it's a little
fiddly. In this article I'll try to explain the details.
I'm assuming that you're writing code using the official C/C++ SDK,
which uses CMake, for better or worse. The SDK provides
two specific API calls for modifying the flash --
flash_range_erase()
and flash_range_program()
. These API calls are defined
in a header file hardware/flash.h
.
Addressing
When reading, the flash ROM appears in the ARM address space starting at address 0x1000 0000 (that's 256Mb from the start of the 32-bit address space of the device). When uploading code in UF2 format, using the default bootloader, the user code is loaded into the start of flash and, broadly speaking, occupies a contiguous range of addresses that is the size of the program. So, for example, 256kB of program code will occupy the first 256kB of flash, and will appear in the ARM address space as the range 0x10000000 to 0x1004 0000.
The SDK defines a constant for the start of flash
in the memory map -- it is XIP_BASE
,
which will be defined if you #include flash.h
.
So if, for some reason, you wanted to read the first byte of your own program code, you could do:
#include <hardware/flash.h> char *p = (char *)XIP_BASE; char first_byte = *p;
The above applies to reading the flash. When writing or erasing it,
things are different. The flash_range_erase()
and flash_range_program()
functions both take as their
first an argument an offset into the flash. That makes sense,
in a way -- operations that are specific to the flash are not
operations in the ARM memory map. Still, it does mean that reading data
from flash, and writing data to flash, use different addressing
schemes.
For example, data your program writes at, say, offset 0x5 0000
in the flash, will appear in the memory map at address
0x1005 0000. It's probably more future-proof to think of this
address as XIP_BASE + 0x50000
.
Erasing the flash
In principle, you just need to do:
flash_range_erase (offset, size);
offset
, as I described above, is the position in the flash,
not a memory address. size
must be a multiple of the
"sector size", which is defined as the constant FLASH_SECTOR_SIZE,
with a value of 4kB.
Writing the flash
Again, in principle, you need:
flash_range_program (offset, buffer, size);
The buffer
argument points to the data to be written,
which is of size size
. This size must be a multiple
of the "page size",
which is defined as the constant FLASH_PAGE_SIZE,
with a value of 256 bytes. Notice that the block sizes for writing
and erasing are different.
Interrupts
In practice, I've found it necessary to disable interrupts when writing the flash. So a flash write will be like this:
uint32_t ints = save_and_disable_interrupts(); flash_range_erase (...); restore_interrupts (ints);
It may not be necessary to disable interrupts if there are, in fact, no active interrupts that will run code in the flash. However, this is difficult to ensure -- the USB UART code, for example, will generate interrupts. Failing to suppress interrupts leads to highly unpredictable, always undesirable, behaviour.
So far as I know, the save_and_disable_interrupts()
function
only works on the core from which it is called. This means that
writing flash safely in a dual-core application, with both cores running
code in flash, is likely to be extremely fiddly.
Deciding where to store data
Providing that you start on a 4kB boundary, you can write any part of the flash that isn't used by program code.
If you only want to store a few bytes, deciding where to store those bytes probably isn't a big problem -- just store it at the very end of flash. It's unlikely that the program code will expand to that size and, if it does, you've got bigger problems.
Storing large volumes of data is more of a problem, because
it's all to easy to overwrite the program code.
At runtime, you can determine the end of the program in flash
from the intrinsic variable
__flash_binary_end
(which is a memory address, not an
offset in flash). There's no particular reason to
think that this position will align on a 4kB boundary so, if
you wanted to use it to locate the start of the writeable area
of flash, you'd have to round it up.
However, if you expect the data to remain valid over multiple revisions of the program, it's inadvisable to use flash directly after the program code for storage. If the program code gets larger in later revisions -- as it tends to -- you'll have a conflict.
You can get an idea of the amount of flash occupied by the program,
without running it,
by examining the value of __flash_binary_end
in the
ELF file generated by the compiler:
$ objdump --all myprog.elf | grep flash_binary_end 10043d64 g .ARM.attributes 00000000 __flash_binary_end
In this case, my program is 0x43D64 bytes in size -- that value
is obtained by subtracting XIP_BASE
from the address
in the ELF file.
CMake settings
If you're building using CMake -- and it's difficult-to-impossible
to do otherwise -- you'll need to include the libraries for the
flash and interrupt control functions. In CMakeLists.txt
you'll need something like:
target_link_libraries (my_binary pico_stdlib hardware_flash hardware_sync)
Summary
Here are the key points to bear in mind.
The SDK functions that write flash take an argument that is the offset from the start of flash. The ARM memory address this corresponds to can be obtained by adding
XIP_BASE
.It's almost certainly necessary to disable interrupts when writing flash.
You can write any part of flash that isn't occupied by program code; program code is loaded at the start of flash.
It's probably not advisable to use the part of flash directly after the program, in case the program gets larger in later releases.
One final point: flash memory cannot be rewritten indefinitely. It's unlikely that you'll wear out the flash by uploading program code to it; but if, for example, you put a filesystem in flash, you might have to consider matters like wear leveling.
On the one hand, the low cost of the pico means we might not have to worry too much about this kind of optimization. On the other hand, replacing a Pico that is soldered into a piece of equipment in a remote location might be a bit of a nuisance.