Data Logger with Raspberry Pi Pico and Micro SD Card

In this tutorial, we will create a data logger using Raspberry Pi Pico and MicroPython. We will log temperature and humidity readings acquired from DHT22 sensor to a microSD card. We will create a .txt file in our microSD card through programming our board and consequently log temperature and humidity readings to that file after every 30 seconds. Users can use this tutorial, to learn how to acquire data from the DHT22 sensor and store it in a microSD card using Raspberry Pi Pico.

DHT22 Data Logger with Raspberry Pi Pico and Micro SD Card

For a getting started guide to microSD card with Raspberry Pi Pico, read the article: MicroPython: Micro SD Card Interfacing with Raspberry Pi Pico using MicroSD Module

This article is divided into these sections:

  • Introduction to DHT22 sensor and its connection with Raspberry Pi Pico and the microSD card module
  • Formatting the microSD card
  • DHT22 data logging to microSD card (Installing libraries, MicroPython sketch and demonstration)

Prerequisites

Before we start this lesson, make sure you are familiar with and have the latest version of Python3 installed in your system and set up MicroPython in your Raspberry Pi Pico. Additionally, you should have a running Integrated Development Environment(IDE) to do the programming. We will be using the same Thonny IDE as we have done previously when we learned how to blink and chase LEDs in MicroPython here:

If you are using uPyCraft IDE, you can check this getting started guide:

We will require the following components for this user guide:

Required Components

  1. Raspberry Pi Pico board
  2. MicroSD card
  3. MicroSD card module
  4. DHT22 Sensor
  5. Breadboard
  6. Connecting Wires

DHT22 Introduction

The DHT22 is an inexpensive sensor which measures relative humidity and temperature. It provides a calibrated digital output with a 1-wire protocol. It measures temperature and humidity with higher accuracy and supports a wider range as compared to DHT11.

DHT sensors are pre-calibrated. We can directly connect them with our Raspberry Pi Pico to obtain sensor output reading. They are internally composed of a humidity sensing sensor and a thermistor. These two components measure humidity and temperature.

DHT22 Pinout

The following figure shows the pinout diagram of DHT sensors. DHT sensor consists of four pins. But on DHT modules only three pins are exposed to the pinout of the module and the 10k ohm pull-up resistor is internally connected to pin 2.

dht11 dht22 pinout diagram

The following lists the pinout of the DHT sensor and their brief description. Pin number starts from left to right when you hold the sensor from the front end. It also shows how these pins will be connected with our board.

DHT22 Pins Raspberry Pi Pico
1 (VCC)This is the power supply pin (3.3V-5V). We will connect with 5V.
2 (Data)Any GPIO pin of Raspberry Pi Pico along with 10k ohm pull-up resistor. We will use GPIO2
3 (NC)Not used
4 (GND)Ground
  • VCC is the power supply pin. Apply voltage in a range of 3.3 V to 5.0 V to this pin.
  • Data Out is the digital output pin. It sends out the value of measured temperature and humidity in the form of serial data
  • N/C is not connected
  • GND: Connect the GND pin

Recommended Reading: DHT11 DHT22 with Raspberry Pi Pico using MicroPython

Interfacing Raspberry Pi Pico with DHT22 and microSD card module

This section shows how to connect Raspberry Pi Pico with DHT22 sensor and the microSD card module.

The DHT22 sensor has 4 terminals which we will connect with Raspberry Pi Pico. As it is 5V tolerant, hence we will connect the VCC terminal with 5V pin of the board. The data out pin will be connected with GPIO2 with 10k ohm pull-up resistor. The resistor is not required if you are using the DHT22 module instead. You can also choose any other appropriate Raspberry Pi Pico GPIO pin to connect with data out. The third pin is not used.

Additionally, we will connect the VCC terminal of microSD card module with 5V pin of Raspberry Pi Pico which will be common with the VCC pin of the sensor. All three grounds will be common. The SPI GPIO pins of Raspberry Pi Pico are being used to connect with each of the remaining SPI terminals of the microSD card module. Let us first learn about the Raspberry Pi Pico SPI interface.

Raspberry Pi Pico SPI Pins

Raspberry Pi Pico supports two SPI peripherals. Both SPI module pins are accessible through GPIO pins of Raspberry Pi Pico. The following table shows the connection of GPIO pins with both SPI modules. Each connection of SPI controller pins can be configured through multiple GPIO pins as shown in the figure. But before using SPI, you should configure in software which GPIO pins you want to use with a specific SP peripheral. 

The table below shows the SPI pins of Raspberry Pi Pico.

SPI ControllerGPIO Pins
SPI0_RXGP0/GP4/GP16
SPI0_TXGP3/GP7/GP19
SPI0_SCKGP2/GP6/GP18
SPI0_CSnGP1/GP5/GP17
SPI1_RXGP8/GP12
SPI1_TXGP11/GP15
SPI1_SCKGP10/GP14
SPI1_CSnGP9/GP13

The figure below shows the SPI pins of Raspberry Pi Pico.

Raspberry Pi Pico pinout diagram

Schematic Diagram

Now let us see how to connect the microSD card module and DHT22 with Raspberry Pi Pico.

The table below shows the connections between Raspberry Pi Pico and microSD card module:

MicroSD card module Raspberry Pi Pico
GNDGND
VCC5V
CSGP9 (SPI1_CSn)
MOSIGP11 (SPI1_TX)
SCKGP10 (SPI1_SCK)
MISOGP8 (SPI1_RX)

The table below shows the connections between Raspberry Pi Pico and DHT22 sensor:

DHT22 SensorRaspberry Pi Pico
1 ( VCC)5V
2 (Data Out)GP2
3 (NC)NC
4 (GND)GND

As shown from the tables, we will connect 5V pin of Raspberry Pi Pico with the VCC terminal of the MicroSD card module and VCC pin of DHT22 sensor. All three grounds will be common. For microSD card module, we have used the same connections for SPI pins as specified in the table above. However you can use other combinations of SPI pins as well.

Raspberry Pi Pico with microSD card module and DHT22 sensor connection diagram
Raspberry Pi Pico with microSD card module and DHT22 sensor connection diagram
Raspberry Pi Pico with microSD card module and DHT22 sensor

Now, as we know how to interface the microSD card module, DHT22 sensor and Raspberry Pi Pico together let us learn how to prepare the microSD card for file handling in MicroPython.

Formatting the MicroSD card

As we have to use our microSD card with Raspberry Pi Pico, so we would have to format it as FAT32. We will have to follow a series of steps to accomplish it successfully.

  • First, insert your microSD card in your laptop/computer. Now go to ‘This PC’ and click on SD card icon. Then click on Format by right clicking the SD card icon.
microSD card formatting pic1
  • The following window will appear. Select FAT32 from the dialog box of ‘File System’ and click on ‘START.’
microSD card formatting pic2
  • You will receive a warning message that formatting will erase all previous data saved on the microSD card. Click ‘OK.’
microSD card formatting pic3
  • After a few moments, your microSD card will be formatted successfully. Click ‘OK.’
microSD card formatting pic4

Installing SD Card Library

For this project we will require sdcard.py library. Copy this library and save it in your Raspberry Pi Pico with the respective file name from the GitHub link.

Open a new file in Thonny. Copy the library given below or from the link given above. Save it to Raspberry Pi Pico with the name sdcard.py under the lib folder.

sdcard.py

"""
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 | csd[7] << 2 | (csd[8] & 0b11000000) << 4
            c_size_mult = ((csd[9] & 0b11) << 1) | csd[10] >> 7
            self.sectors = (c_size + 1) * (2 ** (c_size_mult + 2))
        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):
            self.cmd(55, 0, 0)
            if self.cmd(41, 0, 0) == 0:
                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)
                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
                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):
        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):
        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

Raspberry Pi Pico MicroPython: DHT22 Data Logging on microSD card

Open your Thonny IDE and go to File > New to open a new file. Copy the following code in that file.

This sketch will acquire sensor data from the DHT22 and save it in a .txt file on the micro SD card. The sensor data will consist of current temperature in Celsius and humidity in percentage. New readings will be added after every 30 seconds.

from machine import Pin
from time import sleep
import dht
import sdcard
import uos

sensor = dht.DHT22(Pin(2)) 

CS = machine.Pin(9, machine.Pin.OUT)
spi = machine.SPI(1,baudrate=1000000,polarity=0,phase=0,bits=8,firstbit=machine.SPI.MSB,sck=machine.Pin(10),mosi=machine.Pin(11),miso=machine.Pin(8))

sd = sdcard.SDCard(spi,CS)

vfs = uos.VfsFat(sd)
uos.mount(vfs, "/sd")
 
while True:
    sensor.measure()
    Temperature = sensor.temperature()
    Humidity = sensor.humidity()
    print("Temperature: {}°C   Humidity: {:.0f}% ".format(Temperature, Humidity))
    
    file = open("/sd/data.txt", "a")
    print("Writing to data.txt...")
    file.write("Temperature: {}°C   Humidity: {:.0f}% \r\n".format(Temperature, Humidity))
    file.close()
    
    print("Done")
    print("")
    sleep(30)

How the Code Works?

Now let us understand how each part of the code works.

Importing Libraries

We will start by importing the Pin class from the machine module and the sleep class from the time module. We will also import dht, sdcard and uos modules required for this task.

from machine import Pin
from time import sleep
import dht
import sdcard
import uos

Next, we will define a dht object named ‘sensor’ and assign the data pin to it. Here we are using DHT22 sensor with data pin connected at GPIO2.

sensor = dht.DHT22(Pin(2))

Configure the GPIO pin connected with the CS pin of microSD card module as an output pin. This is done by using the Pin() method and passing the GPIO number as the first parameter and Pin.OUT as the second parameter.

CS = machine.Pin(9, machine.Pin.OUT)

The next step is to initialize the SPI interface with the required parameters including the SPI channel number, baud rate, SPI pins etc:

spi = machine.SPI(1,baudrate=1000000,polarity=0,phase=0,bits=8,firstbit=machine.SPI.MSB,sck=machine.Pin(10),mosi=machine.Pin(11),miso=machine.Pin(8))

Initializing the microSD card

The following line of code will initialize the microSD card using the SDcard() function on the sdcard library. The function takes in the spi object as the first parameter and the CS Pin as an second parameter. Thus, it will start the SPI communication at the stated parameters.

sd = sdcard.SDCard(spi,CS)

Next, we will mount the filesystem using the following lines of code:

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

Data Logging

Inside the infinite loop, we will obtain the temperature and humidity reading and save it in ‘Temperature’ and ‘Humidity’ respectively. These will be printed in the shell console after a delay of 2 seconds.

 sensor.measure()
    Temperature = sensor.temperature()
    Humidity = sensor.humidity()
    print("Temperature: {}°C   Humidity: {:.0f}% ".format(Temperature, Humidity))

Next, we will open the data.txt file on the microSD card using open() and specify the first parameter as the file name and second parameter ‘a’ indicating that we want to append to the text file. If the file does not exist, it will get created. In the Thonny shell terminal we will print “Writing to data.txt…”

Using the write() method on the file object, we will log the temperature and humidity readings in the data.txt file. After that, we will close the file using file.close(). This will ensure that the data written on the file gets saved as well. In the Thonny shell terminal we will print “Done.”

We will acquire the sensor data every 30 seconds and save it in our data.txt file.

file = open("/sd/data.txt", "a")
    print("Writing to data.txt...")
    file.write("Temperature: {}°C   Humidity: {:.0f}% \r\n".format(Temperature, Humidity))
    file.close()
    
    print("Done")
    print("")
    sleep(30)

Reading data from file

Now, we will read the data which we just wrote on our data.txt file. To do that we will first open the data.txt file by using open() method and specify the file name as the first parameter and ‘r’ as the second parameter indicating that we want to read from the file. Then by using read() method on the file object, we will read the data.txt file and print the data in the serial monitor.

with open("/sd/data.txt", "r") as file:
    print("Reading data.txt...")
    data = file.read()
    print(data)

Demonstration

Upload the above code as main.py file to Raspberry Pi Pico. You will view the messages in the Thonny shell terminal that we are writing to the data.txt file. The data written to the file (highlighted in red) can also be seen printed on the terminal:

Raspberry Pi Pico DHT22 data logging on microSD card thonny shell

After a few minutes, take the microSD card out of its module and insert it in your system to view the data.txt file.

Raspberry Pi Pico with microSD card 1

Inside the file you will be able to view the sensor readings consisting of Temperature in degree Celsius followed by humidity in percentage.

Raspberry Pi Pico DHT22 data logging on microSD card

Conclusion

In conclusion, we learned how to successfully log DHT22 sensor data on a micro SD card with Raspberry Pi Pico.

You may like to read these SD card guides for Arduino and sensors:

SD card interfacing with other development boards:

Leave a Comment