Project Overview
Raspberry Pi Pico 2 W + Arducam OV2640: This guide shows how to use a Raspberry Pi Pico 2 W with an Arducam Mini Module (OV2640, 2MP) in CircuitPython to capture a JPEG photo and save it to the board filesystem (/images) for reliable on-device storage.
- Time: 20 to 40 minutes
- Skill level: Beginner to Intermediate
- What you will build: A minimal camera capture pipeline that saves a single JPEG image to CIRCUITPY on boot
Parts List
From ShillehTek
- No ShillehTek-specific product links were provided in the original article - use compatible Pico 2 W accessories from ShillehTek as needed.
External
- Raspberry Pi Pico 2 W (RP2350) - the microcontroller running CircuitPython
- Arducam Mini Module Camera Shield (OV2640, 2MP) - captures JPEG images
- Jumper wires - for SPI, I2C, power, and ground
- USB cable for the Pico 2 W - power and file access
- Your computer with Thonny - to copy files to CIRCUITPY
Note: The camera must be powered from 3.3V. SPI is used for image FIFO reads and I2C (SCCB) is used for camera registers. Pin assignments are defined in the Arducam example files you copy to the board.
Step-by-Step Guide
Step 1 - Install CircuitPython on the Pico 2 W
Goal: Put CircuitPython on the Pico so it boots to a CIRCUITPY drive and runs code.py.
What to do: Download the Pico 2 W UF2 from circuitpython.org. Hold BOOTSEL while plugging in the Pico, copy the UF2 to the RPI-RP2 drive, and the board will reboot as CIRCUITPY.
Expected result: You can see a CIRCUITPY drive on your computer and can copy files onto it.
Step 2 - Pull Arducam example files onto CIRCUITPY
Goal: Get the Arducam library and example files on the board so the camera code runs.
What to do: Clone Arducam's repository locally with git clone https://github.com/ArduCAM/PICO_SPI_CAM.git. Open PICO_SPI_CAM/Python/ and copy everything in that folder except boot.py to the root of the CIRCUITPY drive. The Arducam library files include the camera driver and helper code referenced by the capture script.
Expected result: The Arducam Python files are present on CIRCUITPY and available to import from your capture script.
Step 3 - Wire the Arducam to the Pico 2 W
Goal: Power and connect the camera using SPI for FIFO reads and I2C (SCCB) for register access.
What to do: Wire the Arducam Mini as required by the Arducam example code. Typical connections are:
- Power: Arducam VCC to 3V3, GND to GND
- SPI: SCK, MOSI, MISO plus CS (chip select) to the pins defined in the Arducam files
- I2C (SCCB): SCL and SDA to the pins defined in the Arducam files
Note: If you need different pins, edit Arducam.py on the board and change the SPI/I2C pin assignments to match your wiring. Double-check CS and I2C pins if the camera does not initialize, then power-cycle the board.
Expected result: The camera is powered from 3.3V and signal lines match the pin map in the Arducam library files.
Step 4 - Copy the capture script (code.py)
Goal: Save a single-shot capture script that writes a JPEG into /images on CIRCUITPY.
What to do: Create or replace code.py on the CIRCUITPY drive with the following script. This streams the camera FIFO in 512 byte bursts and writes a timestamped JPEG into /images.
# code.py - ArduCAM capture -> /images on CIRCUITPY
import time as utime
import board, digitalio, os
from Arducam import *
ONCE = 512
RESOLUTION = 0x04
DIR_PATH = "/images"
buf = bytearray(ONCE)
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False
def log(msg):
print("[{:.3f}] {}".format(utime.monotonic(), msg))
def set_resolution(cam, value):
if value == 0x00: cam.OV2640_set_JPEG_size(OV2640_160x120)
elif value == 0x01: cam.OV2640_set_JPEG_size(OV2640_176x144)
elif value == 0x02: cam.OV2640_set_JPEG_size(OV2640_320x240)
elif value == 0x03: cam.OV2640_set_JPEG_size(OV2640_352x288)
elif value == 0x04: cam.OV2640_set_JPEG_size(OV2640_640x480)
elif value == 0x05: cam.OV2640_set_JPEG_size(OV2640_800x600)
elif value == 0x06: cam.OV2640_set_JPEG_size(OV2640_1024x768)
elif value == 0x07: cam.OV2640_set_JPEG_size(OV2640_1280x1024)
elif value == 0x08: cam.OV2640_set_JPEG_size(OV2640_1600x1200)
else: cam.OV2640_set_JPEG_size(OV2640_640x480)
def ensure_dir(path):
try:
os.listdir(path)
except OSError:
try:
os.mkdir(path)
except Exception as e:
log("Failed to create {}: {}".format(path, e))
def unique_name():
t = int(utime.monotonic())
return "{}/image_{:010d}.jpg".format(DIR_PATH, t)
def save_fifo_to_file(cam, length, path):
log("Saving {} bytes to {}".format(length, path))
led.value = True
with open(path, "wb") as f:
cam.SPI_CS_LOW()
cam.set_fifo_burst()
sent = 0
try:
while sent < length:
n = ONCE if (length - sent) >= ONCE else (length - sent)
cam.spi.readinto(buf, start=0, end=n) # <-- fixed signature
f.write(memoryview(buf)[:n])
sent += n
finally:
cam.SPI_CS_HIGH()
cam.clear_fifo_flag()
led.value = False
log("Saved {}".format(path))
def capture_one(cam):
cam.flush_fifo()
cam.clear_fifo_flag()
cam.start_capture()
t0 = utime.monotonic()
while cam.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK) == 0:
if utime.monotonic() - t0 > 5.0:
raise RuntimeError("Timeout waiting for CAP_DONE")
utime.sleep(0.005)
length = cam.read_fifo_length()
if length == 0 or length > 8_000_000:
raise RuntimeError("Bad FIFO length: {}".format(length))
save_fifo_to_file(cam, length, unique_name())
def main():
ensure_dir(DIR_PATH)
log("Init camera")
cam = ArducamClass(OV2640)
cam.Camera_Init()
set_resolution(cam, RESOLUTION)
utime.sleep(0.3)
capture_one(cam)
log("Done.")
if __name__ == "__main__":
try:
main()
except Exception as e:
led.value = False
log("ERROR: {}".format(e))
while True:
log("HB: error state")
utime.sleep(2)
Expected result: On boot the script initializes the camera, captures one JPEG, and saves it to a file path like /images/image_XXXXXXXXXX.jpg on CIRCUITPY.
Step 5 - Replace boot.py to keep CIRCUITPY writable
Goal: Allow your code to write files to internal flash while keeping the host read-only access to CIRCUITPY.
What to do: Replace the board's boot.py with this snippet. It keeps the serial REPL enabled and remounts internal storage as writable for your code.
import storage, usb_cdc
# Keep serial REPL working
usb_cdc.enable(console=True, data=True)
# Make internal flash writable by your code (host will see it read-only)
storage.remount("/", readonly=False)
Expected result: The computer can read files from CIRCUITPY but cannot modify them while the device is running. To update files later, comment out storage.remount or boot into UF2 mode.
Step 6 - Power it up and verify
Goal: Run the capture on boot and confirm the JPEG appears in the images folder.
What to do: Safely eject CIRCUITPY, unplug and replug the Pico 2 W so it runs code.py. After capture, open CIRCUITPY and check for /images/image_XXXXXXXXXX.jpg.
Expected result: The LED toggles while writing and a timestamped JPEG file is saved in /images on CIRCUITPY.
Conclusion
You built a minimal CircuitPython camera pipeline using a Raspberry Pi Pico 2 W and an Arducam OV2640 that captures a JPEG and saves it to on-device storage for offline collection and later retrieval. This pattern is a reliable starting point for time-lapse capture, motion-triggered photos, or low-power data logging.
Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help productionizing captures, uploading to cloud storage, or building a custom solution, check out our IoT consulting: https://shillehtek.com/pages/iot-consulting.