If you’re coming to the RP2040 chip from the world of AVR microcontrollers like me, you might be surprised to find out that writing user/application data to the onboard 2MB of nonvolatile memory is not as simple as importing an EEPROM library and calling a read() or write() function. Because…it isn’t EEPROM, it’s flash. The main difference being that EEPROM is byte-wise erasable and can endure around 100k write cycles, while flash can only be erased in relatively large chunks and wears out considerably sooner, perhaps after ~20k cycles. But, there’s a lot more of the flash, so that makes the durability issue a bit less painful (especially if we’re careful – read on).

These and other considerations make working directly with the RP2040 onboard flash a bit tricksy if you’re new to it. Currently you need to use a couple of chip-specific functions to erase and write (yes, those are separate functions), and you’ll have to additionally adhere to some fairly strict constraints due to the way in which the hardware works. On the positive side, the address space of the entire flash is memory-mapped in the ARM cores, making reading from it an absolute joy. This article attempts to deconvolute all this, culminating in a simple-to-intermediate example code that works in the Arduino IDE.

Alternatives

Before getting too far into the weeds, I think it’s important to note that there are libraries out there which emulate EEPROM, that you can run on top of the flash. There is also at least one rudimentary file system, which can infuse your application with a rich set of data management functions. So if you want to cut to the chase and use one of those, feel free! But if your needs are simpler (I just needed to keep track of four bytes of data), or just want to learn a bit more about what happens under the hood of those tools, stick around.

one flash to rule them all

First, a bit about the layout of the flash. This flash is the same device that stores the program code of your microcontroller application. That implies a few things. First and most obvious is that you don’t want to corrupt your application code by overwriting it with your data. Fortunately, the application code is always programmed to the front of the flash. So you should ensure that your data is, conversely, written at the end. There are a few handy macros in the headers that facilitate this:

PICO_FLASH_SIZE_BYTES # The total size of the RP2040 flash, in bytes
FLASH_SECTOR_SIZE     # The size of one sector, in bytes (the minimum amount you can erase)
FLASH_PAGE_SIZE       # The size of one page, in bytes (the mimimum amount you can write)

To put some numbers to these, on the RP2040 chip, PICO_FLASH_SIZE_BYTES is 2MB or 2097152 bytes. FLASH_SECTOR_SIZE is 4K or 4096 bytes. FLASH_PAGE_SIZE is 256 bytes. As I mentioned above, my use case is keeping track of 4 bytes. So I only need one sector of flash – the minimum amount that you can erase. It should be the last physical sector to eliminate the chance that it will interfere with program code. That sector starts at the address:

PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE

This value can and will be used directly as an address into the flash, below. This will keep my user data well away from the program code.

SDK functions to manage RP2040 flash

There are two functions in the Pi Pico SDK used to write into the flash:

flash_range_erase(uint32_t flash_offs, size_t count);
flash_range_program(uint32_t flash_offs, const uint8_t *data, size_t count);

The prototypes for these, as well as the macros FLASH_SECTOR_SIZE and FLASH_PAGE_SIZE, are in hardware/flash.h. Due to the way these functions are stored in the library file (with C naming conventions), this header file needs to be included in the Arduino IDE like:

extern "C" {
  #include <hardware/flash.h>
};

The flash_range_erase() function resets count bytes of flash (which needs to be a multiple of the sector size, 4096) beginning at address flash_offs, to 0xFF (all ones). This task appears to be the failure mode of flash when it wears out, as some of the bits won’t be flipped from zero back to one. Thus, you want to do this as infrequently as possible on each sector, to avoid wearing the media out. Once you have one or more sectors in this known state, use the flash_range_program() function to program one or more 256-byte pages (stored in *data) to the count bytes beginning at address flash_offs. Remember that in this case count needs to be a multiple of FLASH_PAGE_SIZE (256). It will then flip some of the bits to zeroes, in order to program one or more pages of flash to the values stored in *data. In my case I want to write one 32-bit integer to the first four bytes of the first page of the last sector:

int buf[FLASH_PAGE_SIZE/sizeof(int)];  // One page worth of 32-bit ints
int mydata = 123456;  // The data I want to store

buf[0] = mydata;  // Put the data into the first four bytes of buf[]

// Erase the last sector of the flash
flash_range_erase((PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE), FLASH_SECTOR_SIZE);

// Program buf[] into the first page of this sector
flash_range_program((PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE), (uint8_t *)buf, FLASH_PAGE_SIZE);

Note that when I called flash_range_program(), I cast the page buffer into (uint8_t *), as the function expects a pointer that increments on one byte boundaries. It then happily puts the four bytes of my int at the first four positions of the specified page of flash. During this exercise I experimentally determined that RP2040 is little endian, in case you were curious.

don’t interrupt me

For reasons that I don’t fully understand, but which are related to the fact that the ARM cores are executing memory-mapped code out of the same flash device that you’re manipulating with the above functions, they are not interrupt-safe, or thread-safe. And your application may not be using interrupts, but the board (USB for example) may be doing so on your behalf. So, you must diligently sandwich these function calls between additional calls that save and disable interrupts, then restores them:

extern "C" {
  #include <hardware/sync.h>
  #include <hardware/flash.h>
};

uint32_t ints = save_and_disable_interrupts();
flash_range_program((PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE), (uint8_t *)buf, FLASH_PAGE_SIZE);
restore_interrupts (ints);

Furthermore, if you are executing code on both cores, you must manually ensure that no execute-in-place accesses of flash occur during erasing/programming. Hopefully if you are that advanced you are not here reading this, because as a hobbiest I am currently only tangentially aware of such capabilities on a microcontroller architecture.

Reading from memory-mapped flash

The ARM cores have the entire address space of the flash memory-mapped. What that means is that programatically, you simply create a pointer, set its address to the desired location in flash, and read the value directly as if it were in RAM. This makes traversing the flash for reads very simple as you can just increment a pointer in a loop and go. One minor bookeeping item that you must take into account is that the RAM is included in this address space. This means that the memory-mapped flash addresses will be offset by the RAM size as compared to the addresses we used above when erasing and programming the flash. On the RP2040, there are XIP_BASE bytes of RAM. So when you compute addresses for reading, use that as an additional offset. Here I grab an int from the first four bytes of the final sector:

// Flash-based address of the last sector
#define FLASH_TARGET_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)

int *p, addr, value;

// Compute the memory-mapped address, remembering to include the offset for RAM
addr = XIP_BASE +  FLASH_TARGET_OFFSET
    p = (int *)addr; // Place an int pointer at our memory-mapped address
    value = *p; // Store the value at this address into a variable for later use
}
Wear protection

This heading refers not to drug store products, but protecting your flash from wear by minimizing the number of times you need to erase it. In my simple example here, I’d want to avoid erasing the sector until I’ve used up all 16 pages of it. Since I’m writing one page at a time (really, just four bytes but let’s make one page the smallest unit we consider here for simplicity), I should be able to do16 writes (there are 16 pages per sector) before I have to erase again. So each time I want to update my value, I will read through the sector one page at a time until I find the first empty page, then put the new value there. Similarly, when I want to read my value, I’ll read the sector one page at a time until I find the last page that holds a value. By convention, this will be the “current” value of my data and we will ignore the rest, if any.

// Flash-based address of the last sector
#define FLASH_TARGET_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)

int *p, page, addr, lastval, newval;
int first_empty_page = -1;

for(page = 0; page < FLASH_SECTOR_SIZE/FLASH_PAGE_SIZE; page++)
{
    addr = XIP_BASE +  FLASH_TARGET_OFFSET + (page * FLASH_PAGE_SIZE);
    p = (int *)addr; // place an int pointer at our memory-mapped address
    // 0xFFFFFFFF cast as an int is -1 so this is how we detect an empty page
    if( *p == -1 && first_empty_page < 0){
      first_empty_page = page;  // we found our first empty page, remember it
    }
}

With that information, I can either get the (latest) value from the previous page, or write an updated value on the first empty one. By wrapping my data storage/retrieval routines in this code, I’ve increased the lifetime of these sectors of flash by a factor of 16. If your application is updating data frequently enough that you are still concerned about wear, you could get even more granular and use un-programmed sub-pages of flash, programming over the same page more than once (it will only ever flip remaining ones to zeroes, so this would be valid if you do your bookeeping correctly!). There is at least one somewhat famous example of why this might be important.

Github example

The above code fragments were for teaching purposes. A full working example of a simple RP2040 flash-writing code, including the wear-leveling logic is available on my github. Fire up the Arduino IDE, enable the Pi Pico (or your favorite supported RP2040-based board) through board manager, load it up and go. Each time it executes, it will write one int to the first four bytes of the first empty page in the last sector of flash. Once the sector is full, it will erase it and start over.