这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » [ESP-IDF]使用ESP32P4-EYE完成屏幕实时显示摄像头画面

共1条 1/1 1 跳转至

[ESP-IDF]使用ESP32P4-EYE完成屏幕实时显示摄像头画面

工程师
2026-03-08 23:22:22     打赏

简介

ESP32-P4-EYE 是一款基于 ESP32-P4 芯片的视觉开发板,主要面向摄像头应用。ESP32-P4 搭载双核 RISC-V 处理器,支持最大 32 MB PSRAM。此外,ESP32-P4 支持 USB 2.0 标准, MIPI-CSI/DSI, H264 Encoder 等多种外设,可满足客户对低成本、高性能、低功耗的多媒体产品的开发需求。 (来源于espressif)这个板子的芯片和M5 stack Tab 5 一样,但是这次来这边没有带Tab5,而且这几天正好再研究Edge impulse突然想到Edge impulse中就有对这个板子的支持。但是详细检查完毕之后发现里面的支持是对ESP32-S3芯片的支持,所以如果使用Edge impulse的话就需要额外的适配摄像头。


ESP32-P4-EYE 正面图(点击放大)背面图

ESP32-P4-EYE 背面图(点击放大)

于是便是放弃了Arduino环境的开发想法,转向使用了ESP-IDF进行开发。乐鑫的产品有一点比较好的就是说它会给你提供详细的BSP

image.png

但是对于Demo中的摄像头采集并且显示在屏幕上的Demo是不可用的,可能是仓库的迁移或者是删除了。已经处于了404的状态。

image.png所以我便把BSP已经使用组件管理器的方式维护到了本地。

## IDF Component Manager Manifest File
dependencies:
  ## Required IDF version
  idf:
    version: '>=4.1.0'
  # # Put list of dependencies here
  # # For components maintained by Espressif:
  # component: "~1.0.0"
  # # For 3rd party components:
  # username/component: ">=1.0.0,<2.0.0"
  # username2/component2:
  #   version: "~1.0.0"
  #   # For transient dependencies `public` flag can be set.
  #   # `public` flag doesn't have an effect dependencies of the `main` component.
  #   # All dependencies of `main` are public by default.
  #   public: true
  espressif/esp32_p4_eye: ^2.0.2

之后IDF便会把对应的组件进行下载。

image.png

上述的组件内容分别是

espressif__button/: 按钮输入组件。
espressif__cmake_utilities/: CMake工具组件。
espressif__esp32_p4_eye/: ESP32-P4-EYE开发板特定组件。
espressif__esp_cam_sensor/: 摄像头传感器组件。
espressif__esp_codec_dev/: 编解码器设备组件。
espressif__esp_h264/: H.264视频编解码组件。
espressif__esp_ipa/: 图像处理加速组件。
espressif__esp_lvgl_port/: LVGL图形库端口。
espressif__esp_sccb_intf/: SCCB接口组件。
espressif__esp_video/: 视频处理组件。
espressif__knob/: 旋钮输入组件。
espressif__led_indicator/: LED指示器组件。
espressif__led_strip/: LED灯带组件。
espressif__usb_host_uvc/: USB UVC主机组件。
lvgl__lvgl/: LVGL轻量级图形库。


这个项目的主要功能包括:

初始化ESP32-P4-EYE的显示屏和摄像头

通过V4L2接口捕获摄像头数据

使用LVGL创建画布显示图像

通过旋钮控制图像缩放(0.1x到5.0x)

按下编码器按钮重置缩放为1.0x


实现步骤详解1. 初始化显示和背光

首先需要初始化显示屏和打开背光。这是使用BSP(Board Support Package)提供的接口:

// 1) 初始化显示
ESP_LOGI(TAG, "Initializing display...");
lv_display_t *disp = bsp_display_start();
if (disp == NULL) {
    ESP_LOGE(TAG, "Failed to initialize display");
    return;
}

// 2) 打开背光
ESP_LOGI(TAG, "Turning on backlight...");
bsp_display_backlight_on();

2. 初始化摄像头

调用BSP的摄像头初始化函数:

// 3) 初始化摄像头(BSP会完成底层相关初始化)
ESP_LOGI(TAG, "Initializing camera...");
esp_err_t ret = bsp_camera_start(NULL);
if (ret != ESP_OK) {
    ESP_LOGE(TAG, "Failed to initialize camera: %s", esp_err_to_name(ret));
    return;
}

这里比较好的就是说不需要用户去手动的初始化了,直接使用BSP

3. 打开视频设备并枚举格式

使用Linux V4L2接口访问摄像头设备:

// 4) 打开视频设备节点
ESP_LOGI(TAG, "Opening video device...");
int fd = open(VIDEO_DEVICE, O_RDWR);
if (fd < 0) {
    ESP_LOGE(TAG, "Failed to open video device");
    return;
}

// 5) 枚举并打印摄像头支持的像素格式
struct v4l2_fmtdesc fmtdesc = {0};
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ESP_LOGI(TAG, "Supported formats:");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
    ESP_LOGI(TAG, "  %d: %s (0x%08x)", fmtdesc.index, fmtdesc.description, fmtdesc.pixelformat);
    fmtdesc.index++;
}

4. 设置视频格式(视频格式这里比较复杂)

尝试设置不同的分辨率,优先选择适合小屏幕的格式:

// 6) 尝试常见分辨率(优先低分辨率,便于在240x240屏上显示)
uint32_t resolutions[][2] = {
    {640, 480},   // VGA - 对240x240显示较友好
    {800, 600},   // SVGA
    {1280, 720},  // HD
    {1920, 1080}, // Full HD
};

struct v4l2_format format = {0};
bool format_set = false;

for (int i = 0; i < 4; i++) {
    format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    format.fmt.pix.width = resolutions[i][0];
    format.fmt.pix.height = resolutions[i][1];
    format.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
    format.fmt.pix.field = V4L2_FIELD_NONE;

    if (ioctl(fd, VIDIOC_S_FMT, &format) == 0) {
        ESP_LOGI(TAG, "Successfully set format: %dx%d RGB565",
                 format.fmt.pix.width, format.fmt.pix.height);
        format_set = true;
        break;
    }
}

5. 申请和映射V4L2缓冲区

为视频流申请缓冲区并映射到用户空间:

// 8) 申请V4L2缓冲区
struct v4l2_requestbuffers req = {
    .count = BUFFER_COUNT,
    .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .memory = V4L2_MEMORY_MMAP,
};
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
    ESP_LOGE(TAG, "Failed to request buffers");
    close(fd);
    return;
}

// 9) 查询并映射缓冲区到用户空间
uint8_t *buffers[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i++) {
    struct v4l2_buffer buf = {
        .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        .memory = V4L2_MEMORY_MMAP,
        .index = i,
    };
    if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
        ESP_LOGE(TAG, "Failed to query buffer");
        close(fd);
        return;
    }
    buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
    // ... 入队缓冲区
}

6. 开启视频流并创建显示画布

// 10) 开启视频流
if (ioctl(fd, VIDIOC_STREAMON, &req.type) < 0) {
    ESP_LOGE(TAG, "Failed to start streaming");
    close(fd);
    return;
}

// 11) 创建240x240画布(与屏幕分辨率一致)
const uint32_t canvas_w = 240;
const uint32_t canvas_h = 240;
static lv_color_t *canvas_buf = heap_caps_malloc(canvas_w * canvas_h * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);

bsp_display_lock(0);
lv_obj_t *canvas = lv_canvas_create(lv_scr_act());
lv_canvas_set_buffer(canvas, canvas_buf, canvas_w, canvas_h, LV_COLOR_FORMAT_RGB565);
lv_obj_center(canvas);
bsp_display_unlock();

7. 初始化旋钮和按键控制

使用ESP-IDF的knob和button组件:

// 12) 初始化旋钮:用于缩放控制
knob_config_t knob_cfg = {
    .default_direction = 0,
    .gpio_encoder_a = GPIO_NUM_48,
    .gpio_encoder_b = GPIO_NUM_47,
};
knob_handle_t knob = iot_knob_create(&knob_cfg);
iot_knob_register_cb(knob, KNOB_LEFT, knob_left_cb, NULL);
iot_knob_register_cb(knob, KNOB_RIGHT, knob_right_cb, NULL);

// 13) 初始化编码器按键:按下恢复1.0x
button_config_t btn_cfg = { .long_press_time = 0, .short_press_time = 0 };
button_gpio_config_t btn_gpio_cfg = { .gpio_num = ENCODER_PRESS_GPIO, .active_level = 0 };
button_handle_t encoder_btn;
iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &encoder_btn);
iot_button_register_cb(encoder_btn, BUTTON_PRESS_DOWN, NULL, encoder_press_cb, NULL);

8. 主循环:捕获、处理和显示帧

核心的图像处理逻辑:

while (1) {
    // 14) 取出一帧
    struct v4l2_buffer buf = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .memory = V4L2_MEMORY_MMAP };
    ioctl(fd, VIDIOC_DQBUF, &buf);

    // 15) 根据缩放倍率从原图中心区域裁剪
    uint16_t *src = (uint16_t *)buffers[buf.index];
    uint16_t *dst = (uint16_t *)canvas_buf;

    uint32_t crop_w = (uint32_t)(canvas_w / zoom_level);
    uint32_t crop_h = (uint32_t)(canvas_h / zoom_level);
    // 防止裁剪尺寸超过源图
    if (crop_w > actual_width) crop_w = actual_width;
    if (crop_h > actual_height) crop_h = actual_height;

    uint32_t src_x_offset = (actual_width - crop_w) / 2;
    uint32_t src_y_offset = (actual_height - crop_h) / 2;

    // 16) 将裁剪区域缩放拷贝到240x240画布
    for (uint32_t y = 0; y < canvas_h; y++) {
        for (uint32_t x = 0; x < canvas_w; x++) {
            uint32_t src_x = src_x_offset + (x * crop_w) / canvas_w;
            uint32_t src_y = src_y_offset + (y * crop_h) / canvas_h;
            uint32_t src_offset = src_y * actual_width + src_x;
            uint32_t dst_offset = y * canvas_w + x;
            dst[dst_offset] = src[src_offset];
        }
    }

    // 17) 通知LVGL刷新画布
    bsp_display_lock(0);
    lv_obj_invalidate(canvas);
    bsp_display_unlock();

    // 18) 将缓冲区重新入队
    ioctl(fd, VIDIOC_QBUF, &buf);
}

效果如下

b6e1d2687c361f962c8604d13b30bbe2.jpg

广角

99f3b5a7c3e1483744f9ad1239321019.jpg

放大最大eaa5eecb8f859639241ee2a080268e8d.jpg

部分缩放

完整代码如下

sample_project.zip

总结

这个实现展示了ESP32-P4-EYE开发板强大的多媒体处理能力。通过V4L2接口,我们可以高效地访问摄像头数据;结合LVGL的canvas组件,可以实现流畅的图像显示;而旋钮控制则提供了良好的用户交互体验。代码已经在ESP32-P4-EYE上测试通过,支持实时预览和缩放控制。





关键词: ESP-IDF     ESP32P4-EYE    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]