FireBeetle 2 ESP32 P4 使用树莓派兼容CSI接口摄像头(ESP-IDF)
从 FireBeetle 2 ESP32 P4 的WiKi可以了解到如下的信息:也就是说,在这块开发板上,可以使用树莓派4B兼容的CSI摄像头,不用单独配置摄像头了。
这次活动,DFRobot也非常贴心的附带了一颗树莓派兼容摄像头:
可以直接连接到开发板上的CSI接口上:
注意连接的时候,使用的是反向15P间距1.0mm的FPC排线,排线的银色面贴着接触点,蓝色面贴着黑色卡扣。
之前点亮过ST7789屏幕(见:FireBeetle 2 ESP32 P4 点亮 ILI9341、ST7789屏幕),所以这次测试,就是获取摄像的画面数据,然后显示到ST7789屏幕上。
程序的主逻辑流程如下:
因为摄像头获取数据的分辨率和显示屏的分辨率不同,所以最终显示的时候,做了缩放处理。
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);
}
```
摄像头工作流程如下:
这个部分涉及到的代码如下:
```
// 摄像头事件回调
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(" Get new buffer %d", trans_finished_count);
static int buffer_index = 0;
trans->buffer = cam_buffers;
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(" Trans finished: %d", trans_finished_count);
// 将帧数据放入队列供显示任务处理
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(display_queue, &trans->buffer, &xHigherPriorityTaskWoken);
trans_finished_count++;
return xHigherPriorityTaskWoken == pdTRUE;
}
```
显示部分,用一个线程来进行处理,其工作流程如下:
这个部分涉及到的代码如下:
```
// 初始化缩放参数和映射表
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);
}
}
}
// 显示处理任务
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);
}
}
}
```
以上各个部分关联起来,工作流程如下:
前面的代码中,都有详细的注释。
从代码中可以看到,使用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_HOSTSPI2_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_LEVEL1
#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中进行配置,具体如下:
需要注意的是,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
```
运行后,实际效果如下:
另外,我手头上,也有之前用过的树莓派摄像头:
经过实测,都能正常使用上。
大佬~ 太强了,我用MicroPython驱动摄像头
页:
[1]