icon

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:

Ā 

CODE
// 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:

CODE
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:

CODE
/ 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:

CODE
// 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:

CODE
#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:

Ā 

CODE
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.

Ā 

Ā 

License
All Rights
Reserved
licensBg
3