Nuoeriris · 2020年06月18日

MM32 USB功能学习笔记 —— USB HID设备

在接下来的章节里,小编将和大家一起学习使用MM32 MCU的USB功能。对于USB来说,主要应用是HID、CDC、MSC以及WINUSB等功能,此节将先介绍如何使用MM32 MCU的HID功能。

对于USB设备来说,其中有一大类就是HID设备,即Human Interface Devices,人机接口设备。这类设备包括鼠标、键盘等,其主要用于人与计算机进行交互。它是USB协议最早支持的一种设备类。HID设备可以作为低速、全速、高速设备用。由于HID设备要求用户输入能得到及时响应,所以其传输方式通常采用中断方式,而且无需安装驱动就能进行交互,简单方便。

在USB通信协议中,HID设备的定义放置在接口描述符中,USB的设备描述符和配置描述符中不包含HID设备的信息。所以对于某些特定的HID设备,我们可以定义多个接口,只要其中一个接口为HID设备类即可,在学习HID之前,先来复习一下USB协议的相关内容。

一、USB设备描述符-概述

当插入USB设备后,主机需要发送比较短的请求来确认设备的身份、类型、速度等信息,这个过程称之为枚举。

那什么是设备描述符呢?Descriptor即描述符,是一个完整的数据结构,可以通过C语言等编程实现,并存储在USB设备中,用于描述一个USB设备的所有属性,USB主机是通过一系列命令来要求设备发送这些信息的。

描述符的作用就是通过命令操作作来给主机传递信息,从而让主机知道设备具有什么功能、属于哪一类设备、要占用多少带宽、使用哪类传输方式及数据量的大小,只有主机确定了这些信息之后,设备才能真正开始工作。

USB有那些标准描述符呢?对于 USB来说有5种标准描述符:设备描述符、配置描述符、字符描述符、接口描述符、端点描述符 。

描述符之间有一定的关系,一个设备只有一个设备描述符,而一个设备描述符可以包含多个配置描述符,而一个配置描述符可以包含多个接口描述符,一个接口使用了几个端点,就有几个端点描述符。由此我们可以看出来,USB的描述符之间的关系是一层一层的,最上一层是设备描述符,下面是配置描述符,再下面是接口描述符,然后是端点描述符。在获取描述符时,先获取设备描述符,然后再获取配置描述符,根据配置描述符中的配置集合长度,一次将配置描述符、接口描述符、端点描述符一起一次读回。其中可能还会有获取设备序列号,厂商字符串,产品字符串等。

枚举的过程:

  1. 等待稳定:主机通过电平差检测到设备,等待100ms让设备电平趋于稳定;
  2. 首次复位:HUB发起复位,让设备进入初始的地址0模式;
  3. 首次查询设备描述符:GET\_DESCRIPTOR 主机查询设备描述符,只要前8字节 ==> 80 06 01 00 00 00 12 00 ;
  4. 二次复位:在接收到设备描述符前8个字节后,再次重启设备;
  5. 设置地址:SET\_ADDRESS 主机下发设置地址命令,设备获取新地址 ==> 00 05 01 00 00 00 00 00 ;
  6. 二次查询设备描述符:GET\_DEVICE\_DESCRPTOR获取整个18字节的设备描述符 ==> 80 06 01 00 00 00 12 00 ;
  7. 获取配置描述符:GET\_CONFIGURATION 获取9字节配置描述符 ==> 80 06 02 00 00 00 09 00 ;
  8. 完成配置:SET\_CONFIGURATION

二、HID设备简述

2.1 HID设备的特点

交换的数据储存在称为报表(Report)的结构内,设备的固件必须支持HlD报表的格式。主机通过控制和中断传输中的传送和请求报表来传送和接收数据。报表的格式非常灵活。

每一笔事务可以携带小量或中量的数据。低速设备每一笔事务最大是8B ,全速设备每一笔事务最大是64B,高速设备每一笔事务最大是1024B,一个报表可以使用多笔事务。

设备可以在未预期的时间传送信息给主机,例如键盘的按键或是鼠标的移动。所以主机会定时轮询设备,以取得最新的数据。

HID 设备的最大传输速度有限制。主机可以保证低速的中断端点每10ms 内最多 1笔事务,每一秒最多是 800B,保证全速端点每1ms 一笔事务,每一秒最多是64000B,保证高速端点每125 us 三笔事务,每一秒最多是 24.576MB。

HID 设备没有保证的传输速率。如果设备是设置在 10ms 的时距,事务之间的时间可能等于或小于10ms。除非设备是设置在全速时在每个帧传输数据,或是在高速时在每个微帧传输数据。这个是最快的轮询速率,所以端点可以保证有正确的带宽可供使用。

HID 设备除了传送数据给主机外,它也会从主机接收数据。只要能够符合HlD 类别规范的设备都可以是HID 设备。设备除了HlD 接口之外,它可能同时还包含有其他的USB 接口。

2.2 HID设备的硬件要求

HID 接口必须要符合 Device Class Definition for Human interface Devices 规范内所定义的 HID 类别的需求。在此文件内描述了所需的描述符、传输的频率以及传输的类型等。为了符合规范,HID 接口的端点与描述符都必须符合数个要求。所有的 HID 传输都是使用默认控制管道或是一个中断管道,HID设备必须有一个中断输入端点来传送数据到主机,中断输出端点则不是必需的。Control管道用于接收和响应USB控制和类数据的请求,在由HID类驱动程序轮询时传输数据(使用Get\_Reportrequest),从主机接收数据。

对于主机与设备之间所交换的数据,可以分成两种类型:低延迟的数据,必须尽快地到达目的;配置或其他的数据,没有严格时间限制的需求。中断管道是控制管道之外的另一种数据交换的方式,特别适合使用在接收端需要定时或是尽可能及时收到数据的时候。中断输入管道携带数据到主机,中断输出管道则是携带数据到设备。在总线忙的时候,控制管道可能会被延迟,而中断管道保证会有可得到的带宽。HID不需要一定有中断输出管道。如果没有中断输出管道,主机会在控制管道上使用HID 设备特有的 Set\_Report 请求来传送所有的报表。

2.3 HID的程序要求

主机的驱动程序要与 HID 设备通信,其设备的固件必须符合如下几个需求,设备的描述符必须识别该设备包含有 HID 接口(描述符)。除了默认控制管道外,固件必须另外支持一个中断输入管道。固件必须包含一个报表描述符来定义要传送与接收的设备数据。如果要传送数据,固件必须支持 Get\_Report 控制传输与中断输入传输。如果要接收数据,固件必须支持 Set\_Report 控制传输与选择性的中断输出传输。所有的 HID 数据都必须使用定义过的报表格式来定义报表中数据的大小与内容。设备可以支持一个或多个报表。在固件中的一个报表描述符用来描述此报表,以及如何使用报表数据的信息。在每一个报表中的一个数值,定义此报表是一个输入(Input )、输出(Output )或是特征(Feature )报表。主机在输入报表中接收数据,在输出报表中传送数据,特征报表可以在任何方向传递。

三、HID 描述符

HID 设备除了支持 USB 设备的 5 种标准描述符之外,还支持 HID 设备特有的 3 种描述符。这些描述符是:1、USB 标准描述符:设备、配置、接口、端点和字符串描述符;2、HID 特有的描述符: HID 、报表(Report )和实体(Physical )描述符。从描述符的关联关系看, HID 描述符是关联于接口。所以如果一个 HID 设备有 2 个端点,设备不需要每个端点有一个 HID 描述符,具体参考如下代码:

设备描述符
struct _DEVICE_DEscriptOR_STRUCT
{
BYTE   bLength;      //设备描述符的字节数大小
BYTE   bDescriptorType;   //描述符类型编号,为0x01
WORD  bcdUSB;      //USB版本号
BYTE  bDeviceClass;   //USB分配的设备类代码,0x01~0xfe为标准设备类,0xff为厂商自定义类型,0x00不是在设备描述符中定义的,如HID
BYTE   bDeviceSubClass;  //USB分配的子类代码,同上,值由USB规定和分配的,HID设备此值为0
BYTE  bDeviceProtocl;  //USB分配的设备协议代码,同上HID设备此值为0
BYTE   bMaxPacketSize0;  //端点0的最大包的大小
WORD   idVendor;  //厂商编号
WORD   idProduct;  //产品编号
WORD  bcdDevice;  //设备出厂编号
BYTE   iManufacturer;   //描述厂商字符串的索引
BYTE   iProduct;   //描述产品字符串的索引
BYTE   iSerialNumber;  //描述设备序列号字符串的索引
BYTE   bNumConfiguration;   //可能的配置数量
}

配置描述符 
struct _CONFIGURATION_DEscriptOR_STRUCT
{
BYTE  bLength;   //配置描述符的字节数大小
BYTE   bDescriptorType;   //描述符类型编号,为0x02
WORD   wTotalLength;   //配置所返回的所有数量的大小
BYTE   bNumInterface;  //此配置所支持的接口数量
BYTE   bConfigurationVale;   //Set_Configuration命令需要的参数值
BYTE   iConfiguration;  //描述该配置的字符串的索引值
BYTE  bmAttribute;  //供电模式的选择
BYTE   MaxPower;   //设备从总线提取的最大电流
}

字符描述符 
struct _STRING_DEscriptOR_STRUCT
{
BYTE bLength;     //字符串描述符的字节数大小
BYTE bDescriptorType;    //描述符类型编号,为0x03
BYTE SomeDescriptor[36];   //UNICODE编码的字符串
} 

接口描述符
struct _INTERFACE_DEscriptOR_STRUCT
{
BYTE bLength;     //接口描述符的字节数大小
BYTE bDescriptorType;    //描述符类型编号,为0x04
BYTE bInterfaceNunber;    //接口的编号
BYTE bAlternateSetting;   //备用的接口描述符编号
BYTE bNumEndpoints;    //该接口使用端点数,不包括端点0
BYTE bInterfaceClass;    //接口类型 HID设备此值为0x03
BYTE bInterfaceSubClass;   //接口子类型 HID设备此值为0或者1
BYTE bInterfaceProtocol;   //接口所遵循的协议
BYTE iInterface;   //描述该接口的字符串索引值
}

端点描述符
struct _ENDPOIN_DEscriptOR_STRUCT
{
BYTE bLength;     //端点描述符的字节数大小
BYTE bDescriptorType;     //描述符类型编号,为0x05
BYTE bEndpointAddress;    //端点地址及输入输出属性
BYTE bmAttribute;      //端点的传输类型属性
WORD wMaxPacketSize;    //端点收、发的最大包的大小
BYTE bInterval;     //主机查询端点的时间间隔
}

四、MM32 MCU HID代码实现

本次我们采用MM32L373 miniboard作为测试开发板。为了方便大家使用MM32 MCU的HID功能,我们已经封装好全部代码,用户不需要自己配置以上的那些描述符等参数,只需要了解MM32 MCU HID的VID和PID以及如何处理HID的数据接收和发送即可。

软件资源如下:
以下为函数初始化配置及相关全局变量定义内容,代码如下:

#define USBD_POWER                    0
#define USBD_MAX_PACKET0             64
#define USBD_DEVDESC_IDVENDOR      0x2F81
#define USBD_DEVDESC_IDPRODUCT     0x0001

以上是定义的MM32 MCU HID设备VID和PID,灵动微电子已经获得USB组织授权的VID和PID。当设备插入电脑上,可以查看到如上标识的HID设备,如图1所示:
1.jpg
图1 PC设备管理器列表
对于MM32 MCU的HID功能来说,在使用HID功能之前先调用USB初始化函数来初始化USB协议栈。

int main(void)
{
// USB Device Initialization and connect
usbd_init();
usbd_connect(__TRUE);
while (!usbd_configured())   // Wait for USB Device to configure
{
}
while (1)
{
}
}

然后就是HID数据收发处理函数,USB数据处理函数如下:

static volatile uint8_t  USB_ResponseIdle;
static HID_queue HID_Cmd_queue;
 
void hid_send_packet()
{
uint8_t *sbuf;
int slen;
if (HID_queue_get_send_buf(&HID_Cmd_queue, &sbuf, &slen))
{
if (slen > USBD_HID_OUTREPORT_MAX_SZ)
{
util_assert(0);
}
else
{
usbd_hid_get_report_trigger(0, sbuf, USBD_HID_OUTREPORT_MAX_SZ);
}
}
}
 
// USB HID Callback: when system initializes
void usbd_hid_init(void)
{
USB_ResponseIdle = 1;
HID_queue_init(&HID_Cmd_queue);
}
 
// USB HID Callback: when data needs to be prepared for the host
int usbd_hid_get_report(U8 rtype, U8 rid, U8 *buf, U8 req)
{
uint8_t *sbuf;
int slen;
switch (rtype)
{
case HID_REPORT_INPUT:
switch (req)
{
case USBD_HID_REQ_PERIOD_UPDATE:
break;
 
case USBD_HID_REQ_EP_CTRL:
case USBD_HID_REQ_EP_INT:
if (HID_queue_get_send_buf(&HID_Cmd_queue, &sbuf, &slen))
{
if (slen > USBD_HID_OUTREPORT_MAX_SZ)
{
util_assert(0);
}
else
{
memcpy(buf, sbuf, slen);
return (USBD_HID_OUTREPORT_MAX_SZ);
}
}
else if (req == USBD_HID_REQ_EP_INT)
{
USB_ResponseIdle = 1;
}
break;
}
 
break;
 
case HID_REPORT_FEATURE:
break;
}

return (0);
}
 
// USB HID override function return 1 if the activity is trivial or response is null
__attribute__((weak))
uint8_t usbd_hid_no_activity(U8 *buf)
{
return 0;
}

// USB HID Callback: when data is received from the host
void usbd_hid_set_report(U8 rtype, U8 rid, U8 *buf, int len, U8 req)
{
uint8_t *rbuf;
main_led_state_t led_next_state = MAIN_LED_FLASH;
switch (rtype)
{
case HID_REPORT_OUTPUT:
if (len == 0)
{
break;
}
if (buf[0] == ID_HID_TransferAbort)
{
HID_TransferAbort = 1;
break;
}


// execute and store to HID_queue
if (HID_queue_execute_buf(&HID_Cmd_queue, buf, len, &rbuf))
{
if (usbd_hid_no_activity(rbuf) == 1)
{
//revert HID LED to default if the response is null
led_next_state = MAIN_LED_DEF;
}
if (USB_ResponseIdle)
{
hid_send_packet();
USB_ResponseIdle = 0;
}
}
else
{
util_assert(0);
}
 
break;

case HID_REPORT_FEATURE:
break;
}
}
 
void HID_queue_init(HID_queue *queue)
{
queue->recv_idx = 0;
queue->send_idx = 0;
queue->free_count = FREE_COUNT_INIT;
queue->send_count = SEND_COUNT_INIT;
}

BOOL HID_queue_get_send_buf(HID_queue *queue, uint8_t **buf, int *len)
{
if (queue->send_count)
{
queue->send_count--;
*buf = queue->USB_Request[queue->send_idx];
*len = queue->resp_size[queue->send_idx];
queue->send_idx = (queue->send_idx + 1) % HID_PACKET_COUNT;
queue->free_count++;
return (__TRUE);
}
return (__FALSE);
}
 
BOOL HID_queue_execute_buf(HID_queue *queue, const uint8_t *reqbuf, int len, uint8_t **retbuf)
{
uint32_t rsize;
if (queue->free_count > 0)
{
if (len > HID_PACKET_SIZE)
{
len = HID_PACKET_SIZE;
}
queue->free_count--;
memcpy(queue->USB_Request[queue->recv_idx], reqbuf, len);
rsize = HID_ExecuteCommand(reqbuf, queue->USB_Request[queue->recv_idx]);
queue->resp_size[queue->recv_idx] = rsize & 0xFFFF; //get the response size
*retbuf = queue->USB_Request[queue->recv_idx];
queue->recv_idx = (queue->recv_idx + 1) % HID_PACKET_COUNT;
queue->send_count++;
return (__TRUE);
}
return (__FALSE);
}

如上,我们只需要实现修改如下HID\_ExecuteCommand可处理我们收到的收据,并且填入我们发送数据出去队列即可发送出去。
本次我们使用HID工具V1.3.3测试我们的HID功能,打开软件如图2所示:
2.jpg
图2  HID工具连接

点击选择HID设备,选择我们MM32 MCU的HID设备,如图3:
3.jpg
图3  HID工具设备选择

点击连接,发送数据,可以看到图4结果,发送两次共128字节,收到两次数据,共128字节。
4.jpg
图4  HID工具数据收发

以上就是MM32 MCU USB的HID功能,下一节我们继续介绍MM32 MCU USB的WINUSB功能。

推荐阅读
关注数
6152
内容数
276
灵动MM32 MCU相关技术知识,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息