PicoCamera User Guide
September 9, 2026 · View on GitHub
This guide walks through configuring, starting and using the camera.
For the full symbol reference see the API pages of pico_camera.h and
sensor.h; for runnable sketches see the
examples.
1. Configuration: camera_config_t
Everything the driver needs is supplied once, at init time, through
camera_config_t. There is no board-level hardcoding.
| Field | Meaning | Notes |
|---|---|---|
pin_pwdn | Camera power-down GPIO | -1 if not connected |
pin_reset | Camera reset GPIO | -1 if not connected |
pin_xclk | Master clock (XCLK) output GPIO | Driven by a PWM slice |
pin_sccb_sda / pin_sccb_scl | SCCB (I2C) data/clock GPIOs | Used to detect and configure the sensor. Pins are hard-muxed: SDA even, SCL odd, both routing to sccb_i2c_port ((pin / 2) % 2 == port). Set pin_sccb_sda = -1 to reuse an already initialized I2C bus (esp32-camera parity; pin_sccb_scl is then ignored) — see the shared-bus note below |
pin_d0 .. pin_d7 | 8 parallel data lines | Must be 8 consecutive GPIOs (pin_dN == pin_d0 + N, a PIO hardware constraint; validated at init) |
pin_vsync / pin_href / pin_pclk | Frame sync / line sync / pixel clock inputs | Free choice of GPIO |
xclk_freq_hz | XCLK frequency in Hz | 0 selects the default 10 MHz; 10–24 MHz is the typical sensor range |
sccb_i2c_port | RP2040 I2C peripheral used for SCCB | 0 or 1; in shared-bus mode (pin_sccb_sda = -1) selects which initialized bus to reuse |
pixel_format | PIXFORMAT_RGB565, PIXFORMAT_YUV422, PIXFORMAT_GRAYSCALE or PIXFORMAT_JPEG | YUV422 streams packed YUYV (2 bytes/pixel, supported on OV2640/OV3660/OV5640/OV7670/GC2145/GC0308). GRAYSCALE is Y-only (1 byte/pixel, GC0308). JPEG requires a sensor with an on-chip encoder (OV2640/OV3660/OV5640 yes, OV7670/GC2145/GC0308/GC032A no) |
frame_size | FRAMESIZE_* enum | Beyond-sensor sizes are clamped to the sensor maximum with a warning instead of failing |
jpeg_quality | 0–63, lower = higher quality | JPEG mode only |
fb_count | Number of frame buffers to allocate | Buffers live in SRAM (264 KB total) unless PSRAM is used; see the memory notes below |
fb_location | Where frame buffers live | PICO_CAMERA_FB_AUTO (default): PSRAM when available, SRAM otherwise. PICO_CAMERA_FB_IN_PSRAM: PSRAM only, init fails if unavailable (esp32-camera parity). PICO_CAMERA_FB_IN_SRAM: on-chip SRAM only |
Available frame sizes (see framesize_t): 96x96, QQVGA 160x120, QCIF
176x144, HQVGA 240x176, 240x240, QVGA 320x240, CIF 400x296, HVGA 480x320,
VGA 640x480, SVGA 800x600, XGA 1024x768, HD 1280x720, SXGA 1280x1024,
UXGA 1600x1200.
Memory planning (RP2040 has 264 KB SRAM, no PSRAM; RP2350 boards with a
PSRAM chip can place buffers in PSRAM when the core's PSRAM support is
enabled — see fb_location above):
- RGB565 needs
width * height * 2bytes per buffer — QVGA (153 KB) is the practical ceiling in SRAM; VGA (600 KB) does not fit but works from PSRAM on RP2350. - JPEG buffers are allocated as
width * height / 4 + 8 KB, which covers typical scenes; pathological noise can overflow it.
Shared SCCB bus (esp32-camera parity): to put the sensor on an I2C bus you
already use for other devices, initialize that bus yourself
(Wire.begin() / Wire1.begin() under Arduino, i2c_init() under the
bare Pico SDK), then set config.pin_sccb_sda = -1 and
config.sccb_i2c_port = 0 or 1. The library skips all pin/bus setup and
never deinitializes a shared bus at pico_camera_deinit().
2. Bringing the camera up and capturing frames
The lifecycle is intentionally the same shape as esp32-camera:
- Fill
camera_config_tand callpico_camera_init(&config). This probes the sensor over SCCB (returnsPICO_CAMERA_ERR_NOT_DETECTEDif nothing answers), validates the pin layout, rejectsPIXFORMAT_JPEGon sensors without an encoder (PICO_CAMERA_ERR_NOT_SUPPORTED), allocates the frame buffers, and arms the PIO + DMA capture engine. Call it once; callpico_camera_deinit()before re-initializing with a different config. - Capture with
camera_fb_t *fb = pico_camera_fb_get();— it blocks until one full frame has been DMA'd into a buffer, and returnsNULLon error or when no free buffer is available. - Consume the frame through
fb->buf/fb->len/fb->width/fb->height(details in the next section), then release it withpico_camera_fb_return(fb). Buffers are recycled; not returning them starves the driver once allfb_countbuffers are checked out. - Shutdown (optional) with
pico_camera_deinit(), which stops the PIO/DMA engine, releases the SCCB bus and frees all buffers.
What you do with each frame is up to the application: push it to a TFT
(camera_render_to_tft example), stream it over USB serial
(push_image_to_python example), or hand fb->buf to your own
processing pipeline (e.g. a vision/inference task). Grab the frame,
process, return the buffer — that loop is all the driver asks.
Error codes returned by pico_camera_init() / pico_camera_deinit():
| Code | Meaning |
|---|---|
PICO_CAMERA_OK (0) | Success |
PICO_CAMERA_ERR_NOT_DETECTED | No sensor answered on SCCB |
PICO_CAMERA_ERR_NOT_SUPPORTED | e.g. JPEG requested on OV7670 |
PICO_CAMERA_ERR_INVALID_ARG | Bad config (e.g. non-consecutive data pins) |
PICO_CAMERA_ERR_INVALID_STATE | Init called twice, or deinit without init |
PICO_CAMERA_ERR_NO_MEM | Frame buffer allocation failed |
PICO_CAMERA_ERR_TIMEOUT | Capture timed out |
PICO_CAMERA_ERR_FAILED_TO_SET_FRAME_SIZE / ..._SET_OUT_FORMAT | Sensor rejected the format/size |
3. camera_fb_t: the frame buffer
typedef struct {
uint8_t *buf; // pixel data
size_t len; // used bytes in buf
size_t width; // pixels
size_t height; // pixels
pixformat_t format; // PIXFORMAT_RGB565 or PIXFORMAT_JPEG
struct timeval timestamp; // capture time since boot
} camera_fb_t;
- RGB565:
len == width * height * 2, one 16-bit pixel per 2 bytes. - JPEG:
bufis a complete, standalone JPEG file — starts with0xFF 0xD8(SOI), ends with0xFF 0xD9(EOI);lenvaries per frame. You can write it straight to a file or socket.
Functions operating on frame buffers:
| Function | Purpose |
|---|---|
pico_camera_fb_get() | Borrow a buffer with a freshly captured frame (blocking) |
pico_camera_fb_return(fb) | Give the buffer back for reuse — always call this |
With fb_count > 1 the DMA engine can fill the next buffer while your
code still processes the previous one; with fb_count == 1 each
fb_get() waits for a new frame to be captured after the previous
buffer was returned.
4. sensor_t: runtime sensor control
pico_camera_sensor_get() returns the detected sensor's control
structure (or NULL before init). All controls are function pointers;
a sensor only implements a subset — always NULL-check before calling:
sensor_t *s = pico_camera_sensor_get();
if (s && s->set_vflip) s->set_vflip(s, 1);
Available operations:
| Function | Purpose |
|---|---|
set_pixformat(fmt) | Switch between RGB565 and JPEG at runtime |
set_framesize(size) | Change resolution at runtime |
set_brightness(level) / set_contrast(level) / set_saturation(level) / set_sharpness(level) | Image tuning, small-integer levels (on OV2640 brightness/contrast/saturation accept −2…2) |
set_gainceiling(gc) | AGC ceiling, GAINCEILING_2X … GAINCEILING_128X |
set_quality(q) | JPEG quality 0–63, lower = better |
set_colorbar(on) | Test color-bar pattern |
set_whitebal(on) / set_wb_mode(mode) | Auto white balance switch / WB preset mode |
set_gain_ctrl(on) / set_exposure_ctrl(on) | AGC / AEC switch |
set_ae_level(level) / set_aec_value(value) | AE target level / manual exposure value |
set_hmirror(on) / set_vflip(on) | Horizontal mirror / vertical flip |
set_special_effect(effect) | Effect index (grayscale, negative, …) |
set_reg(reg, mask, value) / get_reg(reg, mask) | Raw register write/read; mask selects which bits to touch. Escape hatch for anything not wrapped above |
set_xclk(timer, xclk) | Change XCLK at runtime (timer unused on RP2040) |
reset() | Sensor soft reset |
Per-sensor support:
| Operation | OV2640 | OV3660 | OV5640 | OV7670 | GC2145 | GC0308 | GC032A |
|---|---|---|---|---|---|---|---|
| pixformat / framesize | ✅ | ✅ | ✅ | ✅ | ✅ (RGB565 only) | ✅ (RGB565 only) | ✅ (RGB565 only) |
| hmirror / vflip | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| brightness / contrast / saturation / sharpness | ✅ | — | — | — | — | — | — |
| gainceiling / quality | ✅ | — | — | — (no JPEG) | — (no JPEG) | — (no JPEG) | — (no JPEG) |
| colorbar | ✅ | — | — | ✅ | — | — | ✅ |
| whitebal / wb_mode | ✅ | — | — | whitebal only | — | — | — |
| gain_ctrl / exposure_ctrl | ✅ | — | — | ✅ | — | — | — |
| ae_level / aec_value / special_effect | ✅ | — | — | — | — | — | — |
| set_reg / get_reg | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
5. Sensor identification
pico_camera_sensor_info_get() returns static information about the
detected model: name, sccb_addr, pid, max_size (largest
framesize_t supported) and support_jpeg. Useful for adapting your
config at runtime before or after init.
6. Using from a Pico SDK project (no Arduino)
The library is pure Pico SDK code — nothing in src/ touches the
Arduino core — so a bare CMake project can use it directly. Requires
Pico SDK ≥ 1.5.0 (the pio_encode_* API).
In your project's CMakeLists.txt:
include(pico_sdk_import.cmake) # standard SDK boilerplate
project(my_app C CXX ASM)
pico_sdk_init()
add_subdirectory(path/to/PicoCamera) # this repository
add_executable(my_app main.c)
target_link_libraries(my_app pico_stdlib pico_camera)
pico_enable_stdio_usb(my_app 1) # camera logs use printf
pico_add_extra_outputs(my_app)
Or with FetchContent:
include(FetchContent)
FetchContent_Declare(PicoCamera
GIT_REPOSITORY https://github.com/umeiko/PicoCamera.git
GIT_TAG main)
FetchContent_MakeAvailable(PicoCamera)
target_link_libraries(my_app pico_stdlib pico_camera)
In your code, #include "PicoCamera.h" and use the API exactly as shown
in sections 1–5 — the only difference from Arduino sketches is that you
manage stdio_init_all() yourself. Driver log messages go through
printf, so enable USB or UART stdio if you want to see them.
A complete, buildable project lives in
examples/pico_sdk_capture;
it is compiled against the latest Pico SDK in CI on every change to
src/, so what you see there is guaranteed to build.