Interface Raspberry Pi Pico with Micro SD Card Module (MicroPython Code)

When you want to expand the storage capacity in your Raspberry Pi Pico projects, an SD card is a game changer. An SD card is an ideal choice to log data from sensors, save user preferences, or store large files like images and audio. But how to read and write to an SD card from Raspberry Pi Pico? A micro SD card module allows us to connect a micro SD card to microcontrollers easily.

This article will guide you step-by-step on how to interface a micro SD card module with Raspberry Pi Pico. Learn how to wire them together and read and write data using MicroPython code. By the end, you’ll have a reliable way to store data that persists even when Raspberry Pi Pico is powered down.

Prerequisites and Setup

Components Required:

  • A Raspberry Pi Pico development board
  • A micro SD card module
  • Connecting wires and breadboard

Your Raspberry Pi Pico needs to be flashed with a MicroPython UF2 file to program it in MicroPython. You can read our getting started guide for Raspberry Pi Pico where we show all the steps required to start programming Raspberry Pi Pico & Pico W. If you are using macOS, follow our guide to program Raspberry Pi Pico on macOS using Thonny IDE.

The Micro SD Card Module

Here is an image of a generic Micro SD Card Module that we shall use in this guide:

Pinout configuration of the micro SD card module:

PINDESCRIPTION
VCCPositive of supply voltage (4.5V to 5.5V)
GNDGround of supply
MISOMaster In Slave Out (SPI)
MOSIMaster Out Slave In (SPI)
SCKSerial Clock (SPI)
CSChip Select (SPI)

The micro SD card module consists of:

  • A micro SD card slot
  • AMS1117 LDO voltage regulator
  • 74HC125 Quadruple tri-state buffer IC
  • SPI interface pins
  • Passive components such as resistors and capacitors

The micro SD card module supports FAT filesystem and Micro SDHC up to 32GB. The tri-state buffer IC acts as a logic level converter.

The module can be interfaced using a Serial Peripheral Interface (SPI). SPI is a synchronous serial communication protocol used primarily for short-distance communication between microcontrollers and peripheral devices. SPI supports bi-directional data transfer (full-duplex protocol).

The operating voltage of a micro SD card is around 3.3V. The onboard voltage regulator and logic shifter help to connect the module directly with even 5V-operated dev boards such as the Arduino UNO. Here we shall power the module using 5V output from the VBUS pin in Raspberry Pi Pico.

Wiring Raspberry Pi Pico with Micro SD Card Module

The Pico has two SPI peripherals with a programmable clock rate and programmable data size. The two SPI peripherals are named SPI0 and SPI1. Here is a pinout diagram with the SPI pins highlighted:

Raspberry Pi Pico W SPI Pinout

Our Raspberry Pi Pico Pinout Guide article discusses the pinout of Pico in-depth.

Here, we shall use the SPI0 interface in Raspberry Pi Pico to communicate with the Micro SD Card Module. The diagram below illustrates how to connect Raspberry Pi Pico with a Micro SD Card Module using just 6 wires.

You can use any of the ground pins in Raspberry Pi Pico. (Tip: The third pin from every corner in Pico is a GND pin). If you wish to use other SPI pins in Pico, ensure that you make the necessary changes in the code.

Also read another article that uses SPI in Pico: Raspberry Pi Pico with MAX6675 K-Type Thermocouple

Preparing the Micro SD Card

To use the Micro SD Card with Pico, we must first format it in the FAT32 file system.

To format using a computer, you may need a micro SD card reader module such as the one shown below.

Some laptops have in-built micro SD or mini SD card slots which you may use. You might need an adapter for the SD card in this case.

After inserting the micro SD card into the card reader, take a backup of important data before you format.

To format a micro SD card in Windows:

  • Insert a micro SD card in the card reader and connect it to the computer.
  • Open Windows File Explorer and go to the “This PC” section where the SD card will appear as a removable drive.
  • Right click on the removable drive and select “Format.”
  • Select FAT32 as the file system.
  • Click “Start” and wait for the process to complete.

For any other OS, follow this guide on the Sunfounder website to format your SD card.

After formatting the micro SD card in FAT32 format, insert it into the micro SD card module.

Now that all the setup for hardware components is done, let us learn about the software part.

MicroPython Library

To keep our main code simple, we shall use a MicroPython library to interface a micro SD card with Raspberry Pi Pico. You can also check out the GitHub link of the library.

Here is the full code of the MicroPython library:

"""
MicroPython driver for SD cards using SPI bus.

Requires an SPI bus and a CS pin.  Provides readblocks and writeblocks
methods so the device can be mounted as a filesystem.

Example usage on pyboard:

    import pyb, sdcard, os
    sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
    pyb.mount(sd, '/sd2')
    os.listdir('/')

Example usage on ESP8266:

    import machine, sdcard, os
    sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15))
    os.mount(sd, '/sd')
    os.listdir('/')

"""

from micropython import const
import time


_CMD_TIMEOUT = const(100)

_R1_IDLE_STATE = const(1 << 0)
# R1_ERASE_RESET = const(1 << 1)
_R1_ILLEGAL_COMMAND = const(1 << 2)
# R1_COM_CRC_ERROR = const(1 << 3)
# R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
# R1_ADDRESS_ERROR = const(1 << 5)
# R1_PARAMETER_ERROR = const(1 << 6)
_TOKEN_CMD25 = const(0xFC)
_TOKEN_STOP_TRAN = const(0xFD)
_TOKEN_DATA = const(0xFE)


class SDCard:
    def __init__(self, spi, cs, baudrate=1320000):
        self.spi = spi
        self.cs = cs

        self.cmdbuf = bytearray(6)
        self.dummybuf = bytearray(512)
        self.tokenbuf = bytearray(1)
        for i in range(512):
            self.dummybuf[i] = 0xFF
        self.dummybuf_memoryview = memoryview(self.dummybuf)

        # initialise the card
        self.init_card(baudrate)

    def init_spi(self, baudrate):
        try:
            master = self.spi.MASTER
        except AttributeError:
            # on ESP8266
            self.spi.init(baudrate=baudrate, phase=0, polarity=0)
        else:
            # on pyboard
            self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)

    def init_card(self, baudrate):
        # init CS pin
        self.cs.init(self.cs.OUT, value=1)

        # init SPI bus; use low data rate for initialisation
        self.init_spi(100000)

        # clock card at least 100 cycles with cs high
        for i in range(16):
            self.spi.write(b"\xff")

        # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
        for _ in range(5):
            if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
                break
        else:
            raise OSError("no SD card")

        # CMD8: determine card version
        r = self.cmd(8, 0x01AA, 0x87, 4)
        if r == _R1_IDLE_STATE:
            self.init_card_v2()
        elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
            self.init_card_v1()
        else:
            raise OSError("couldn't determine SD card version")

        # get the number of sectors
        # CMD9: response R2 (R1 byte + 16-byte block read)
        if self.cmd(9, 0, 0, 0, False) != 0:
            raise OSError("no response from SD card")
        csd = bytearray(16)
        self.readinto(csd)
        if csd[0] & 0xC0 == 0x40:  # CSD version 2.0
            self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
        elif csd[0] & 0xC0 == 0x00:  # CSD version 1.0 (old, <=2GB)
            c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
            c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7
            read_bl_len = csd[5] & 0b1111
            capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len)
            self.sectors = capacity // 512
        else:
            raise OSError("SD card CSD format not supported")
        # print('sectors', self.sectors)

        # CMD16: set block length to 512 bytes
        if self.cmd(16, 512, 0) != 0:
            raise OSError("can't set 512 block size")

        # set to high data rate now that it's initialised
        self.init_spi(baudrate)

    def init_card_v1(self):
        for i in range(_CMD_TIMEOUT):
            time.sleep_ms(50)
            self.cmd(55, 0, 0)
            if self.cmd(41, 0, 0) == 0:
                # SDSC card, uses byte addressing in read/write/erase commands
                self.cdv = 512
                # print("[SDCard] v1 card")
                return
        raise OSError("timeout waiting for v1 card")

    def init_card_v2(self):
        for i in range(_CMD_TIMEOUT):
            time.sleep_ms(50)
            self.cmd(58, 0, 0, 4)
            self.cmd(55, 0, 0)
            if self.cmd(41, 0x40000000, 0) == 0:
                self.cmd(58, 0, 0, -4)  # 4-byte response, negative means keep the first byte
                ocr = self.tokenbuf[0]  # get first byte of response, which is OCR
                if not ocr & 0x40:
                    # SDSC card, uses byte addressing in read/write/erase commands
                    self.cdv = 512
                else:
                    # SDHC/SDXC card, uses block addressing in read/write/erase commands
                    self.cdv = 1
                # print("[SDCard] v2 card")
                return
        raise OSError("timeout waiting for v2 card")

    def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
        self.cs(0)

        # create and send the command
        buf = self.cmdbuf
        buf[0] = 0x40 | cmd
        buf[1] = arg >> 24
        buf[2] = arg >> 16
        buf[3] = arg >> 8
        buf[4] = arg
        buf[5] = crc
        self.spi.write(buf)

        if skip1:
            self.spi.readinto(self.tokenbuf, 0xFF)

        # wait for the response (response[7] == 0)
        for i in range(_CMD_TIMEOUT):
            self.spi.readinto(self.tokenbuf, 0xFF)
            response = self.tokenbuf[0]
            if not (response & 0x80):
                # this could be a big-endian integer that we are getting here
                # if final<0 then store the first byte to tokenbuf and discard the rest
                if final < 0:
                    self.spi.readinto(self.tokenbuf, 0xFF)
                    final = -1 - final
                for j in range(final):
                    self.spi.write(b"\xff")
                if release:
                    self.cs(1)
                    self.spi.write(b"\xff")
                return response

        # timeout
        self.cs(1)
        self.spi.write(b"\xff")
        return -1

    def readinto(self, buf):
        self.cs(0)

        # read until start byte (0xff)
        for i in range(_CMD_TIMEOUT):
            self.spi.readinto(self.tokenbuf, 0xFF)
            if self.tokenbuf[0] == _TOKEN_DATA:
                break
            time.sleep_ms(1)
        else:
            self.cs(1)
            raise OSError("timeout waiting for response")

        # read data
        mv = self.dummybuf_memoryview
        if len(buf) != len(mv):
            mv = mv[: len(buf)]
        self.spi.write_readinto(mv, buf)

        # read checksum
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        self.cs(1)
        self.spi.write(b"\xff")

    def write(self, token, buf):
        self.cs(0)

        # send: start of block, data, checksum
        self.spi.read(1, token)
        self.spi.write(buf)
        self.spi.write(b"\xff")
        self.spi.write(b"\xff")

        # check the response
        if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
            self.cs(1)
            self.spi.write(b"\xff")
            return

        # wait for write to finish
        while self.spi.read(1, 0xFF)[0] == 0:
            pass

        self.cs(1)
        self.spi.write(b"\xff")

    def write_token(self, token):
        self.cs(0)
        self.spi.read(1, token)
        self.spi.write(b"\xff")
        # wait for write to finish
        while self.spi.read(1, 0xFF)[0] == 0x00:
            pass

        self.cs(1)
        self.spi.write(b"\xff")

    def readblocks(self, block_num, buf):
        # workaround for shared bus, required for (at least) some Kingston
        # devices, ensure MOSI is high before starting transaction
        self.spi.write(b"\xff")

        nblocks = len(buf) // 512
        assert nblocks and not len(buf) % 512, "Buffer length is invalid"
        if nblocks == 1:
            # CMD17: set read address for single block
            if self.cmd(17, block_num * self.cdv, 0, release=False) != 0:
                # release the card
                self.cs(1)
                raise OSError(5)  # EIO
            # receive the data and release card
            self.readinto(buf)
        else:
            # CMD18: set read address for multiple blocks
            if self.cmd(18, block_num * self.cdv, 0, release=False) != 0:
                # release the card
                self.cs(1)
                raise OSError(5)  # EIO
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                # receive the data and release card
                self.readinto(mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            if self.cmd(12, 0, 0xFF, skip1=True):
                raise OSError(5)  # EIO

    def writeblocks(self, block_num, buf):
        # workaround for shared bus, required for (at least) some Kingston
        # devices, ensure MOSI is high before starting transaction
        self.spi.write(b"\xff")

        nblocks, err = divmod(len(buf), 512)
        assert nblocks and not err, "Buffer length is invalid"
        if nblocks == 1:
            # CMD24: set write address for single block
            if self.cmd(24, block_num * self.cdv, 0) != 0:
                raise OSError(5)  # EIO

            # send the data
            self.write(_TOKEN_DATA, buf)
        else:
            # CMD25: set write address for first block
            if self.cmd(25, block_num * self.cdv, 0) != 0:
                raise OSError(5)  # EIO
            # send the data
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                self.write(_TOKEN_CMD25, mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            self.write_token(_TOKEN_STOP_TRAN)

    def ioctl(self, op, arg):
        if op == 4:  # get number of blocks
            return self.sectors
        if op == 5:  # get block size in bytes
            return 512

Copy this code and save it to the flash memory of Raspberry Pi Pico with the filename sdcard.py.

If you use Thonny IDE, then follow these steps to upload the library:

1. Connect Raspberry Pi Pico to your computer using a USB cable. Open Thonny IDE. Navigate to Tools>Options>Interpreter and set the interpreter to ‘Raspberry Pi Pico’.

Thonny IDE select interpreter as MicroPython Raspberry Pi Pico

2. Go to File>New and paste the library code into the code space.

3. Save the script to Raspberry Pi Pico.

Thonny Save to

4. Name the code file as sdcard.py.

MicroPython Code for Micro SD Card

With the library saved to Pico, we can now upload the following code to Raspberry Pi Pico to read to and write from the micro SD card.

import machine
import uos
import sdcard

cs_pin = machine.Pin(17, machine.Pin.OUT)
spi = machine.SPI(0, baudrate=1000000, polarity=0, phase=0,bits=8, firstbit=machine.SPI.MSB,
                  sck=machine.Pin(18), mosi=machine.Pin(19), miso=machine.Pin(16))

sd = sdcard.SDCard(spi, cs_pin)

vfs = uos.VfsFat(sd)
uos.mount(vfs, "/sd")
fn = "/sd/sdtest.txt"

with open(fn, "w") as f:
    print("Writing data to file sdtest.txt...")
    f.write("This is a test for micro SD card\r\n")
    print("Writing to file completed")
    
with open(fn, "r") as f:
    print("Reading data from file sdtest.txt...")
    data = f.read()
    print("Data read completed")
    print("Data:",data)
Code language: JavaScript (javascript)

Save the code as main.py or any other filename with a “.py” extension.

Code Explanation

The code starts by importing three essential modules: machine, uos, and sdcard. The machine module provides access to hardware functionalities of Pico like controlling pins and SPI interfaces. The uos module handles file system operations and the sdcard module provides functions to communicate with an SD card using SPI.

import machine
import uos
import sdcardCode language: JavaScript (javascript)

Next, we define the chip select pin(cs_pin). GPIO 17 is assigned as chip select pin. You can refer to the wiring diagram where we connect GPIO 17 of Pico with the chip select pin of the Micro SD Card Module.

cs_pin = machine.Pin(17, machine.Pin.OUT)

The SPI peripheral is then initialized. SPI0 peripheral is set up with parameters like baudrate, polarity, phase, and bits for communication settings. The specific pins for SCK, MOSI, and MISO are also defined.

spi = machine.SPI(0, baudrate=1000000, polarity=0, phase=0,bits=8, firstbit=machine.SPI.MSB,
                  sck=machine.Pin(18), mosi=machine.Pin(19), miso=machine.Pin(16))

After initializing the SPI bus, the SD card is set up using the sdcard.SDCard class, which takes the spi object and the cs_pin as arguments. This prepares the RPi Pico to interact with the SD card via SPI, using the chip select pin to control communication.

sd = sdcard.SDCard(spi, cs_pin)

A Virtual File System (VFS) object vfs is created using the uos.VfsFat class. This sets up the micro SD card as a FAT file system.

vfs = uos.VfsFat(sd)

The SD card is then mounted to the /sd directory using uos.mount, making it accessible for reading and writing files.

uos.mount(vfs, "/sd")Code language: JavaScript (javascript)

Next, we assign the variable fn the file path /sd/sdtest.txt. This specifies where on the SD card the file sdtest.txt will be stored or accessed.

fn = "/sd/sdtest.txt"Code language: JavaScript (javascript)

To write data to the SD card, the file sdtest.txt is opened in write mode ("w"). This will also create the file if it doesn’t exist. A string is written to the file using f.write. After writing, the file is automatically closed when the block ends. The print statements provide feedback on the writing status.

with open(fn, "w") as f:
    print("Writing data to file sdtest.txt...")
    f.write("This is a test for micro SD card\r\n")
    print("Writing to file completed")Code language: PHP (php)

To verify that data has been successfully written to the micro SD card, we can try to read the data from the micro SD card.

So next, the file sdtest.txt is opened in read mode ("r"), and its content is read and printed. This confirms that the data written earlier is successfully stored on the micro SD card.

with open(fn, "r") as f:
    print("Reading data from file sdtest.txt...")
    data = f.read()
    print("Data read completed")
    print("Data:",data)Code language: PHP (php)

Demonstration

Raspberry Pi Pico with micro SD card module

When you run the code, you must see an output in the terminal of your IDE as highlighted in the screenshot below.

The file (sdtest.txt) that we created can also be viewed in any File Explorer application.

To view the file, I connected the micro SD card to a card reader. When connected to a computer, it appeared as a removable drive in the File Explorer. The file is listed as shown:

Upon opening the file with the Notepad application, I was able to view the contents saved earlier through our code.

Troubleshooting

1. “OSError: no SD card”

This error may be caused by one of the following causes:

  • Incorrect wiring or defective jumper wires. Test the continuity of the wires and re-verify the connection.
  • Micro SD card not properly inserted into the module
  • Unsupported SD card or unsupported file system

2. “OSError: timeout waiting for v2 card”

This error occurs when the micro SD card reader is powered using 3.3V instead of 5V. Connect the VCC pip to either the VBUS pin or the VSYS onboard Raspberry Pi Pico.

Conclusion

In this guide, we discussed how to interface Raspberry Pi Pico with a micro SD card using MicroPython code to communicate via the SPI interface. This guide can be helpful while building many projects where data logging is required.

You can also view our other guides that will help you to log data:

  1. RPi Pico W: Save Data to Flash Permanently Using MicroPython.
  2. Raspberry Pi Pico W Data Logger Flash Memory (MicroPython Code) – Example Temperature Logging

Please leave your valuable feedback and queries in the comments below.


Posted

in

by

Comments

Leave a Reply

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