目录
链接快速定位
前沿
1 描述符讲解
1.1 设备描述符
1.2 配置描述符
1.3 接口描述符
1.4 功能描述符
1.5 端点描述符
1.6 字符串描述符
1.7 报告描述符
1.7.1 报告描述符自动生成
2 关键函数讲解
2.1 GPIO配置讲解
2.2 非零端点函数讲解
2.3 中断函数讲解--USB复位函数讲解
2.4 主函数讲解
3 ST源码运行演示
4 自定义HID环回功能讲解
4.1 描述符修改
4.1.1 描述符修改
4.2 主函数代码修改
4.3 非零端点函数修改
4.4 USB复位函数修改
4.5 其他函数修改
4.6 上位机讲解
4.7 代码演示
4.8 测速演示
USB -- 初识USB协议(一)
源码下载请参考链接: USB -- STM32-FS-USB-Device驱动代码简述(二)
USB -- STM32F103虚拟串口bulk传输讲解(三)
HID报告描述符生成器
HID参考手册
STM32自定义HID工具
前面两节主要是对USB的基本概念做了简单讲解,学习USB的最本质目的还是要回到USB的应用方向,接下来的几章主要讲解USB的各类应用,包括:
主要还是带领大家找相关的资料及相关的工具和代码。
描述符是USB能够正常通信的前提,没有描述符,USB就不知道当前是什么样的设备,所以描述符在USB整个通信过程中占有十分重要的地位,所以这里重点讲解一下USB的各类描述符。
ST的例程为我们配置好了应用的描述符,我们不需要关注也能正常运行程序,但是我们这里讲解一下怎么通过查找资料来知道具体描述符的含义。
以下是STM32F103系列的自定义HID的设备描述符代码(每个字节对应的含义可以在USB -- 初识USB协议(一)中查看):
/* USB Standard Device Descriptor */
const uint8_t CustomHID_DeviceDescriptor[CUSTOMHID_SIZ_DEVICE_DESC] =
{
0x12, /*bLength */
USB_DEVICE_DESCRIPTOR_TYPE, /*bDescriptorType*/
0x00, /*bcdUSB */
0x02,
0x00, /*bDeviceClass*/
0x00, /*bDeviceSubClass*/
0x00, /*bDeviceProtocol*/
0x40, /*bMaxPacketSize40*/
0x83, /*idVendor (0x0483)*/
0x04,
0x50, /*idProduct = 0x5750*/
0x57,
0x00, /*bcdDevice rel. 2.00*/
0x02,
1, /*Index of string descriptor describing
manufacturer */
2, /*Index of string descriptor describing
product*/
3, /*Index of string descriptor describing the
device serial number */
0x01 /*bNumConfigurations*/
}
; /* CustomHID_DeviceDescriptor */
设备描述符顾名思义就是描述USB设备的基本信息,主要包括以下信息:
设备类型可以通过USB官网Class查询,我们这里是HID设备,所以bDeviceClass=0x03,其他两项任意填写(实例代码这里没有填写,它在接口描述符中填写的)。
制造商的字符串描述符索引、产品的字符串描述符索引和设备序号的字符串描述符索引主要是制造商、产品和设备序列号在字符串描述符的索引位置。
比如这里举个例子,以下三个是字符串描述符,分别是制造商描述符、产品描述符和序列号描述符:
const uint8_t CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR] =
{
CUSTOMHID_SIZ_STRING_VENDOR, /* Size of Vendor string */
USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType*/
/* Manufacturer: "STMicroelectronics" */
'S', 0, 'T', 0, 'M', 0, 'i', 0, 'c', 0, 'r', 0, 'o', 0, 'e', 0,
'l', 0, 'e', 0, 'c', 0, 't', 0, 'r', 0, 'o', 0, 'n', 0, 'i', 0,
'c', 0, 's', 0
};
const uint8_t CustomHID_StringProduct[CUSTOMHID_SIZ_STRING_PRODUCT] =
{
CUSTOMHID_SIZ_STRING_PRODUCT, /* bLength */
USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType */
'S', 0, 'T', 0, 'M', 0, '3', 0, '2', 0, ' ', 0, 'C', 0,
'u', 0, 's', 0, 't', 0, 'm', 0, ' ', 0, 'H', 0, 'I', 0,
'D', 0
};
uint8_t CustomHID_StringSerial[CUSTOMHID_SIZ_STRING_SERIAL] =
{
CUSTOMHID_SIZ_STRING_SERIAL, /* bLength */
USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType */
'S', 0, 'T', 0, 'M', 0,'3', 0,'2', 0
};
由下面代码可知,他们的位置也是根据索引号排列的,所以主机请求index为1的描述符,那么设备就发送制造商描述符给主机,告诉主机USB的制造商是什么。
ONE_DESCRIPTOR String_Descriptor[4] =
{
{(uint8_t*)CustomHID_StringLangID, CUSTOMHID_SIZ_STRING_LANGID},
{(uint8_t*)CustomHID_StringVendor, CUSTOMHID_SIZ_STRING_VENDOR},
{(uint8_t*)CustomHID_StringProduct, CUSTOMHID_SIZ_STRING_PRODUCT},
{(uint8_t*)CustomHID_StringSerial, CUSTOMHID_SIZ_STRING_SERIAL}
};
如果设备描述符的索引值修改了,那么String_Descriptor数组存放数据的顺序也需要相应的修改。
VID可以通过USB官网:USB_Members查看,比如这里的是ST的VID,0x0483的10进制是1155,我们在USB官网查看,如下:
配置描述符顾名思义就是描述USB设备的配置信息,主要包括以下信息:
const uint8_t CustomHID_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] =
{
0x09, /* bLength: Configuration Descriptor size */
USB_CONFIGURATION_DESCRIPTOR_TYPE, /* bDescriptorType: Configuration */
CUSTOMHID_SIZ_CONFIG_DESC,
/* wTotalLength: Bytes returned */
0x00,
0x01, /* bNumInterfaces: 1 interface */
0x01, /* bConfigurationValue: Configuration value */
0x00, /* iConfiguration: Index of string descriptor describing
the configuration*/
0xC0, /* bmAttributes: Self powered */
0x32, /* MaxPower 100 mA: this current is used for detecting Vbus */
}
这里的接口描述符有1个,由配置描述符指定个数。我们举例说明一下含义。
uint8_t CustomHID_InterfaceDescriptor[ ] =
{
0x09, /* bLength: Interface Descriptor size */
USB_INTERFACE_DESCRIPTOR_TYPE,/* bDescriptorType: Interface descriptor type */
0x00, /* bInterfaceNumber: Number of Interface */
0x00, /* bAlternateSetting: Alternate setting */
0x02, /* bNumEndpoints */
0x03, /* bInterfaceClass: HID */
0x00, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
0x00, /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
0, /* iInterface: Index of string descriptor */
}
首先下载HID参考手册,打开“hid1_11.pdf”文件,找到4小节,这里有对接口描述符的类、子类和协议的解释,由手册可知,bInterfaceSubClass为no boot,bInterfaceProtocol为none,其它位也相同,查找手册,找出对应的解释。
这里对bInterfaceSubClass的boot和no boot的解释手册已经说得很清楚,大致就是说boot是BIOS支持标准类(键盘鼠标等),如果是自定义HID,则选择no boot。
uint8_t CustomHID_FunctionalDescriptor[ ] =
{
0x09, /* bLength: HID Descriptor size */
HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID */
0x10, /* bcdHID: HID Class Spec release number */
0x01,
0x00, /* bCountryCode: Hardware target country */
0x01, /* bNumDescriptors: Number of HID class descriptors to follow */
0x22, /* bDescriptorType */
CUSTOMHID_SIZ_REPORT_DESC,/* wItemLength: Total length of Report descriptor */
0x00,
}
功能描述符主要是为后面的报告描述符做简介,也是通过“hid1_11.pdf”手册查看具体的功能,这里不展开讲解,需要了解的小伙伴自行查找,bDescriptorType=0x22表示功能描述符指向报告描述符。
这里用到两个端点,具体的解释可参见USB -- 初识USB协议(一)。
uint8_t CustomHID_EndpointDescriptor[ ] =
{
0x81, /* bEndpointAddress: Endpoint Address (IN) */
0x03, /* bmAttributes: Interrupt endpoint */
0x02, /* wMaxPacketSize: 2 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (32 ms) */
/* 34 */
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */
/* Endpoint descriptor type */
0x01, /* bEndpointAddress: */
/* Endpoint Address (OUT) */
0x03, /* bmAttributes: Interrupt endpoint */
0x02, /* wMaxPacketSize: 2 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (20 ms) */
}
在设备描述符中已经对制造商描述符、产品描述符和设备序号描述符做了说明,这里仅对语言ID描述符做简单说明。
语言ID描述符相对很简单,就是告诉主机用的哪种语言编码,这里一般选择0x0409,使用美国的编码。一般使用最多的也是美国编码。
/* USB String Descriptors (optional) */
const uint8_t CustomHID_StringLangID[CUSTOMHID_SIZ_STRING_LANGID] =
{
CUSTOMHID_SIZ_STRING_LANGID,
USB_STRING_DESCRIPTOR_TYPE,
0x09,
0x04
}
; /* LangID = 0x0409: U.S. English */
不管是自定义HID还是标准HID设备,都需要一个报告描述符来详细的描述次HID的用途。以下是ST官方的自定义HID报告描述符,这里不细讲,有兴趣的读者可以参见“hid1_11.pdf”手册。
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
{
0x06, 0xFF, 0x00, /* USAGE_PAGE (Vendor Page: 0xFF00) */
0x09, 0x01, /* USAGE (Demo Kit) */
0xa1, 0x01, /* COLLECTION (Application) */
/* 6 */
/* Led 1 */
0x85, 0x01, /* REPORT_ID (1) */
0x09, 0x01, /* USAGE (LED 1) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, 0x01, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, 0x01, /* REPORT_ID (1) */
0x09, 0x01, /* USAGE (LED 1) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 26 */
/* Led 2 */
0x85, 0x02, /* REPORT_ID 2 */
0x09, 0x02, /* USAGE (LED 2) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, 0x01, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, 0x02, /* REPORT_ID (2) */
0x09, 0x02, /* USAGE (LED 2) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 46 */
/* Led 3 */
0x85, 0x03, /* REPORT_ID (3) */
0x09, 0x03, /* USAGE (LED 3) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, 0x01, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, 0x03, /* REPORT_ID (3) */
0x09, 0x03, /* USAGE (LED 3) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 66 */
/* Led 4 */
0x85, 0x04, /* REPORT_ID 4) */
0x09, 0x04, /* USAGE (LED 4) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x95, 0x01, /* REPORT_COUNT (1) */
0xB1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x85, 0x04, /* REPORT_ID (4) */
0x09, 0x04, /* USAGE (LED 4) */
0x91, 0x82, /* OUTPUT (Data,Var,Abs,Vol) */
/* 86 */
/* key Push Button */
0x85, 0x05, /* REPORT_ID (5) */
0x09, 0x05, /* USAGE (Push Button) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x09, 0x05, /* USAGE (Push Button) */
0x75, 0x01, /* REPORT_SIZE (1) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x75, 0x07, /* REPORT_SIZE (7) */
0x81, 0x83, /* INPUT (Cnst,Var,Abs,Vol) */
0x85, 0x05, /* REPORT_ID (2) */
0x75, 0x07, /* REPORT_SIZE (7) */
0xb1, 0x83, /* FEATURE (Cnst,Var,Abs,Vol) */
/* 114 */
/* Tamper Push Button */
0x85, 0x06, /* REPORT_ID (6) */
0x09, 0x06, /* USAGE (Tamper Push Button) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x09, 0x06, /* USAGE (Tamper Push Button) */
0x75, 0x01, /* REPORT_SIZE (1) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
0x75, 0x07, /* REPORT_SIZE (7) */
0x81, 0x83, /* INPUT (Cnst,Var,Abs,Vol) */
0x85, 0x06, /* REPORT_ID (6) */
0x75, 0x07, /* REPORT_SIZE (7) */
0xb1, 0x83, /* FEATURE (Cnst,Var,Abs,Vol) */
/* 142 */
/* ADC IN */
0x85, 0x07, /* REPORT_ID (7) */
0x09, 0x07, /* USAGE (ADC IN) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x26, 0xff, 0x00, /* LOGICAL_MAXIMUM (255) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x82, /* INPUT (Data,Var,Abs,Vol) */
0x85, 0x07, /* REPORT_ID (7) */
0x09, 0x07, /* USAGE (ADC in) */
0xb1, 0x82, /* FEATURE (Data,Var,Abs,Vol) */
/* 161 */
0xc0 /* END_COLLECTION */
}; /* CustomHID_ReportDescriptor */
这里为大家推荐一个HID报告描述符生成器,它能够生成你想要的任何报告描述符,并且操作简单,当然它也有一些标准的HID报告描述符,可以直接拿来使用。我这里选择了标准的鼠标HID报告描述符,见下图。
这里对USB的D+外部上拉电阻进行了配置,如果读者使用的开发板和ST官网的外部上拉不一致,需要修改这个位置的IO口定义。这里也需要注意,ST开发板使用的是PNP型三极管作为上拉电阻的使能信号,所以这里GPIO输出低D+上拉,GPIO输出高D+为低。
非0端点传输函数主要在“usb_endp.c”文件中实现,我们打开usb_endp.c文件,看到只有两个函数,分别是对IN端点和OUT端点的处理,ST的这两个函数是为他自定义的HID软件设计的程序,主要是控制LED灯的亮灭,后面我们会下载ST的自定义HID进行简单的演示。
USB复位函数十分的重要,因为在USB主机识别到设备的时候,首先发出复位指令,USB设备收到复位指令之后,会去初始化一些使用到的端点(端点0必须初始化),然后再通过相应的端点实现设备与主机间的通信。
下图是利用DSVIEW抓取的USB枚举过程的波形,每次在USB设备枚举之前,都会产生一个复位信号。
下面代码是USB复位信号来临之后,USB所做的一些事情,主要就是初始化了端点的状态,为后续枚举过程做准备。
/*******************************************************************************
* Function Name : CustomHID_Reset.
* Description : Custom HID reset routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void CustomHID_Reset(void)
{
/* Set CustomHID_DEVICE as not configured */
pInformation->Current_Configuration = 0;
pInformation->Current_Interface = 0;/*the default Interface*/
/* Current Feature initialization */
pInformation->Current_Feature = CustomHID_ConfigDescriptor[7];
SetBTABLE(BTABLE_ADDRESS);
/* Initialize Endpoint 0 */
SetEPType(ENDP0, EP_CONTROL);
SetEPTxStatus(ENDP0, EP_TX_STALL);
SetEPRxAddr(ENDP0, ENDP0_RXADDR);
SetEPTxAddr(ENDP0, ENDP0_TXADDR);
Clear_Status_Out(ENDP0);
SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
SetEPRxValid(ENDP0);
/* Initialize Endpoint 1 */
SetEPType(ENDP1, EP_INTERRUPT);
SetEPTxAddr(ENDP1, ENDP1_TXADDR);
SetEPRxAddr(ENDP1, ENDP1_RXADDR);
SetEPTxCount(ENDP1, 2);
SetEPRxCount(ENDP1, 2);
SetEPRxStatus(ENDP1, EP_RX_VALID);
SetEPTxStatus(ENDP1, EP_TX_NAK);
/* Set this device to response on default address */
SetDeviceAddress(0);
bDeviceState = ATTACHED;
}
主函数代码相对简单,就是实现了USB的初始化。
/*******************************************************************************
* Function Name : main.
* Description : main routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
int main(void)
{
Set_System();
USB_Interrupts_Config();
Set_USBClock();
USB_Init();
while (1)
{
}
}
通过ST官网下载STM32自定义HID工具,安装好之后,打开软件,能够看到USB HID Target已经找到ST的HID了,具体的操作这里不详细说明,读者可以自己研究。
1. 修改报告描述符的长度,修改如下宏定义,由163改为33.
2. 端点wMaxPacketSize修改,由0x02修改为0x40。
3. 修改报告描述符,这里直接贴代码。
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
{
//0x05, 0x8c, // USAGE_PAGE (ST Page) ,全局项目,指定用户功能,0x8c表示 Bar Code Scanner Page,建议还是改成0xFF(用户自定义)
0x05, 0xff,
0x09, 0x00, // USAGE (Demo Kit) ,用法索引值,表示对项目或者集合建议的用法
0xa1, 0x01, // COLLECTION (Application) ,这是一个主项目,集合,0x01表示这是一个应用集合,性质由前面的用途页定义用户自定义
/*The Input report*/
0x09,0x01, // USAGE ID - Vendor defined
0x15,0x00, // LOGICAL_MINIMUM (0) ,全局项目,逻辑最小值为0
0x26,0x00, 0xFF, // LOGICAL_MAXIMUM (255) ,全局项目,逻辑最大值为255
0x75,0x08, // REPORT_SIZE (8bit) ,全局项目,每个数据域的长度为8bit,即一个字节
0x95,0x40, // REPORT_COUNT (64Byte) , 全局项目,数据域的长度为64个字节
0x81,0x02, // INPUT (Data,Var,Abs) ,主项目,定义输入列表,主机利用该信息解析设备提供的数据(定义一个从设备接收两个字节的输入报表)
/*The Output report*/
0x09,0x02, // USAGE ID - Vendor defined
0x15,0x00, // LOGICAL_MINIMUM (0) ,全局项目,逻辑最小值为0
0x26,0x00,0xFF, // LOGICAL_MAXIMUM (255) ,全局项目,逻辑最大值为255
0x75,0x08, // REPORT_SIZE (8bit) ,全局项目,每个数据域的长度为8bit,即一个字节
0x95,0x40, // REPORT_COUNT (64Byte) ,全局项目,数据域的长度为64个字节
0x91,0x02, // OUTPUT (Data,Var,Abs) ,主项目,创建输出报表(定义一个发送2个字节到设备的输出报表)
0xc0 //END_COLLECTION ,这个主项目用来关闭前面的集合
}; /* CustomHID_ReportDescriptor */
这里直接贴代码。
void Delay(__IO uint32_t nCount);
__IO uint32_t TimingDelay = 0;
__IO uint8_t PrevXferComplete = 1;
__IO uint8_t Receive_Buffer[0x40];
uint32_t Receive_length = 0;
int main(void)
{
Set_System();
USB_Interrupts_Config();
Set_USBClock();
USB_Init();
while (1)
{
if (PrevXferComplete == 1)
{
PrevXferComplete = 0;
UserToPMABufferCopy((uint8_t*)Receive_Buffer, ENDP1_TXADDR, Receive_length);
SetEPTxCount(ENDP1, Receive_length);
SetEPTxValid(ENDP1);
}
}
}
直接贴代码。
extern __IO uint8_t Receive_Buffer[0x40];
extern __IO uint8_t PrevXferComplete;
extern uint32_t Receive_length;
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
/*******************************************************************************
* Function Name : EP1_OUT_Callback.
* Description : EP1 OUT Callback Routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void EP1_OUT_Callback(void)
{
Receive_length = GetEPRxCount(ENDP1);
PMAToUserBufferCopy((uint8_t*)Receive_Buffer, ENDP1_RXADDR, Receive_length);
SetEPRxStatus(ENDP1, EP_RX_VALID);
PrevXferComplete = 1;
}
/*******************************************************************************
* Function Name : EP1_IN_Callback.
* Description : EP1 IN Callback Routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void EP1_IN_Callback(void)
{
}
主要是修改SetEPTxCount(ENDP1, 2)和SetEPRxCount(ENDP1, 2)函数,改为SetEPTxCount(ENDP1, 0x40)和SetEPRxCount(ENDP1, 0x40)。
为了不让其他程序干扰,需要屏蔽以下函数:
其它细节比如中断未用到的ADC和按键中断等,需要自己去屏蔽,也可以不屏蔽。
上位机是QT编写的自定义HID收发软件,这里提供源码给大家下载,并且能够测试HID接收的速度。
我这里以win10进行演示,自定义HID环回测试演示如下:
win7系统可能会出现这类问题,但是只需要向下找到VID是0x0483和PID是0x5750的设备,打开即可。
此上位机软件提供测速功能,修改main函数代码,把PrevXferComplete = 0改为PrevXferComplete = 1。可以看到,HID中断传输的速率大概为1.94Kb/s。
接下来讲解STM32 USB的U盘功能,敬请期待。。。