《ESP32-P4开发指南—V1.0》第四十八章 USB鼠标(Host)实验

第四十八章 USB鼠标(Host)实验


       本章我们介绍如何使用ESP32-P4的USB HOST来驱动USB鼠标。

       本章分为如下几个部分:

       48.1 USB鼠标键盘简介

       48.2 硬件设计

       48.3 程序设计

       48.4 下载验证


        48.1 USB鼠标键盘简介 

       传统的鼠标是采用PS/2接口和电脑通信的,但是现在PS/2接口在电脑上逐渐消失,所以现在越来越多的鼠标采用的是USB接口,而不是PS/2接口的了。

       USB鼠标属于USB HID设备。USB HID即:Human Interface Device(人机交互设备)的缩写,键盘、鼠标与游戏杆等都属于此类设备。不过HID设备并不一定要有人机接口,只要符合HID类别规范的设备都是HID设备。关于USB HID的知识,我们这里就不详细介绍了,请大家自行百度学习。

       ESP32-P4的USB OTG HS接口是支持鼠标功能的。Espressif官方在ESP-IDF中提供了针对USB HOST HID人机交互的示例代码,路径为:esp-idf\examples\peripherals\usb\host\hid。本章实验将参考官方示例代码,通过ESP32-P4的USB HOST接口实现对鼠标设备操作。


        48.2 硬件设计


       48.2.1 程序功能

       本实验代码,开机的时候先显示一些提示信息,然后初始化USB HOST,并不断轮询。当检测到USB鼠标的插入后,PC机通过USB接口将显示鼠标移动的坐标(X,Y坐标)和左右按键的状态发送至DNSP32P4开发板,然后把这些数据显示在LCD上。


       48.2.2 硬件资源

       1)RGBLCD/MIPILCD(引脚太多,不罗列出来)

       2)USB_HOST HS接口

       3)本实验接入的是小米蓝牙鼠标无线双模静音版鼠标


       48.2.3 原理图

       USB HOST原理图已在47.2.3小节中详细阐述,为避免重复,此处不再赘述。


        48.3 程序设计


       48.3.1 USB HOST HID的IDF驱动

       usb_host_hid组件驱动位于ESP-IDF在线组件注册表中。如果需要将该组件添加到项目工程中,可按照以下步骤操作:

       1)打开ESP-IDF注册表。

       2)搜索 “usb_host_hid”组件。

       3)将组件安装到项目中。

       组件安装完成后,系统会自动更新main文件夹中的特殊组件清单文件idf_component.yml在项目编译时,系统会根据清单文件从注册表中下载并集成该组件到工程中。关于上述操作流程,可参考本书籍第八章的内容。

       为了使用 usb_host_hid组件提供的功能,首先需要在代码中导入以下头文件:

#include "usb/usb_host.h"
#include "usb/hid_host.h"
#include "usb/hid_usage_mouse.h"

       接下来,作者将本章节实验用到的usb_host_hid函数,这些函数的描述及其作用如下:


       1,安装USB HID主机的USB Host HID类驱动hid_host_install

       该函数用于安装 USB HID 主机的 USB Host HID 类驱动,其函数原型如下:

esp_err_t hid_host_install(const hid_host_driver_config_t *config);

       函数形参:


表48.3.1.1 hid_host_install函数形参描述


       返回值:

       ESP_OK表示配置成功。

       其他错误码表示配置失败。

       config为指向USB Host HID 类驱动的配置结构体。接下来,笔者将详细介绍hid_host_driver_config_t结构体中的各个成员变量,如下代码所示:

/**
 * @brief HID 配置结构体
 */
typedef struct {
/* 当设置为 true 时,将创建一个处理 USB 事件的后台任务。*/
    bool create_background_task;
    size_t task_priority;                    /* 创建的后台任务的任务优先级 */
    size_t stack_size;                       /* 创建的后台任务的堆栈大小 */
    BaseType_t core_id; /* 选择后台任务运行的核心或 tskNO_AFFINITY */
    hid_host_driver_event_cb_t callback;/* HID驱动事件发生时调用的回调函数,不能为空 */
    void *callback_arg;                    /* 用户提供的参数,将传递给回调函数 */
} hid_host_driver_config_t;

       上述结构体用于传递USB Host HID 类配置参数,以下对各个成员做简单介绍。

       1)create_background_task:

       若此字段设置为true,则创建USB事件后台任务。

       2)task_priority:

       设置USB事件后台任务优先级。

       3)stack_size:

       设置USB事件后台任务堆栈大小。

       4)core_id:

       将USB事件任务运行在哪个内核。

       5)callback

       USB事件任务调用此函数。

       6)callback_arg

       回调函数的传入形参。


       2,获取HID设备参数hid_host_device_get_params

       该函数用于获取HID设备参数,其函数原型如下:

esp_err_t hid_host_device_get_params(hid_host_device_handle_t hid_dev_handle,
                                     hid_host_dev_params_t *dev_params);

       函数形参:


表48.3.1.2 hid_host_device_get_params函数形参描述


       返回值:

       ESP_OK表示获取信息成功。

       其他错误码表示获取信息失败。


       3,打开USB HID主机设备hid_host_device_open

       该函数用于打开USB HID主机设备,其函数原型如下:

esp_err_t hid_host_device_open(hid_host_device_handle_t hid_dev_handle,
                               const hid_host_device_config_t *config);

       函数形参:


表48.3.1.3 hid_host_device_open函数形参描述


       返回值:

       ESP_OK表示打开设备成功。

       其他错误码表示打开设备失败。


       4,设置HID请求协议hid_class_request_set_protocol

       该函数用于设置HID请求协议,其函数原型如下:

esp_err_t hid_class_request_set_protocol(
hid_host_device_handle_t hid_dev_handle,
                                        hid_report_protocol_t protocol);

       函数形参:


表48.3.1.4 hid_class_request_set_protocol函数形参描述


       返回值:

       ESP_OK表示设置协议成功。

       其他错误码表示设置协议失败。


       5,设置HID请求协议的空闲时间hid_class_request_set_idle

       该函数用于设置HID请求协议的空闲时间,其函数原型如下:

esp_err_t hid_class_request_set_idle(hid_host_device_handle_t hid_dev_handle,
                                     uint8_t duration,
                                     uint8_t report_id);

       函数形参:


表48.3.1.5 hid_class_request_set_idle函数形参描述


       返回值:

       ESP_OK表示设置协议空闲时间成功。

       其他错误码表示设置协议空闲时间失败。


       6,启动HID设备hid_host_device_start

       该函数用于启动HID设备,其函数原型如下:

esp_err_t hid_host_device_start(hid_host_device_handle_t hid_dev_handle);

       函数形参:


表48.3.1.6 hid_host_device_start函数形参描述


       返回值:

       ESP_OK表示启动HID设备成功。

       其他错误码表示启动HID设备失败。


       7,获取设备的原始输入报告数据hid_host_device_get_raw_input_report_data

       该函数用于获取设备的原始输入报告数据,其函数原型如下:

esp_err_t hid_host_device_get_raw_input_report_data(
hid_host_device_handle_t hid_dev_handle,
                                        uint8_t *data,
                                        size_t data_length_max,
                                        size_t *data_length);

       函数形参:


表48.3.1.7 hid_host_device_get_raw_input_report_data函数形参描述


       返回值:

       ESP_OK表示获取输入报告数据成功。

       其他错误码表示获取输入报告数据失败。


       8,关闭USB HID主机设备hid_host_device_close

       该函数用于关闭USB HID主机设备,其函数原型如下:

esp_err_t hid_host_device_close(hid_host_device_handle_t hid_dev_handle);

       函数形参:


表48.3.1.8 hid_host_device_close函数形参描述


       返回值:

       ESP_OK表示关闭HID设备成功。

       其他错误码表示关闭HID设备失败。


       48.3.2 程序流程图


图48.3.2.1 USB鼠标实验程序流程图


       48.3.3 程序解析

       本章节的例程是基于13_lcd实验编写的,所以笔者重点讲解有区别的文件。


       1,USB HID 驱动

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USB HID MSC驱动源码包括两个文件:usb_hid_mouse.c和usb_hid_mouse.h。

       usb_hid_mouse.h主要用于声明usb_hid_mouse函数和USB消息等结构体,以便在其他文件中调用,具体内容不再赘述。

       下面我们再解析usb_hid_mouse.c的程序,这里笔者分了几个部分来讲解,如下代码所示:


       1,usb_hid_mouse

       该函数初始化USB HID鼠标功能。它创建USB主机任务,安装HID主机驱动程序,并创建消息队列来接收和处理来自鼠标的事件。

/**
 * @brief      鼠标初始化
 * @param      无
 * @retval     ESP_OK:初始化成功
 */
esp_err_t usb_hid_mouse(void)
{
    /* 创建USB线程 */
    BaseType_t task_created = xTaskCreatePinnedToCore(usb_lib_task,
                                                     "usb_events",
                                                     4096,
                                                    xTaskGetCurrentTaskHandle(),
                                                     2, NULL, 0);
    assert(task_created == pdTRUE);
 
    /* 等待usb_lib_task的通知继续 */
    ulTaskNotifyTake(false, 1000);
 
    /* HID 主机配置 */
    const hid_host_driver_config_t hid_host_driver_config = {
        .create_background_task = true,             /* 创建回调函数任务 */
        .task_priority = 5,                         /* 优先级5 */
        .stack_size = 4096,                         /* 任务堆栈 */
        .core_id = 0,                               /* 内核ID */
        .callback = hid_host_device_callback,       /* 回调函数,即任务函数 */
        .callback_arg = NULL                        /* 回调函数传入参数 */
    };
    /* 初始化hid host */
    ESP_ERROR_CHECK(hid_host_install(&hid_host_driver_config));
 
    /* 创建消息队列 */
    app_event_queue = xQueueCreate(10, sizeof(app_event_queue_t));
 
    return ESP_OK;
}


       2,hid_host_device_callbac

       该函数是HID设备事件的回调函数。当设备发生连接、断开等事件时,它会将事件信息发送到消息队列中,以便其他任务或者回调实时处理。

/**
 * @brief      HID设备回调函数
 * @param     hid_device_handle:HID设备句柄
 * @param      event:HID设备事件
 * @param      arg:未使用
 * @retval     无
 */
void hid_host_device_callback(hid_host_device_handle_t hid_device_handle,
                              const hid_host_driver_event_t event,
                              void *arg)
{
    const app_event_queue_t evt_queue = {
        .event_group = APP_EVENT_HID_HOST,              /* HID事件 */
        .hid_host_device.handle = hid_device_handle,    /* hid句柄 */
        .hid_host_device.event = event,                 /* 事件 */
        .hid_host_device.arg = arg,                     /* 传入参数 */
        .mouse_flag = CONNECT,                          /* 鼠标处于在线状态 */
        .coordinate_buf = {0},                          /* 无坐标数据 */
    };
 
    if (app_event_queue)
    {
        xQueueSend(app_event_queue, &evt_queue, 0);     /* 发送消息 */
    }
}


       3,usb_lib_task

       该函数是一个USB主机库任务,负责初始化USB主机并处理USB事件。它会在事件发生时调用相应的回调函数,并在所有事件处理完毕后注销USB主机库并删除任务。

/**
 * @brief      USB主机库的任务函数
 * @param      arg:未使用
 * @retval      无
 */
static void usb_lib_task(void *arg)
{
    /* 主机配置 */
    const usb_host_config_t host_config = {
        .skip_phy_setup = false,
        .intr_flags = ESP_INTR_FLAG_LEVEL1,
    };
    /* usb host安装 */
    ESP_ERROR_CHECK(usb_host_install(&host_config));
    xTaskNotifyGive(arg);
 
    while (true)
    {
        uint32_t event_flags;
        usb_host_lib_handle_events(portMAX_DELAY, &event_flags);
        /* 只能注册一个客户端 */
        if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS)
        {
            ESP_ERROR_CHECK(usb_host_device_free_all());
            break;
        }
    }
 
    vTaskDelay(10); /* 必须延时 */
    /* 注销USB HOST */
    ESP_ERROR_CHECK(usb_host_uninstall());
    /* 删除当前任务 */
    vTaskDelete(NULL);
}


       4,hid_print_new_device_report_heade

       该函数根据当前协议类型输出报告的头部信息。当协议类型变化时(例如从鼠标到键盘或其他设备),它会打印设备类型(如“Mouse”或“Keyboard”)到日志中。

/**
 * @brief      根据报告输出协议类型生成新行
 * @param      proto:当前要输出的协议
 * @retval    无
 */
static void hid_print_new_device_report_header(hid_protocol_t proto)
{
    static hid_protocol_t prev_proto_output = -1;
 
    if (prev_proto_output != proto)
    {
        prev_proto_output = proto;
 
        if (proto == HID_PROTOCOL_MOUSE)
        {
            ESP_LOGI(usb_hid_mouse_tag,"Mouse\r\n");
        }
        else if (proto == HID_PROTOCOL_KEYBOARD)
        {
            ESP_LOGI(usb_hid_mouse_tag,"Keyboard\r\n");
        }
        else
        {
            ESP_LOGI(usb_hid_mouse_tag,"Generic\r\n");
        }
    }
}


       5,hid_host_mouse_report_callbac

       该函数用于处理来自USB HID鼠标设备的输入报告。它解析报告中的位移信息,更新鼠标的当前位置,并将坐标数据发送到消息队列中。

/**
 * @brief      USB HID主机鼠标接口报告回调处理程序
 * @param      data:输入报表数据缓冲区的指针
 * @param      length:输入报表数据缓冲区的长度
 * @retval    无
 */
static void hid_host_mouse_report_callback(const uint8_t *const data, 
const int length)
{
hid_mouse_input_report_boot_t *mouse_report = 
(hid_mouse_input_report_boot_t *)data;
 
    if (length < sizeof(hid_mouse_input_report_boot_t))
    {
        return;
    }
 
    static int x_pos = 0;
    static int y_pos = 0;
 
    /* 从位移计算绝对位置 */
    x_pos += mouse_report->x_displacement;
    y_pos += mouse_report->y_displacement;
 
    hid_print_new_device_report_header(HID_PROTOCOL_MOUSE);
 
    app_event_queue_t evt_queue = {0};
    evt_queue.event_group = APP_EVENT_HID_HOST;     /* HID事件 */
    evt_queue.mouse_flag = COORDINATE_DATA;         /* 坐标数据 */
    /* 存储坐标数据 */
sprintf(evt_queue.coordinate_buf,"X:%06d,Y:%06d,|%c|%c|",x_pos, 
y_pos,(mouse_report->buttons.button1 ? 'o' : ' '),
(mouse_report->buttons.button2 ? 'o' : ' '));
 
    if (app_event_queue)
    {
        xQueueSend(app_event_queue, &evt_queue, 0);     /* 发送消息 */
    }
}


       6,hid_host_generic_report_callbac

       该函数用于处理来自USB HID设备(如键盘、触摸板等)的通用输入报告。它将接收到的数据逐字节打印到日志中。

/**
 * @brief     USB HID主机通用接口报告回调处理程序
 * @param     data:输入报表数据缓冲区的指针
 * @param    length:输入报表数据缓冲区的长度
 * @retval    无
 */
static void hid_host_generic_report_callback(const uint8_t *const data, 
const int length)
{
    hid_print_new_device_report_header(HID_PROTOCOL_NONE);
 
    for (int i = 0; i < length; i++)
    {
        ESP_LOGI(usb_hid_mouse_tag,"%02X", data[i]);
    }
}


       7,hid_host_interface_callbac

       该函数是处理USB HID接口事件的回调函数。根据不同的事件(如输入报告、设备断开连接、传输错误),它会调用不同的处理函数,并在必要时将事件发送到消息队列。

/**
 * @brief      USB HID Host 接口事件
 * @param    hid_device_handle:设备句柄
 * @param      event:设备事件
 * @param     arg:未使用
 * @retval    无
 */
void hid_host_interface_callback(hid_host_device_handle_t hid_device_handle,
                                 const hid_host_interface_event_t event,
                                 void *arg)
{
    uint8_t data[64] = { 0 };
    size_t data_length = 0;
    hid_host_dev_params_t dev_params;
    ESP_ERROR_CHECK(hid_host_device_get_params(hid_device_handle, &dev_params));
 
    switch (event)
    {
        case HID_HOST_INTERFACE_EVENT_INPUT_REPORT:
            ESP_ERROR_CHECK(hid_host_device_get_raw_input_report_data(
hid_device_handle,
                                                                    data,
                                                                    64,
&data_length));
 
            if (HID_SUBCLASS_BOOT_INTERFACE == dev_params.sub_class)
            {
                if (HID_PROTOCOL_MOUSE == dev_params.proto)
                {
                    hid_host_mouse_report_callback(data, data_length);
                }
            }
            else
            {
                hid_host_generic_report_callback(data, data_length);
            }
 
            break;
        case HID_HOST_INTERFACE_EVENT_DISCONNECTED:
 
            const app_event_queue_t evt_queue = {
                .event_group = APP_EVENT,   /* 一般事件 */
                .hid_host_device = {0},     /* 设备信息清零 */
                .mouse_flag = DISCONNECT,   /* 鼠标处于离线状态 */
                .coordinate_buf = {0},      /* 无坐标数据 */
            };
 
            if (app_event_queue)
            {
                xQueueSend(app_event_queue, &evt_queue, 0);     /* 发送消息 */
            }
 
            ESP_ERROR_CHECK(hid_host_device_close(hid_device_handle));
            break;
        case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR:
            break;
        default:
            break;
    }
}


       8,hid_host_device_even

       该函数处理HID设备连接或事件。它根据设备的协议类型(如键盘或鼠标)打开并配置相应的HID设备,并启动设备的通信。

/**
 * @brief      USB HID Host 设备事件
 * @param      hid_device_handle:设备句柄
 * @param     event:设备事件
 * @param     arg:未使用
 * @retval   无
 */
void hid_host_device_event(hid_host_device_handle_t hid_device_handle,
                           const hid_host_driver_event_t event,
                           void *arg)
{
    hid_host_dev_params_t dev_params;
    ESP_ERROR_CHECK(hid_host_device_get_params(hid_device_handle, &dev_params));
 
    switch (event)
    {
        case HID_HOST_DRIVER_EVENT_CONNECTED:
            ESP_LOGI(usb_hid_mouse_tag, "HID Device, protocol '%s' CONNECTED",
                    hid_proto_name_str[dev_params.proto]);
 
            const hid_host_device_config_t dev_config = {
                .callback = hid_host_interface_callback,    /* hid接口回调函数 */
                .callback_arg = NULL                        /* 回调函数传入参数 */
            };
            /* 打开hid host设备 */
            ESP_ERROR_CHECK(hid_host_device_open(hid_device_handle, 
&dev_config));
 
            if (HID_SUBCLASS_BOOT_INTERFACE == dev_params.sub_class)
            {
                ESP_ERROR_CHECK(hid_class_request_set_protocol(
hid_device_handle, 
HID_REPORT_PROTOCOL_BOOT));
 
                if (HID_PROTOCOL_KEYBOARD == dev_params.proto)
                {
                    ESP_ERROR_CHECK(hid_class_request_set_idle(
hid_device_handle, 0, 0));
                }
            }
            /* 开始hid host 设备 */
            ESP_ERROR_CHECK(hid_host_device_start(hid_device_handle));
            break;
        default:
            break;
    }
}


       2,main.c驱动代码

       在main.c里面编写如下代码。

void app_main(void)
{
    esp_err_t ret;
    app_event_queue_t evt_queue;
    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());
    }
 
    lcd_init();           /* LCD屏初始化 */
    usb_hid_mouse();      /* HID mouse初始化 */
 
    lcd_show_string(30, 50,  200, 16, 16, "ESP32-P4", RED);
    lcd_show_string(30, 70,  200, 16, 16, "USB mouse TEST", RED);
    lcd_show_string(30, 90,  200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "status:disconnect", RED);
 
    while (1)
    {
        /* 等待消息队列 */
        if (xQueueReceive(app_event_queue, &evt_queue, portMAX_DELAY))
        {
            if (APP_EVENT_HID_HOST ==  evt_queue.event_group)
            {
                if (CONNECT == evt_queue.mouse_flag)
                {
                    lcd_show_string(84, 110, 200, 16, 16, "connected.", BLUE);
                    hid_host_device_event(evt_queue.hid_host_device.handle,
                                          evt_queue.hid_host_device.event,
                                          evt_queue.hid_host_device.arg);
                }
                else if (COORDINATE_DATA == evt_queue.mouse_flag)
                {
                    lcd_show_string(30, 130, lcddev.width, 16, 
16, evt_queue.coordinate_buf, BLUE);
                }
            }
            else if (APP_EVENT ==  evt_queue.event_group)
            {
                if (DISCONNECT == evt_queue.mouse_flag)
                {
                    lcd_show_string(84, 110, 200, 16, 16, "disconnect", BLUE);
                }
            }
        }
    }
}

       首先,程序初始化了NVS存储和LCD屏幕,并通过 usb_hid_mouse 函数初始化了USB HID鼠标设备。在LCD上显示设备的状态信息。进入主循环后,程序通过消息队列(xQueueReceive)监听和处理来自其他任务或事件的消息。当接收到鼠标设备的连接事件时,LCD显示“connected”;当接收到鼠标坐标数据时,LCD显示坐标信息;若设备断开连接,则LCD显示“disconnect”。通过这种方式,程序不仅管理了鼠标设备的连接状态,还将实时的设备状态和数据在LCD上显示。


        48.4 下载验证

       将程序下载到开发板后,将USB鼠标插入开发板的USB_HOST端口。如果鼠标被成功识别,LCD屏幕会显示连接成功的提示信息。此时,通过移动鼠标或按下左右按键,LCD将实时显示鼠标的当前坐标值和按键状态,如下图所示。


图48.4.1 鼠标提示信息


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