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
- Download the Pico 2 W UF2 from circuitpython.org.
- Hold the BOOTSEL button and plug in the Pico; a drive named RPI-RP2 appears.
- Drag the UF2 onto that drive. The board will reboot as a CIRCUITPY drive.
2) Pull Arducam’s example files
- Clone Arducam’s repo:
git clone https://github.com/ArduCAM/PICO_SPI_CAM.git
- Open
PICO_SPI_CAM/Python/
. -
Copy everything in that folder except
boot.py
to the root of the CIRCUITPY drive on your Pico. (We’ll use a customboot.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
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)
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
- Safely eject CIRCUITPY.
- Unplug/re-plug the Pico 2 W. It will run
code.py
and capture one photo. - 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
- Arducam repo used: github.com/ArduCAM/PICO_SPI_CAM
- CircuitPython downloads: circuitpython.org
- Editor: thonny.org