Camera (PicoCamera) Library
The PicoCamera library adds support for camera sensors with a parallel
(DVP) interface, such as the Omnivision OV2640, OV3660, and OV7670, and the
Galaxycore GC2145 and GC0308. While
the RP2040/RP2350 chips do not include a dedicated camera peripheral, the
library uses a PIO (Programmable I/O) state machine together with DMA to
capture frames in the background with almost no CPU involvement.
The API is intentionally shaped after the well-known esp32-camera
library: the camera is configured once at init time through a
camera_config_t structure, frames are captured into pre-allocated
buffers which the application borrows and returns, and the sensor can be
adjusted at runtime through a set of control function pointers.
RGB565, YUV422 (packed YUYV) and grayscale raw frames are supported, as well as JPEG frames from sensors with an on-chip JPEG encoder, such as the OV2640 and OV3660.
Include the library header and define the configuration before use:
#include <PicoCamera.h>
Configuration (camera_config_t)
Everything the driver needs is supplied once, at init time, through
camera_config_t. There is no board-level hardcoding. A complete
configuration looks like this:
camera_config_t config = {
.pin_pwdn = -1,
.pin_reset = -1,
.pin_xclk = 10,
.pin_sccb_sda = 12,
.pin_sccb_scl = 13,
.pin_d0 = 2, .pin_d1 = 3, .pin_d2 = 4, .pin_d3 = 5,
.pin_d4 = 6, .pin_d5 = 7, .pin_d6 = 8, .pin_d7 = 9,
.pin_vsync = 11,
.pin_href = 14,
.pin_pclk = 15,
.xclk_freq_hz = 10000000,
.sccb_i2c_port = 0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QVGA,
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = PICO_CAMERA_FB_AUTO,
};
The most important fields are:
pin_pwdn/pin_reset- camera power-down and reset GPIOs. Set to-1if not connected.pin_xclk- master clock (XCLK) output GPIO, driven by a PWM slice.pin_sccb_sda/pin_sccb_scl- SCCB (I2C) data and clock GPIOs, used to detect and configure the sensor. Pins are hard-muxed: SDA must be even, SCL odd, both routing tosccb_i2c_port. Setpin_sccb_sda = -1to reuse an I2C bus already initialized by the application (see the shared bus note below);pin_sccb_sclis then ignored.pin_d0…pin_d7- the 8 parallel data lines. These 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 and pixel clock inputs. Any free GPIO may be used.xclk_freq_hz- XCLK frequency in Hz.0selects the default of 10 MHz; 10-24 MHz is the typical sensor range.sccb_i2c_port- the I2C peripheral used for SCCB,0or1. In shared-bus mode (pin_sccb_sda = -1) it selects which already initialized bus to reuse.pixel_format-PIXFORMAT_RGB565,PIXFORMAT_YUV422,PIXFORMAT_GRAYSCALEorPIXFORMAT_JPEG. YUV422 streams packed YUYV (2 bytes per pixel, supported on OV2640/OV3660/OV7670/GC2145/ GC0308); grayscale is Y-only (1 byte per pixel, GC0308). JPEG requires a sensor with an on-chip encoder (OV2640/OV3660 yes, the others no).frame_size- aFRAMESIZE_*value. Sizes beyond the sensor maximum are clamped with a warning instead of failing. Available sizes include 96x96, QQVGA (160x120), QCIF (176x144), HQVGA (240x176), 240x240, QVGA (320x240), CIF (400x296), HVGA (480x320), VGA (640x480), SVGA (800x600), XGA (1024x768), HD (1280x720), SXGA (1280x1024), and UXGA (1600x1200).jpeg_quality- 0-63, lower means higher quality. JPEG mode only.fb_count- number of frame buffers to allocate.fb_location- where frame buffers live.PICO_CAMERA_FB_AUTO(the default) uses PSRAM when available and SRAM otherwise,PICO_CAMERA_FB_IN_PSRAMforces PSRAM (init fails if unavailable), andPICO_CAMERA_FB_IN_SRAMforces on-chip SRAM.
Memory Considerations
Frame buffers are the largest consumer of memory in this library, so it is worth planning ahead. The RP2040 has 264 KB of on-chip SRAM, while the RP2350 has 520 KB; the frame buffers, the rest of your sketch, and the Arduino core itself all share this space.
For RGB565 and YUV422 frames each buffer needs width * height * 2
bytes (grayscale needs half of that):
QQVGA (160x120) - about 38 KB per buffer.
QVGA (320x240) - about 150 KB per buffer, near the practical ceiling on the RP2040.
VGA (640x480) - 600 KB per buffer, does not fit in SRAM on either chip.
SVGA (800x600) - about 940 KB per buffer.
For JPEG frames each buffer is allocated as width * height / 4 + 8 KB,
which covers typical scenes (the actual JPEG data is usually smaller, so
only part of the buffer is used):
QVGA (320x240) - about 27 KB per buffer.
VGA (640x480) - about 83 KB per buffer.
SVGA (800x600) - about 125 KB per buffer.
UXGA (1600x1200) - about 477 KB per buffer.
Boards with a PSRAM chip (available on many RP2350 boards) have a much
more generous allowance: when the core’s PSRAM support is enabled, the
frame buffers can be placed in PSRAM (see fb_location above), making
large RGB565 frames and multi-megapixel JPEG captures practical.
Initializing the Camera
With the configuration filled in, start the camera by calling
pico_camera_init() once:
int err = pico_camera_init(&config);
if (err != PICO_CAMERA_OK) {
// no sensor found, bad pin layout, out of memory, ...
// see the Error Codes section below
}
This probes the sensor over SCCB, validates the pin layout, allocates the
frame buffers, and arms the PIO + DMA capture engine. Afterwards frames
can be captured with pico_camera_fb_get(). Call
pico_camera_deinit() before re-initializing with a different
configuration.
The Frame Buffer (camera_fb_t)
Captured frames are delivered in a camera_fb_t structure:
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 / YUV422 / GRAYSCALE / JPEG
struct timeval timestamp; // capture time since boot
} camera_fb_t;
For RGB565 and YUV422 frames, len == width * height * 2 (YUV422 is
packed YUYV: one luminance byte per pixel, chroma shared per pixel pair).
For grayscale frames, len == width * height. For JPEG frames, buf
holds a complete, standalone JPEG file
(starting with 0xFF 0xD8 and ending with 0xFF 0xD9) and len
varies per frame; it can be written straight to a file or socket.
With fb_count > 1 the DMA engine can fill the next buffer while the
application still processes the previous one. With fb_count == 1 each
pico_camera_fb_get() waits for a new frame to be captured after the
previous buffer was returned.
Reading and Processing Frames
The capture loop is always the same shape: borrow a frame buffer with
pico_camera_fb_get(), do something with the pixels, then give the
buffer back with pico_camera_fb_return(). The following loop(),
adapted from the push_image_to_python example shipping with the
library, grabs an RGB565 frame and streams it raw over the USB serial
port; the spot marked in the middle is where your own processing (image
filtering, feature detection, driving a display, and so on) goes:
void loop() {
camera_fb_t *fb = pico_camera_fb_get();
if (fb) {
// fb->buf holds fb->width * fb->height 16-bit RGB565 pixels.
// Process the frame here, e.g. walk the pixels:
//
// uint16_t *pixels = (uint16_t *)fb->buf;
// for (size_t i = 0; i < fb->width * fb->height; i++) {
// uint16_t pixel = pixels[i];
// ...
// }
// Stream the raw frame to the PC, framed with start/end markers
Serial.write("SRGB", 4);
Serial.write(fb->buf, fb->len);
Serial.write("ERGB", 4);
Serial.flush(); // wait until the whole frame left the FIFO
pico_camera_fb_return(fb);
}
delay(10);
}
A matching Python receiver (push_image_to_python.py) that displays the
stream is included next to the example in
libraries/PicoCamera/examples/push_image_to_python.
Runtime Sensor Control
pico_camera_sensor_get() returns the detected sensor’s control
structure. All controls are function pointers, and a sensor only
implements a subset of them, so always NULL-check before calling:
sensor_t *s = pico_camera_sensor_get();
if (s && s->set_vflip) {
s->set_vflip(s, 1);
}
Available operations include:
set_pixformat(fmt)- switch between RGB565, YUV422, grayscale and JPEG at runtime (whichever the sensor supports).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 the OV2640, brightness/contrast/saturation accept -2…2).set_gainceiling(gc)- AGC ceiling,GAINCEILING_2X…GAINCEILING_128X.set_quality(q)- JPEG quality 0-63, lower is better.set_colorbar(on)- enable a test color-bar pattern.set_whitebal(on)/set_wb_mode(mode)- auto white balance switch / white balance preset mode.set_gain_ctrl(on)/set_exposure_ctrl(on)- AGC / AEC switch.set_ae_level(level)/set_aec_value(value)- auto-exposure target level / manual exposure value.set_hmirror(on)/set_vflip(on)- horizontal mirror / vertical flip.set_special_effect(effect)- effect index (grayscale, negative, etc.).set_reg(reg, mask, value)/get_reg(reg, mask)- raw register write/read, wheremaskselects which bits to touch. This is the escape hatch for anything not wrapped above.set_xclk(timer, xclk)- change the XCLK frequency at runtime (timeris unused).reset()- sensor soft reset.
Not every sensor implements every operation. The OV2640 implements the
full set; the OV7670 has no JPEG encoder and lacks the image-tuning
controls; the OV3660 falls in between; the Galaxycore sensors (GC2145,
GC0308) have no JPEG encoder and expose the basics (pixformat, framesize,
hmirror, vflip, raw register access). Always check the function pointer
for NULL before calling, as shown above.
Shutting Down the Camera
When the camera is no longer needed, release it with
pico_camera_deinit():
pico_camera_deinit();
This stops the PIO + DMA capture engine, frees all frame buffers, and
releases the SCCB bus. A shared bus (passed in with
pin_sccb_sda = -1) is left running, since the library did not set it
up. Afterwards the camera can be re-initialized with a fresh
configuration by calling pico_camera_init() again.
Camera API
int pico_camera_init(const camera_config_t *config)
Probes the sensor over SCCB, validates the pin layout, allocates the frame
buffers, and arms the PIO + DMA capture engine. Returns
PICO_CAMERA_OK on success or one of the error codes listed below.
Call it once; call pico_camera_deinit() before re-initializing with a
different configuration.
camera_fb_t *pico_camera_fb_get()
Blocks until one full frame has been captured by DMA into a buffer and
returns that buffer. Returns NULL on error or when no free buffer is
available.
void pico_camera_fb_return(camera_fb_t *fb)
Returns a frame buffer to the driver for reuse. Always call this when
done with a frame; buffers are recycled, and not returning them starves
the driver once all fb_count buffers are checked out.
int pico_camera_deinit()
Stops the PIO/DMA engine, releases the SCCB bus (unless shared), and frees all frame buffers.
sensor_t *pico_camera_sensor_get()
Returns the detected sensor’s runtime control structure, or NULL
before init. See the runtime sensor control section above.
const sensor_info_t *pico_camera_sensor_info_get()
Returns static information about the detected sensor model: name,
sccb_addr, pid, max_size (largest framesize_t supported),
and support_jpeg. Useful for adapting the configuration at runtime.
const char *pico_camera_err_str(int err)
Maps an error code to a human-readable string, handy for logging:
Serial.printf("init failed: %s\n", pico_camera_err_str(err));
Error Codes
pico_camera_init() and pico_camera_deinit() return one of the
following (pass the code to pico_camera_err_str() for a printable
description):
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 an OV7670.PICO_CAMERA_ERR_INVALID_ARG- bad configuration, such as 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/PICO_CAMERA_ERR_FAILED_TO_SET_OUT_FORMAT- the sensor rejected the requested format or size.
Example Sketch
The following sketch captures JPEG frames from an OV2640 and prints the frame size over the serial port:
#include <PicoCamera.h>
camera_config_t config = {
.pin_pwdn = -1,
.pin_reset = -1,
.pin_xclk = 10,
.pin_sccb_sda = 12,
.pin_sccb_scl = 13,
.pin_d0 = 2, .pin_d1 = 3, .pin_d2 = 4, .pin_d3 = 5,
.pin_d4 = 6, .pin_d5 = 7, .pin_d6 = 8, .pin_d7 = 9,
.pin_vsync = 11,
.pin_href = 14,
.pin_pclk = 15,
.xclk_freq_hz = 10000000,
.sccb_i2c_port = 0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QVGA,
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = PICO_CAMERA_FB_AUTO,
};
void setup() {
Serial.begin(115200);
int err = pico_camera_init(&config);
if (err != PICO_CAMERA_OK) {
Serial.printf("Camera init failed: %s (%d)\n",
pico_camera_err_str(err), err);
while (true) { /* halt */ }
}
}
void loop() {
camera_fb_t *fb = pico_camera_fb_get();
if (fb) {
Serial.printf("Frame: %dx%d, %d bytes\n",
(int)fb->width, (int)fb->height, (int)fb->len);
pico_camera_fb_return(fb);
}
}
Additional examples, including rendering frames to a TFT display and
streaming images to a host PC, ship with the library under
libraries/PicoCamera/examples.
For more information, see the PicoCamera project home page at https://github.com/umeiko/PicoCamera