Blogs

Raspberry Pi Pico 2 W + Arducam OV2640: Simple Photo Capture to On-Device Storage (CircuitPython)

This guide shows how to take a photo with an Arducam Mini Module (OV2640, 2MP) on a Raspberry Pi Pico 2 W using CircuitPython, and save the JPEG directly to the board’s filesystem (/images). It’s a minimal, reliable starting point for camera projects, data collection, or low-power IoT devices that snap and stash pictures locally.

If you want to push images to the cloud (S3, Google Cloud, Azure), add motion detection, or build a dashboard and alerts, that’s exactly what we do. Get help from ShillehTek’s IoT consulting and we’ll productionize this for you.


What you’ll need

  • Raspberry Pi Pico 2 W (RP2350)
  • Arducam Mini Module Camera Shield (OV2640, 2MP)
  • Jumper Wires
  • USB cable for the Pico 2 W
  • Your computer with Thonny

Why this setup?

  • Fast iteration: CircuitPython runs code.py on boot—no toolchains or flashing loops.
  • Reliable capture: Uses Arducam’s FIFO burst read to stream JPEG bytes straight to a file.
  • Device-first storage: Saves to the Pico filesystem, so you can unplug, copy images, and keep going.

1) Install CircuitPython on the Pico 2 W

  1. Download the Pico 2 W UF2 from circuitpython.org.
  2. Hold the BOOTSEL button and plug in the Pico; a drive named RPI-RP2 appears.
  3. Drag the UF2 onto that drive. The board will reboot as a CIRCUITPY drive.

2) Pull Arducam’s example files

  1. Clone Arducam’s repo: git clone https://github.com/ArduCAM/PICO_SPI_CAM.git
  2. Open PICO_SPI_CAM/Python/.
  3. Copy everything in that folder except boot.py to the root of the CIRCUITPY drive on your Pico. (We’ll use a custom boot.py below.)

Reference: Arducam PICO_SPI_CAM on GitHub

3) Wiring (SPI + I²C)

The Arducam Mini uses SPI to read image data and I²C (SCCB) for camera registers. The exact pins are defined in the Arducam library you copied. If you need different pins, open Arducam.py on the board and change the SPI/I²C pin assignments there to match your wiring. Be sure to power the camera from 3.3V (not 5V).

  • SPI: SCK / MOSI / MISO + CS (chip select)
  • I²C (SCCB): SCL / SDA
  • Power: 3.3V and GND
Tip: If the camera doesn’t initialize, double-check CS and I²C pins, then power-cycle the board.

4) Use this code.py (captures one photo to /images)

Create (or replace) code.py on the CIRCUITPY drive with the following:

# 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)
Resolution options (change RESOLUTION)
  • 0x00 160×120
  • 0x01 176×144
  • 0x02 320×240
  • 0x03 352×288
  • 0x04 640×480 (default)
  • 0x05 800×600
  • 0x06 1024×768
  • 0x07 1280×1024
  • 0x08 1600×1200

5) Use this boot.py (important)

Replace the board’s boot.py with the snippet below. It keeps the serial REPL available, and mounts the internal flash read-write for your code while the computer sees it as read-only. This prevents PC writes from corrupting files during active camera writes.

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)
Note: With this boot.py, your computer can read files from CIRCUITPY (e.g., copy images off) but cannot modify them while the device is running. To update files later, temporarily comment out the storage.remount line, or boot into UF2 mode (BOOTSEL) and re-copy files.

6) Power it up

  1. Safely eject CIRCUITPY.
  2. Unplug/re-plug the Pico 2 W. It will run code.py and capture one photo.
  3. Open the CIRCUITPY drive and look for /images/image_XXXXXXXXXX.jpg.

How it works (quick overview)

  • ArducamClass(OV2640) initializes the camera and configures JPEG size.
  • On capture, we wait for the FIFO “capture done” flag, read the FIFO length, and stream bytes in 512-byte bursts to a file.
  • The LED toggles while writing to make it obvious something is happening.

Where to go next

  • Trigger captures on a GPIO or timer loop instead of a single shot.
  • Buffer and upload images to cloud storage (S3, GCS, Azure) with timestamps.
  • Add motion detection to avoid saving empty frames.
  • Expose a simple REST or MQTT API for remote capture.
  • Build a secure web portal to browse and label images.

Want a production-ready version with cloud pipelines, dashboards, and web access? Work with ShillehTek’s IoT consulting team. If you prefer Upwork, we’re happy to collaborate there too.


Full reference & credits

Need help integrating cameras with the cloud, dashboards, and alerting? Contact ShillehTek. We build end-to-end solutions that are reliable, maintainable, and cost-aware.

Create a free account to access full content.

All access to code and resources on ShillehTek.

Signup Now

Already a member? Sign In

Need Project Support?