Pico logging—the practice of using the Raspberry Pi RP2040 microcontroller to poll sensors and write time-series data to non-volatile storage—is one of the most reliable ways to build offline environmental monitors. Unlike ESP32-based loggers that often struggle with deep-sleep current spikes and WiFi brownouts, the Pico W offers a stable, low-power baseline with dual-core processing and native MicroPython support.

This guide targets the Raspberry Pi Pico W running MicroPython v1.22 or newer. We will build a self-contained data logger that reads the RP2040’s internal temperature sensor and writes timestamped CSV data to a MicroSD card via SPI. The code provided is fully compilable out-of-the-box, requiring no external sensor libraries to get your first log file running.

Hardware Spec Sheet & Component Selection

Before wiring, you need to understand the electrical constraints of your components. The Pico operates at 3.3V logic. Feeding 5V into any GPIO pin will permanently damage the RP2040 silicon. Furthermore, not all SD card breakouts are created equal; many cheap modules include 5V level shifters that fail to pass clean 3.3V SPI clock edges, resulting in corrupted writes.

Table 1: Pico Logging Hardware Specifications (2026 Baseline)
Component Exact Variant / Part Number Interface & Logic Active / Sleep Current Avg. Price (2026)
Microcontroller Raspberry Pi Pico W (RP2040) I2C/SPI/UART, 3.3V ~21mA (active) / ~1.3mA (sleep) $6.00
Storage SanDisk 32GB MicroSDHC (Class 10) SPI (via breakout), 3.3V ~20mA (write) / 0.1mA (idle) $8.50
SD Breakout Adafruit Micro SD Breakout (1486) SPI, 3.3V native (no level shifter) N/A (Passive + LDO) $7.50
Sensor (Optional Ext.) Bosch BME280 (Breakout) I2C, 1.8V - 3.6V 3.6µA (sleep) / 360µA (active) $4.50
Bench Warning: Avoid generic "Micro SD Card Reader" modules with a 6-pin LFCN level-shifter IC unless you are powering their VCC with exactly 5V while driving the Pico's 3.3V pins into the module's inputs. For pure 3.3V systems, buy a breakout with no logic translation (like the Adafruit 1486 or SparkFun 13743) to prevent SPI clock skew.

Pin Mapping & Wiring Guide

We will use the Pico’s hardware SPI0 bus for the SD card. Hardware SPI is mandatory here; software (bit-banged) SPI is too slow for SD card block writes and will cause the card's internal controller to time out.

Table 2: Pico W to MicroSD SPI Breakout Pin Mapping
Pico W GPIO RP2040 Pin Function SD Breakout Pin Wire Color (Suggested)
GP18SPI0 SCKCLKYellow
GP19SPI0 TX (MOSI)DI / MOSIGreen
GP16SPI0 RX (MISO)DO / MISOBlue
GP17GPIO (Chip Select)CSOrange
3V3(OUT)Power (3.3V)VCC / 3V3Red
GNDGroundGNDBlack

Wiring Steps

  1. Prep the SD Card: Format your MicroSD card to FAT32. MicroPython’s uos.VfsFat driver does not support exFAT. Use the official SD Memory Card Formatter tool, not your OS's default quick-format, to ensure correct cluster alignment.
  2. Connect SPI Lines: Wire GP18, GP19, and GP16 to the CLK, MOSI, and MISO pins respectively. Keep these wires under 10cm (4 inches) to prevent signal degradation at 1MHz+ baud rates.
  3. Wire Chip Select (CS): Connect GP17 to the CS pin. The RP2040 does not have internal pull-ups on all pins during boot; the SD breakout must have a 10kΩ pull-up resistor on the CS line (most quality breakouts include this).
  4. Power the Bus: Connect 3V3(OUT) and GND. Do not power the SD card from the VBUS (5V) pin unless your specific breakout board has an onboard 3.3V LDO regulator.

MicroPython Firmware & Complete Logging Code

Target Environment: This code is written for the Raspberry Pi Pico W running MicroPython v1.22+. It uses the built-in sdcard module and the RP2040’s internal temperature sensor (ADC4) to guarantee it compiles and runs without downloading third-party sensor libraries.

Save the following code as main.py on your Pico. The script includes robust error handling: if the SD card fails to mount, it gracefully falls back to logging on the Pico's internal flash storage so you don't lose data during a field deployment.

import machine
import sdcard
import uos
import time

# --- PIN DEFINITIONS (SPI0) ---
SPI_SCK = 18
SPI_MOSI = 19
SPI_MISO = 16
SD_CS = 17

# --- INITIALIZATION ---
# Initialize hardware SPI0 at 1MHz (safe baseline for most SD cards)
spi = machine.SPI(0,
                  baudrate=1000000,
                  polarity=0,
                  phase=0,
                  sck=machine.Pin(SPI_SCK),
                  mosi=machine.Pin(SPI_MOSI),
                  miso=machine.Pin(SPI_MISO))

cs = machine.Pin(SD_CS, machine.Pin.OUT)

# Attempt to mount SD card, fallback to internal flash
try:
    sd = sdcard.SDCard(spi, cs)
    uos.mount(sd, '/sd')
    print("[OK] SD Card mounted at /sd")
    log_path = "/sd/pico_log.csv"
except OSError as e:
    print(f"[WARN] SD Card mount failed: {e}. Falling back to internal flash.")
    log_path = "internal_log.csv"

# Initialize RP2040 Internal Temperature Sensor (ADC4)
sensor_temp = machine.ADC(4)
conversion_factor = 3.3 / (65535)

# Write CSV header if the file does not already exist
try:
    with open(log_path, 'r') as f:
        pass # File exists
except OSError:
    with open(log_path, 'w') as f:
        f.write("timestamp_ms,temperature_c\n")
    print(f"[OK] Created new log file: {log_path}")

# --- MAIN LOGGING LOOP ---
print("Starting data logging... Press Ctrl+C to stop.")
try:
    while True:
        # Read ADC and convert to Celsius
        # Formula derived from RP2040 datasheet: 27C baseline, 0.706V offset, -1.721mV/C slope
        reading = sensor_temp.read_u16() * conversion_factor
        temp_c = 27 - (reading - 0.706) / 0.001721
        timestamp = time.ticks_ms()
        
        # Write to storage
        try:
            with open(log_path, 'a') as f:
                f.write(f"{timestamp},{temp_c:.2f}\n")
            print(f"Logged: {temp_c:.2f}C at {timestamp}ms")
        except OSError as e:
            print(f"[ERROR] File write failed: {e}")
            
        # Sleep for 10 seconds (use machine.lightsleep() for battery builds)
        time.sleep(10)
        
except KeyboardInterrupt:
    print("\n[STOP] Logging interrupted by user.")
finally:
    # Safely unmount SD card to prevent filesystem corruption
    if log_path.startswith("/sd"):
        try:
            uos.umount('/sd')
            print("[OK] SD Card unmounted safely.")
        except Exception:
            pass

Debugging: First Three Things to Check When It Fails

SD card logging on microcontrollers is notoriously finicky. If your Pico throws an error on boot, work through this ranked decision path before rewriting your code.

1. The Filesystem Format (Error: OSError: [Errno 19] ENODEV)

The Cause: MicroPython’s FAT driver only understands FAT12, FAT16, and FAT32. If you bought a 64GB or larger SDXC card, your computer likely formatted it as exFAT by default. The Pico cannot read exFAT and will throw an ENODEV (No such device) or EIO (I/O error) when attempting to mount the VfsFat object.

The Fix: Use a tool like diskpart on Windows or mkfs.fat -F 32 on Linux to force a FAT32 format. Note that Windows natively blocks FAT32 formatting for drives larger than 32GB; use the official SD Association Formatter to bypass this.

2. SPI Baudrate & Signal Integrity (Error: OSError: [Errno 5] EIO)

The Cause: The SD card initialized but dropped packets during the block-read phase. This usually happens when the SPI clock speed is too high for the physical wire length, or when using a 5V level-shifter module on a 3.3V bus, causing the clock edge to slew too slowly.

The Fix: Drop the baudrate in the machine.SPI() initialization from 1000000 (1MHz) down to 250000 (250kHz). If it mounts at 250kHz but fails at 1MHz, you have a signal integrity issue. Shorten your jumper wires or switch to a native 3.3V SD breakout.

3. Chip Select (CS) Pin State (Error: RuntimeError: SD card not found or silent hangs)

The Cause: The SPI bus is shared. If the CS pin is not driven HIGH immediately upon boot, the SD card will hog the MISO line, interfering with the RP2040's boot sequence or causing the SPI controller to read garbage data.

The Fix: Ensure your breakout board has a physical 10kΩ pull-up resistor on the CS line. If you are using a raw SD card socket, add a 10kΩ resistor between GP17 and 3V3. In code, you can also force the pin high before initializing SPI: cs = machine.Pin(17, machine.Pin.OUT, value=1).

Extending and Simplifying the Build

Once your baseline logger is running, you will likely want to adapt it for a specific deployment. Here is how to scale the architecture up or down.

Table 3: Architecture Modifications for Pico Logging
Goal Modification Trade-offs & Code Changes
Simplify: Drop the SD Card Log to internal RP2040 Flash using LittleFS. Pros: No wiring, lower BOM cost, lower power.
Cons: Limited to ~1.5MB of space. Requires formatting the Pico's flash with LittleFS via mpremote.
Extend: True Environmental Data Add a Bosch BME280 via I2C (GP4/GP5). Pros: Accurate temp/humidity/pressure.
Cons: Requires importing a third-party bme280.py library. Internal RP2040 temp sensor reads ~3°C high due to PCB heat.
Extend: Real-Time Timestamps Add a DS3231 RTC module via I2C. Pros: Accurate ISO8601 timestamps without WiFi NTP sync.
Cons: Adds a coin cell battery and I2C polling overhead to the main loop.
Extend: Ultra-Low Power Replace time.sleep() with machine.lightsleep(). Pros: Drops current from ~20mA to ~1.5mA.
Cons: USB CDC serial disconnects during sleep; you must rely on physical SD logs rather than Thonny REPL monitoring.

For further reading on the RP2040's internal ADC characteristics and sleep modes, refer to the Raspberry Pi Pico Python SDK documentation. If you are pushing the limits of SD card write speeds and need to understand block alignment, the SD Association Physical Layer Simplified Specification provides the exact timing diagrams required for custom SPI drivers.