ESP32-CAM Camera Settings: Frame Size, JPEG Quality, XCLK and Buffers
Most ESP32-CAM sketches copy the same camera_config_t block without changing it. That works, but it hides the choices that decide image detail, file size, frame rate and memory use. If your stream is slow, your photos are blurry or esp_camera_fb_get() sometimes returns NULL, the answer is usually in these few settings.
This tutorial explains each important field of camera_config_t and the most useful run-time sensor controls, then gives a test sketch that captures at several frame sizes and quality values and prints the JPEG size and capture time, so you can choose numbers based on your own measurements.
Settings Fixed at Initialisation
These fields are read once by esp_camera_init(). Changing them needs esp_camera_deinit() and a new init.
| Field | Typical value | Effect |
|---|---|---|
| xclk_freq_hz | 20000000 | Clock sent to the OV2640 on GPIO 0. 20 MHz is the standard; 10 MHz can help if you see stripes or corrupt frames, at a lower frame rate |
| pixel_format | PIXFORMAT_JPEG | The OV2640 compresses in hardware. RGB565 or GRAYSCALE give raw pixels but need far more memory |
| frame_size | FRAMESIZE_UXGA when PSRAM is present | Sets the size of the buffers allocated at init; you can later switch smaller but not larger |
| jpeg_quality | 10 to 12 | 0 to 63, lower means better quality and bigger files |
| fb_count | 1 for stills, 2 for streaming | Number of frame buffers; 2 lets the driver fill one while you send the other |
| fb_location | CAMERA_FB_IN_PSRAM | Large JPEG buffers need PSRAM; DRAM only suits small frames |
| grab_mode | CAMERA_GRAB_LATEST | LATEST always returns the newest frame; WHEN_EMPTY returns frames in order and can hand you an old image |
Common Frame Sizes on the OV2640
| Constant | Resolution | Typical use |
|---|---|---|
| FRAMESIZE_QQVGA | 160 x 120 | Very fast processing, thumbnails |
| FRAMESIZE_QVGA | 320 x 240 | Smooth streaming, face detection |
| FRAMESIZE_CIF | 400 x 296 | Streaming on a weak Wi-Fi link |
| FRAMESIZE_VGA | 640 x 480 | Good balance for live video |
| FRAMESIZE_SVGA | 800 x 600 | Clear stills, slower stream |
| FRAMESIZE_XGA | 1024 x 768 | Detailed stills |
| FRAMESIZE_SXGA | 1280 x 1024 | High-detail stills |
| FRAMESIZE_UXGA | 1600 x 1200 | Full sensor resolution, stills only |
Run-Time Sensor Controls
After init, esp_camera_sensor_get() returns a sensor_t pointer whose function pointers change sensor registers immediately. The most useful ones are listed here.
set_framesize(s, FRAMESIZE_VGA)andset_quality(s, 10)for size and compression.set_brightness,set_contrast,set_saturation: values from -2 to 2.set_whitebal(s, 1)turns automatic white balance on;set_wb_modepicks presets such as sunny or office.set_exposure_ctrl(s, 1)enables auto exposure; with it off,set_aec_valuesets a manual exposure.set_gain_ctrl(s, 1)enables automatic gain;set_gainceilinglimits noise in low light.set_hmirrorandset_vflipcorrect a camera mounted sideways or upside down.set_lenc(s, 1)enables lens correction, which reduces dark corners.
Measuring Size and Speed
The sketch initialises at UXGA, then loops through several frame sizes and two quality values. For each combination it throws away one frame (so the new setting takes effect), captures another and prints the resolution, byte count and time taken.
#include "esp_camera.h"
// AI-Thinker ESP32-CAM pin map
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
bool initCamera() {
camera_config_t c = {};
c.ledc_channel = LEDC_CHANNEL_0;
c.ledc_timer = LEDC_TIMER_0;
c.pin_d0 = Y2_GPIO_NUM; c.pin_d1 = Y3_GPIO_NUM;
c.pin_d2 = Y4_GPIO_NUM; c.pin_d3 = Y5_GPIO_NUM;
c.pin_d4 = Y6_GPIO_NUM; c.pin_d5 = Y7_GPIO_NUM;
c.pin_d6 = Y8_GPIO_NUM; c.pin_d7 = Y9_GPIO_NUM;
c.pin_xclk = XCLK_GPIO_NUM; c.pin_pclk = PCLK_GPIO_NUM;
c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
c.pin_pwdn = PWDN_GPIO_NUM; c.pin_reset = RESET_GPIO_NUM;
c.xclk_freq_hz = 20000000; // try 10000000 if frames are corrupt
c.pixel_format = PIXFORMAT_JPEG;
c.frame_size = FRAMESIZE_UXGA; // allocate for the largest size
c.jpeg_quality = 10;
c.fb_count = 1;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.grab_mode = CAMERA_GRAB_LATEST;
return esp_camera_init(&c) == ESP_OK;
}
const framesize_t sizes[] = {FRAMESIZE_QVGA, FRAMESIZE_VGA, FRAMESIZE_SVGA,
FRAMESIZE_XGA, FRAMESIZE_UXGA};
const int qualities[] = {10, 30};
void setup() {
Serial.begin(115200);
delay(500);
if (!psramFound() || !initCamera()) {
Serial.println("PSRAM missing or camera init failed");
while (true) delay(1000);
}
sensor_t *s = esp_camera_sensor_get();
s->set_lenc(s, 1); // lens correction on
Serial.println("width x height | quality | bytes | ms");
for (framesize_t fs : sizes) {
for (int q : qualities) {
s->set_framesize(s, fs);
s->set_quality(s, q);
camera_fb_t *fb = esp_camera_fb_get(); // discard: old settings
if (fb) esp_camera_fb_return(fb);
unsigned long t0 = millis();
fb = esp_camera_fb_get();
unsigned long dt = millis() - t0;
if (!fb) {
Serial.println("capture failed");
continue;
}
Serial.printf("%4u x %4u | %2d | %6u | %lu\n",
fb->width, fb->height, q, fb->len, dt);
esp_camera_fb_return(fb);
}
}
}
void loop() {
}
Run it pointed at a busy scene and then at a plain wall. JPEG size depends strongly on image content, so a detailed scene can be several times larger than a blank one at the same settings. Use the worst case when planning SD card space, upload time or Telegram limits.
Reading the Results
Capture time is mostly the time to read out one frame from the sensor, so it rises with frame size. JPEG size rises with both frame size and image detail, and drops quickly as the quality number goes up. In practice, quality values between 10 and 15 look almost the same on a phone screen, while values above 30 show visible blocks around edges. For a stream, aim for frames that are small enough to send many times per second over your Wi-Fi link; for a stored photo, choose the largest size and lowest quality number that still captures reliably.
Record your measurements in a small table for your own board and lighting. Numbers from other people's boards are only a rough guide, because sensor batches, lenses and Wi-Fi conditions differ.
Choosing Settings and Avoiding Problems
- fb_get returns NULL at high resolution with low quality numbers: the JPEG does not fit the buffer. Raise the quality number to 12 or more, or reduce the frame size.
- Greenish or dark first photo: auto exposure and white balance need a few frames to settle. Discard two or three frames after power-up or after changing settings.
- Old image returned for a single photo: you are using
CAMERA_GRAB_WHEN_EMPTYwith buffers filled earlier. UseCAMERA_GRAB_LATESTor discard one frame first. - Choppy stream: drop to VGA or CIF and quality 12 to 15 before blaming Wi-Fi.
- Out of memory errors: two UXGA buffers plus Wi-Fi need PSRAM; without PSRAM stay at QVGA or smaller with one buffer in DRAM.
Summary
Frame size and JPEG quality decide image detail and file size, fb_count and grab_mode decide whether you get fresh frames and smooth streaming, and xclk_freq_hz trades speed for stability. Initialise at the largest size you need, adjust with the sensor functions at run time, discard a frame after changes, and measure with the benchmark sketch rather than guessing.