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

demi的头像
demi 发布于:周四, 01/02/2020 - 17:01 ,关键词:

转眼间来到了2020年,新年伊始,小编将和大家一起学习使用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 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 HID工具连接

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


图3 HID工具设备选择

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


图4 HID工具数据收发

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

来源: 灵动MM32MCU

围观 797