|
第五十章 USB摄像头实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
在现代嵌入式系统中,USB摄像头的应用日益广泛,涵盖了从简单的图像采集到复杂的视频流处理等场景。USB摄像头作为一种即插即用的设备,具有兼容性强、传输效率高的优势,使其成为视频监控、机器视觉、图像识别等领域的理想选择。本章节将介绍如何在ESP32-P4平台上实现USB摄像头功能,并在图像数据实时显示在LCD上。
50.1 USB摄像头简介
50.2 硬件设计
50.3 程序设计
50.4 下载验证
50.1 USB摄像头简介
USB摄像头是一种通过USB接口连接计算机或其他设备,用于捕获图像和视频的数字化设备。其具备即插即用、便捷高效的特性,广泛应用于视频通信、监控、直播等领域,已成为相关场景中的主流选择。对于想深入了解USB摄像头协议与工作原理的读者,可以查阅相关技术文档和手册。本章节将专注于如何利用ESP32-P4的USB OTG接口实现USB摄像头通信。
ESP32-P4芯片的USB OTG HS接口支持USB视频设备(UVC)功能。Espressif官方在ESP-IDF中提供了USB HOST UVC的示例代码,路径为:esp-idf\examples\peripherals\usb\host\uvc。本实验将基于官方示例代码,通过ESP32-P4的USB HOST接口完成对USB摄像头设备的图像数据读取,并带领读者逐步实现这一功能。
50.2 硬件设计
50.2.1 程序功能
将USB2.0摄像头模组插入开发板上的USB HOST接口,然后根据用户接入的屏幕分辨率来获取USB摄像头图像数据,最后在LCD上显示。
本章节实验包含两个示例:
1)41_usb_camera实验:实现USB摄像头图像数据显示功能更。
2)42_usb_camera_phtot实验:实现USB摄像头图像数据显示和拍照功能。
读者可以根据实际需求选择对应的实验进行操作。
50.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD/MIPILCD(引脚太多,不罗列出来)
3)USB2.0 摄像头(USB HOST)
4)SD卡(42_usb_camera_phtot实验需要)
CMD - IO44
CLK - IO43
D0 - IO39
D1 - IO40
D2 - IO41
D3 - IO42
50.2.3 原理图
USB HOST原理图已在48.2.3小节中详细阐述,为避免重复,此处不再赘述。
50.3 程序设计
50.3.1 USB UVC的IDF驱动
usb_host_uvc组件驱动位于ESP-IDF在线组件注册表中。如果需要将该组件添加到项目工程中,可按照以下步骤操作:
1)打开ESP-IDF注册表。
2)搜索 “usb_host_uvc”组件。
3)将组件安装到项目中。
组件安装完成后,系统会自动更新main文件夹中的特殊组件清单文件idf_component.yml在项目编译时,系统会根据清单文件从注册表中下载并集成该组件到工程中。关于上述操作流程,可参考本书籍第八章的内容。
为了使用usb_host_uvc组件提供的功能,首先需要在代码中导入以下头文件:
- #include "libuvc/libuvc.h"
- #include "libuvc_helper.h"
- #include "libuvc_adapter.h"
- #include "usb/usb_host.h"
复制代码 接下来,作者将介绍本章节实验用到的usb_host_uvc函数,这些函数的描述及其作用如下:
1,为libuvc适配器配置参数libuvc_adapter_set_config
该函数用于为libuvc适配器配置参数,其函数原型如下:
- void libuvc_adapter_set_config(libuvc_adapter_config_t *config);
复制代码 函数形参:
表50.3.1.1 libuvc_adapter_set_config函数形参描述
返回值:
无。
config为指向配置libuvc初始化的结构体。接下来,笔者将详细介绍libuvc_adapter_config_t结构体中的各个成员变量,如下代码所示:
- /**
- * @brief 配置结构体
- */
- typedef struct {
- /* 当设置为 true 时,会创建事件处理的后台任务。
- 否则,用户需要通过调用 libuvc_adapter_handle_events 来处理事件 */
- bool create_background_task;
- uint8_t task_priority; /* 后台任务的优先级 */
- uint32_t stack_size; /* 后台任务的堆栈大小 */
- libuvc_adapter_cb_t callback; /* 用于通知连接和断开事件的回调函数 */
- } libuvc_adapter_config_t;
复制代码 上述结构体用于配置libuvc初始化参数,以下对各个成员做简单介绍。
1)create_background_task:
若该字段为true,则创建事件处理的后台任务;若该字段为false,则需手动调用libuvc_adapter_handle_events来处理libuvc事件。
2)task_priority:
若create_background_task为true时,该字段才有效。它用来配置libuvc后台任务的优先级。
3)stack_size:
若create_background_task为true时,该字段才有效。它用来配置libuvc后台任务的堆栈。
4)callback:
用于通知连接和断开事件的回调函数。
2,初始化UVC uvc_init
该函数用于初始化UVC,其函数原型如下:
- uvc_error_t uvc_init(uvc_context_t **ctx, struct libusb_context *usb_ctx);
复制代码 函数形参:
表50.3.1.2 uvc_init函数形参描述
返回值:
uvc_error_t错误码。
3,查找摄像头设备uvc_find_device
该函数用于查找摄像头设备,其函数原型如下:
- uvc_error_t uvc_find_device(uvc_context_t *ctx, uvc_device_t **dev,
- int vid, int pid, const char *sn);
复制代码 函数形参:
表50.3.1.3 uvc_find_device函数形参描述
返回值:
uvc_error_t错误码。
4,打开uvc设备uvc_open
该函数用于打开uvc设备,其函数原型如下:
- uvc_error_t uvc_open(uvc_device_t *dev,uvc_device_handle_t **devh);
复制代码 函数形参:
表50.3.1.4 uvc_open函数形参描述
返回值:
uvc_error_t错误码。
5,获取协商后的流控制块uvc_get_stream_ctrl_format_size
该函数用于获取协商后的流控制块,其函数原型如下:
- uvc_error_t uvc_get_stream_ctrl_format_size( uvc_device_handle_t *devh,
- uvc_stream_ctrl_t *ctrl,
- enum uvc_frame_format cf,
- int width, int height,
- int fps)
复制代码 函数形参:
表50.3.1.5 uvc_get_stream_ctrl_format_size函数形参描述
返回值:
uvc_error_t错误码。
6,开启数据流uvc_start_streaming
该函数用于开启数据流,其函数原型如下:
- uvc_error_t uvc_start_streaming(uvc_device_handle_t *devh,
- uvc_stream_ctrl_t *ctrl,uvc_frame_callback_t *cb,
- void *user_ptr,uint8_t flags)
复制代码 函数形参:
表50.3.1.6 uvc_get_stream_ctrl_format_size函数形参描述
返回值:
uvc_error_t错误码。
7,关闭uvc设备 uvc_close
该函数用于关闭uvc设备,其函数原型如下:
- void uvc_close(uvc_device_handle_t *devh);
复制代码 函数形参:
表50.3.1.7 uvc_close函数形参描述
返回值:
无。
8,退出uvc设备 uvc_exit
该函数用于退出uvc设备,其函数原型如下:
- void uvc_exit(uvc_context_t *ctx);
复制代码 函数形参:
表50.3.1.8 uvc_close函数形参描述
返回值:
无。
50.3.2 程序流程图
图50.3.2.1 USB摄像头实验程序流程图
50.3.3 程序解析
1,UVC驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USB UVC驱动源码包括两个文件:usb_camera.c和usb_camera.h。
usb_camera.h主要用于声明usb_camera_init函数和USB消息等结构体,以便在其他文件中调用,具体内容不再赘述。
下面我们再解析usb_camera.c的程序,这里笔者分了几个部分来讲解,如下代码所示:
1,usb_camera_init
usb_camera_init 函数完成了USB摄像头的整个初始化过程。它首先初始化了UVC协议,以确保能够与USB摄像头建立通信,并配置了JPEG解码器来处理接收到的图像数据。随后,函数通过调用usb_host_lib_init启动USB主机库,配置相关的信号量和任务,确保USB设备能够正常连接并与系统交互。在设备连接后,函数进行设备查询并协商数据流格式,一旦流媒体传输开始,解码器便开始处理摄像头传输过来的JPEG图像数据,解码后将图像数据显示在LCD屏幕上。整个流程确保USB摄像头能够正常工作并显示实时图像。
- /**
- * @brief USB摄像头拍照
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void usb_camera_init(void)
- {
- uvc_context_t *ctx;
- uvc_device_t *dev;
- uvc_device_handle_t *devh;
- uvc_stream_ctrl_t ctrl;
- g_usb_camera.usb_state = USB_CAMERA_INVALID;
- if (lcddev.id == 0X4342) /* RGBLCD 0x4342 */
- {
- rgblcd_display_dir(1); /* 必须设置为横屏 */
- camera_width = 320;
- camera_height = 240;
- }
- else if (lcddev.id == 0X4384) /* RGBLCD 0x4384 */
- {
- rgblcd_display_dir(1); /* 必须设置为横屏 */
- camera_width = 640;
- camera_height = 480;
- }
- else if (lcddev.id == 0x7084) /* RGBLCD 0x7084 */
- {
- rgblcd_display_dir(1); /* 必须设置为横屏 */
- camera_width = 640;
- camera_height = 480;
- }
- else if (lcddev.id == 0x7016) /* RGBLCD 0x7016 */
- {
- rgblcd_display_dir(1); /* 必须设置为横屏 */
- camera_width = 800;
- camera_height = 600;
- }
- else if (lcddev.id == 0x8394) /* MIPILCD 0x8394 */
- {
- camera_width = 640;
- camera_height = 480;
- }
- else if (lcddev.id == 0x8399) /* MIPILCD 0x8399 */
- {
- camera_width = 800;
- camera_height = 600;
- }
- else if (mipidev.id == 0x9881)
- {
- camera_width = 800;
- camera_height = 600;
- }
- /* 配置JPEG硬件解码器 */
- jpeg_decode_engine_cfg_t decode_eng_cfg = {
- .intr_priority = 1, /* 优先级 */
- .timeout_ms = 50, /* 超时时间 */
- };
- /* 配置JPEG解码器 */
- jpeg_new_decoder_engine(&decode_eng_cfg, &jpgd_handle);
- /* 根据USB摄像头输出图像数据申请buf */
- size_t rx_buffer_size = 0;
- rx_buf = (uint8_t*)jpeg_alloc_decoder_mem(camera_width * camera_height * 10,
- &rx_mem_cfg, &rx_buffer_size);
-
- if (rx_buf == NULL)
- {
- ESP_LOGE(__FUNCTION__, "alloc rx buffer error");
- return ;
- }
- /* 创建事件组 */
- app_flags = xEventGroupCreate();
- assert(app_flags);
- /* host初始化 */
- ESP_ERROR_CHECK(usb_host_lib_init());
- /* libuvc配置 */
- libuvc_adapter_config_t config = {
- .create_background_task = true, /* 开启libuvc回调任务 */
- .task_priority = 5, /* libuvc回调任务优先级 */
- .stack_size = 4096, /* 设置libuvc回调任务堆栈 */
- .callback = libuvc_adapter_cb /* libuvc回调函数 */
- };
- /* libuvc配置 */
- libuvc_adapter_set_config(&config);
- /* 初始化uvs */
- UVC_CHECK(uvc_init(&ctx, NULL));
- lcd_clear(BLACK);
- while(1)
- {
- /* 等待设备连接 */
- ESP_LOGI(TAG, "Waiting for USB UVC device connection ...");
- wait_for_event(UVC_DEVICE_CONNECTED);
- /* 查询设备? */
- if (uvc_find_device(ctx, &dev, PID, VID, SERIAL_NUMBER) != UVC_SUCCESS)
- {
- ESP_LOGW(TAG, "UVC device not found");
- continue; /* 继续等待UVC设备 */
- }
- /* 发现设备 */
- ESP_LOGI(TAG, "UVC device found");
- g_usb_camera.usb_state = USB_CAMERA_FIND_DEV;
- /* 打开UVC设备 */
- UVC_CHECK(uvc_open(dev, &devh));
- /* 输出设备信息 */
- uvc_print_diag(devh, stderr);
- /* 协商数据流 */
- if (UVC_SUCCESS == uvc_negotiate_stream_profile(devh, &ctrl))
- {
- /* 必须覆盖到MPS(最大数据包大小) */
- ctrl.dwMaxPayloadTransferSize = 512;
- /* 输出配置参数 */
- uvc_print_stream_ctrl(&ctrl, stderr);
- /* 开启数据流 */
- UVC_CHECK(uvc_start_streaming(devh, &ctrl, frame_callback,NULL, 0));
- ESP_LOGI(TAG, "Streaming...");
- g_usb_camera.usb_state = USB_CAMERA_CONNET;
- /* 等待关闭事件组 */
- wait_for_event(UVC_DEVICE_DISCONNECTED);
- /* 停止摄像头数据流传输 */
- uvc_stop_streaming(devh);
- ESP_LOGI(TAG, "Done streaming.");
- }
- else
- {
- g_usb_camera.usb_state = USB_CAMERA_DISCONNECT;
- /* 等待摄像头连接 */
- wait_for_event(UVC_DEVICE_DISCONNECTED);
- }
- /* 关闭uvs */
- uvc_close(devh);
- }
- /* 退出uvs */
- uvc_exit(ctx);
- ESP_LOGI(TAG, "UVC exited");
- /* 卸载usb host */
- usb_host_lib_uinit();
- }
复制代码 2,uvc_negotiate_stream_profile
此函数用于协商USB摄像头的数据流配置,包括分辨率和帧率。
- /**
- * @brief uvs协商
- * @param devh:设备句柄
- * @param ctrl:设备参数指针
- * @retval UVC错误类型,请看uvc_error_t共用体
- */
- static uvc_error_t uvc_negotiate_stream_profile(uvc_device_handle_t *devh,
- uvc_stream_ctrl_t *ctrl)
- {
- uvc_error_t res;
- int attempt = 10;
- /* 请求10次 */
- while (attempt--)
- {
- /* 获取摄像头图像大小 */
- res = uvc_get_stream_ctrl_format_size(devh, ctrl, FORMAT,
- camera_width, camera_height, FPS);
- if (UVC_SUCCESS == res)
- {
- break;
- }
- ESP_LOGE(TAG, "Negotiation failed. Try again (%d) ...", attempt);
- }
- if (UVC_SUCCESS == res)
- {
- ESP_LOGI(TAG, "Negotiation complete.");
- }
- else
- {
-
- }
- return res;
- }
复制代码 3,wait_for_event
该函数用于等待特定的事件标志,直到事件发生。
- /**
- * @brief 等待事件
- * @param event:事件标志位
- * @retval 触发事件
- */
- static EventBits_t wait_for_event(EventBits_t event)
- {
- return xEventGroupWaitBits(app_flags, event, pdTRUE, pdFALSE, portMAX_DELAY)
- & event;
- }
复制代码 4,libuvc_adapter_cb
这个函数用于接收libuvc事件,并设置相应的事件标志位。事件如连接、断开USB设备等。
- /**
- * @brief libuvc回调函数
- * @param event:uvs状态(连接/断开)
- * @retval 无
- */
- static void libuvc_adapter_cb(libuvc_adapter_event_t event)
- {
- xEventGroupSetBits(app_flags, event);
- }
复制代码 5,frame_callback
该函数在每次接收到摄像头的图像帧时被调用。首先,使用jpeg_decoder_process函数将接收到的MJPEG图像数据解码为RGB格式。解码后的图像数据存储在指定的缓冲区中,准备进行显示。接着,调用esp_lcd_panel_draw_bitmap函数,将解码后的RGB图像数据绘制到LCD屏幕上,并根据屏幕大小和分辨率调整图像显示的位置。这样,图像就可以实时显示在LCD屏幕上,呈现出从USB摄像头获取的图像内容。
- /**
- * @brief 图像帧回调函数
- * @param frame:图像帧指针
- * @param ptr:无
- * @retval 无
- */
- void frame_callback(uvc_frame_t *frame, void *ptr)
- {
- /* 计算居中绘制的起始坐标 */
- int x_offset = (lcddev.width - camera_width) / 2;
- int y_offset = (lcddev.height - camera_height) / 2;
- /* 确保坐标合法性 */
- x_offset = x_offset < 0 ? 0 : x_offset;
- y_offset = y_offset < 0 ? 0 : y_offset;
- /* MJPEG解码 */
- esp_err_t ret = jpeg_decoder_process(jpgd_handle, &decode_cfg_rgb,
- frame->data, frame->data_bytes,
- rx_buf, camera_width * camera_height
- * 10, &out_size);
-
- if (ret != ESP_OK)
- {
- return;
- }
- /* LCD显示图像 */
- esp_lcd_panel_draw_bitmap(lcddev.lcd_panel_handle, x_offset, y_offset,
- camera_width + x_offset, camera_height
- + y_offset, rx_buf);
- }
复制代码 6,usb_host_lib_uinit
在usb_host_lib_uinit函数中,首先调用xSemaphoreTake等待信号量ready_to_uninstall_usb,直到USB设备卸载完成。这个信号量用于确保在卸载USB主机库之前,所有USB设备相关的操作已经处理完毕。然后,调用usb_host_uninstall卸载USB主机库,释放所有USB主机资源,以确保系统能够正常退出USB主机模式并恢复到其他操作状态。如果卸载失败,使用 ESP_LOGE 打印错误日志以便排查问题。
- static void usb_host_lib_uinit(void)
- {
- xSemaphoreTake(ready_to_uninstall_usb, portMAX_DELAY);
- vSemaphoreDelete(ready_to_uninstall_usb);
- /* 卸载usb host */
- if (usb_host_uninstall() != ESP_OK)
- {
- ESP_LOGE(TAG, "Failed to uninstall usb_host");
- }
- }
复制代码 7,usb_host_lib_init
usb_host_lib_init函数中,首先调用usb_host_install安装USB主机库,初始化USB主机功能,使得系统能够与USB设备进行通信和交互。若安装成功,接着调用xSemaphoreCreateBinary创建一个二值信号量ready_to_uninstall_usb,该信号量用于同步卸载USB主机库的操作,确保在所有USB设备操作完成后再进行卸载。然后,使用xTaskCreate创建一个任务usb_lib_handler_task,该任务负责处理USB事件,如设备的插拔、设备状态的变化等。这个任务会持续运行,监听USB主机库的事件,并根据事件触发相应的处理逻辑,确保系统的USB功能正常运行。
- /**
- * @brief usb host初始化
- * @param 无
- * @retval ESP_OK:初始化成功,其他:初始化失败
- */
- static esp_err_t usb_host_lib_init(void)
- {
- TaskHandle_t task_handle = NULL;
- /* usb host配置 */
- const usb_host_config_t host_config = {
- .intr_flags = ESP_INTR_FLAG_LEVEL1
- };
- /* 初始化usb host */
- esp_err_t err = usb_host_install(&host_config);
- if (err != ESP_OK)
- {
- return err;
- }
- /* 创建二值信号量 */
- ready_to_uninstall_usb = xSemaphoreCreateBinary();
- if (ready_to_uninstall_usb == NULL)
- {
- usb_host_uninstall();
- return ESP_ERR_NO_MEM;
- }
- /* 创建usb_events任务 */
- if (xTaskCreate(usb_lib_handler_task, "usb_events", 4096, NULL, 2,
- &task_handle) != pdPASS)
- {
- vSemaphoreDelete(ready_to_uninstall_usb);
- usb_host_uninstall();
- return ESP_ERR_NO_MEM;
- }
- return ESP_OK;
- }
复制代码 8,usb_lib_handler_task
该任务负责处理USB主机库的事件,包括设备连接和断开事件。
- /**
- * @brief 处理常见的USB主机lib事件
- * @param args:未使用
- * @retval 无
- */
- static void usb_lib_handler_task(void *args)
- {
- args = args;
- while (1)
- {
- uint32_t event_flags;
- usb_host_lib_handle_events(portMAX_DELAY, &event_flags);
- /* 在所有客户端注销后释放设备 */
- if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS)
- {
- usb_host_device_free_all();
- }
- /* USB主机库已释放所有设备 */
- if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE)
- {
- xSemaphoreGive(ready_to_uninstall_usb);
- }
- }
- vTaskDelete(NULL);
- }
复制代码
2,main.c驱动代码
在main.c里面编写如下代码。
- void app_main(void)
- {
- esp_err_t ret;
- ret = nvs_flash_init(); /* 初始化NVS */
- if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
- {
- ESP_ERROR_CHECK(nvs_flash_erase());
- ESP_ERROR_CHECK(nvs_flash_init());
- }
- led_init(); /* LED初始化 */
- key_init(); /* KEY初始化 */
- lcd_init(); /* LCD屏初始化 */
- usb_camera_init(); /* USB摄像头 */
- }
复制代码 这部分函数的主要功能是对LED、KEY和LCD进行初始化,然后进入USB摄像头操作。至于USB摄像头拍照的实验,可以参考“42_usb_camera_phtot”实验,区别在于本实验增加了SD卡挂载和图像数据保存的功能。
50.4 下载验证
下载程序后,将USB 2.0摄像头的USB A口插入到开发板上的HOST接口,此时MCU与USB摄像头经过协商后输出我们所需格式的图像数据,然后将图像数据经过JPEG硬件解码显示在LCD上。实验效果如下图所示。
1)如果运行的是41_usb_camera实验,需插入USB摄像头和LCD设备。
2)如果运行的是42_usb_camera_phtot实验,需插入USB摄像头、LCD和SD卡设备,方能对实时图像进行拍照。
图50.4.1 USB摄像头实验效果图 |