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 monitorResults
The system successfully captures and displays video frames.
Ā

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

Ā




![āā[FireBeetle 2 ESP32 P4 Development Kit] Home Assistant IoT Thermometerā](https://dfimg.dfrobot.com/652ce179aa9508d63a4326b8/community/f12945e3ba0c4e6eac979ce8b7c07921_224x164.png)



