
Using a Raspberry Pi-Compatible CSI Camera with FireBeetle 2 ESP32 P4 (ESP-IDF)
This guide explains how to interface a Raspberry Pi-compatible CSI camera with the FireBeetle 2 ESP32 P4 development board using the ESP-IDF framework.
Per the official FireBeetle 2 ESP32 P4 Wiki, the board features a CSI camera interface compatible with Raspberry Pi 4B. This allows seamless integration with cameras from the Raspberry Pi ecosystem without additional configuration.
For this project, DFRobot provided a compatible CSI camera, which connects directly to the board’s CSI interface.


When connecting, use a 15-pin, 1.0mm pitch FPC cable with the silver side facing the contact points and the blue side toward the black locking clip.
Building on a previous project where an ST7789 display was interfaced, this guide focuses on capturing video frames from the CSI camera and rendering them on the ST7789 display.
Core Program Logic
The camera’s resolution differs from the display’s, so captured frames are scaled before rendering. The CSI camera captures data, processes it via a callback function, and places it into a queue. This simplifies the main program loop, which only needs to initialize and start the camera capture.
Below is the relevant code for initializing the display and handling screen rotation:
// Screen rotation
static void lcd_rotate(uint16_t rotation) {
switch (rotation) {
case 0:
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, !CAMERA_SELFIE_MODE, false);
break;
case 90:
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, CAMERA_SELFIE_MODE, false);
break;
case 180:
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, CAMERA_SELFIE_MODE, true);
break;
case 270:
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, !CAMERA_SELFIE_MODE, true);
break;
}
}
// Initialize ST7789 display
static void init_lcd_display(void) {
// Configure backlight GPIO
gpio_config_t bk_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << LCD_PIN_NUM_BK_LIGHT
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
gpio_set_level(LCD_PIN_NUM_BK_LIGHT, LCD_DISP_BK_LIGHT_OFF_LEVEL);
// Initialize SPI bus
spi_bus_config_t buscfg = {
.sclk_io_num = LCD_PIN_NUM_SCLK,
.mosi_io_num = LCD_PIN_NUM_MOSI,
.miso_io_num = LCD_PIN_NUM_MISO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = LCD_DISP_H_RES * LCD_DISP_V_RES * sizeof(uint16_t),
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
// Install panel I/O
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = LCD_PIN_NUM_LCD_DC,
.cs_gpio_num = LCD_PIN_NUM_LCD_CS,
.pclk_hz = LCD_DISP_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.spi_mode = 0,
.trans_queue_depth = 10,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle));
// Create ST7789 panel
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_PIN_NUM_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
// Reset and initialize panel
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, false));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
// Turn on backlight
gpio_set_level(LCD_PIN_NUM_BK_LIGHT, LCD_DISP_BK_LIGHT_ON_LEVEL);
}
The main application flow is as follows:
void app_main(void) {
ESP_LOGI(TAG, "Application starting...");
// 1. Initialize LCD display
init_lcd_display();
lcd_rotate(LCD_DISP_ROTATE);
ESP_LOGI(TAG, "LCD initialized");
// 2. Allocate scaling buffer
init_scaling_params();
scaled_buffer = heap_caps_malloc(LCD_DISP_H_RES * LCD_DISP_V_RES * sizeof(uint16_t),
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
assert(scaled_buffer != NULL);
ESP_LOGI(TAG, "Scaled buffer allocated: %d bytes",
LCD_DISP_H_RES * LCD_DISP_V_RES * sizeof(uint16_t));
// 3. Create display queue
display_queue = xQueueCreate(5, sizeof(uint8_t*));
assert(display_queue != NULL);
ESP_LOGI(TAG, "Display queue created");
// 4. Create display task
xTaskCreate(display_task, "display_task", 4096, NULL, 8, NULL);
ESP_LOGI(TAG, "Display task created");
// 5. Initialize CSI camera
cam_buffer_size = CSI_MIPI_CSI_DISP_HRES * CSI_MIPI_CSI_DISP_VRES * 2; // RGB565: 2 bytes per pixel
ESP_LOGI(TAG, "Camera buffer size: %d bytes", cam_buffer_size);
// Initialize MIPI LDO
esp_ldo_channel_handle_t ldo_mipi_phy = NULL;
esp_ldo_channel_config_t ldo_config = {
.chan_id = CSI_USED_LDO_CHAN_ID,
.voltage_mv = CSI_USED_LDO_VOLTAGE_MV,
};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_config, &ldo_mipi_phy));
ESP_LOGI(TAG, "LDO initialized");
// Allocate camera frame buffers
size_t frame_buffer_alignment = 0;
ESP_ERROR_CHECK(esp_cache_get_alignment(0, &frame_buffer_alignment));
for (int i = 0; i < NUM_CAM_BUFFERS; i++) {
cam_buffers[i] = heap_caps_aligned_calloc(frame_buffer_alignment, 1,
cam_buffer_size,
MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA);
assert(cam_buffers[i] != NULL);
ESP_LOGI(TAG, "Camera buffer %d allocated: %p", i, cam_buffers[i]);
}
ESP_LOGI(TAG, "%d camera buffers allocated", NUM_CAM_BUFFERS);
// Initialize camera sensor
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
example_sensor_handle_t sensor_handle;
#else
i2c_master_bus_handle_t sensor_handle;
#endif
example_sensor_config_t sensor_config = {
.i2c_port_num = I2C_NUM_0,
.i2c_sda_io_num = CSI_MIPI_CSI_CAM_SCCB_SDA_IO,
.i2c_scl_io_num = CSI_MIPI_CSI_CAM_SCCB_SCL_IO,
.port = ESP_CAM_SENSOR_MIPI_CSI,
.format_name = CSI_CAM_FORMAT,
};
ESP_ERROR_CHECK(example_sensor_init(&sensor_config, &sensor_handle));
ESP_LOGI(TAG, "Camera sensor initialized");
// Initialize CSI controller
esp_cam_ctlr_csi_config_t csi_config = {
.ctlr_id = 0,
.h_res = CSI_MIPI_CSI_DISP_HRES,
.v_res = CSI_MIPI_CSI_DISP_VRES,
.lane_bit_rate_mbps = CSI_MIPI_CSI_LANE_BITRATE_MBPS,
.input_data_color_type = CAM_CTLR_COLOR_RAW8,
.output_data_color_type = CAM_CTLR_COLOR_RGB565,
.data_lane_num = 2,
.byte_swap_en = false,
.queue_items = 1,
};
esp_cam_ctlr_handle_t cam_handle = NULL;
ESP_ERROR_CHECK(esp_cam_new_csi_ctlr(&csi_config, &cam_handle));
ESP_LOGI(TAG, "CSI controller initialized");
// Register event callbacks
esp_cam_ctlr_evt_cbs_t cbs = {
.on_get_new_trans = camera_get_new_buffer,
.on_trans_finished = camera_trans_finished,
};
ESP_ERROR_CHECK(esp_cam_ctlr_register_event_callbacks(cam_handle, &cbs, NULL));
ESP_ERROR_CHECK(esp_cam_ctlr_enable(cam_handle));
ESP_LOGI(TAG, "Camera event callbacks registered");
// Initialize ISP processor
isp_proc_handle_t isp_proc = NULL;
esp_isp_processor_cfg_t isp_config = {
.clk_hz = 80 * 1000 * 1000,
.input_data_source = ISP_INPUT_DATA_SOURCE_CSI,
.input_data_color_type = ISP_COLOR_RAW8,
.output_data_color_type = ISP_COLOR_RGB565,
.has_line_start_packet = true,
.has_line_end_packet = true,
.h_res = CSI_MIPI_CSI_DISP_HRES,
.v_res = CSI_MIPI_CSI_DISP_VRES,
};
ESP_ERROR_CHECK(esp_isp_new_processor(&isp_config, &isp_proc));
ESP_ERROR_CHECK(esp_isp_enable(isp_proc));
ESP_LOGI(TAG, "ISP processor initialized");
// Start camera capture
ESP_ERROR_CHECK(esp_cam_ctlr_start(cam_handle));
ESP_LOGI(TAG, "Camera capture started");
// Main loop - monitor performance
int64_t last_log_time = esp_timer_get_time();
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
int64_t now = esp_timer_get_time();
if (now - last_log_time > 1000000) {
ESP_LOGI(TAG, "Frames processed: %d, Queue depth: %d",
trans_finished_count, uxQueueMessagesWaiting(display_queue));
trans_finished_count = 0;
last_log_time = now;
}
}
// Cleanup (unreachable in this example)
ESP_ERROR_CHECK(esp_cam_ctlr_stop(cam_handle));
ESP_ERROR_CHECK(esp_cam_ctlr_disable(cam_handle));
ESP_ERROR_CHECK(esp_cam_ctlr_del(cam_handle));
ESP_ERROR_CHECK(esp_isp_disable(isp_proc));
ESP_ERROR_CHECK(esp_isp_del_processor(isp_proc));
ESP_ERROR_CHECK(esp_ldo_release_channel(ldo_mipi_phy));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
example_sensor_deinit(sensor_handle);
#endif
for (int i = 0; i < NUM_CAM_BUFFERS; i++) {
heap_caps_free(cam_buffers[i]);
}
heap_caps_free(scaled_buffer);
vQueueDelete(display_queue);
}
Camera and Display Workflow
The camera captures frames and processes them through event callbacks:
/ Camera event callbacks
static bool IRAM_ATTR camera_get_new_buffer(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) {
static int buffer_index = 0;
trans->buffer = cam_buffers[buffer_index];
trans->buflen = cam_buffer_size;
buffer_index = (buffer_index + 1) % NUM_CAM_BUFFERS;
return false;
}
static bool IRAM_ATTR camera_trans_finished(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(display_queue, &trans->buffer, &xHigherPriorityTaskWoken);
trans_finished_count++;
return xHigherPriorityTaskWoken == pdTRUE;
}
The display operates in a separate task, processing frames as follows:
// Initialize scaling parameters
static void init_scaling_params(void) {
src_width = CSI_MIPI_CSI_DISP_HRES;
src_height = CSI_MIPI_CSI_DISP_VRES;
dst_width = LCD_DISP_H_RES;
dst_height = LCD_DISP_V_RES;
// Calculate scaling ratio (use the smaller of width or height ratio)
const float width_ratio = (float)dst_width / src_width;
const float height_ratio = (float)dst_height / src_height;
scale = (width_ratio < height_ratio) ? width_ratio : height_ratio;
// Calculate scaled dimensions
scaled_width = (int)(src_width * scale);
scaled_height = (int)(src_height * scale);
// Calculate center offsets
x_offset = (dst_width - scaled_width) / 2;
y_offset = (dst_height - scaled_height) / 2;
}
// Scale image using nearest-neighbor interpolation
static void IRAM_ATTR scale_image(uint16_t *src, uint16_t *dst) {
memset(dst, 0x00, dst_width * dst_height * sizeof(uint16_t));
for (int y = 0; y < scaled_height; y++) {
for (int x = 0; x < scaled_width; x++) {
const int src_x = (int)(x / scale);
const int src_y = (int)(y / scale);
const int safe_src_x = (src_x < src_width) ? src_x : src_width - 1;
const int safe_src_y = (src_y < src_height) ? src_y : src_height - 1;
dst[(y + y_offset) * dst_width + (x + x_offset)] =
__builtin_bswap16(src[safe_src_y * src_width + safe_src_x]);
}
}
}
// Display processing task
void display_task(void *arg) {
uint8_t *frame_data;
int64_t last_frame_time = esp_timer_get_time();
while (1) {
if (xQueueReceive(display_queue, &frame_data, pdMS_TO_TICKS(50))) {
// Process only the latest frame
while (uxQueueMessagesWaiting(display_queue) > 0) {
xQueueReceive(display_queue, &frame_data, 0);
}
// Scale the image
int64_t start = esp_timer_get_time();
scale_image((uint16_t*)frame_data, scaled_buffer);
int64_t scale_time = esp_timer_get_time() - start;
// Draw to LCD
start = esp_timer_get_time();
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0,
LCD_DISP_H_RES, LCD_DISP_V_RES,
scaled_buffer);
int64_t draw_time = esp_timer_get_time() - start;
// Log performance
int64_t now = esp_timer_get_time();
int64_t frame_interval = now - last_frame_time;
last_frame_time = now;
ESP_LOGI(TAG, "Frame processed: scale=%lldµs, draw=%lldµs, interval=%lldµs",
scale_time, draw_time, frame_interval);
}
}
}
System Integration
The workflow integrates as follows: The esp_cam_ctlr_register_event_callbacks() function registers camera callbacks. When a frame is captured, camera_get_new_buffer() assigns a buffer, and camera_trans_finished() queues the data. The display_task() retrieves frames from the queue, scales them using scale_image(), and renders them with esp_lcd_panel_draw_bitmap().
Configuration
A config.h file centralizes the configuration:
#ifndef _CONFIG_H_
#define _CONFIG_H_
// Camera configuration
#define CSI_USED_LDO_CHAN_ID 3
#define CSI_USED_LDO_VOLTAGE_MV 2500
#define CSI_RGB565_BITS_PER_PIXEL 16
#define CSI_MIPI_CSI_LANE_BITRATE_MBPS 200
#define CSI_MIPI_CSI_CAM_SCCB_SCL_IO 8
#define CSI_MIPI_CSI_CAM_SCCB_SDA_IO 7
#define CSI_MIPI_CSI_DISP_HRES 800
#define CSI_MIPI_CSI_DISP_VRES 640
#define CSI_CAM_FORMAT "MIPI_2lane_24Minput_RAW8_800x640_50fps"
// Display configuration (ST7789)
#define LCD_HOST SPI2_HOST
#define LCD_PIN_NUM_SCLK 4
#define LCD_PIN_NUM_MOSI 5
#define LCD_PIN_NUM_MISO -1
#define LCD_PIN_NUM_LCD_DC 21
#define LCD_PIN_NUM_LCD_RST 20
#define LCD_PIN_NUM_LCD_CS 22
#define LCD_PIN_NUM_BK_LIGHT 23
#define LCD_DISP_H_RES 240
#define LCD_DISP_V_RES 320
#define LCD_DISP_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
#define LCD_DISP_BK_LIGHT_ON_LEVEL 1
#define LCD_DISP_BK_LIGHT_OFF_LEVEL !LCD_DISP_BK_LIGHT_ON_LEVEL
#define LCD_DISP_ROTATE 0
#define CAMERA_SELFIE_MODE true
// Double buffering
#define NUM_CAM_BUFFERS 2
#endif // _CONFIG_H_
In addition to the adaptations in the code, configuration is also required in menuconfig, as detailed below:

Ensure CSI_MIPI_CSI_DISP_HRES and CSI_MIPI_CSI_DISP_VRES match the selected CSI_CAM_FORMAT in config.h. The OV5647 camera supports the following modes:
MIPI_2lane_24Minput_RAW8_800x1280_50fps
MIPI_2lane_24Minput_RAW8_800x640_50fps
MIPI_2lane_24Minput_RAW8_800x800_50fps
MIPI_2lane_24Minput_RAW10_1920x1080_30fps
MIPI_2lane_24Minput_RAW10_1280x960_binning_45fps
Select the appropriate mode for your application. Additionally, configure the ESP-IDF project settings via menuconfig to align with the hardware setup.
Build and Flash
Compile and flash the code using:
idf.py build flash monitor
Results
The system successfully captures and displays video frames.

Testing with an older Raspberry Pi camera yielded comparable results, confirming compatibility.

