The Raspberry Pi Pico is a new microcontroller from the Raspberry Pi Foundation. I won’t cover its features here, many other sources have already done a great job of this – check out the Tom’s Hardware article, and the official page from the Raspberry Pi Foundation.

One of the great things about the Pico is the availability of a public fork of MicroPython. It’s very much in its infancy, having been merged upstream less than a week ago on GitHub – as such, some of the features that are present in other MicroPython ports haven’t quite made their way to the Pico yet. In this article I’m going to focus on the steps necessary to compile MicroPython for the Pico, with support for an SD card over an SPI interface, and a FAT driver with support for both FAT16 and FAT32. If you already have a MicroPython build environment, skip to the “Customising the Pico Port” section.

Compiling MicroPython

The steps to compile MicroPython are simple. This article assumes a Linux environment – go ahead and set up a VM if you don’t already run a Linux distribution, or these steps can also be followed on a Raspberry Pi. To start with we’ll build the MicroPython source as it exists, to validate our build environment, before making any of the changes necessary to support an SD card and the FAT filesystem.

First, it’s a good idea to create a directory to keep all Raspberry Pi Pico related checkouts in. The following instructions create a pico directory in your home folder.

$ cd ~/
$ mkdir pico
$ cd pico

Then, clone the MicroPython git repository. This instruction differs slightly from the Raspberry Pi Pico Python SDK documentation at the time of writing, in that it clones from the MicroPython main repository, rather than the fork owned by RaspberryPi. I believe the documentation will soon be updated to reflect this change, too, as the Pico port of MicroPython is now held upstream.

$ git clone https://github.com/micropython/micropython.git

Once the download is complete, the MicroPython source code should be available in a new directory called micropython. The MicroPython repository contains submodules to specific libraries it needs to run on a particular board (in our case, the RP2040/Pico SDK). Use the following commands to fetch these:

$ cd micropython
$ git submodule update --init -- lib/pico-sdk
$ cd lib/pico-sdk
$ git submodule update --init

To build MicroPython, you’ll need some extra packages. This article assumes a Debian-based system (Raspbian, Ubuntu, etc.). If you have previously set up an environment to build for the Pico with the C SDK, the following step is unnecessary.

$ sudo apt update
$ sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential

Now we have the necessary tools, we can begin compiling MicroPython. To start building, you’ll need to change directory into the root directory containing the MicroPython source code. If you’ve followed all the instructions so far, you’ll need to go up two directories.

$ cd ../..

First, MicroPython requires you build a specific bootstrap tool, which ships with the source code. This is relevant to all MicroPython ports, and is not specific to the Pico.

$ make -C mpy-cross

Now that’s complete, we can build the RP2040 specific port. Notice I include the -j4 parameter in the make command, this tells make how many processes it may run simultaneously – 4 processes in this case. You could change this to a different number based on the number of cores/threads available on your system. The Raspberry Pi 4 has 4 cores with 4 threads, so this is a safe number. The system I use has 8 cores with 16 threads, so I use -j16 to allow make to fully utilise the resources I have.

$ cd ports/rp2
$ make -j4

If everything went well here, there should be a new directory called build within the ports/rp2 directory. Within this, there will be several files related to the build process. The most important of which is the firmware.uf2 file – this is the UF2 binary file, which can be copied to a Pico in BOOTSEL mode, that contains the MicroPython interpreter we’ve just built. The firmware.elf file is helpful too – it can be loaded by a debugger (e.g. gdb with openocd, instructions to achieve this can be found on the Raspberry Pi website, or maybe a following article will show how to achieve this with another Pico).

Go ahead and copy the newly built firmware.uf2 file to a Pico, and validate that it works. I prefer to use the Thonny IDE to do this, though you can use basically any serial terminal software. If you see a MicroPython prompt in your preferred IDE/serial terminal, you’ve succeeded in building MicroPython for a Pico.

Customising the Pico Port

Now we’ve validated that our MicroPython build environment works, and generates working code for a Pico, we can start customising the RP2040 MicroPython port to add native support for an SD card and the FAT filesystem. All of our work from here will focus on the ports/rp2 directory. If you’ve followed the instructions so far, your current working directory will already be set here.

Adding Python libraries – enabling SD card support

It is possible to add Python libraries to the build process, such that they are available permanently. Have a look at the manifest.py file, right now it’ll probably contain these three lines:

freeze("modules")
freeze("$(MPY_DIR)/drivers/onewire")
include("$(MPY_DIR)/extmod/uasyncio/manifest.py")

The freeze command adds a Python module, contained within a directory, to the MicroPython build. In the first line, we see the modules directory being “frozen” – this is the modules directory in the specific MicroPython port, in our case contained in the ports/rp2 directory. The majority of MicroPython ports contain a _boot.py file, which sets up the execution environment. Most often you’ll observe a root filesystem being mounted in this file.

The second line uses the $(MPY_DIR) environment variable. This expands to the root of the MicroPython source – in our case ~/pico/micropython – and can be used to include non-port specific drivers. If we browse the MicroPython root source directory, we can see an SD card driver in micropython/drivers/sdcard, containing sdcard.py and sdtest.py.

You may notice that the sdtest.py file is actually a test routine. We can use this code to test later, but it relies on working filesystem support, so we’ll look at this later in the article. Add this line to the manifest.py file to include, or “freeze”, the sdcard module.

freeze("$(MPY_DIR)/drivers/sdcard")

Now we’ve included the sdcard module, rebuild MicroPython. From the ports/rp2 directory, run the following commands.

$ make clean
$ make -j4

A new firmware.uf2 file will be generated. Place your Pico into BOOTSEL mode. This can be done within the MicroPython interpreter with the following commands.

>>> import machine
>>> machine.bootloader()

Copy the new firmware.uf2 file to your Pico, and reconnect your preferred IDE/serial terminal. Next, wire up your Pico to an SD card module. This article will assume you’re using the following pins, which are the default pins for the SPI0 interface in this port of MicroPython.

Pico PinGPIO pinSD module pin
6GP4 (SPI0 RX)DO
9GP6 (SPI0 SCK)CLK
10GP7 (SPI0 TX)DI
34GP28CS
Pico->SD card module wiring table

While wiring your Pico to an SD card module, you must use pins on the same SPI interface (SPI* RX, SCK, TX). However, the CS (Chip Select) pin is arbitrary and may be assigned to any unused GPIO pin. I’ve hooked my Pico up to an Adafruit Micro SD card breakout module.

Adafruit Micro SD card breakout module, hooked up to a Raspberry Pi Pico on a breadboard.

Once hooked up, it’s time to test we can read from the SD card! I’ve written the following program, which you could use to test. Important to note, is that the FAT filesystem still isn’t available to us, so we can only read raw data from the SD card.

from machine import Pin, SPI
import sdcard

def sdrawtest():
    sd = sdcard.SDCard(SPI(0), Pin(28))    
    buf = bytearray(512)
    
    sd.readblocks(0, buf)
    
    for i in range(0, 512):
        if i % 16 == 8:
            print('    ',end='')
            
        if i % 16 == 0:
            print()
            print('{:04X}'.format(i), end=': ')
    
        print('{:02X}'.format(buf[i]), end=' ')
    
sdrawtest()

This program will read the first sector, often used as the Master Boot Record, from the SD card and output the contents in a hexadecimal format. While running it, a number of errors may occur – if this happens to you, double check your wiring, and try again. Otherwise, once satisfied that reading raw data is working, we can move onto the next step – compiling FAT support for MicroPython.

Adding C modules – enabling FAT support

MicroPython’s implementation of the FAT filesystem is written in C code, primarily in the micropython/extmod/vfs_fat.c file. Within a few lines, we can see the file requires MICROPY_VFS_FAT to be declared – and includes the py/mpconfig.h file, which in turn includes an mpconfigport.h file from the specific MicroPython port. In our case, the file is located in micropython/ports/rp2/mpconfigport.h. The Pico port’s version of this file is well documented, look for the section labelled “Extended modules”. At the time of writing the following two lines are at the end of the section.

#define MICROPY_VFS                             (1)
#define MICROPY_VFS_LFS2                        (1)

As we can see, VFS support is being compiled, as is LittleFS2. However, FAT is nowhere to be seen! Add the following line to this section, to enable compiling the FAT module.

#define MICROPY_VFS_FAT                         (1)

This is certainly enough to allow the build system to compile the vfs_fat.c file we looked at earlier. However, enabling this is not enough – the FAT implementation requires some supporting defines. Insert the following lines into mpconfigport.h, which I’ve derived from other MicroPython ports. Where exactly in the file matters little, but I’d recommend including them just after the “Extended modules” section.

// fatfs configuration
#define MICROPY_FATFS_ENABLE_LFN                (1)
#define MICROPY_FATFS_LFN_CODE_PAGE             437 /* 1=SFN/ANSI 437=LFN/U.S.(OEM) */
#define MICROPY_FATFS_RPATH                     (2)
#define MICROPY_FATFS_NORTC                     (1)

One of these defines may stand out, MICROPY_FATFS_NORTC. The RP2040 certainly does contain a Real-Time Clock. However, at the time of writing, RTC functionality for the RP2040 hasn’t yet been ported to MicroPython, although examples do exist in the Pico SDK. I’m not sure if the RTC in RP2040 is capable of ticking while the board is powered off, however, so enabling this support may not prove useful without access to a time server.

Now we’ve enabled the FAT filesystem driver, and provided some configuration, we can recompile MicroPython and test it works. Rebuild MicroPython using the same steps as before, and copy it to your Pico. Before we test the FAT implementation, ensure the root directory of your SD card has some files. The contents won’t matter for our test. With some files on your SD card, run the following program to test the FAT implementation, and list the files in the root of the SD card.

from machine import Pin, SPI
import uos

def fattest():
    sd = sdcard.SDCard(SPI(0), Pin(28))
    uos.mount(sd, '/sd')
    
    for name in uos.listdir('/sd'):
        print(name)

    uos.umount('/sd')

fattest()

If this program lists the contents of your SD card’s root directory, then our work is complete! We now have a MicroPython binary for the RP2040, compiled with support for SD cards and the FAT filesystem. Keep an eye out here for more content on the Raspberry Pi Pico!

Edit to add: I have submitted a pull request to the MicroPython GitHub repo, for the changes highlighted above to be included in future builds:
https://github.com/micropython/micropython/pull/6840

13 thoughts on “Raspberry Pi Pico – SD card in MicroPython

  1. Beano says:

    Just wanted to say thanks for the really clear tutorial on getting a SD Card Reader breakout board working with the Raspberry Pico. Following this I was able to get an old “full size” SD Card module working with the Pico – now tested reading and writing files successfully 🙂 – Breakout board tested was a “MH – SD Card Module” using full size SD cards, tested with a “Canon” 16Mb (FAT) card and a much more useful “Transcend” 4Gb SD HC (FAT32) card with no issues, and tested with a “SanDisk” 2Gb (FAT32) which failed – usually with time-out and card-not-found errors.
    Once again thanks for a really well written tutorial.

  2. A says:

    Great work; thanks! I’m just starting to need this.

    But is this a typo in the last example?
    print(name) uos.umount(‘/sd’)

  3. stereorocker says:

    Hey, thanks @Beano! I’m glad I could help you, and really great to hear back that this is working for others as well.

    @A – Great spot, yes, this is a typo. I’ve now corrected it, thanks for your help!

  4. Timmi says:

    Hi – Many thanks for this. I did have a wee issue though: It wasn’t building due to not finding tusb.h, so I had to add the tinyusb lib as in:

    git submodule update –init — lib/pico-sdk lib/tinyusb

  5. stereorocker says:

    Hi @Timmi, thanks for sharing your experience! I’m sure this will help anyone else who has the same problem as you.

  6. Mike says:

    I’ve just had the same error as Timmi – “not finding utusb.h. I tried the fix recommended and I’m still getting the same error. Any suggestions would be appreciated.

  7. HITCHER says:

    I did this today with micropython SDK 1.1 (from git master), and added the 250MHz overclock (+100% speed).
    Found description for overclock there:
    https://www.raspberrypi.org/forums/viewtopic.php?f=146&t=304360

    You can download the firmware i just compiled in follogin link, and test ist right away.
    https://tinyurl.com/yfnwp5oo
    (NOTE FROM WEBSITE AUTHOR: THE ABOVE URL HAS BEEN VALIDATED TO CONTAIN A UF2 BINARY FILE WITHIN AN ARCHIVE. USE THIS BINARY AT YOUR OWN RISK. STEREOROCKER ACCEPTS NO LIABILITY FOR THE CONSEQUENCES OF USING A BINARY FROM A THIRD PARTY SOURCE.)

    I’m happy, sdcard is working now!

  8. Mike says:

    Update on my march 16 post. I changed a couple items in the following 4 lines.
    $ cd micropython
    $ git submodule update –init — lib/pico-sdk
    $ cd lib/pico-sdk
    $ git submodule update –init

    First, I added lib/tinyusb to the second line as Timmie recommended.
    Second, after the last line in that group, I did the following
    $ cd ../tinyusb
    $ git submodule update –init

    I have a theory about why this was needed, but I’m not enough of a git-person to say.

  9. Mike says:

    One more comment – As of 3/17, when I pulled micropython from GIT, all the changes in the “Adding C modules – enabling FAT support” section except one were already in the mpconfigport.h file. The one line not included was

    #define MICROPY_FATFS_NORTC (1)

    I didn’t add this or recompile micropython and both sdrawtest() and fattest() worked perfectly.

    Thanks a ton to Stereo rocker for posting this.

  10. stereorocker says:

    Thanks Mike, I’m glad I could help!

    Yes, I realise I haven’t updated this article since, but I submitted a pull request to the MicroPython repo to incorporate the FAT driver. In the PR we actually implemented RTC support, and opted not to freeze the SD card module to save space – as the Pico doesn’t have an SD card slot onboard.

  11. Vincent says:

    I’ve managed to get this to work on a Macbook Pro, 32GB SD card, Pico and SD card adaptor. The wiring diagram I followed was https://github.com/printnplay/Diagrams/blob/main/SD%20Card%20wiring%20diagram%20-%20Copy.png.

    To compile on MacOS I needed to:
    $ brew tap ArmMbed/homebrew-formulae
    $ brew install arm-none-eabi-gcc

    My working python script which gets temperature readings and lists the contents of my SD card is below. I will now write the sensor readings to the pico.txt file.

    $ import machine, onewire, ds18x20, time, sdcard, uos
    $ from machine import Pin, Timer, SPI
    $ from time import sleep
    $
    $ ds_pin = machine.Pin(17)
    $ ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))
    $ led = Pin(25, Pin.OUT)
    $ tim = Timer()
    $
    $ roms = ds_sensor.scan()
    $
    $ def tick(timer):
    $ global led
    $ led.toggle()
    $ global ds_sensor
    $ ds_sensor.convert_temp()
    $ for rom in roms:
    $ print(ds_sensor.read_temp(rom))
    $
    $ tim.init(freq=1, mode=Timer.PERIODIC, callback=tick)
    $
    $ def fattest():
    $ spisd = SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
    $ card = sdcard.SDCard(spisd, Pin(13))
    $ uos.mount(card, '/sd')
    $
    $ for name in uos.listdir('/sd'):
    $ print(name)
    $
    $ uos.umount('/sd')
    $
    $ fattest()

  12. Raviolish says:

    I’m no expert here, but I think something’s wrong when I’m trying to do this.

    After doing:
    `$ git submodule update –init`
    in the `rp2/tinyusb` directory, as per the fix Mike posted, which fixed the `tusb.h` error, I get the following:

    `error: RPC failed; curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
    fatal: the remote end hung up unexpectedly
    fatal: early EOF
    fatal: index-pack failed
    fatal: clone of ‘https://github.com/ARM-software/CMSIS_5.git’ into submodule path ‘/home/mainuser/Documents/pico/micropython/lib/tinyusb/lib/CMSIS_5’ failed
    Failed to clone ‘lib/CMSIS_5’. Retry scheduled
    Cloning into ‘/home/mainuser/Documents/pico/micropython/lib/tinyusb/lib/FreeRTOS’…`

    After continuing as normal following the instructions, the firmware.uf2 file is successfully built and placed in the `build-PICO` directory.
    When I try to use this firmware.uf2 file on the Pico, it seems to work properly, but doesn’t connect to Thonny, and shows up as “Board in FS mode” in my connected devices.

    Does anyone know what I could be doing wrong?

  13. Raviolish says:

    Update on my previous comment:
    I got everything to successfully install and update, and have been able to compile micropython just fine. The only issue I have now is that the board disconnects from my computer when the sdcard is plugged into the 3v3 pin.

Leave a Reply to A Cancel reply

Your email address will not be published. Required fields are marked *

You may also like