How to use the RP2040’s Flash in CircuitPython apps

Here’s a very useful technique if you’re working on a CircuitPython program that you need to store data on the host microcontroller’s Flash — and to continue to be able to mount and access the device from your computer. I’ve used it with a Raspberry Pi RP2040-based board, but it should work with other CircuitPython devices too.

A typical mounted CircuitPython device: files are accessible, but the app can’t access the Flash
A typical mounted CircuitPython device: files are accessible, but the app can’t access the Flash

CircuitPython has a great advantage that is also a great disadvantage. It is designed to make the host microcontroller’s Flash accessible to a connected computer as a USB flash drive. This is very handy because it allows you to copy your code across without any extra tools. The downside, of course, is that code can’t write data to disk at the same time. If it could, the two ‘views’ of the Flash — the microcontroller’s and the computer’s — could get out of sync, which is bad.

Credit to CircuitPython’s developers: they understood this might be an issue for some programmers, so provided a solution. You can choose whether you want the computer have access to the device (‘disk mode’) or to allow app-to-Flash writes (‘Flash mode’). Setting up for either mode is easy, but it requires finessing if you’re to support both modes as and when you need them: say when you’re running code that needs access to the Flash, but may latter need to be updated.

CircuitPython’s API to control access to the Flash is this:

storage.remount("/", (True or False))

If you pass True as the second argument, CircuitPython activates disk mode; pass False instead to switch to Flash mode.

This code should be used in your boot.py file so it can switch on Flash mode before your application code, in code.py, starts to run.

In Flash mode, the CircuitPython device will still mount as a drive if connected to your computer, but as a read-only volume. However, your app code can write data — application preferences, for example — to storage.

In Flash mode, the CIRCUITPY disk is locked, but the app can write to Flash
In Flash mode, the CIRCUITPY disk is locked — note the icon — but the app can write to Flash

To flip between the two modes at will, you need some sort of switch. You could connect a button to GPIO or use an on-board button if one is available. My project doesn’t have the latter and I’m keen to avoid the former to reduce clutter. But I do have an I²C segment LED attached via a Stemma QT cable, so I chose to use that as a a proxy switch: if the LED is attached, assume the application is in use so run in Flash mode. Otherwise, the code is to be updated, so run in disk mode.

Again, all this takes place in boot.py so the system is fully prepared when code.py runs. Here’s the detection code:

'''
RP2040 Segment Clock - boot.py
'''
import board
import busio
import storage

# Assume we'll use disk mode
display_present = False

# Instantiate I2C and check whether a display is
# connected. If it is, set the Trinkey Flash to be accessible
# to code; otherwise make it accessible as a USB drive.
i2c = busio.I2C(board.SCL, board.SDA)
while not i2c.try_lock():
    pass
devices = i2c.scan()
if len(devices) > 0:
    for device in devices:
        if int(device) in (0x3C, 0x70):
            display_present = True
            break

# For the second parameter of `storage.remount()`:
# Pass True to make the `CIRCUITPY` drive writable by your computer. 
# Pass False to make the `CIRCUITPY` drive writable by CircuitPython.
# This is the opposite of `display_present`, ie. when the display is 
# not connected, you can update the code
storage.remount("/", (False if display_present is True else True))
print("CIRCUITPY","LOCKED" if display_present is True else "UNLOCKED")

Checking for an I²C device this way is quite handy, and I also use it in the application code so I’m not running unnecessary tasks when the setup is in disk mode.