7浏览
查看: 7|回复: 0

[ESP8266/ESP32] FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)

[复制链接]
从 FireBeetle 2 ESP32 P4 的WiKi可以了解到如下的信息:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图3
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图2
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图1
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图4

也就是说,在这块开发板上,可以使用树莓派4B兼容的CSI摄像头,不用单独配置摄像头了。

这次活动,DFRobot也非常贴心的附带了一颗树莓派兼容摄像头:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图5

可以直接连接到开发板上的CSI接口上:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图6

注意连接的时候,使用的是反向15P间距1.0mm的FPC排线,排线的银色面贴着接触点,蓝色面贴着黑色卡扣。

之前点亮过ST7789屏幕(见:FireBeetle 2 ESP32 P4 点亮 ILI9341、ST7789屏幕),所以这次测试,就是获取摄像的画面数据,然后显示到ST7789屏幕上。

程序的主逻辑流程如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图8

因为摄像头获取数据的分辨率和显示屏的分辨率不同,所以最终显示的时候,做了缩放处理。

CSI摄像头获取数据后,可以通过回调来处理数据放入队列,所以在主逻辑流程中,只需要做好设置,启动摄像头捕捉数据即可。

这个部分的代码如下:

```
// 屏幕旋转
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;
    }
}

// 初始化ST7789显示屏
static void init_lcd_display(void)
{
    // 配置背光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);

    // 初始化SPI总线
    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));

    // 安装面板IO
    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));

    // 创建ST7789面板
    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));

    // 复位并初始化面板
    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_mirror(panel_handle, true, false));
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));

    // 开启背光
    gpio_set_level(LCD_PIN_NUM_BK_LIGHT, LCD_DISP_BK_LIGHT_ON_LEVEL);
}

void app_main(void)
{
    ESP_LOGI(TAG, "Application starting...");

    // 1. 初始化LCD显示屏
    init_lcd_display();
    lcd_rotate(LCD_DISP_ROTATE);
    ESP_LOGI(TAG, "LCD initialized");

    // 2. 分配缩放缓冲区
    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. 创建显示队列
    display_queue = xQueueCreate(5, sizeof(uint8_t*));
    assert(display_queue != NULL);
    ESP_LOGI(TAG, "Display queue created");

    // 4. 创建显示任务
    xTaskCreate(display_task, "display_task", 4096, NULL, 8, NULL);
    ESP_LOGI(TAG, "Display task created");

    // 5. 初始化CSI摄像头
    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);

    // 初始化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");

    // 分配摄像头帧缓冲区
    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 = heap_caps_aligned_calloc(frame_buffer_alignment, 1,
                                                 cam_buffer_size,
                                                 MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA);  // 添加DMA标志
        assert(cam_buffers != NULL);
        ESP_LOGI(TAG, "Camera buffer %d allocated: %p", i, cam_buffers);
    }
    ESP_LOGI(TAG, "%d camera buffers allocated", NUM_CAM_BUFFERS);

    // 初始化摄像头传感器
#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,
    };
    example_sensor_init(&sensor_config, &sensor_handle);
    ESP_LOGI(TAG, "Camera sensor initialized");

    // 初始化CSI控制器
    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");

    // 注册事件回调
    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");

    // 初始化ISP处理器
    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");

    // 6. 启动摄像头捕获
    ESP_ERROR_CHECK(esp_cam_ctlr_start(cam_handle));
    ESP_LOGI(TAG, "Camera capture started");

    // 主循环 - 监控性能
    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;
        }
    }

    // 清理代码(实际不会执行到这里)
    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);
    }
    heap_caps_free(scaled_buffer);
    vQueueDelete(display_queue);
}
```


摄像头工作流程如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图9

这个部分涉及到的代码如下:
```
// 摄像头事件回调
static bool IRAM_ATTR camera_get_new_buffer(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data)
{
    // esp_rom_printf("[CAM] Get new buffer %d", trans_finished_count);
    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)
{
    // esp_rom_printf("[CAM] Trans finished: %d", trans_finished_count);
    // 将帧数据放入队列供显示任务处理
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(display_queue, &trans->buffer, &xHigherPriorityTaskWoken);
    trans_finished_count++;
    return xHigherPriorityTaskWoken == pdTRUE;
}
```

显示部分,用一个线程来进行处理,其工作流程如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图10
这个部分涉及到的代码如下:
```
// 初始化缩放参数和映射表
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;

    // 计算缩放比例(取宽高比中较小的比例)
    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;

    // 计算缩放后的实际尺寸
    scaled_width = (int)(src_width * scale);
    scaled_height = (int)(src_height * scale);

    // 计算居中偏移量
    x_offset = (dst_width - scaled_width) / 2;
    y_offset = (dst_height - scaled_height) / 2;
}

// 图像缩放函数
static void IRAM_ATTR scale_image(uint16_t *src, uint16_t *dst)
{
    // 将整个目标图像初始化为0(黑色)
    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]);
        }
    }
}

// 显示处理任务
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))) {
            // 跳过堆积的帧(只处理最新帧)
            while (uxQueueMessagesWaiting(display_queue) > 0) {
                xQueueReceive(display_queue, &frame_data, 0);
            }

            // 缩放图像
            int64_t start = esp_timer_get_time();
            scale_image((uint16_t*)frame_data, scaled_buffer);
            int64_t scale_time = esp_timer_get_time() - start;

            // 显示到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;

            // 性能监控
            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);
        }
    }
}
```


以上各个部分关联起来,工作流程如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图11

前面的代码中,都有详细的注释。
从代码中可以看到,使用esp_cam_ctlr_register_event_callbacks()注册了摄像头回调函数,摄像头在获取到数据后,会调用camera_get_new_buffer(),并将获取到的数据,放入trans->buffer中。
在 display_task()中,检测到数据可用,就会调用scale_image()缩放画面数据,最后使用esp_lcd_panel_draw_bitmap()显示出来。

另外,还有一个配置文件config.h来进行统一的配置:
```
#ifndef _CONFIG_H_
#define _CONFIG_H_

// 摄像头配置
#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"

// 显示屏配置 (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       // 0, 90, 180, 270
#define CAMERA_SELFIE_MODE          true    // true-同向(自拍模式)  false-反向(观察模式)

// 双缓冲
#define NUM_CAM_BUFFERS 2

#endif  // _CONFIG_H_
```

除了代码上面的适配,还需要在menuconfig中进行配置,具体如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图13

需要注意的是,config.h中的CSI_MIPI_CSI_DISP_HRES、CSI_MIPI_CSI_DISP_VRES需要与CSI_CAM_FORMAT保持一致。从ov5647的driver可以得知可选的模式:
* 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

在使用中,可以根据实际需要,选择合适的模式。


最后,将完整的代码编译烧录:
```
idf.py budil flash monitor
```

运行后,实际效果如下:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图12


另外,我手头上,也有之前用过的树莓派摄像头:
FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)图7

经过实测,都能正常使用上。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail