1. 简介
HID(Human Input Device),全称人体输入设备。字如其名,它能够提供人体数据的输入输出,如鼠标、键盘、摇杆等等。这种设备最早是在USB协议中被引入的,在我之前的文章(从零开始学GD32单片机 | USB通用串行总线接口+HID键盘例程)中有对HID设备类详细的讲解,对底层协议感兴趣的可以回看。
ESP-IDF框架对HID设备的API进行了封装,使用时基本不需要了解底层协议的逻辑。当然,因为这个HID是基于蓝牙实现的,所以仍然会涉及GAP协议等蓝牙协议,也可以在栏目目录中找到相关的介绍文章。
2. 例程
例程中会实现一个HID蓝牙键盘,配置开发板中的一枚按键,使按下按键时向主机输入字母‘a’。
2.1 menuconfig配置
使用HID设备需要在menuconfig中打开相关配置项。首先在Bluedroid选项中开启经典蓝牙HID。
进入前面的选项,打开HID设备选项。
2.2 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_hidd_api.h"
#include "driver/gpio.h"
#define TAG "app"
#define OWN_NAME "ESP32"
#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
static uint8_t hid_keyboard_desc[] = {
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
0x09, 0x06, /* USAGE (Keyboard) */
0xa1, 0x01, /* COLLECTION (Application) */
0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x95, 0x08, /* REPORT_COUNT (8) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x81, 0x02, /* INPUT (Data,Var,Abs) */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x26, 0xFF, 0x00, /* LOGICAL_MAXIMUM (255) */
0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
0xc0 /* END_COLLECTION */
};
static esp_hidd_app_param_t hidd_param;
static esp_hidd_qos_param_t hidd_qos;
static EventGroupHandle_t event_group;
static TaskHandle_t task_handle;
static void hid_keyboard_task(void* args)
{
while (1) {
/* 等待按键事件 */
if (pdTRUE == xEventGroupWaitBits(event_group, 0x01, pdTRUE, pdTRUE, portMAX_DELAY)) {
uint8_t buf[8] = {0};
buf[2] = 0x04;
esp_bt_hid_device_send_report(ESP_HIDD_REPORT_TYPE_INTRDATA, 0, 8, buf);
buf[2] = 0x00;
esp_bt_hid_device_send_report(ESP_HIDD_REPORT_TYPE_INTRDATA, 0, 8, buf);
}
}
}
static void IRAM_ATTR gpio_isr_handler(void* args)
{
BaseType_t higher_task_woken = pdFALSE;
xEventGroupSetBitsFromISR(event_group, 0x01, &higher_task_woken);
if (higher_task_woken) {
portYIELD_FROM_ISR();
}
}
static void startup_app()
{
event_group = xEventGroupCreate();
xTaskCreate(hid_keyboard_task, "hid_keyboard_task", 4096, NULL, 5, &task_handle);
/* 初始化GPIO */
const gpio_config_t gpio_cfg = {
.pin_bit_mask = (1 << GPIO_NUM_0),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_POSEDGE
};
gpio_config(&gpio_cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, NULL);
}
static void shutdown_app()
{
gpio_isr_handler_remove(GPIO_NUM_0);
gpio_uninstall_isr_service();
vTaskDelete(task_handle);
vEventGroupDelete(event_group);
}
static void esp_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
/* 验证完成 */
case ESP_BT_GAP_AUTH_CMPL_EVT:
{
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "authentication success: %s [" BDASTR "]", param->auth_cmpl.device_name, BDA2STR(param->auth_cmpl.bda));
} else {
ESP_LOGE(TAG, "authentication failed, status:%d", param->auth_cmpl.stat);
}
break;
}
default:
break;
}
}
void esp_bt_hidd_cb(esp_hidd_cb_event_t event, esp_hidd_cb_param_t *param)
{
switch (event) {
/* 初始化完成 */
case ESP_HIDD_INIT_EVT:
if (param->init.status == ESP_HIDD_SUCCESS) {
hidd_param.name = "Keyboard";
hidd_param.description = "Keyboard on ESP32";
hidd_param.provider = "Espressif";
hidd_param.subclass = ESP_HID_CLASS_KBD;
hidd_param.desc_list = hid_keyboard_desc;
hidd_param.desc_list_len = sizeof(hid_keyboard_desc);
esp_bt_hid_device_register_app(&hidd_param, &hidd_qos, &hidd_qos);
} else {
ESP_LOGE(TAG, "init hidd failed!");
}
break;
/* 应用注册完成 */
case ESP_HIDD_REGISTER_APP_EVT:
if (param->register_app.status == ESP_HIDD_SUCCESS) {
/* 设置设备可发现 */
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
if (param->register_app.in_use) {
ESP_LOGI(TAG, "start virtual cable plug!");
esp_bt_hid_device_connect(param->register_app.bd_addr);
}
} else {
ESP_LOGE(TAG, "setting hid parameters failed!");
}
break;
/* 主机连接打开 */
case ESP_HIDD_OPEN_EVT:
if (param->open.status == ESP_HIDD_SUCCESS) {
if (param->open.conn_status == ESP_HIDD_CONN_STATE_CONNECTING) {
ESP_LOGI(TAG, "connecting...");
} else if (param->open.conn_status == ESP_HIDD_CONN_STATE_CONNECTED) {
ESP_LOGI(TAG, "connected to " BDASTR, BDA2STR(param->open.bd_addr));
startup_app();
/* 设置设备不可发现 */
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
} else {
ESP_LOGE(TAG, "unknown connection status");
}
} else {
ESP_LOGE(TAG, "open failed!");
}
break;
/* 主机连接关闭 */
case ESP_HIDD_CLOSE_EVT:
if (param->close.status == ESP_HIDD_SUCCESS) {
if (param->close.conn_status == ESP_HIDD_CONN_STATE_DISCONNECTING) {
ESP_LOGI(TAG, "disconnecting...");
} else if (param->close.conn_status == ESP_HIDD_CONN_STATE_DISCONNECTED) {
ESP_LOGI(TAG, "disconnected!");
shutdown_app();
/* 设置设备可发现 */
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
} else {
ESP_LOGE(TAG, "unknown connection status");
}
} else {
ESP_LOGE(TAG, "close failed!");
}
break;
/* 报文发送 */
case ESP_HIDD_SEND_REPORT_EVT:
if (param->send_report.status == ESP_HIDD_SUCCESS) {
ESP_LOGI(TAG, "send report, id: 0x%02X, type: %d",
param->send_report.report_id,
param->send_report.report_type);
} else {
ESP_LOGE(TAG, "send report failed, id: 0x%02X, type: %d, status: %d, reason: %d",
param->send_report.report_id,
param->send_report.report_type,
param->send_report.status,
param->send_report.reason);
}
break;
/* GET_REPORT请求 */
case ESP_HIDD_GET_REPORT_EVT:
ESP_LOGI(TAG, "GET_REPORT, id: 0x%02X, type: %d, size: %d",
param->get_report.report_id,
param->get_report.report_type,
param->get_report.buffer_size);
break;
/* SET_REPORT请求 */
case ESP_HIDD_SET_REPORT_EVT:
ESP_LOGI(TAG, "SET_REPORT, id: 0x%02X, type: %d, size: %d",
param->set_report.report_id,
param->set_report.report_type,
param->set_report.len);
break;
/* SET_PROTOCOL请求 */
case ESP_HIDD_SET_PROTOCOL_EVT:
if (param->set_protocol.protocol_mode == ESP_HIDD_BOOT_MODE) {
ESP_LOGI(TAG, " - boot protocol");
} else if (param->set_protocol.protocol_mode == ESP_HIDD_REPORT_MODE) {
ESP_LOGI(TAG, " - report protocol");
}
break;
/* 虚拟接口断开 */
case ESP_HIDD_VC_UNPLUG_EVT:
if (param->vc_unplug.status == ESP_HIDD_SUCCESS) {
if (param->close.conn_status == ESP_HIDD_CONN_STATE_DISCONNECTED) {
ESP_LOGI(TAG, "disconnected!");
shutdown_app();
/* 设置设备可发现 */
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
} else {
ESP_LOGE(TAG, "unknown connection status");
}
} else {
ESP_LOGE(TAG, "close failed!");
}
break;
default:
break;
}
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
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());
}
/* 释放低功耗蓝牙资源 */
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
/* 初始化蓝牙 */
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能蓝牙控制器 */
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 初始化blueroid */
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能blueroid */
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "enable bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 注册GAP协议回调 */
if ((ret = esp_bt_gap_register_callback(esp_gap_cb)) != ESP_OK) {
ESP_LOGE(TAG, "gap register failed: %s", esp_err_to_name(ret));
return -1;
}
/* 设置GAP设备名 */
esp_bt_gap_set_device_name(OWN_NAME);
/* 设置GAP设备类 */
esp_bt_cod_t cod = {0};
cod.major = ESP_BT_COD_MAJOR_DEV_PERIPHERAL;
cod.minor = ESP_BT_COD_MINOR_PERIPHERAL_KEYBOARD;
esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR);
/* 注册HID回调 */
esp_bt_hid_device_register_callback(esp_bt_hidd_cb);
/* 初始化HID */
esp_bt_hid_device_init();
return 0;
}
代码中关于Bluedroid框架、控制器和GAP协议相关的参考之前的文章。
GAP协议方面比之前多了一个注册设备类的配置。
typedef struct {
uint32_t reserved_2: 2; /*!< undefined */
uint32_t minor: 6; /*!< minor class */
uint32_t major: 5; /*!< major class */
uint32_t service: 11; /*!< service class */
uint32_t reserved_8: 8; /*!< undefined */
} esp_bt_cod_t;
设备类分为主设备类、副设备类和服务。主设备类有下面这些:
typedef enum {
ESP_BT_COD_MAJOR_DEV_MISC = 0, /*!< Miscellaneous */
ESP_BT_COD_MAJOR_DEV_COMPUTER = 1, /*!< Computer */
ESP_BT_COD_MAJOR_DEV_PHONE = 2, /*!< Phone(cellular, cordless, pay phone, modem */
ESP_BT_COD_MAJOR_DEV_LAN_NAP = 3, /*!< LAN, Network Access Point */
ESP_BT_COD_MAJOR_DEV_AV = 4, /*!< Audio/Video(headset, speaker, stereo, video display, VCR */
ESP_BT_COD_MAJOR_DEV_PERIPHERAL = 5, /*!< Peripheral(mouse, joystick, keyboard) */
ESP_BT_COD_MAJOR_DEV_IMAGING = 6, /*!< Imaging(printer, scanner, camera, display */
ESP_BT_COD_MAJOR_DEV_WEARABLE = 7, /*!< Wearable */
ESP_BT_COD_MAJOR_DEV_TOY = 8, /*!< Toy */
ESP_BT_COD_MAJOR_DEV_HEALTH = 9, /*!< Health */
ESP_BT_COD_MAJOR_DEV_UNCATEGORIZED = 31, /*!< Uncategorized: device not specified */
} esp_bt_cod_major_dev_t;
对于本例程需要选择ESP_BT_COD_MAJOR_DEV_PERIPHERAL。对于前面选择的主设备,副设备类有以下:
typedef enum {
ESP_BT_COD_MINOR_PERIPHERAL_KEYBOARD = 0x10, /*!< Keyboard */
ESP_BT_COD_MINOR_PERIPHERAL_POINTING = 0x20, /*!< Pointing */
ESP_BT_COD_MINOR_PERIPHERAL_COMBO = 0x30, /*!< Combo
ESP_BT_COD_MINOR_PERIPHERAL_KEYBOARD, ESP_BT_COD_MINOR_PERIPHERAL_POINTING
and ESP_BT_COD_MINOR_PERIPHERAL_COMBO can be OR'd with one of the
following values to identify a multifunctional device. e.g.
ESP_BT_COD_MINOR_PERIPHERAL_KEYBOARD | ESP_BT_COD_MINOR_PERIPHERAL_GAMEPAD
ESP_BT_COD_MINOR_PERIPHERAL_POINTING | ESP_BT_COD_MINOR_PERIPHERAL_SENSING_DEVICE
*/
ESP_BT_COD_MINOR_PERIPHERAL_JOYSTICK = 0x01, /*!< Joystick */
ESP_BT_COD_MINOR_PERIPHERAL_GAMEPAD = 0x02, /*!< Gamepad */
ESP_BT_COD_MINOR_PERIPHERAL_REMOTE_CONTROL = 0x03, /*!< Remote Control */
ESP_BT_COD_MINOR_PERIPHERAL_SENSING_DEVICE = 0x04, /*!< Sensing Device */
ESP_BT_COD_MINOR_PERIPHERAL_DIGITIZING_TABLET = 0x05, /*!< Digitizing Tablet */
ESP_BT_COD_MINOR_PERIPHERAL_CARD_READER = 0x06, /*!< Card Reader */
ESP_BT_COD_MINOR_PERIPHERAL_DIGITAL_PAN = 0x07, /*!< Digital Pan */
ESP_BT_COD_MINOR_PERIPHERAL_HAND_SCANNER = 0x08, /*!< Hand Scanner */
ESP_BT_COD_MINOR_PERIPHERAL_HAND_GESTURAL_INPUT = 0x09, /*!< Hand Gestural Input */
} esp_bt_cod_minor_peripheral_t;
对于本例程,选择ESP_BT_COD_MINOR_PERIPHERAL_KEYBOARD。服务的话暂时不需要配置。
之后调esp_bt_hid_device_register_callback配置HID的回调和调esp_bt_hid_device_init初始化HID,HID应用的相关业务都会在回调函数中完成,因此下面重点介绍。
初始化完成事件(ESP_HIDD_INIT_EVT):当HID初始化完成后会触发,通常在这里会调用esp_bt_hid_device_register_app函数启动HID应用,参数一传入配置结构体:
typedef struct {
const char *name; /*!< service name */
const char *description; /*!< service description */
const char *provider; /*!< provider name */
uint8_t subclass; /*!< HID device subclass */
uint8_t *desc_list; /*!< HID descriptor list */
int desc_list_len; /*!< size in bytes of HID descriptor list */
} esp_hidd_app_param_t;
- name:设备名;
- description:服务描述(可选);
- provider:服务提供商(可选);
- subclass:子类,选项如下:
#define ESP_HID_CLASS_UNKNOWN (0x00<<2) /*!< unknown HID device subclass */
#define ESP_HID_CLASS_JOS (0x01<<2) /*!< joystick */
#define ESP_HID_CLASS_GPD (0x02<<2) /*!< game pad */
#define ESP_HID_CLASS_RMC (0x03<<2) /*!< remote control */
#define ESP_HID_CLASS_SED (0x04<<2) /*!< sensing device */
#define ESP_HID_CLASS_DGT (0x05<<2) /*!< digitizer tablet */
#define ESP_HID_CLASS_CDR (0x06<<2) /*!< card reader */
#define ESP_HID_CLASS_KBD (0x10<<2) /*!< keyboard */
#define ESP_HID_CLASS_MIC (0x20<<2) /*!< pointing device */
#define ESP_HID_CLASS_COM (0x30<<2) /*!< combo keyboard/pointing */
- desc_list:HID设备描述符,用于定义设备之间数据的传输的格式和规范,必须按照USB官方的定义来编写;
- desc_list_len:HID设备描述符长度。
参数二和三分别是输入数据和输出数据的QoS配置,这里直接传空结构体表示不配置。
应用注册完成事件(ESP_HIDD_REGISTER_APP_EVT):HID应用注册完成时触发,这里通常会调esp_bt_gap_set_scan_mode配置设备为可发现状态。这里还需要判断主机是否使用了虚拟电缆主机地址,如果使用了的话需要手动调esp_bt_hid_device_connect函数连接主机。
主机连接打开事件(ESP_HIDD_OPEN_EVT):当有主机尝试连接设备时触发,这里需要判断连接处于什么阶段;若主机成功连接后,就可以初始化业务相关的资源同时设置设备不可发现来禁止其他设备尝试连接。
我这里是初始化一个事件队列和创建一个任务用于发送键值,同时配置按键管脚。按键管脚是配置成中断输入,中断服务函数中会向事件队列发送事件,发送任务收到事件后会调用esp_bt_hid_device_send_report函数向主机发送键值,参数一表示报告类型:
typedef enum {
ESP_HIDD_REPORT_TYPE_OTHER = 0, /*!< unknown report type */
ESP_HIDD_REPORT_TYPE_INPUT, /*!< input report */
ESP_HIDD_REPORT_TYPE_OUTPUT, /*!< output report */
ESP_HIDD_REPORT_TYPE_FEATURE, /*!< feature report */
ESP_HIDD_REPORT_TYPE_INTRDATA, /*!< special value for reports to be sent on interrupt channel, INPUT is assumed */
} esp_hidd_report_type_t;
HID设备的键值上报属于中断传输,所以要选最后一个(ESP_HIDD_REPORT_TYPE_INTRDATA)。参数二是报告ID,默认是0;参数三是数据包数组;参数四是数据包长度。HID键盘的数据包长度固定是8字节,定义如下:
Modifier keys主要就是以下的键位:
在这里只需要配置byte 1就可以了,对应键位的键值需要参考USB官方的文档,字母‘a’对应的是0x04。需要注意的是,如果表示按键触发一次,那么发送完键值后需要立即发送一个空数据包,表示按键松开,不然主机会一直输入之前的按键。
主机关闭事件(ESP_HIDD_CLOSE_EVT):当主机断开连接时触发,这里就是释放业务部分的资源并重新设置设备为可发现状态。
报文发送事件(ESP_HIDD_SEND_REPORT_EVT):当数据包由上层发送到底层时触发,一般不需要操作,这里这时打印信息。
GET_REPORT报文事件(ESP_HIDD_GET_REPORT_EVT)、SET_REPORT报文事件(ESP_HIDD_SET_REPORT_EVT)和SET_PROTOCOL报文事件(ESP_HIDD_SET_PROTOCOL_EVT):这三个都是HID协议相关的事件,GET_REPORT用于主机接收报文,SET_REPORT用于主机发送报文,SET_PROTOCOL用于主机切换从机协议。
虚拟电缆断开事件(ESP_HIDD_VC_UNPLUG_EVT) :当从机主动断开虚拟电缆时触发,操作其实就跟主机关闭事件一样。
2.3 测试
编译代码并烧录到开发板,电脑进入蓝牙配置,搜索蓝牙设备,这时可以看到名字为“ESP32”的输入设备。
连接它,随便打开一个文本文件或word文档,然后按下我们配置的按键,就会看到打印出对应的字母了。