本次使用的是Keil 5+STM32F103ZE(正点原子的板子)+STD标准库+USB的官方例程修改的。
因为新入职了一家公司,需要使用USB复合设备,不然每次使用都需要切换来切换去的很是麻烦,所以开始学习USB的复合设备相关知识。
本文章需要对事先了解一些USB的基础知识,不然在后面的修改中很容易卡住。
USB设备描述符介绍(转帖):https://www.cnblogs.com/lifexy/p/7634511.html?tdsourcetag=s_pctim_aiomsg
IAD描述符介绍(转帖):https://www.cnblogs.com/shangdawei/p/4712305.html?tdsourcetag=s_pctim_aiomsg
PS:这个IAD描述符是用来连接多个接口描述符用的,请务必认真阅读。
其实多个接口组合在一起有2种情况
第一种叫做USB复合设备(Compound Device):
就是此类设备内部会集成一个HUD,然后多个USB连接到此HUD上,然后每个USB都会有自己独立的PID,VID。
第二种叫做USB组合设备(Composite Device):
此类设备则内部只有一个PID,VID,通过定义多个接口描述符来实现多个功能的组合。
在USB官方例程中,例程名字就是叫做Composite_Example,其实这次的教程其实应该叫做组合设备的,只是大多数人认识叫做复合设备,所以我才写成复合设备,希望大家注意一下。
如果修改USB描述符成功,电脑能识别出来其实就已经成功了一半了
1.USB设备描述符
因为用到IAP描述符(上面的介绍IAD的地址)重点是bDeviceClass,bDeviceSubClass,bDeviceProtocol这3个值一定要改为如下图。
修改后的USB设备描述符如下所示:
/* USB Standard Device Descriptor */
const uint8_t Composite_DeviceDescriptor[Composite_DEVICE_DESC] =
{
0x12, /* 设备描述符的长度bLength */
USB_DEVICE_DESCRIPTOR_TYPE, /* 此描述符类型,这里是USB设备描述符类型bDescriptorType */
0x10,0x01, /* bcdUSB = 2.00 */
0xEF, /* bDeviceClass 设备类 复合设备 */
0x02, /* bDeviceSubClass 设备子类*/
0x01, /* bDeviceProtocol 设备协议*/
0x40, /* bMaxPacketSize0 端点0对应包的的最大包的大小*/
0x83,0x04, /* idVendor = 0x0483 */
0x40,0x57, /* idProduct = 0x5740 */
0x00,0x02, /* bcdDevice = 2.00 */
1, /* Index of string descriptor describing manufacturer */
2, /* Index of string descriptor describing product */
3, /* Index of string descriptor describing the device's serial number */
0x01 /* bNumConfigurations 配置描述符个数*/
}
2.USB配置描述符
USB描述符主要有:USB设备描述符,USB配置描述符,接口描述符,端点描述符,报告描述符(在HID类中采用,在VCP虚拟串口中没有这个描述符)。
但是在USB代码中,将USB配置描述符,接口描述符,端点描述符这3个整为一个USB描述符,然后还是叫做USB配置描述符,关系如下图
/* USB Configuration Descriptor */
/* All Descriptors (配置描述符, 接口描述符, 端点描述符, 类描述符 */
const uint8_t Composite_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] =
{
配置描述符 //Configuration Descriptor 只能有1个
/*功能1——VCP虚拟串口接口*/
IAD描述符 //复合设备才有 在单接口的设备这个可以不要
接口1描述符 //Interface Descriptor
类描述符 //Class Desdriptor
端点描述符 //Endpoint Descriptor
接口2描述符 //Interface Descriptor
类描述符 //Class Desdriptor
端点描述符 //Endpoint Descriptor
端点描述符 //Endpoint Descriptor如果此接口有多个端点,可接着添加端点描述符
/*如果有多个接口 下面还可以继续添加以下描述符*/
/*功能2——HID键盘*/
IAD描述符 //复合设备才有 在单接口的设备这个可以不要
接口描述符 //Interface Descriptor
类描述符 //Class Desdriptor
端点描述符 //Endpoint Descriptor
/*如果有多个接口 下面还可以继续添加以下描述符*/
/*接口3 */
...
}
接下来分析实际的USB配置描述符,这里配置了:
1.HID键盘,一个接口,两个端点4(IN)和端点4(OUT)用于PC接收USB设备发送的键值和USB接收PC发送的数据(比如亮大小写和Num Lock的灯)。
2.VCP虚拟串口,两个接口,三个端点,端点1(IN),端点2(IN),端点3(OUT)。
从设备发送给PC是IN,PC发送给从设备是OUT。
还有需要注意的是端点号不能重复,且端点0不能使用。
/* USB Configuration Descriptor */
/* All Descriptors (Configuration, Interface, Endpoint, Class, Vendor */
const uint8_t Composite_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] =
{
0x09, /* bLength: Configuration Descriptor size */
USB_CONFIGURATION_DESCRIPTOR_TYPE, /* bDescriptorType: Configuration */
CUSTOMHID_SIZ_CONFIG_DESC,
/* wTotalLength: Bytes returned */
0x00,
0x03, /* bNumInterfaces: 3 interface 共3个接口 CDC 2个 HID一个*/
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 HID键盘**************************************/
/*IAD描述符*/
0x08, //bLength:IAD描述符大小
0x0B, //bDescriptorType:IAD描述符类型
0x00, //bFirstInterface:功能1 HID键盘的第一个接口描述符是在总的配置描述符中的第几个从0开始数
0x01, //bInferfaceCount:功能1 HID键盘有1个接口描述符
0x03, //bFunctionClass:同单HID功能时,设备符中的bDeviceClass
0x00, //bFunctionSubClass:同单HID功能时,设备符中的bDeviceSubClass
0x01, //bFunctionProtocol:同单HID功能时,设备符中的bDeviceProtocol
0x00, //iFunction:字符串描述中关于此设备的索引(个人理解是一个字符串描述符中有比如0~5是功能1的字符串,
//6~10是功能2的字符串,如果是功能2的话,此值为6)
/************** Descriptor of Custom HID interface ****************/
/* 09 */
0x09, /* bLength: Interface Descriptor size */
USB_INTERFACE_DESCRIPTOR_TYPE,/* bDescriptorType: Interface descriptor type */
0x00, /* bInterfaceNumber: Number of Interface */ //<接口 0>
0x00, /* bAlternateSetting: Alternate setting */
0x02, /* bNumEndpoints */
0x03, /* bInterfaceClass: HID */
0x01, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
0x01, /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
0, /* iInterface: Index of string descriptor */
/******************** Descriptor of Custom HID HID ********************/
/* 18 */
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,
//KEYBOARD_SIZ_REPORT_DESC,/* wItemLength: Total length of Report descriptor */
0x00,
/******************** Descriptor of Custom HID endpoints ******************/
/* 27 */
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */
0x84, /* bEndpointAddress: Endpoint Address (IN) */
0x03, /* bmAttributes: Interrupt endpoint */
0x08, /* wMaxPacketSize: 8 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (32 ms) */
/* 34 */
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */
0x04, /* bEndpointAddress:Endpoint Address (OUT) */
0x03, /* bmAttributes: Interrupt endpoint */
0x01, /* wMaxPacketSize: 1 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (20 ms) */
/* 41 */
/********************************功能2 VCP虚拟串口*****************************/
/*IAD描述符*/
/* Interface Association Descriptor(IAD Descriptor) */
0x08, /* bLength */
0x0B, /* bDescriptorType*/
0x01, /* bFirstInterface*/
0x02, /* bInterfaceCount*/
0x02, /* bFunctionClass --CDC*/
0x02, /* bFunctionSubClass*/
0x01, /* bFunctionProtocoll*/
0x00, /* iFunction */
/**VCP虚拟串口**/
/*Interface Descriptor接口描述符*/
0x09, /* bLength: Interface Descriptor size */
USB_INTERFACE_DESCRIPTOR_TYPE, /* bDescriptorType: Interface */
/* Interface descriptor type */
0x01, /* bInterfaceNumber: Number of Interface */ //<接口 1>
0x00, /* bAlternateSetting: Alternate setting */
0x01, /* bNumEndpoints: One endpoints used 该接口非0端点数*/
0x02, /* bInterfaceClass: Communication Interface Class */
0x02, /* bInterfaceSubClass: Abstract Control Model */
0x01, /* bInterfaceProtocol: Common AT commands */
0x00, /* iInterface: */
/*Header Functional Descriptor类描述符*/
0x05, /* bLength: Endpoint Descriptor size */
0x24, /* bDescriptorType: CS_INTERFACE */
0x00, /* bDescriptorSubtype: Header Func Desc */
0x10, /* bcdCDC: spec release number */
0x01,
/*Call Management Functional Descriptor*/
0x05, /* bFunctionLength */
0x24, /* bDescriptorType: CS_INTERFACE */
0x01, /* bDescriptorSubtype: Call Management Func Desc */
0x00, /* bmCapabilities: D0+D1 */
0x01, /* bDataInterface: 1 */
/*ACM Functional Descriptor*/
0x04, /* bFunctionLength */
0x24, /* bDescriptorType: CS_INTERFACE */
0x02, /* bDescriptorSubtype: Abstract Control Management desc */
0x02, /* bmCapabilities */
/*Union Functional Descriptor*/
0x05, /* bFunctionLength */
0x24, /* bDescriptorType: CS_INTERFACE */
0x06, /* bDescriptorSubtype: Union func desc */
0x00, /* bMasterInterface: Communication class interface */
0x01, /* bSlaveInterface0: Data Class Interface */
/*Endpoint 2 Descriptor端点描述符*/
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: Endpoint */
0x82, /* bEndpointAddress: (IN2) */
0x03, /* bmAttributes: Interrupt */
VIRTUAL_COM_PORT_INT_SIZE, /* wMaxPacketSize: */
0x00,
0xFF, /* bInterval: */
/*Data class interface descriptor类描述符*/
0x09, /* bLength: Endpoint Descriptor size */
USB_INTERFACE_DESCRIPTOR_TYPE, /* bDescriptorType: */
0x02, /* bInterfaceNumber: Number of Interface *///<接口 2>
0x00, /* bAlternateSetting: Alternate setting */
0x02, /* bNumEndpoints: Two endpoints used */
0x0A, /* bInterfaceClass: CDC */
0x00, /* bInterfaceSubClass: */
0x00, /* bInterfaceProtocol: */
0x00, /* iInterface: */
/*Endpoint 3 Descriptor端点描述符*/
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: Endpoint */
0x03, /* bEndpointAddress: (OUT3) */
0x02, /* bmAttributes: Bulk */
VIRTUAL_COM_PORT_DATA_SIZE, /* wMaxPacketSize: */
0x00,
0x00, /* bInterval: ignore for Bulk transfer */
/*Endpoint 1 Descriptor 端点描述符*/
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: Endpoint */
0x81, /* bEndpointAddress: (IN1) */
0x02, /* bmAttributes: Bulk */
VIRTUAL_COM_PORT_DATA_SIZE, /* wMaxPacketSize: */
0x00,
0x00 /* bInterval */
}; /* CustomHID_ConfigDescriptor */
2.USB HID报告描述符
这里就不过多累赘了,报告描述符是用来描述HID类设备在收发数据的时候,该如何解释数据。比如有个报告描述符,描述了该键盘有8个字节的输入报告(该输入是针对于电脑的输入,设备是输出),发送给电脑,然后输出报告(这个不是必须的,可省略),用来发送给USB设备,控制CAPS LOCK和NUM LOCK灯的亮灭。鼠标只有1个字节的输入报告,用来描述鼠标的左键,右键,和鼠标的移动量。
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
{
//0x05:0000 01 01 这是个全局条目,用途页选择为普通桌面页
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
//0x09:0000 10 01 这是个全局条目,用途选择为键盘
0x09, 0x06, // USAGE (Keyboard)
//0xa1:1010 00 01 这是个主条目,选择为应用集合,
0xa1, 0x01, // COLLECTION (Application)
//0x05:0000 01 11 这是个全局条目,用途页选择为键盘/按键
0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad)
//0x19:0001 10 01 这是个局部条目,用途的最小值为0xe0,对应键盘上的左ctrl键
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
//0x29:0010 10 01 这是个局部条目,用途的最大值为0xe7,对应键盘上的有GUI(WIN)键
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
//0x15:0001 01 01 这是个全局条目,说明数据的逻辑值最小值为0
0x15, 0x00, // LOGICAL_MINIMUM (0)
//0x25:0010 01 01 这是个全局条目,说明数据的逻辑值最大值为1
0x25, 0x01, // LOGICAL_MAXIMUM (1)
//0x95:1001 01 01 这是个全局条目,数据域的数量为8个
0x95, 0x08, // REPORT_COUNT (8)
//0x75:0111 01 01 这是个全局条目,每个数据域的长度为1位
0x75, 0x01, // REPORT_SIZE (1)
//0x81:1000 00 01 这是个主条目,有8*1bit数据域作为输入,属性为:Data,Var,Abs
0x81, 0x02, // INPUT (Data,Var,Abs)
//0x95:1001 01 01 这是个全局条目,数据域的数量为1个
0x95, 0x01, // REPORT_COUNT (1)
//0x75:0111 01 01 这是个全局条目,每个数据域的长度为8位
0x75, 0x08, // REPORT_SIZE (8)
//0x81:1000 00 01 这是个主条目,有1*8bit数据域作为输入,属性为:Cnst,Var,Abs
0x81, 0x03, // INPUT (Cnst,Var,Abs)
//0x95:1001 01 01 这是个全局条目,数据域的数量为6个
0x95, 0x06, // REPORT_COUNT (6)
//0x75:0111 01 01 这是个全局条目,每个数据域的长度为8位
0x75, 0x08, // REPORT_SIZE (8)
//0x25:0010 01 01 这是个全局条目,逻辑最大值为255
0x25, 0xFF, // LOGICAL_MAXIMUM (255)
//0x19:0001 10 01 这是个局部条目,用途的最小值为0
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
//0x29:0010 10 01 这是个局部条目,用途的最大值为0x65
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
//0x81:1000 00 01 这是个主条目,有6*8bit的数据域作为输入,属相为属性为:Data,Var,Abs
0x81, 0x00, // INPUT (Data,Ary,Abs)
//0x25:0010 01 01 这是个全局条目,逻辑的最大值为1
0x25, 0x01, // LOGICAL_MAXIMUM (1)
//0x95:1001 01 01 这是个全局条目,数据域的数量为2
0x95, 0x02, // REPORT_COUNT (2)
//0x75:0111 01 01 这是个全局条目,每个数据域的长度为1位
0x75, 0x01, // REPORT_SIZE (1)
//0x05:0000 01 01 这是个全局条目,用途页选择为LED页
0x05, 0x08, // USAGE_PAGE (LEDs)
//0x19:0001 10 01 这是个局部条目,用途的最小值为0x01,对应键盘上的Num Lock
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
//0x29:0010 10 01 这是个局部条目,用途的最大值为0x02,对应键盘上的Caps Lock
0x29, 0x02, // USAGE_MAXIMUM (Caps Lock)
//0x91:1001 00 01 这是个主条目,有2*1bit的数据域作为输出,属性为:Data,Var,Abs
0x91, 0x02, // OUTPUT (Data,Var,Abs)
//0x95:1001 01 01 这是个全局条目,数据域的数量为1个
0x95, 0x01, // REPORT_COUNT (1)
//0x75:0111 01 01 这是个全局条目,每个数据域的长度为6bit,正好与前面的2bit组成1字节
0x75, 0x06, // REPORT_SIZE (6)
//0x91:1001 00 01 这是个主条目,有1*6bit数据域最为输出,属性为:Cnst,Var,Abs
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0xc0 // END_COLLECTION
}; /* CustomHID_ReportDescriptor */
3.端点缓存地址修改
在usb_conf.h文件中有关于USB端点的配置,我们需要来修改一下。
首先是端点数量,这里是使用的总端点数,我们共使用了端点0~4,共5个,修改如下。
#define EP_NUM (5)
接下来是关于端点的缓存区进行配置,配置每个端点使用的地址在内存中的偏移。在USB模块中有一个所有端点一起使用的RAM区(Packet Buffer)共512字节,其中有一个USB Description Table用于存储每个端点的在RAM区中的偏移和缓存大小。
每个端点都有2个方向(IN/OUT),共有8个端点,所有共有 8x2 = 16个方向,然后每个方向都有2个寄存器,每个寄存器有4字节,所以USB Description Table最大共占内存 16x4x2 = 128字节 = 0x80H(这个是最大值,有些实际未必会用到这么多端点)。
本例子中使用了:
EP0(IN)
EP0(OUT)
EP1(IN)
EP2(IN)
EP3(OUT)
EP4(IN)
EP4(OUT)
共7个端点,所以USB Description Table共占内存 7x4x2 = 56字节 = 0x38H,然后取个整数大于0x38即可,那就取0x40H。所以端点0的发送缓存地址起始地址0x40H。
/*-------------------------------------------------------------*/
/* -------------- Buffer Description Table -----------------*/
/*-------------------------------------------------------------*/
/* buffer table base address */
#define BTABLE_ADDRESS (0x00)
/* EP0 */
/* rx/tx buffer base address */
#define ENDP0_RXADDR (0x40)
#define ENDP0_TXADDR (0x80)
/* EP1 2 3 */
/* tx buffer base address */
#define ENDP1_TXADDR (0xC0)
#define ENDP2_TXADDR (0x100)
#define ENDP3_RXADDR (0x110)
/* EP4 */
/* rx/tx buffer base address */
#define ENDP4_RXADDR (0x150)
#define ENDP4_TXADDR (0x190)
端口的初始化是放在CustomHID_Reset中的,需要添加新加的端口初始化,我是基于HID的例程修改的,所以只需要添加虚拟串口的端点,并且将HID的端口号改为4。
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_BULK);
SetEPTxAddr(ENDP1, ENDP1_TXADDR);
SetEPTxStatus(ENDP1, EP_TX_NAK);
SetEPRxStatus(ENDP1, EP_RX_DIS);
/* Initialize Endpoint 2 */
SetEPType(ENDP2, EP_INTERRUPT);
SetEPTxAddr(ENDP2, ENDP2_TXADDR);
SetEPRxStatus(ENDP2, EP_RX_DIS);
SetEPTxStatus(ENDP2, EP_TX_NAK);
/* Initialize Endpoint 3 */
SetEPType(ENDP3, EP_BULK);
SetEPRxAddr(ENDP3, ENDP3_RXADDR);
SetEPRxCount(ENDP3, VIRTUAL_COM_PORT_DATA_SIZE);
SetEPRxStatus(ENDP3, EP_RX_VALID);
SetEPTxStatus(ENDP3, EP_TX_DIS);
/* Initialize Endpoint 4 */
SetEPType(ENDP4, EP_INTERRUPT);
SetEPTxAddr(ENDP4, ENDP4_TXADDR);
SetEPRxAddr(ENDP4, ENDP4_RXADDR);
SetEPTxCount(ENDP4, 8);
SetEPRxCount(ENDP4, 1);
SetEPRxStatus(ENDP4, EP_RX_VALID);
SetEPTxStatus(ENDP4, EP_TX_NAK);
/* Set this device to response on default address */
SetDeviceAddress(0);
bDeviceState = ATTACHED;
}
只是将VCP例程中的Data_Setup和HID的Data_Setup整合在一起。
RESULT CustomHID_Data_Setup(uint8_t RequestNo)
{
uint8_t *(*CopyRoutine)(uint16_t);
// if (pInformation->USBwIndex != 0) ???????????????????????????????????????????????????
// return USB_UNSUPPORT;
CopyRoutine = NULL;
if (RequestNo == GET_LINE_CODING)
{
if (Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT))
{
CopyRoutine = Virtual_Com_Port_GetLineCoding;
}
}
else if (RequestNo == SET_LINE_CODING)
{
if (Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT))
{
CopyRoutine = Virtual_Com_Port_SetLineCoding;
}
Request = SET_LINE_CODING;
}
else if (RequestNo == GET_DESCRIPTOR)
{
if(Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
{
if (pInformation->USBwValue1 == REPORT_DESCRIPTOR)
{
CopyRoutine = CustomHID_GetReportDescriptor;
}
else if (pInformation->USBwValue1 == HID_DESCRIPTOR_TYPE)
{
CopyRoutine = CustomHID_GetHIDDescriptor;
}
}
}
/*** GET_PROTOCOL, GET_REPORT, SET_REPORT ***/
else if ( (Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT)) )
{
switch( RequestNo )
{
case GET_PROTOCOL:
CopyRoutine = CustomHID_GetProtocolValue;
break;
case SET_REPORT:
CopyRoutine = CustomHID_SetReport_Feature;
Request = SET_REPORT;
break;
default:
break;
}
}
if (CopyRoutine == NULL)
{
return USB_UNSUPPORT;
}
pInformation->Ctrl_Info.CopyData = CopyRoutine;
pInformation->Ctrl_Info.Usb_wOffset = 0;
(*CopyRoutine)(0);
return USB_SUCCESS;
}
其实此时电脑就已经能够同时识别出HID和CDC了,接下来就是发送和接收了,这部分有点USB基础的应该都能做出来,在这里就不过多累赘了。
以下是代码下载地址
USB复合设备代码
https://download.csdn.net/download/juqi_/11220896