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
0