The Core Decision: Which Raspberry Pi Pico Board for Your Lessons?
When structuring embedded pico lessons for a classroom, makerspace, or your own bench learning, the first hurdle is hardware selection. The Raspberry Pi Pico ecosystem has fractured into several variants, and picking the wrong one leads to mid-lesson interruptions over missing wireless stacks or incompatible pinouts.
Use this decision matrix to select the exact board for your curriculum. Follow the logic down to the default recommendation.
| Condition / Requirement | Board Variant | Approx. Cost (2026) |
|---|---|---|
| Needs WiFi/BLE telemetry (MQTT, HTTP) | Raspberry Pi Pico W (RP2040) | $6.00 |
| Requires 5V logic tolerance or higher clock speeds | Pimoroni Pico Plus 2 (RP2350) | $12.00 |
| Offline sensor logging, maximum GPIO count, standard 3.3V logic | Raspberry Pi Pico 2 (RP2350) | $5.00 |
Project Build: I2C Environmental Datalogger (The Ultimate Pico Lesson)
This project is designed to teach three critical embedded concepts in one build: I2C bus communication, SPI peripheral mounting, and non-blocking sensor polling. We are building a self-contained datalogger that reads temperature and humidity, displays it on an OLED, and logs it to a microSD card.
Difficulty & Time Rating
- Difficulty: Intermediate (2/5) - Requires basic I2C addressing and SPI filesystem mounting.
- Bench Time: 45 minutes for wiring, 15 minutes for code deployment and debugging.
Exact Parts List
- MCU: Raspberry Pi Pico 2 (RP2350) with pre-soldered headers ($5.00)
- Sensor: Adafruit AHT20 Temperature & Humidity Sensor Breakout (Product ID: 4566) ($5.95)
- Display: Adafruit Monochrome 0.96" 128x64 OLED Graphic Display (I2C, Product ID: 326) ($9.95)
- Storage: Adafruit MicroSD Card Breakout Board (Product ID: 254) ($7.50)
- Consumables: Half-size breadboard, 22 AWG solid core jumper wires, 8GB microSD card (FAT32 formatted).
Note: The code provided below specifically targets the Raspberry Pi Pico 2 (RP2350) running MicroPython firmware v1.23.0 or newer. If you are using an original RP2040 Pico, the pin mappings remain identical, but ensure you flash the RP2040-specific .uf2 file.
Wiring and Pin Mapping
Mixing I2C and SPI on the same board requires strict adherence to the Pico's GPIO multiplexing rules. The RP2350 allows I2C and SPI on multiple pins, but sticking to the default hardware blocks (I2C0 and SPI0) prevents DMA routing conflicts in more advanced lessons.
| Pico 2 Pin (GPIO) | Function | Target Component | Wire Color |
|---|---|---|---|
| GP4 (Pin 6) | I2C0 SDA | AHT20 & OLED SDA | Blue |
| GP5 (Pin 7) | I2C0 SCL | AHT20 & OLED SCL | Yellow |
| GP16 (Pin 21) | SPI0 RX (MISO) | MicroSD DO | Orange |
| GP17 (Pin 22) | SPI0 CSn | MicroSD CS | Green |
| GP18 (Pin 24) | SPI0 SCK | MicroSD CLK | Purple |
| GP19 (Pin 25) | SPI0 TX (MOSI) | MicroSD DI | Gray |
| 3V3 (Pin 36) | VCC / Power | All Sensor VCC pins | Red |
| GND (Pin 38) | Ground | All Sensor GND pins | Black |
Complete MicroPython Code with Error Handling
This script is entirely self-contained. It includes a minimal I2C driver for the AHT20 to avoid external library dependencies, ensuring it compiles and runs the moment you hit 'Run' in Thonny. Save this as main.py on your Pico.
# pico_lessons_datalogger.py
# Target: Raspberry Pi Pico 2 (RP2350) / MicroPython v1.23+
import machine
import ssd1306
import sdcard
import os
import time
import framebuf
# --- PIN DEFINITIONS ---
I2C_SDA = machine.Pin(4)
I2C_SCL = machine.Pin(5)
SPI_SCK = machine.Pin(18)
SPI_MOSI = machine.Pin(19)
SPI_MISO = machine.Pin(16)
SPI_CS = machine.Pin(17)
# --- HARDWARE INITIALIZATION ---
# Initialize I2C0 at 400kHz
i2c = machine.I2C(0, sda=I2C_SDA, scl=I2C_SCL, freq=400000)
# Initialize SPI0 for SD Card
spi = machine.SPI(0, baudrate=1000000, polarity=0, phase=0,
sck=SPI_SCK, mosi=SPI_MOSI, miso=SPI_MISO)
# --- AHT20 SENSOR CLASS (Self-Contained) ---
class AHT20:
def __init__(self, i2c, address=0x38):
self.i2c = i2c
self.addr = address
# Initialize sensor
self.i2c.writeto(self.addr, b'\xBE\x08\x00')
time.sleep(0.04)
def read_data(self):
# Trigger measurement
self.i2c.writeto(self.addr, b'\xAC\x33\x00')
time.sleep(0.08)
data = self.i2c.readfrom(self.addr, 6)
# Parse raw bytes
raw_hum = ((data[1] << 12) | (data[2] << 4) | (data[3] >> 4))
raw_temp = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
humidity = (raw_hum / 0x100000) * 100
temperature = ((raw_temp / 0x100000) * 200) - 50
return round(temperature, 2), round(humidity, 2)
# --- MAIN EXECUTION BLOCK ---
try:
# 1. Setup OLED (Assumes standard ssd1306.py is in /lib)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
oled.fill(0)
oled.text('System Boot...', 0, 0)
oled.show()
# 2. Setup Sensor
sensor = AHT20(i2c)
# 3. Mount SD Card
sd = sdcard.SDCard(spi, SPI_CS)
vfs = os.VfsFat(sd)
os.mount(vfs, '/sd')
oled.text('SD Mounted OK', 0, 10)
oled.show()
# 4. Datalogging Loop
log_file = '/sd/pico_log.csv'
with open(log_file, 'a') as f:
if f.tell() == 0:
f.write('Timestamp,Temp_C,Humidity_pct\n')
cycle = 0
while True:
temp, hum = sensor.read_data()
timestamp = time.localtime()
time_str = '{:02d}:{:02d}:{:02d}'.format(timestamp[3], timestamp[4], timestamp[5])
# Update OLED
oled.fill(0)
oled.text(f'Time: {time_str}', 0, 0)
oled.text(f'Temp: {temp} C', 0, 20)
oled.text(f'Hum: {hum} %', 0, 35)
oled.show()
# Write to SD
with open(log_file, 'a') as f:
f.write(f'{time_str},{temp},{hum}\n')
cycle += 1
time.sleep(5)
except OSError as e:
# Catch I2C or SPI hardware faults
oled.fill(0)
oled.text('HARDWARE FAULT', 0, 0)
oled.text(str(e), 0, 20)
oled.show()
print(f'Critical Hardware Error: {e}')
except Exception as e:
print(f'Unexpected Error: {e}')
Debugging: Fixing the "OSError: [Errno 121] EIO" I2C Fault
If you run the script and immediately hit a wall, you will likely see this exact error string in the Thonny REPL:
OSError: [Errno 121] EIO
This is the universal MicroPython error for an I2C bus communication failure. It means the Pico sent a clock pulse but received no acknowledgment (NACK) from the target device, or the SDA line is stuck low.
The First Three Things to Check When It Fails
- Verify the Common Ground: The most frequent cause of EIO on a breadboard is a floating ground. Ensure the GND pin on the Pico is physically wired to the GND rails of both the AHT20 and the OLED. A missing ground reference causes the I2C logic high to float above the Pico's 3.3V tolerance, triggering internal protection diodes and halting communication.
- Run an I2C Bus Scan: Comment out the SD card code and run
print(i2c.scan()). You should see[56, 60](the decimal addresses for 0x38 and 0x3C). If the list is empty, your SDA/SCL wires are swapped, or a wire is broken internally. - Check Breadboard Power Rail Continuity: Many half-size breadboards have a split in the middle of the red/blue power rails. If your Pico is plugged into row 1, but your sensor is plugged into row 40, the VCC might not be crossing the gap.
Ranked Causes for Persistent I2C Errors
| Rank | Cause | Fix / Measurement |
|---|---|---|
| 1 | SDA and SCL swapped | Swap blue/yellow wires. I2C0 SDA is strictly GP4, SCL is GP5. |
| 2 | Bus capacitance too high | Keep I2C jumper wires under 30cm. Measure SDA rise time with a scope; if >300ns, drop I2C freq to 100kHz. |
| 3 | Address collision | Ensure no other device on the bus defaults to 0x38 or 0x3C. |
| 4 | 3.3V vs 5V logic mismatch | If using a 5V Arduino-style sensor, you MUST use a logic level shifter (e.g., Adafruit 757). |
Extending or Simplifying the Build
Not every classroom or weekend session has time for a full SPI filesystem mount. Here is how to adapt this lesson plan on the fly.
How to Simplify (The 15-Minute Version)
If students are struggling with SPI wiring or SD card formatting issues, drop the storage requirement entirely.
- Remove the SD card breakout and all SPI pin definitions from the code.
- Replace the file-writing block with a simple
print(f'{time_str} | {temp}C | {hum}%')statement. - Use the Thonny plotter feature to visualize the serial output in real-time. This keeps the focus strictly on I2C sensor polling and OLED rendering.
How to Extend (The Advanced Telemetry Version)
For students who master the baseline build quickly, introduce asynchronous network telemetry.
- Hardware Swap: Upgrade to the Raspberry Pi Pico W.
- Software Addition: Import the
networkandumqtt.simplelibraries. - Architecture Shift: Move the sensor reading loop into a secondary core using
_thread.start_new_thread(). Use the main core exclusively to handle the WiFi stack and MQTT publishing to a local Mosquitto broker or Home Assistant instance. This teaches critical RTOS concepts regarding thread safety and I2C bus locking.
For deeper reading on the RP2350 architecture and MicroPython I2C implementations, consult the official Raspberry Pi Pico Python SDK documentation and the MicroPython machine.I2C class reference. For hardware-specific wiring and pull-up resistor calculations, the Adafruit AHT20 Learning Guide provides excellent schematic breakdowns.






