7| 0
|
[ESP8266/ESP32] 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("[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; } ``` 显示部分,用一个线程来进行处理,其工作流程如下: ![]() 这个部分涉及到的代码如下: ``` // 初始化缩放参数和映射表 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); } } } ``` 以上各个部分关联起来,工作流程如下: ![]() 前面的代码中,都有详细的注释。 从代码中可以看到,使用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中进行配置,具体如下: ![]() 需要注意的是,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 ``` 运行后,实际效果如下: ![]() 另外,我手头上,也有之前用过的树莓派摄像头: ![]() 经过实测,都能正常使用上。 |
© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed