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:
PIN | DESCRIPTION |
VCC | Positive of supply voltage (4.5V to 5.5V) |
GND | Ground of supply |
MISO | Master In Slave Out (SPI) |
MOSI | Master Out Slave In (SPI) |
SCK | Serial Clock (SPI) |
CS | Chip 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:
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’.
2. Go to File>New and paste the library code into the code space.
3. Save the script to Raspberry Pi Pico.
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 sdcard
Code 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
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:
- RPi Pico W: Save Data to Flash Permanently Using MicroPython.
- Raspberry Pi Pico W Data Logger Flash Memory (MicroPython Code) – Example Temperature Logging
Please leave your valuable feedback and queries in the comments below.
Leave a Reply