目录
1. SETUP阶段
【标准请求】
a. GET_STATUS
b. CLEAR_FEATURE
c. SET_FEATURE
d. SET_ADDRESS
e. GET_DESCRIPTOR
f. SET_DESCRIPTOR
g. GET_CONFIGURATION
h. SET_CONFIGURATION
i. GET_INTERFACE
j. SET_INTERFACE
k. SYNCH_FRAME
2. DATA阶段
【Data In过程】
【Data Out过程】
3. STATUS过程
【Status In过程】
【Status Out过程】
实例
总结一下STM32F103的USB端点0的数据处理过程。一个请求过程可能包含三个过程。第一个阶段是SETUP阶段,主机发送SETUP数据;第二个阶段是可选的,主机和设备之间传输数据;第三个阶段,主机和设备之间传输状态,状态的方向和数据方向相反,如果没有数据(即没有第二阶段),状态的方向是从设备到主机。
更详细的内容可以参考USB规范第9章: https://usb.org/document-library/usb-20-specification
用一个变量gUsbDevState.state记录USB的状态,开始的时候USB的状态为USB_CTRL_IDLE。
1. SETUP阶段
HOST第一笔数据是SETUP数据,即告知Device接下来的数据要如何处理。从USB buffer中读入8字节数据获取Setup信息。获取到SETUP数据前把gUsbDevState.state设置为USB_CTRL_SETUP。
下面是这8个字节具体对应的含义。
#pragma pack (1)
typedef struct _stUSBReqInfo
{uint8_t bmRequestType;uint8_t bRequest;uint16_t wValue;uint16_t wIndex;uint16_t wLength;
}stUSBReqInfo;
#pragma pack ()
设备根据bmRequestType和bRequest解析执行USB命令。下图是获取设备描述符的SETUP数据:
bmRequestType:
Bit 7 |
Bit 6:5 |
Bit 4:0 |
表示后续数据包(必须是DATA0)的方向,0为OUT,1为IN |
请求类型,0表示标准请求,1表示Class的请求,2表示厂商的请求,3是预留值 |
接收这个SETUP包的对象,0是设备,1是接口,2是端点,3是其他未知的,4-31是预留值。 |
bRequest、wValue、wIndex、wLength和具体的bmRequestType有关。bRequest一般表示请求类型,wValue一般表示传送参数给对象标明这是什么请求,wLength表示下一阶段发送数据的长度。
【标准请求】
USB2.0定义了如下的标准请求:
#define GET_STATUS 0
#define CLEAR_FEATURE 1
#define SET_FEATURE 3
#define SET_ADDRESS 5
#define GET_DESCRIPTOR 6
#define SET_DESCRIPTOR 7
#define GET_CONFIGURATION 8
#define SET_CONFIGURATION 9
#define GET_INTERFACE 10
#define SET_INTERFACE 11
#define SYNCH_FRAME 12
a. GET_STATUS
返回选定接收者的状态。bmRequestType中的Bit 4:0指明接收对象。返回值是指定接收者的状态。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Get_Status | 10000000B 10000001B 10000010B |
GET_STATUS | 0 | 0(返回设备状态) 接口号(对象是接口时) 端点号(对象是端点时) |
2 | 设备状态 |
GET_STATUS固定返回2个字节。
对象是设备时:返回设备的Remote Wakeup使能和设备是总线供电还是自供电。
uint8_t status[2] = {0, 0};
if((gUsbDevInfo.feature & (1 << 5)) > 0) //Remote Wakeup, 1: enabled, 0: disabled
{status[0] |= 0x02;
}
if((gUsbDevInfo.feature & (1 << 6)) > 0) //1:Bus-powered, 0: Self-powered
{status[0] |= 0x01;
}
对象是接口时:固定返回2字节0x00
端点:主要是返回端点是否为STALL状态。wIndex表示哪个端点和端点方向。
uint8_t ep = (gUsbReqInfo.wIndex & 0x7f);
uint32_t epStatus = usbGetEpStatus(ep);
if(gUsbReqInfo.wIndex & 0x80)
{if(epStatus == USB_EP_TX_STALL)status[0] |= 0x01;
}
else
{if(epStatus == USB_EP_RX_STALL)status[0] |= 0x01;
}
b. CLEAR_FEATURE
用来清除或禁止某个指定的属性(属性与具体的实现有关,值的具体定义应该与驱动有关)。该命令不会返回数据。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Clear_Feature | 00000000B 00000001B 00000010B |
CLEAR_FEATURE | 特性选择符(1表示设备,0表示端点) | 0 接口号 端点号 |
0 | 无 |
当对象是设备时:禁止Remote Wakeup
gUsbDevInfo.feature = (uint8_t)(~(1 << 5)); //Disable Remote Wakeup
当对象是端点时:清除对应端点的STALL状态。
if(gUsbReqInfo.wValue == ENDPOINT_HALT)
{uint8_t ep = gUsbReqInfo.wIndex & 0x7F;if((gUsbReqInfo.wIndex & 0x80) > 0){//IN EndpointusbSetEpStatus(ep, USB_EP_TX_VALID);}else{//OUT EndpointusbSetEpStatus(ep, USB_EP_RX_VALID);}
}
c. SET_FEATURE
用来设置或使能某个指定的属性(与CLEAR_FEATURE对应)。该命令不会返回数据。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Set_Feature | 00000000B 00000001B 00000010B |
SET_FEATURE | 特性选择符(1表示设备,0表示端点) | 0 接口号 端点号 |
0 | 无 |
这里wIndex是一直为0,如果不为0(同时bmRequestType为0),则表示TEST_MODE。这里不支持这种情况。具体可以参考USB2.0的规格书。
当对象是设备时:使能Remote Wakeup
gUsbDevInfo.feature |= (1 << 5); //Enable Remote Wakeup
当对象是端点时:设置对应端点为STALL状态。
uint8_t ep = gUsbReqInfo.wIndex & 0x7F;
if((gUsbReqInfo.wIndex & 0x80) > 0)
{//IN EndpointusbSetEpStatus(ep, USB_EP_TX_STALL);
}
else
{//OUT EndpointusbSetEpStatus(ep, USB_EP_RX_STALL);
}
d. SET_ADDRESS
设置设备的USB地址。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
SET_ADDRESS | 00000000B | SET_ADDRESS | 设备地址 | 0 | 0 | 无 |
这里不要直接设置新的地址,等状态传输(第三阶段结束)完后再设置新的地址。
e. GET_DESCRIPTOR
如果描述符存在,则此请求返回指定的描述符。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Get_Descriptor | 10000000B | GET_DESCRIPTOR | 描述表种类(高字节)和索引(低字节) | 0或语言标志 | 描述表长度 | 描述表 |
wValue描述表种类(高字节)和索引(低字节),
uint16_t type = gUsbReqInfo.wValue & 0xFF00;
uint16_t index = gUsbReqInfo.wValue & 0x00FF;
其中有7种标准描述符(USB2.0):
#define DEVICE_DESC_TYPE 0x0100
#define CONFIGURATION_DESC_TYPE 0x0200
#define STRING_DESC_TYPE 0x0300
#define INTERFACE_DESC_TYPE 0x0400
#define ENDPOINT_DESC_TYPE 0x0500
#define DEV_QUALIFIER_DESC_TYPE 0x0600
#define SPEED_CONFIGURATION_DESC_TYPE 0x0700
其中DEV_QUALIFIER_DESC_TYPE和SPEED_CONFIGURATION_DESC_TYPE是USB2.0设备才有的描述符。
f. SET_DESCRIPTOR
此请求是可选的,可用于更新现有描述符或添加新描述符。
g. GET_CONFIGURATION
此请求返回当前设备配置值。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Get_Configuration | 10000000B | GET_CONFIGURATION | 0 | 0 | 1 | 配置值 |
固定返回1字节配置值。
h. SET_CONFIGURATION
此请求设备配置值。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Set_Configuration | 00000000B | SET_CONFIGURATION | 配置值(高字节为0,低字节表示要设置的配置值) | 0 | 0 | 无 |
if (gUsbReqInfo.wValue <= usbDeviceDescriptor[USB_DEVICE_DESC_LEN - 1]&& (gUsbReqInfo.wIndex == 0)) /*call Back usb spec 2.0*/
{gUsbDevInfo.curConfig = gUsbReqInfo.wValue;if (gUsbDevInfo.curConfig != 0){gUsbDevState.state = USB_STATE_CONFIGURED;}
}
i. GET_INTERFACE
此请求返回指定接口的备用设置。某些USB设备的配置具有互斥设置的接口。此请求允许主机确定当前选定的备用设置。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Get_Interface | 10000001B | GET_INTERFACE | 0 | 接口号 | 1 | 可选设置 |
j. SET_INTERFACE
此请求允许主机为指定的接口选择备用设置。如果设备只支持指定接口的默认设置,则可以在请求状态阶段返回STALL。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Set_Interface | 00000001B | SET_INTERFACE | 可选设置 | 接口号 | 0 | 无 |
k. SYNCH_FRAME
此请求用于设置和报告端点的同步帧。
命令 | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
Synch_Frame | 10000010B | SYNCH_FRAME | 0 | 端点号 | 2 | 帧号 |
其他Class和Vender的请求根据具体的应用另外处理。
switch(gUsbReqInfo.bmRequestType & USB_REQ_TYPE_MASK)
{case USB_REQ_TYPE_STANDARD:usbSetupStdReq();break;case USB_REQ_TYPE_CLASS:gUsbDataInfo.dataBuf = usbUsrClassReq();break;case USB_REQ_TYPE_VENDOR:gUsbDataInfo.dataBuf = usbUsrVenderReq();break;default:gUsbReqInfo.wLength = USB_UNSUPPORT;break;
}
2. DATA阶段
bmRequestType的bit7表示接下来数据的方向。如果方向是IN(例如Get Descripter的情况),STM32F103需要立刻发送数据给主机,即使数据长度为0也需要发送一个空包。如果是OUT,分2种情况,如果数据长度为0(例如Set Address的情况),则表示下一个阶段是STATUS IN,如果大于0(标准USB请求没有这个情况,虚拟串口里面的SET_LINE_CODING会是这种情况),STM32F103不是即时接收主机的数据,需要等下一次CTR中断判断进入OUT中断才读取接收缓冲中的数据。
if((gUsbReqInfo.bmRequestType & USB_REQ_DIR_MASK) == USB_REQ_DIR_IN)
{gUsbDevState.ctrlState = USB_CTRL_IN;
}
else
{gUsbDevState.ctrlState = USB_CTRL_DATA_OUT_WAIT;if(gUsbReqInfo.wLength == 0)gUsbDevState.ctrlState = USB_CTRL_STATUS_IN;
}
【Data In过程】
Data In是设备发送数据给主机的过程。USB通讯过程都是由主机发起的,主机会在SETUP阶段通知设备接下来的数据类型和长度,SETUP阶段把数据长度设置成3种情况:错误或不支持(-1)、0字节(即NO DATA的情况)、需要返回数据(即有DATA的情况)。
a. 错误或不支持的情况
如果SETUP数据有错误,这个阶段应该将端点设置为STALL状态通知主机发生错误。如果是SETUP中要求的数据类型不支持,则可以返回空包或者将端点设置为STALL状态?
if(gUsbReqInfo.wLength == USB_UNSUPPORT)
{usbSetEpStatus(USB_EP0, USB_EP_TX_STALL);gUsbDevState.ctrlState = USB_CTRL_IDLE;return;
}
b. NO DATA的情况
gUsbReqInfo.wLength有2种情况下回等于0,一种是主机发送的请求没有数据(例如Set Address),这种情况下SETUP阶段(gUsbReqInfo.bmRequestType & USB_REQ_DIR_MASK) == USB_REQ_DIR_OUT,所以此次IN的过程实际是USB_CTRL_STATUS_IN, USB协议要求设备要在50ms内完成这个阶段。另外一种情况是最后一笔数据正好是64字节,这时候需要多发一个空包。
if(gUsbReqInfo.wLength == 0)
{if(gUsbDevState.ctrlState == USB_CTRL_DATA_IN_LAST){usbWriteBuf(USB_EP0, 0, 0); //Send Empty PacketgUsbDevState.ctrlState = USB_CTRL_STATUS_OUT;}else{usbWriteBuf(USB_EP0, 0, 0); //Send Empty PacketgUsbDevState.ctrlState = USB_CTRL_IDLE;}return;
}
c. DATA的情况
STM32F103的端点0一次最大只能传输64字节,所以如果数据大于64字节,主机会发送多次Data In请求。当数据正好是64字节时,需要发送一个空包。
count = usbWriteBuf(USB_EP0, (uint8_t *)gUsbDataInfo.dataBuf + gUsbDataInfo.offset, gUsbReqInfo.wLength);
gUsbReqInfo.wLength -= count;
gUsbDataInfo.offset += count;
if(gUsbReqInfo.wLength == 0)
{if(count < EP0_MEM_SIZE)gUsbDevState.ctrlState = USB_CTRL_STATUS_OUT;else//count == EP0_MEM_SIZEgUsbDevState.ctrlState = USB_CTRL_DATA_IN_LAST_IDLE;
}
elsegUsbDevState.ctrlState = USB_CTRL_DATA_IN_WAIT;
【Data Out过程】
标准的EP0过程基本没有Out数据的过程。具体的Class则需要另外处理。
if(gUsbDevState.ctrlState == USB_CTRL_OUT)
{usbUsrOut0();
}
3. STATUS过程
【Status In过程】
Status In一般情况是只要发送一个空包即可,特殊情况是Set Address。Set Address在SETUP、DATA IN和STATUS IN都不会改变设备的USB地址,而是在Set Address通信结束后再设置地址。
if(gUsbDevState.ctrlState == USB_CTRL_STATUS_IN)
{if(gUsbDevState.state == USB_STATE_ADDRESS){usbWriteBuf(USB_EP0, 0, 0); //Send Empty PacketgUsbDevState.state = USB_STATE_ADDRESSED;}else if(gUsbDevInfo.address != 0) //SetAddress{Printf("Set Address:%d\r\n", gUsbDevInfo.address);usbSetAddress(gUsbAddress);gUsbDevInfo.address = 0;gUsbDevState.ctrlState = USB_CTRL_IDLE;}else{usbWriteBuf(USB_EP0, 0, 0); //Send Empty PacketgUsbDevState.ctrlState = USB_CTRL_IDLE;}
}
【Status Out过程】
Status Out只需要设置RX有效即可。
if(gUsbDevState.ctrlState == USB_CTRL_STATUS_OUT)
{usbSetEpStatus(USB_EP0, USB_EP_RX_VALID);gUsbDevState.ctrlState = USB_CTRL_IDLE;
}
实例
主机第一笔数据是要求设备提供设备描述符,这个通信过程包括Setup + Data In With Data + Status Out。
1. STM32F103产生一个Setup Out中断,从STM32F103的USB Buffer中读入8字节,即Setup数据
usbReadBuf(USB_EP0, (uint8_t *)(&gUsbReqInfo), sizeof(gUsbReqInfo));
其中gUsbReqInfo.bRequest == USB_GET_DESCRIPTOR,
(uint8_t)((gUsbReqInfo.wValue & 0xFF00) >> 8) == USB_DEVICE_DESCRIPTOR_TYPE
STM32F103需要返回数组usbDeviceDescriptor,长度固定为18字节。
2. State由USB_CTRL_SETUP转为USB_CTRL_IN,STM32F103随即发送18字节给主机。由于设备描述符长度小于64字节,所以一次发送完成,状态由USB_CTRL_IN改为USB_CTRL_STATUS_OUT,等待EP0 Out中断。
3. Out中断后设置Rx为Valid,并将State改为USB_CTRL_IDLE。
4. STM32F103产生一个IN中断表明数据已经接收到,即如果当前状态为USB_CTRL_IDLE,IN中断不需要做处理。
随后主机会设置USB地址(即Set Address),这个通信过程包括Setup + Data In With No Data + Status In + Set Address.
1. 设置USB地址成立条件gUsbReqInfo.bRequest == USB_SET_ADDRESS。
2. State由USB_CTRL_SETUP转为USB_CTRL_STATUS_IN,即发送一个空包结束Set Address通信过程。
3. STM32F103产生一个IN中断表明主机也确认OK,这里要将设备的地址改为新的地址。
主机会再次读入设备描述符确认地址是否设置成功,如果再次读入设备描述符不正确,则会再尝试写新的地址+读入设备描述符,失败则会停止枚举。如果是正确的,接下来就需要读入配置描述符。主机会读2次配置描述符,第一次固定读入9字节,获取全部配置描述符的长度,然后再读入全部的配置描述符。
第一读配置描述符数据:
第二次读入配置描述符:
1. gUsbReqInfo.bRequest == USB_GET_DESCRIPTOR, (uint8_t)((gUsbReqInfo.wValue & 0xFF00) >> 8) == USB_CONFIGURATION_DESCRIPTOR_TYPE, STM32F103返回数组usbDeviceConfigDescriptor,长度与具体应用有关。这里虚拟串口的配置描述符长度为67字节。
2. State由USB_CTRL_SETUP转为USB_CTRL_IN,STM32F103随即发送64字节给主机,由于还有3字节还没有发送,所以State还是USB_CTRL_IN,等待下一次的IN中断。
3. STM32F103产生一个IN中断继续发送剩下的3字节数据。然后状态由USB_CTRL_IN改为USB_CTRL_STATUS_OUT,等待EP0 Out中断。
4. Out中断后设置Rx为Valid,并将State改为USB_CTRL_IDLE。
5. STM32F103产生一个IN中断表示主机已经接收到数据,即如果当前状态为USB_CTRL_IDLE,IN中断不需要做处理。
如果主机得到正确的配置描述符,会发Get Status获取设备状态。
然后主机会读一次265字节长度的配置描述符确认通信是否正常。主机还会读取3个字符串描述符,可以把Serial Number的字符串描述符设置为64字节验证一下正好长度为64字节的情况。
当主机确认枚举成功后会发送SET_CONFIGURATION。
Response是空包,所以没有返回数据。
这样就完成了枚举过程。