esp32系列(6):esp32 蓝牙HID设备demo学习

我最终设想的项目需要用到蓝牙 HID 设备的实现,所以在学习ESP32 蓝牙架构之后,学习一下官方的 HID 设备 demo。

1 USB 相关知识

人机交互设备(Human Interface Devices ,HID),官网

USB(Universal Serial Bus)是用于PC与各类设备互联的四线通信架构。USB实际上是一种双线串行通信链路,运行速度为1.5Mb/s~12Mb/s。USB协议可以在启动时或在运行时插入时配置设备。这些设备被分成不同的设备类。每个设备类都定义了具有类似功能的设备的常见行为和协议。USB设备类:

  • 显示设备
  • 通信设备
  • 音频设备
  • 大容量存储器
  • 人机接口设备

使用 USB 接口的HID类主要由用来控制计算机系统操作的设备组成。HID类设备的典型例子包括:

  • 键盘和指向设备:标准鼠标设备、轨迹球、操纵杆等。
  • 控制面板设备:旋钮、开关、按钮和滑块等。
  • 可以在手机等器件上发现的控制设备:方向盘等。
  • 不需要人交互但是可以但提供与HID类设备类似格式的数据的设备:条形码阅读器、温度计或电压表等。

2 HID 基础知识

2.1 HID 描述符的概念

描述符的概念
重点:关于USB设备的信息存储在其ROM(只读存储器)的段中。这些段称为描述符(descriptor)。interface 描述符可以将设备标识为属于USB设备类的某一种。

USB/HID类设备使用一个相应的 HID类 驱动程序来检索和路由所有数据。数据的路由和检索是通过检查设备的描述符和它提供的数据来完成的。HID的数据通信结构:

在这里插入图片描述

HID类 HID描述符 定义了其他 描述符 是否存在及其大小,比如说 Report 和 Physical 描述符。结构图:

在这里插入图片描述

Report 描述符概念
重点:一个 Report 描述符描述设备生成的每一段数据以及该数据实际表示的内容。(注意Report 描述符的概念,后面要用到)

在这里插入图片描述

比如 Report 描述符定义了描述按钮位置的item,item信息用于:

  • 确定将输入路由到哪里。例如,将输入发送输入到鼠标或操纵杆API。(到主机哪里去)
  • 允许软件分配输入的功能。例如,使用操纵杆输入来移动坦克。(到主机去干嘛)

通过检查一个 item (统称为 Report 描述符),HID类 驱动程序 能够确定来自 HID类设备 的数据报告的大小和组成。

Physical 描述符概念
Physical 描述符集是可选的描述符,它提供有关用于激活设备上的控件的人体部位的信息。

在这里插入图片描述

所有描述符的结构:

在这里插入图片描述

2.2 功能特性

2.2.1 HID Class

USB设备按以下标准分类:

  • 有类似数据传输的需求的。
  • 共享一类驱动程序的。

比如,音频设备(Audio)类设备 需要同步的数据通道。而 HID类设备 有不同的(而且更简单)传输需求。

一个USB设备可以是一个类型,也可以是由多个类型。这可能是因为通过 Interface 描述符而不是 Device 描述符来区分类。

USB核心规范定义了 HID 类代码。对于HID类设备,Interface 描述符的 bInterfaceClass 成员值总是 3 。

2.2.2 Subclass

在HID规范的早期开发中,Subclass子类 被用来识别不同类型的HID类设备的特定协议。虽然这反映了当前行业使用的模型(所有设备都使用由类似的流行设备定义的 协议),但很快就发现这种方法的局限性太大。也就是说,设备需要满足于狭义定义的子类,并且不能提供子类不支持的任何功能。

HID委员会一致认为,为所有可能的(尚未构想的)设备定义子类协议的可能性不大。此外,许多已知的设备似乎跨越了多种分类——例如,带有定位器的键盘,或提供按键的定位器。因此,HID类不使用子类来定义大多数协议。相反,HID类设备通过它的数据协议和在它的Report描述符中提供的数据类型识别

一旦检测到设备,HID类驱动程序就加载和解析Report描述符。现有设备和新设备的协议是通过在Report描述符中混合数据类型创建的。

因为Report描述符的解析器表示大量的代码,所以需要一种更简单的方法来识别需要BIOS支持的设备(Boot device)的设备协议。HID类设备使用Subclass区分支持鼠标设备或键盘预定义协议(也就是说,该设备可以用作Boot Device)。

bInterfaceSubClass 成员定义了 Subclass 识别码:

Subclass码描述
0No Subclass
1Boot Interface Subclass
2-255保留

总结:HID设备的Subclass仅用于识别是否为 Boot 设备,而真正的设备分类是在数据协议以及report描述符中区分的。

2.2.3 Protocols

HID设备支持多种协议, Interface描述符的成员bInterfaceProtocol只有在 bInterfaceSubClass成员声明了支持一个boot接口时,才有意义,否则为0。

Protocol码描述
0None
1Keyboard
2Mouse
3-255保留

2.2.4 Interfaces

重点:一个HID类设备通过 Control (default)通道(pipe)或者 Interrupt 通道(pipe)与 HID 类驱动进行通信:

在这里插入图片描述

Control 通道用于:

  • 接收并响应USB控制和类数据的请求。
  • 在HID类驱动程序轮询时传输数据(使用Get_Report请求)。
  • 从主机接收数据。

Interrupt 通道用于:

  • 从设备接收异步(未请求的)数据。
  • 向设备传输低延迟数据。
通道描述是否必要
ControlUSB控制、类请求码、轮询数据(消息数据)Y
Interrupt In输入的数据,即来自设备的数据Y
Interrupt Out输出的数据,即发往设备的数据N

2.3 操作模型

2.3.1 Device 描述符结构

在最顶层,描述符包括两个信息表,称为 Device描述符 和 String描述符 。一个标准的USB设备描述符指定产品ID和设备的其他信息。例如,Device描述符字段主要包括:

  • class
  • Subclass
  • Vendor
  • Product
  • Version

对于HID类设备:

  • 类型不在Device描述符的Class字段定义,而是 Interface 描述符中的Class字段定义了设备为HID类设备。
  • Subclass 字段用来定义 Boot Device。

即:Device描述符中的 bDeviceClass 和 bDeviceSubClass 字段不应该被用来标识一个设备属于HID类。相反,在Interface描述符中使用 bInterfaceClass 和 bInterfaceSubClass 字段来标识HID类。

2.3.2 Report 描述符结构

重点:上述描述符(Device描述符、Interface 描述符等)由表示信息表的流程图项说明。每个信息表可以被认为是一个数据块
Report描述符不是数据块,而是由信息块组成的。每一条信息都被称为一个Item。

在这里插入图片描述

2.3.3 通用 Item 格式

Item是关于设备的一段信息。所有Item都有一个单字节的前缀,其中包含item tag, item type, 和 item size:

在这里插入图片描述

一个item可以包含item data,每部分数据的大小由item 的基本类型决定。item 有两种基本类型:

  • short item
    可选的数据大小可能为 0,1,2,4 byte。
  • long item
    • bSize 字段的值总为2。

2.3.4 Item 解析器

重点:HID类驱动程序包含一个解析器,用于分析Report描述符中发现的Item。解析器以线性方式从描述符提取信息。
解析器在遍历描述符时收集每个已知Item的状态,并将它们存储在Item状态表中。Item状态表包含各个Item的状态。

从解析器的角度来看,HID类设备看起来如下图所示:

在这里插入图片描述

当遇到某些Item时,将移动Item状态表的内容。
这些Item包括所有的Main、Push和Pop Item。

  • 当找到main item,使用当前Item状态表分配并初始化一个新的报告结构。然后从Item状态表中删除所有Local Item,但保留Global item。通过这种方式,Global item为后续的新Main item设置默认值。具有几个类似控件的设备(例如,六个轴)在第一个Main item之前只需要定义一次Global item。
    main item按照声明的顺序与集合相关联。当解析器到达collection item 时,一个新的集合开始。item解析器将Collection item 和下一个End Collection item 之间定义的所有Main item关联到一个集合。
  • 当遇到Push item时,item状态表被复制并放置到堆栈上,以便以后检索。
  • 当找到Pop item时,item状态表将被堆栈中的顶表替换。

例子:Unit (Meter), Unit Exponent (-3), Push, Unit Exponent (0)

  • 解析器到达Push item时,它将以毫米(10^-3米)为单位的项目放置在堆栈上。
  • 下一个item将item状态表更改为米(10^0米)的单位。

解析器需要解析整个Report描述符以找到所有的Main item。为了分析设备发送的report,这是必要的。

2.3.5 Usage

Usage 是Report描述符的一部分,向应用程序开发人员提供关于控件实际测量什么的信息。此外,Usage标记表示供应商对特定控件或控件组的建议使用。Report描述符描述数据的格式(例如,三个8位字段),Usage标记定义应该如何处理数据(例如,x、y和z输入)。此特性允许供应商确保用户看到跨应用程序控件的一致功能分配。

2.3.6 report

使用USB术语,设备可以每USB帧(1毫秒)发送或接收一个事务。一个事务可以由多个包(packet)(令牌(token)、数据(data)、握手(handshake))组成,但对于低速设备,它的大小限制为8字节,对于高速设备,它的大小限制为64字节。一次传输(transfer)是 一个或多个 创建对设备有意义的一组数据的 事务,例如输入(Input)、输出(Output)和特性(Feature)的report。在本文中,transfer是report的同义词。

在这里插入图片描述

总结:HID设备中,transfer 是通过 report 来实现的,所以传输的是输入(Input)、输出(Output)和特性(Feature)的report。

重点(这一段说明了具体的报文内容):大部分设备通过返回一个 structure 来产生 report(或者叫transfer), 这个 structure 的各数据段是顺序排列的。但是,有些设备可能在一个 endpoint 上有多个 report structure ,每个 report structure 只表示几个数据字段。例如,一个集成了指向设备的键盘可以在同一个 endpoint 上独立报告“按键”数据和“指向”数据。
Report ID item 用于指示在每个 report structure 中表示哪些数据字段。Report ID item 的标签为每个 report structure 分配一个1字节的标识前缀。如果 Report 描述符中没有 Report ID item 标签,可以假设只有一个Input, Output, 和 Feature report structure 存在,它们一起表示设备的所有数据。

只有Input report是通过Interrupt In pipe发送的。Feature和Output report必须由主机通过Control pipe或一个可选的Interrupt Out pipe来初始化。

2.3.7 string

集合或数据字段可以具有与之关联的特定标签(字符串索引)。string是可选的。
项目的Usage标记不一定与与Main item相关联的string 相同。但是,当需要供应商定义的usage时,String可能会有用。String描述符包含设备的文本字符串列表。

2.4 描述符分类

2.4.1 标准描述符

HID类设备使用以下标准USB描述符:

  • Device
  • Configuration
  • Interface
  • Endpoint
  • String

2.4.2 特定于HID类的描述符

  • HID
  • Report
  • Physical

具体的描述符字段定义可以参考《Device Class Definition for Human Interface Devices (HID)》

2.4.3 Report 描述符

Report描述符不同于其他描述符,它不仅仅是一个值表。Report描述符的长度和内容取决于设备report或report所需的数据字段的数量。Report描述符由提供设备信息的item组成。 item的第一部分包含三个字段: item type、item tag和 item size。这些字段一起标识item提供的信息类型。

  • item type
    • Main
    • Global
    • Local
  • Main item tag
    • Input: 指来自设备的一个或多个类似控件的数据。例如变量数据,如读取单个轴或一组操纵杆的位置,或数组数据,如一个或多个按钮或开关。即我们用手柄控制电脑时,将要发给主机的控制数据。
    • Output:指发往设备的一个或多个类似控件的数据。例如设置单个轴或一组操纵杆(变量)的位置。或者,它可以表示一个或多个led的数据(数组数据) 。即我们使用手柄控制电脑时,手柄的灯光以及振动。
    • Feature:描述不供最终用户使用的设备输入和输出,例如软件特性或控件面板切换。
    • Collection:一组有意义的Input、Output和Feature item,例如鼠标、键盘、操纵杆和指针。
    • End Collection:一种终止item,用于指定一组item的结束。

Report描述符提供了设备中每个控件提供的数据的描述。每个Main item标签(Input、Output或Feature)标识了由特定控件返回的数据的大小,并标识该数据是绝对的还是相对的,以及其他相关信息。前面的“Local”和“Global”item定义了数据的最小值和最大值等。Report描述符是设备所有item的完整集合。通过单独查看Report描述符,应用程序知道如何处理传入的数据,以及这些数据可以用于什么。

控件的一个或多个数据段由Main item定义,并由前面的Global和Local item进一步描述。Local item仅描述由下一个Main item定义的数据段。Global item 成为该描述符中所有后续数据字段的默认属性。这就是为什么解析器解析完成当前main item状态表后就要删除 Local item 。

Report描述符 item 例子:

Report Size (3)
Report Count (2)
Input
Report Size (8)
Input
Output

项解析器解释上面的Report描述符item,并创建以下report(LSB在左边)

在这里插入图片描述

一个report描述符可能包含多个main item,且必须包含以下item(其他item是可选的)以描述控件的数据。

  • Input (Output or Feature)
  • Usage
  • Usage Page
  • Logical Minimum
  • Logical Maximum
  • Report Size
  • Report Count

用于定义3按钮鼠标的示例item定义:
在这种情况下,Main item 前面是Global item(如Usage、Report Count或Report Size(每行都是一个新item))。

Usage Page (Generic Desktop),             ;使用 Generic Desktop Usage Page
Usage (Mouse),
    Collection (Application),             ;Start Mouse collection
    Usage (Pointer),
    Collection (Physical),                ;Start Pointer collection
        Usage Page (Buttons)
        Usage Minimum (1),
        Usage Maximum (3),
        Logical Minimum (0),
        Logical Maximum (1),              ;字段返回的值范围:0-1
        Report Count (3),
        Report Size (1),                  ;创建3个1bit字段(button 1, 2, & 3)
        Input (Data, Variable, Absolute), ;添加字段到 input report.
        Report Count (1),
        Report Size (5),                  ;创建5bit常量字段
        Input (Constant),                 ;添加字段到 input report.
        Usage Page (Generic Desktop),
        Usage (X),
        Usage (Y),
        Logical Minimum (-127),
        Logical Maximum (127),            ;字段返回的值范围: -127 - 127
        Report Size (8),
        Report Count (2),                 ;创建2个8bit字段 (X & Y 位置)
        Input (Data, Variable, Relative), ;添加字段到 input report.
    End Collection,                       ;Close Pointer collection
End Collection                            ;Close Mouse collection

对应的数据格式(只有一个report structure 所以没有 report ID item):

bytebit描述
00Button 1
1Button 2
2Button 3
3-7与特定设备相关
10-7X 位移
20-7Y 位移
3-n0-7与特定设备相关(optional)

在具体的数据表示中,short item 与 long item 的具体数据结构为:

在这里插入图片描述

在这里插入图片描述

总结一下,report 描述符由一系列的item组成,item的数据结构有一个byte的前缀,通过前缀区分是什么类型的item。按顺序首先是globol item来描述描述符的数据字段的大小用法等默认属性(Usage,Report Count,Report Size);然后由main item说明具体的需要用的数据。

另外,最终我要实现模拟键盘来控制电脑,所以需要再学习以下键盘的描述符。

Usage Page (Generic Desktop),
Usage (Keyboard),
Collection (Application),
    Report Size (1),
    Report Count (8),
    Usage Page (Key Codes),
    Usage Minimum (224),
    Usage Maximum (231),
    Logical Minimum (0),
    Logical Maximum (1),
    Input (Data, Variable, Absolute), ;Modifier byte
    Report Count (1),
    Report Size (8),
    Input (Constant), ;Reserved byte
    Report Count (5),
    Report Size (1),
    Usage Page (LEDs),
    Usage Minimum (1),
    Usage Maximum (5), 
    Output (Data, Variable, Absolute), ;LED report
    Report Count (1),
    Report Size (3),
    Output (Constant), ;LED report padding
    Report Count (6),
    Report Size (8),
    Logical Minimum (0),
    Logical Maximum(255),
    Usage Page (Key Codes),
    Usage Minimum (0),
    Usage Maximum (255),
    Input (Data, Array),
End Collection

下表表示键盘input report(8字节):

byte描述
0辅助按键("Ctrl"、"Alt"、"Shift"、"Win")
1保留
2Keycode 1
3Keycode 2
4Keycode 3
5Keycode 4
6Keycode 5
7Keycode 6

注意:这个报告的byte1是一个常量。这个字节保留给OEM使用。如果不使用这个字段,BIOS应该忽略它。建议在未使用的字段中返回0。

下表表示键盘output report (1字节):

bit描述
0NUM LOCK
1CAPS LOCK
2SCROLL LOCK
3COMPOSE
4KANA
5-7CONSTANT

注意:led是 absolute output item。这意味着每个LED的状态必须包含在output report中(0 = off, 1 = on)。相关 item将允许只影响选定控件的report(0 =无更改,1=更改)。

2.5 请求

2.5.1 标准请求

HID类使用USB标准中描述的Get_Descriptor(Configuration) 请求规范。它返回Configuration描述符、所有 Interface描述符、所有Endpoint描述符以及每个Interface的HID描述符。不会返回String 描述符、HID Report 描述符或任何其他的HID类描述符。

根据 Descriptor Type 字段的设置,可以使用 Get_Descriptor 检索标准、类和供应商特定的描述符。

Descriptor Type 字段Class Descriptor类型
0x21HID
0x22Report
0x23Physical descriptor
0x24 - 0x2F保留
  • Get_Descriptor 请求
    Get_Descriptor请求返回 设备的描述符

  • Set_Descriptor 请求
    Set_Descriptor请求 让主机更改设备中的描述符。
    对这个请求主机不一定支持。

2.5.2 特殊类(Class-Specific)请求

特殊类请求允许主机查询设备的功能和状态,并设置输出状态和功能item。这些事务是通过 Default pipe 完成的,因此遵循USB规范中定义的 Default pipe 请求的格式。

  • Get_Report 请求
    功能:允许主机通过Control pipe接收一个report。
  • Set_Report 请求
    功能:允许主机向设备发送report,可能设置输入、输出或特征控件的状态。
  • Get_Idle 请求
    功能:读取一个特定的Input report的当前空闲率。
  • Set_Idle 请求
    功能:停止Interrupt In pipe 上的特定report,直到发生新的事件或过了指定的时间。
  • Get_Protocol 请求
    功能:读取当前活动的协议(boot 协议或 report 协议)。
  • Set_Protocol 请求
    功能:在 boot 协议和 report 协议之间切换(反之亦然)。

3 ESP32 实现蓝牙HID设备

例程路径:examples\bluetooth\bluedroid\classic_bt\bt_hid_mouse_device

源码中定义了一个名为 local_param_t 的结构体来保存相关参数:


typedef struct
{
    esp_hidd_app_param_t app_param; // SDP报告的 HID device 特征
    esp_hidd_qos_param_t both_qos;  // HID device 服务质量参数
    uint8_t protocol_mode;          // 协议模式
    SemaphoreHandle_t mouse_mutex;  // 信号量 Semaphore 的句柄 
    xTaskHandle mouse_task_hdl;     // 移动鼠标任务mouse_move_task的句柄 
    uint8_t buffer[4];              // report的数据 
    int8_t x_dir;                   // 鼠标移动方向
} local_param_t;  

typedef struct {
    const char *name;
    const char *description;
    const char *provider;
    uint8_t subclass;
    uint8_t *desc_list;
    int desc_list_len;
} esp_hidd_app_param_t;

程序处理流程:

  • 1 定义好SDP报告的 HID device 特征参数 app_param 。
  • 2 为HID device 服务质量参数 both_qos 申请内存空间。
  • 3 设置协议模式 protocol_mode 为 ESP_HIDD_REPORT_MODE 模式(另一个模式为 BOOT 设备模式)。
  • 4 初始化 bluedroid ,这部分与其他经典蓝牙应用一致:
    • 1 BT_CONTROLLER_INIT_CONFIG_DEFAULT() // 配置初始化 controller
    • 2 esp_bt_controller_init() // 初始化 controller
    • 3 esp_bt_controller_enable() // 使能 controller
    • 4 esp_bluedroid_init() // 初始化 bluedroid
    • 5 esp_bluedroid_enable() // 使能 bluedroid
    • 6 esp_bt_gap_register_callback() // 注册 gap 事件回调函数(gap 事件触发)
  • 5 esp_bt_dev_set_device_name() // 设置经典蓝牙设备名称
  • 6 设置设备类型结构体 esp_bt_cod_t 的主要分类为 Peripheral(mouse, joystick, keyboard) 类
    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;
    
  • 7 esp_bt_gap_set_cod() 调用这个函数来设置设备的类。 结构esp_bt_gap_cb_t将在设置COD结束后用ESP_BT_GAP_SET_COD_EVT调用。
  • 8 esp_bt_hid_device_register_callback() 调用HID设备的初始化回调函数。
  • 9 esp_bt_hid_device_init() 初始化HIDD。 这个函数应该在esp_bluedroid_enable和esp_blueroid_init成功之后调用,并且应该在esp_bt_hid_device_register_callback之后调用。 当操作完成时,回调函数将被ESP_HIDD_INIT_EVT调用。
  • 10 esp_bt_gap_set_pin() 为传统配对设置PIN类型和默认PIN码。

相关回调函数(esp_bt_gap_cb):

  • gap 事件回调函数:
    与其他的gap事件类似,因为esp32 蓝牙架构学习中学过,GAP,Generic Access Profile,是保证不同的设备可以互相发现对方并建立连接的协议。所以我们自己的应用这一部分基本可以参考官方例程来写。

    根据不同的GAP事件进行处理:

    • 认证完成事件:ESP_BT_GAP_AUTH_CMPL_EVT
      输出信息。
    • 传统配对Pin码请求事件:ESP_BT_GAP_PIN_REQ_EVT
      根据需要的PIN码的位宽进行PIN码设置。
    • ESP_BT_GAP_MODE_CHG_EVT:GAP 模式改变事件
      输出信息。
  • HID 设备回调函数(esp_bt_hidd_cb):
    看这个回调函数时参考上面学习的相关请求。
    根据不同的HID设备事件进行处理:

    • 1 HID设备被初始化(init):ESP_HIDD_INIT_EVT
      调用esp_bt_hid_device_register_app(): 向SDP注册HIDD参数并设置l2cap服务质量。这个函数应该在esp_bluedroid_enable()和esp_blueroid_init()成功之后调用,并且必须在esp_bt_hid_device_init之后执行。当操作完成时,回调函数将以ESP_HIDD_REGISTER_APP_EVT事件被调用。
    • HID设备被析构(deinit):ESP_HIDD_DEINIT_EVT
    • 2 HID设备应用 注册(register):ESP_HIDD_REGISTER_APP_EVT
      • esp_bt_gap_set_scan_mode(): 为传统蓝牙设置可发现性和可连接性模式。这个函数应该在esp_bluedroid_enable()成功完成后被调用
      • esp_bt_hid_device_connect(): 此功能将HIDD接口连接到已连接的蓝牙设备。当操作完成时,回调函数将以ESP_HIDD_OPEN_EVT事件被调用。
    • HID设备应用 取消注册(unregister):ESP_HIDD_UNREGISTER_APP_EVT
    • HID设备与主机连接打开:ESP_HIDD_OPEN_EVT
      • bt_app_task_start_up():我们想要完成的功能。
      • esp_bt_gap_set_scan_mode(): 设置为不可连接和不可发现。
    • HID设备与主机连接关闭:ESP_HIDD_CLOSE_EVT
      • bt_app_task_shut_down():
      • esp_bt_gap_set_scan_mode():
    • HID设备向下层发送report:ESP_HIDD_SEND_REPORT_EVT
    • HID设备向下层报告握手错误:ESP_HIDD_REPORT_ERR_EVT
    • HID设备接收主机的GET_REPORT请求:ESP_HIDD_GET_REPORT_EVT
      • check_report_id_type(): 确定report_id与report_type
      • esp_bt_hid_device_send_report(): 根据请求的参数返回report。
    • HID设备接收主机的SET_REPORT请求:ESP_HIDD_SET_REPORT_EVT
    • HID设备接收主机的SET_PROTOCOL请求:ESP_HIDD_SET_PROTOCOL_EVT
    • HID设备从主机接收数据:ESP_HIDD_INTR_DATA_EVT
    • HID设备启动Virtual Cable Unplug:ESP_HIDD_VC_UNPLUG_EVT
      • bt_app_task_shut_down(): 关闭app 任务
      • esp_bt_gap_set_scan_mode(): 设置为可连接和可发现。

鼠标移动任务函数:mouse_move_task:

// move the mouse left and right
void mouse_move_task(void* pvParameters) {
    const char* TAG = "mouse_move_task";

    ESP_LOGI(TAG, "starting");
    for(;;) {
        s_local_param.x_dir = 1;
        int8_t step = 10;
        for (int i = 0; i < 2; i++) {
            xSemaphoreTake(s_local_param.mouse_mutex, portMAX_DELAY);
            s_local_param.x_dir *= -1;
            xSemaphoreGive(s_local_param.mouse_mutex);
            for (int j = 0; j < 100; j++) {
                send_mouse(0, s_local_param.x_dir * step, 0, 0);
                vTaskDelay(50 / portTICK_PERIOD_MS);
            }
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}  

在这个任务中:

  • 首先取出参数中的信号量句柄SemaphoreHandle_t,portMAX_DELAY是最大等待时间。
  • 然后方向取反(x_dir乘-1)
  • void send_mouse(uint8_t buttons, char dx, char dy, char wheel) 改变鼠标位置。
    是通过esp_bt_hid_device_send_report()发送report来实现的。(通过Interrupt In pipe发送Input report)
/**
 * @brief     Send HIDD report. When the operation is complete the callback
 *            function will be called with ESP_HIDD_SEND_REPORT_EVT.
 *
 * @param[in] type:   type of report
 * @param[in] id:     report id as defined by descriptor
 * @param[in] len:    length of report
 * @param[in] data:   report data
 *
 * @return
 *            - ESP_OK: success
 *            - other: failed
 * esp_err_t esp_bt_hid_device_send_report(esp_hidd_report_type_t type, uint8_t id, uint16_t len, uint8_t *data);
 **/

esp_bt_hid_device_send_report(ESP_HIDD_REPORT_TYPE_INTRDATA, 0x00, 4, s_local_param.buffer);  

下面先来新建工程完成示例程序的功能:

首先需要修改menuconfig:
在这里插入图片描述

演示结果:
在这里插入图片描述

可以看到,当电脑连接到HID蓝牙设备后,鼠标开始反复横跳。

4 HID设备模拟键盘的demo

为了学习 ESP32 HID设备的配置方法,现在鼠标例程的基础上配置键盘控制程序,让 HID 设备模拟键盘输入“123456”。有了配置方法,就可以去开发其他复杂的需求了。这也是为什么在了解示例程序之前先去学习 HID 的基本原理。

4.1 设置键盘设备描述符

首先要去查找《HID Usage Tables For Universal Serial Bus(USB)》文件获得键盘设备的描述符对应的编码。

Usage Page (Generic Desktop)为例,根据short item的数据结构:

在这里插入图片描述

查《Device Class Definition for Human Interface Devices (HID)》的“6.2.2 Report Descriptor”章节:

  • Usage Page 数据大小为0,则bSize = 2’b00;
  • Usage Page 为global类型的item,则bType = 2’b01;
  • Usage Page 对应的btag = 4’b0000;
    查《HID Usage Tables For Universal Serial Bus(USB)》的“3 Usage Pages”章节:
  • Generic Desktop 对应的ID为 0x01;

Usage Page (Generic Desktop)item对应的代码为0x05 0x01

实际上规范文档给出了键盘的描述符编码:
在这里插入图片描述

在代码中整理如下:

// a generic desktop page - keyboard - discriptor  
uint8_t hid_descriptor_keyboard_boot_mode[] = {
    0x05, 0x01, // Usage Page (Generic Desktop),
    0x09, 0x06, // Usage (Keyboard),
    0xa1, 0x01, // Collection (Application),
    0x05, 0x07, //     Usage Page (Key Codes),
    0x19, 0xE0, //     Usage Minimum (224),
    0x29, 0xE7, //     Usage Maximum (231),
    0x15, 0x00, //     Logical Minimum (0),
    0x25, 0x01, //     Logical Maximum (1),
    0x75, 0x01, //     Report Size (1),
    0x95, 0x08, //     Report Count (8),
    0x81, 0x02, //     Input (Data, Variable, Absolute), ;Modifier byte
    0x95, 0x01, //     Report Count (1),
    0x75, 0x08, //     Report Size (8),
    0x81, 0x01, //     Input (Constant), ;Reserved byte
    0x95, 0x05, //     Report Count (5),
    0x75, 0x01, //     Report Size (1),
    0x05, 0x08, //     Usage Page (LEDs),
    0x19, 0x01, //     Usage Minimum (1),
    0x29, 0x05, //     Usage Maximum (5), 
    0x91, 0x02, //     Output (Data, Variable, Absolute), ;LED report
    0x95, 0x01, //     Report Count (1),
    0x75, 0x03, //     Report Size (3),
    0x91, 0x01, //     Output (Constant), ;LED report padding
    0x95, 0x06, //     Report Count (6),
    0x75, 0x08, //     Report Size (8),
    0x15, 0x00, //     Logical Minimum (0),
    0x25, 0x65, //     Logical Maximum(101),
    0x05, 0x07, //     Usage Page (Key Codes),
    0x19, 0x00, //     Usage Minimum (0),
    0x29, 0x65, //     Usage Maximum (101),
    0x81, 0x00, //     Input (Data, Array),
    0xC0        // End Collection
};   

4.2 修改代码

local_param_t 结构体新增成员 uint8_t keyboard_buffer[8]; 用于存取键盘设备的 input report structure 。所有使用到 s_local_param.buffer 的位置需要改为 s_local_param.keyboard_buffer 并设置相应的数据长度(如果需要的话)。

int hid_descriptor_keyboard_boot_mode_len = sizeof(hid_descriptor_keyboard_boot_mode); 表示report structure的长度。
app_main 中变量的初始化需要修改为:

s_local_param.app_param.subclass = ESP_HID_CLASS_KBD; // 设备子类subclass 属性为 keyboard  
s_local_param.app_param.desc_list = hid_descriptor_keyboard_boot_mode; // 设置键盘设备描述符
s_local_param.app_param.desc_list_len = hid_descriptor_keyboard_boot_mode_len; // 设置键盘设备描述符长度

新建 keyboard_press_test_task 以及键盘输入函数 keyboard_click:

// click num keyboard  
void keyboard_click()
{
    // 按下数字 1 2 3 4 5 6
    s_local_param.keyboard_buffer[0] = 0x00; // Modifier keys 
    s_local_param.keyboard_buffer[1] = 0x00; // Reserved 
    s_local_param.keyboard_buffer[2] = 0x1E; // Keycode 1 
    s_local_param.keyboard_buffer[3] = 0x1F; // Keycode 2 
    s_local_param.keyboard_buffer[4] = 0x20; // Keycode 3 
    s_local_param.keyboard_buffer[5] = 0x21; // Keycode 4 
    s_local_param.keyboard_buffer[6] = 0x22; // Keycode 5 
    s_local_param.keyboard_buffer[7] = 0x23; // Keycode 6 
    esp_bt_hid_device_send_report(ESP_HIDD_REPORT_TYPE_INTRDATA, 0x00, 8, s_local_param.keyboard_buffer);
    // 松开数字 1 2 3 4 5 6
    s_local_param.keyboard_buffer[2] = 0x00; // Keycode 1 
    s_local_param.keyboard_buffer[3] = 0x00; // Keycode 2 
    s_local_param.keyboard_buffer[4] = 0x00; // Keycode 3 
    s_local_param.keyboard_buffer[5] = 0x00; // Keycode 4 
    s_local_param.keyboard_buffer[6] = 0x00; // Keycode 5 
    s_local_param.keyboard_buffer[7] = 0x00; // Keycode 6 
    esp_bt_hid_device_send_report(ESP_HIDD_REPORT_TYPE_INTRDATA, 0x00, 8, s_local_param.keyboard_buffer);
}


// press keyboard task
void keyboard_press_test_task(void* pvParameters) {
    const char* TAG = "keyboard_press_test_task";

    ESP_LOGI(TAG, "starting");
    for(;;) {
        keyboard_click();
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

4.3 测试结果

连接 ESP32 之后,键盘不断输入 123456 ,与预期结果一致。
在这里插入图片描述

4.4 keyboard 工程源码获取

欢迎关注我的个人微信公众号回复“22021801”获取。
在这里插入图片描述

  • 14
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
要在ESP32 IDF中自定义HID设备,您需要遵循以下步骤: 1. 设置您的ESP32开发环境,并确保您已经熟悉ESP32 IDF框架。 2. 创建一个新的ESP32 IDF项目,并将其配置为使用USB。 3. 在项目中添加HID驱动程序,并定义您的自定义HID报告。 4. 在驱动程序中注册HID接口,并配置HID报告描述符。 5. 在您的应用程序代码中使用HID API来读取和写入HID报告数据。 以下是一些更详细的步骤: 1. 设置您的ESP32开发环境,并确保您已经熟悉ESP32 IDF框架。 如果您还没有设置ESP32开发环境,请参考ESP32 IDF文档中的说明。在您开始创建自定义HID设备之前,您必须了解ESP32 IDF的基本概念和框架。 2. 创建一个新的ESP32 IDF项目,并将其配置为使用USB。 创建一个新的ESP32 IDF项目,并将其配置为使用USB。在Makefile中添加以下行: ``` # Enable USB support CONFIG_USB_DEVICE_ENABLE=y ``` 这将启用USB支持,并配置ESP32USB设备。您还可以配置其他USB选项,例如USB总线电流,USB速度等。 3. 在项目中添加HID驱动程序,并定义您的自定义HID报告。 添加HID驱动程序并定义您的自定义HID报告。在ESP32 IDF中,HID驱动程序包含在USB设备驱动程序中。您可以使用ESP32 IDF中提供的HID API来定义您的自定义HID报告。您的HID报告应该包括HID报告描述符,它描述了您的HID设备的功能和特性。您可以使用HID描述符编辑器来创建和编辑HID报告描述符。 4. 在驱动程序中注册HID接口,并配置HID报告描述符。 在驱动程序中注册HID接口,并配置HID报告描述符。您可以使用esp_hid_device_register_interface函数来注册HID接口。在注册HID接口时,您需要指定HID报告描述符和HID报告描述符的长度。您还需要指定HID接口的ID和其他相关选项。 5. 在您的应用程序代码中使用HID API来读取和写入HID报告数据。 在您的应用程序代码中使用HID API来读取和写入HID报告数据。您可以使用esp_hid_device_send_report函数来发送HID报告数据。您可以使用esp_hid_device_get_report函数来读取HID报告数据。您还可以使用其他HID API来控制和管理您的HID设备。 总之,要在ESP32 IDF中自定义HID设备,您需要了解ESP32 IDF框架和USB支持,创建自定义HID报告并在驱动程序中注册HID接口。然后,您可以使用HID API来读取和写入HID报告数据。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lu-ming.xyz

觉得有用的话点个赞吧 :)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值