简介
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的话就需要额外的适配摄像头。
背面图

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

但是对于Demo中的摄像头采集并且显示在屏幕上的Demo是不可用的,可能是仓库的迁移或者是删除了。已经处于了404的状态。
所以我便把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便会把对应的组件进行下载。

上述的组件内容分别是
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);
}效果如下

广角

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