《ESP32-P4开发指南—V1.0》第五十章 USB摄像头实验

第五十章 USB摄像头实验


       在现代嵌入式系统中,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摄像头拍照
 * @param     无
 * @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摄像头实验效果图


请使用浏览器的分享功能分享到微信等