第四十八章 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 鼠标提示信息