STM32的USB枚举过程介绍
之前的说明:
文中大量引用网上资料,在文后已给出资料的引用说明。文件涉及到的USB各种传输包各个位的含义以及USB标准设备请求的含义都没有做说明,推荐看《圈圈教你玩USB》里面有详细的说明
一、枚举前的工作
系统上电后,程序开始运行,简单介绍一下USB的初始化
根据STM32的USB库做移植,介绍枚举过程
SetSystem函数是一些初始化化设置,因为我在之前已经做了初始化的操作,所以把这个函数就删了。
首先系统执行USB中断设置:USB_Interrupts_Config();//设置中断向量表,设置优先级
然后执行USB时钟设置:Set_USBClock();//时钟设置,USB使用48M时钟,
然后执行USB初始化设置:USB_Init();//这个函数比较重要
在执行USB初始化设置主要进行结构体,与函数指针的配置
void USB_Init(void)
{
pInformation = &Device_Info;//当前的连接状态与信息,关于Device_Info的信息,自己go to 去查看
pInformation->ControlState = 2;//当前的控制状态设置为IN_DATA
pProperty = &Device_Property;//设备本身支持的属性和方法
pUser_Standard_Requests =&User_Standard_Requests;//主机请求的实现方法
/* Initialize devices one by one */
pProperty->Init();//回调设备的初始化例程
}
这里面还调用了pProperty->Init();函数进行初始化,它的函数主体是Joystick_init();函数程序执行到Joystick_init();函数,首先Get_SerialNum();获取STM32内部唯一标识码。然后赋给Joystick_StringSerial,不过这个东西好像并没有什么用,因为后面我们在usb_desc.c文件中给改写了。。
好,接下来执行PowerOn();函数,这个函数首先把D+的上拉电阻上电(我使用的是神舟IIIstm32开发板,对应的D+上拉电阻控制位为PG11,我在之前的函数中已经进行了端口的初始化,你们不要忘记呀),这样电脑就可以检测到设备了。(集线器报告设备连接状态,并收到主机指令后,会复位 USB总线,这需要一定的时间(这段时间内设备应该准备好处理复位指令)。但是现在设备初始化程序将继续往下进行,因为它还没有使能复位中断)
接着执行语句
<span style="font-size:10px;"> /*** CNTR_PWDN = 0 ***/
wRegVal = CNTR_FRES;
_SetCNTR(wRegVal);</span>
这两句话的含义实际上是使能了USB模块电源,因为上电复位时,CNTR寄存器的断电控制位PNWN位是1,模块是断电的,语句将强制复位USB模块,直至
wInterrupt_Mask = 0;
_SetCNTR(wInterrupt_Mask);
语句清除复位信号,然后终止复位操作,这个过程中因为复位中断允许位CNTR_RESETM没有被使能,所以,不会触发复位中断,而是间接使PDWN=0,模块开始工作。
wInterrupt_Mask =CNTR_RESETM | CNTR_SUSPM| CNTR_WKUPM;
_SetCNTR(wInterrupt_Mask);
执行语句后,复位中断,挂起中断,唤醒中断被允许,而此时集线器多半已经开始复位端口了,或者说稍微有限延迟,设备固件还能继续初始化一些部件,但已经不会影响整个工作流程了。那么实际上程序确实直接进入了复位中断。
程序直接进入了USB_LP_CAN1_RX0_IRQHandler的中断口,执行 USB_Istr();,通过匹配发现中断源为复位中断,程序运行到复位中断部分,开始执行复位程序(这里提一下,判断发生复位中断后,首先清中断位。在其它几个中断源也是先清中断位,但是CTR_LP();函数之前却没有清中断位是为什么呢?在STM32参考手册中是这样说的:端点在成功完成一次传输后, CTR位会被硬件置起,如果USB_CNTR上的相应位也被设置的话,就会产生中断。与端点相关的中断标志和USB_CNTR寄存器的CTRM位无关。这两个中断标志位将一直保持有效,直到应用程序清除了USB_EpnR寄存器中的相关中断挂起位(CTR位是个只读位)。)
Device_Property.Reset();
这个函数指针,指向的函数是Joystick_Reset。我们跳过来继续看。
voidJoystick_Reset(void)
{
/* Set Joystick_DEVICE as not configured */
pInformation->Current_Configuration = 0;
pInformation->Current_Interface = 0;/*thedefault Interface*/
/* Current Feature initialization */
pInformation->Current_Feature =Joystick_ConfigDescriptor[7];//供电方式,总线供电,自供电
SetBTABLE(BTABLE_ADDRESS);//设置包缓冲区地址
/* Initialize Endpoint 0 */
SetEPType(ENDP0, EP_CONTROL);//端点0为控制端点
SetEPTxStatus(ENDP0, EP_TX_STALL);//端点状态为发送无效,也就是主机IN令牌包来的时候,回送一个STALL
SetEPRxAddr(ENDP0, ENDP0_RXADDR);//设置端点0的描述符表,包括接收缓冲区地址,最大允许接收的字节数、发送缓冲区地址三个量。
SetEPTxAddr(ENDP0, ENDP0_TXADDR);//发送缓冲区地址
Clear_Status_Out(ENDP0);///清除EP_KIND的STATUS_OUT位,如果改位被设置, 在控制模式下只对0字节数据包相应。其它的都返回STALL。主要用于控制传输的状态过程
SetEPRxCount(ENDP0,Device_Property.MaxPacketSize);///接收缓冲区支持64个字节
SetEPRxValid(ENDP0);//使能端点0的接收,因为很快就要接收SETUP令牌包了
/* Initialize Endpoint 1 */
SetEPType(ENDP1, EP_INTERRUPT);//端点1设为中断端点
SetEPTxAddr(ENDP1, ENDP1_TXADDR);//设置发送缓冲区地址
SetEPTxCount(ENDP1, 4);//每次发送4个字节
SetEPRxStatus(ENDP1, EP_RX_DIS);///接收禁止,只发送 Mouse 信息,而不从主机接收
SetEPTxStatus(ENDP1, EP_TX_NAK);///现在发送端点还不允许发送数据
/* Set this device to response on defaultaddress */
SetDeviceAddress(0);//址默认为 0.
bDeviceState = ATTACHED;//接状态改为已经连接,默认地址状态
}
这个复位的过程我们用一句话总结就是端点初始化。至此RESET中断处理完成,因为我们也没有设置RESET_CALLBACK,所以也不会有RESET_Callback();,不过它本身也是个空函数。在执行完复位函数后PC指针又回到main函数中来,Joystick_init函数往下执行, USB_SIL_Init();函数中使能所有常用中断前面说道,当开复位中断后,程序进入中断,那么复位中断执行完成以后,真正的枚举的过程(获取描述符)并没有开始。而是有一段空余的时间,这段时间内,程序执行了上面的提到的USB_SIL_Init();直至USB_Init();函数结束。那么真正的USB枚举过程都是在中断中完成。
通过我调试发现,在完成复位中断后,主机好像并没有发送获取描述符的命令,在这个过程中,系统两次进入挂起中断,然后触发唤醒中断,然后复位中断。我个人的理解是,这段时间因为stm32没有收到sof,所有触发挂起中断,当设备发送获取设备描述符命令时,触发唤醒中断,在唤醒中断中,系统设置恢复正常工作。至于最后的复位请求,我表示我也不知道了。。。(这里需要说一下,在移植的挂起中断处理程序中,针对stm32部分也做了挂起处理,我觉得(实际上是我看以前的前辈们都觉得)这样做没必要。所以,只保留了USB挂起,而对整个stm32的挂起被屏蔽掉了。)
(此时pInformation->ControlState=IN_DATA, bDeviceState=UNCONNECTED)
二、枚举过程
枚举过程就是USB设备与主机通讯获取设备的一系列描述符的过程。我们贴图理解
据说这是圈圈大神整理的枚举过程图,接下来我们来一步一步的看枚举的过程。整个枚举的过程以我实际调试的结果为准。
1、获取设备描述符
注:packet包是实际传输时的数据包。
首先是建立连接阶段,在经过漫长的等待以后,主机发出对地址0、端点0发出SETUP令牌包和数据包,然后设备返回ACK包。点0寄存器的CTR_RX被置位为1, ISTR的CTR置位为1, DIR=1, EP_ID=0,表示端点0接收到主机来的请求数据。此时设备已经ACK主机,将触发正确传输完成中断。程序进入CTR_LP();中断,我们来详细看一下
void CTR_LP(void)
{
__IO uint16_t wEPVal = 0;
/* stay in loop while pending interrupts */
while (((wIstr = _GetISTR()) & ISTR_CTR)!= 0)
{
/* extract highest priority endpoint number*/
EPindex = (uint8_t)(wIstr & ISTR_EP_ID);//判断读取端点号
if (EPindex == 0)
{
SaveRState = _GetENDPOINT(ENDP0);//USB端点0寄存器 USB_EP0R中的数据
SaveTState = SaveRState & EPTX_STAT;//获取发送状态码
SaveRState &= EPRX_STAT;
_SetEPRxTxStatus(ENDP0,EP_RX_NAK,EP_TX_NAK);//将TXRX的状态都改为NAK
if ((wIstr & ISTR_DIR) == 0)//判断是IN包
{
_ClearEP_CTR_TX(ENDP0);
In0_Process();
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
return;
}
else//判断是OUT包或SETUP包
{
wEPVal = _GetENDPOINT(ENDP0);
if ((wEPVal &EP_SETUP) != 0)//判断是SETUP包
{
Setup0_Process();
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
return;
}
else if ((wEPVal & EP_CTR_RX) !=0)//判断是OUT包
{
_ClearEP_CTR_RX(ENDP0);
Out0_Process();
_SetEPRxTxStatus(ENDP0,SaveRState,SaveTState);
return;
}
}
}/* if(EPindex == 0) */
else//判断是非0端点包
{
}/*if(EPindex == 0) else */
}/* while(...) */
}
其实以上这些判断在实际的USB协议中,都在令牌包中都有体现,但是STM32的USB硬件都帮我们分析了,这些分析的结果以寄存器的形式显示可以看到程序跳转执行Setup0_Process();我们跟踪过来继续看它的跟踪过程
uint8_tSetup0_Process(void)
{
union
{
uint8_t* b;
uint16_t* w;
} pBuf;
uint16_t offset = 1;
pBuf.b = PMAAddr + (uint8_t*)(_GetEPRxAddr(ENDP0) * 2); /* *2 for 32 bits addr 要读取数据的地址*/
if (pInformation->ControlState!= PAUSE)
{
pInformation->USBbmRequestType =*pBuf.b++; /* bmRequestType */
pInformation->USBbRequest = *pBuf.b++;/* bRequest */
pBuf.w += offset; /* word not accessed because of 32 bitsaddressing */
pInformation->USBwValue =ByteSwap(*pBuf.w++); /* wValue */
pBuf.w += offset; /* word not accessed because of 32 bitsaddressing */
pInformation->USBwIndex = ByteSwap(*pBuf.w++); /* wIndex */
pBuf.w += offset; /* word not accessed because of 32 bitsaddressing */
pInformation->USBwLength = *pBuf.w; /*wLength */
}//将USB标准设备请求赋给对应的结构体
pInformation->ControlState = SETTING_UP;
if (pInformation->USBwLength == 0)
{
/* Setup with no data stage */
NoData_Setup0();
}
<span style="color:#ff0000;">else</span>
{
/* Setup with data stage */
Data_Setup0();
}
return Post0_Process();
}
因为这个标准设备请求是来要求设备返回设备描述符的,需要返回数据,所以程序会跳转到Data_Setup0();执行在这个函数里分析要返回的数据。
voidData_Setup0(void)
{
uint32_t Request_No =pInformation->USBbRequest;//获取请求代码
/*GET DESCRIPTOR*/
if (<spanstyle="color:#ff0000;">Request_No ==GET_DESCRIPTOR</span>)//判断为获取设备描述符
{
if(<span style="color:#ff0000;">Type_Recipient ==(STANDARD_REQUEST | DEVICE_RECIPIENT)</span>)//标准请求
{
uint8_t wValue1 =pInformation->USBwValue1;
if (<spanstyle="color:#ff0000;">wValue1 ==DEVICE_DESCRIPTOR</span>)//判断是获取设备描述符
{
<spanstyle="color:#ff0000;">CopyRoutine =pProperty->GetDeviceDescriptor;//复制数据到CopyRoutine</span>
}
}
}
if (<spanstyle="color:#ff0000;">CopyRoutine</span>)//有数据将一些信息写入控制结构体
{
pInformation->Ctrl_Info.Usb_wOffset =wOffset;
pInformation->Ctrl_Info.CopyData =CopyRoutine;
/* sb in the original the cast to word wasdirectly */
/* now the cast is made step by step */
(*CopyRoutine)(0);//这个函数这里调用的目的只是设置了 pInformation中需要写入的描述符的长度
Result = USB_SUCCESS;
}
if (<spanstyle="color:#ff0000;">ValBit(pInformation->USBbmRequestType,7)</span>)//判断是从设备到主机
{
/* Device ==> Host */
__IO uint32_t wLength =pInformation->USBwLength;
/* Restrict the data length to be the onehost asks for */
if (pInformation->Ctrl_Info.Usb_wLength> wLength)
{//设备描述符长度为18
pInformation->Ctrl_Info.Usb_wLength =wLength;
}
pInformation->Ctrl_Info.PacketSize =pProperty->MaxPacketSize;
<spanstyle="color:#ff0000;">DataStageIn();</span>//完成描述符的输出准备
}
return;
}
最后的重点落在 DataStageIn();函数上,我们来看一下
voidDataStageIn(void)
{
ENDPOINT_INFO *pEPinfo =&pInformation->Ctrl_Info;
uint32_t save_wLength =pEPinfo->Usb_wLength;//还需要发送多少字节
uint32_t ControlState =pInformation->ControlState;
DataBuffer= (*pEPinfo->CopyData)(Length);//取得要发送的数据设备描述符
UserToPMABufferCopy(DataBuffer,GetEPTxAddr(ENDP0), Length);将设备描述符复制到用户的发送缓冲区
SetEPTxCount(ENDP0, Length);//设置发送字节数目
pEPinfo->Usb_wLength -= Length;//等于0
pEPinfo->Usb_wOffset += Length;//偏移到18
vSetEPTxStatus(EP_TX_VALID);///使能端点发送,只要主机的 IN令牌包一来,SIE就会将描述符返回给主机
USB_StatusOut();/* 这个实际上是使接收也有效,主机可取消 IN
Expect_Status_Out:
pInformation->ControlState =ControlState;
}
这一次的CTR中断就执行完了,就等着,主机再发IN包把数据读回去。
接下来就是可选的数据传输阶段。主机首先发一个 IN令牌包,由于端点0发送有效, SIE将数据返回主机。主机方返回一个 ACK后,主机发送数据的 CTR标志置位, DIR=0, EP_ID=0,表明主机正确收到了用户发过去的描述符。固件程序由此进入IN中断。所以接下来进入用 In0_Process()进行处理。但是此时设备描述符返回成功了。同样的在用In0_Process()函数中,其目的只是期待主机的0状态字节输出了。
uint8_tIn0_Process(void)
{
uint32_t ControlState =pInformation->ControlState;
if ((ControlState == IN_DATA) || (<spanstyle="color:#ff0000;">ControlState == LAST_IN_DATA</span>))
{
<span style="color:#ff0000;">DataStageIn();//此次调用后,当前状态变成WAIT_STATUS_OUT,表明设备等待状态过程,主机输出0字节</span>
/* ControlState may be changed outside thefunction */
ControlState =pInformation->ControlState;
}
else if (ControlState == WAIT_STATUS_IN)
{
}
else
{
ControlState = STALLED;
}
pInformation->ControlState = ControlState;
return Post0_Process();
}
我们进入DataStageIn();函数
voidDataStageIn(void)
{
ENDPOINT_INFO *pEPinfo =&pInformation->Ctrl_Info;
uint32_t save_wLength = pEPinfo->Usb_wLength;
uint32_t ControlState =pInformation->ControlState;
uint8_t *DataBuffer;
uint32_t Length;
if ((save_wLength == 0) && (<spanstyle="color:#ff0000;">ControlState == LAST_IN_DATA</span>))
{
if(Data_Mul_MaxPacketSize == TRUE)
{
/* No more data to send and empty packet*/
Send0LengthData();
ControlState = LAST_IN_DATA;
Data_Mul_MaxPacketSize = FALSE;
}
<spanstyle="color:#ff0000;"> else </span>
{
/* No more data to send so STALL the TXStatus*/
ControlState = WAIT_STATUS_OUT;
vSetEPTxStatus(EP_TX_STALL);
}
<spanstyle="color:#ff0000;">goto Expect_Status_Out;</span>
}
Length = pEPinfo->PacketSize;
ControlState = (save_wLength <= Length) ?LAST_IN_DATA : IN_DATA;
if (Length > save_wLength)
{
Length = save_wLength;
}
DataBuffer = (*pEPinfo->CopyData)(Length);
UserToPMABufferCopy(DataBuffer,GetEPTxAddr(ENDP0), Length);
SetEPTxCount(ENDP0, Length);
pEPinfo->Usb_wLength -= Length;
pEPinfo->Usb_wOffset += Length;
vSetEPTxStatus(EP_TX_VALID);
USB_StatusOut();/* Expect the host to abortthe data IN stage */
Expect_Status_Out:
pInformation->ControlState =ControlState;
}
主机收到18个字节的描述符后,进入状态事务过程,此过程的主句发送令牌包为OUT,和0字节的数据包,设备需要回一个ACK。中断处理程序会进入Out0_Process()。由于此时状态为WAIT_STATUS_OUT,所以执行以下这段
<spanstyle="font-size:10px;">uint8_t Out0_Process(void)
{
uint32_t ControlState =pInformation->ControlState;
if ((ControlState == IN_DATA) ||(ControlState == LAST_IN_DATA))
{
}
else if ((ControlState == OUT_DATA) ||(ControlState == LAST_OUT_DATA))
{
}
else if (<spanstyle="color:#ff0000;">ControlState ==WAIT_STATUS_OUT</span>)
{
(*pProperty->Process_Status_OUT)();//空函数什么也没干
<spanstyle="color:#ff0000;"> ControlState = STALLED;</span>
}
/* Unexpect state, STALL the endpoint */
else
{
ControlState = STALLED;
}
pInformation->ControlState = ControlState;
return Post0_Process();
}</span>
2、主机发送复位命令
获取设备描述符后,主机再一次复位设备。设备又进入初始状态
3、设置地址
1)重新从复位状态开始
在第一次获取设备描述符后,程序使端点 0 的发送和接收都无效,状态也设置为STALLED,所以主机先发一个复位,使得端点 0接收有效。 虽然说在 NAK和STALL状态下,端点仍然可以响应和接收 SETUP包。
2)设置地址的建立阶段:
主机先发一个 SETUP令牌包,设备端 EP0的 SETUP标志置位。然后主机发了一个OUT 包,共 8个字节,里面包含设置地址的要求(USB标准设备请求)。设备在检验数据后,发一个 ACK握手包。同时 CTR_RX 置位,CTR置位。数据已经保存到 RxADDR 所指向的缓冲区。此时 USB产生数据接收中断。由于 CTR_RX和 SETUP同时置位, 终端处理程序调用 Setup0_Process(),所做的工作仍然是先填充 pInformation结构, 获取请求特征码、 请求代码和数据长度。由于设置地址不会携带数据,所以接下来调用 NoData_Setup0()。执行以下代码:
void NoData_Setup0(void)
{
RESULT Result = USB_UNSUPPORT;
uint32_t RequestNo = pInformation->USBbRequest;
uint32_t ControlState;
if (Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
{
if (RequestNo == SET_CONFIGURATION)
{
}
/*SET ADDRESS*/
else if (RequestNo== SET_ADDRESS)
{
if ((pInformation->USBwValue0 > 127)|| (pInformation->USBwValue1 != 0)
|| (pInformation->USBwIndex != 0)
||(pInformation->Current_Configuration != 0))
/* Device Address should be 127 orless*/
{
ControlState = STALLED;
goto exit_NoData_Setup0;
}
else
{
Result =USB_SUCCESS;//说明设置地址没有做任何工作
}
}
/*SET FEATURE for Device*/
else if (RequestNo == SET_FEATURE)
{
}
/*Clear FEATURE for Device */
else if (RequestNo == CLEAR_FEATURE)
{
}
}
/* Interface Request*/
else if (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
{
}
/* EndPoint Request*/
else if (Type_Recipient == (STANDARD_REQUEST | ENDPOINT_RECIPIENT))
{
}
else
{
Result = USB_UNSUPPORT;
}
if (Result != USB_SUCCESS)
{
}
if (Result != USB_SUCCESS)
{
}
ControlState = WAIT_STATUS_IN;/* After nodata stage SETUP */
USB_StatusIn();
exit_NoData_Setup0:
pInformation->ControlState = ControlState;
return;
}
USB_StatusIn(); //这句话是一个关键,它是一个宏,实际是准备好发送 0 字节的状态数据包。 因为地址设置没有数据过程, 建立阶段后直接进入状态阶段,主机发 IN令牌包,设备返回 0 字节数据包,主机再 ACK。
它对应的宏是这样的:
#define USB_StatusIn() Send0LengthData() //准备发送 0 字节数据
#define Send0LengthData() { _SetEPTxCount(ENDP0, 0); \
vSetEPTxStatus(EP_TX_VALID); \ //设置发送有效,发送字节数为 0}
3)设置地址的状态阶段:
而前面把状态设置为 WAIT_STATUS_IN是给 IN令牌包的处理提供指示。因为建立阶段结束以后, 主机接着发一个 IN 令牌包, 设备返回 0 字节数据包,主机返回ACK包,进入中断。本次中断由 IN0_Process()函数来处理,追踪进入,它执行以下代码:
uint8_t In0_Process(void)
{
uint32_t ControlState = pInformation->ControlState;
if ((ControlState == IN_DATA) || (ControlState == LAST_IN_DATA))
{
}
else if (ControlState == WAIT_STATUS_IN)
{
if ((pInformation->USBbRequest== SET_ADDRESS) &&
(Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT)))
{
SetDeviceAddress(pInformation->USBwValue0);
pUser_Standard_Requests->User_SetDeviceAddress();//这个函数就一个赋值语句, bDeviceState = ADDRESSED。
}
(*pProperty->Process_Status_IN)();
ControlState = STALLED;
}
else
{
ControlState = STALLED;
}
pInformation->ControlState = ControlState;
return Post0_Process();
}
执行设置地址操作、 采用新地址后, 把设备的状态改为 STALLED。 而在处理的出口中调用 Post0_Process()函数,这个所做的工作是:
SetEPRxCount(ENDP0,Device_Property.MaxPacketSize);//将端点 0 的缓冲区大小设置为 64字节
if (pInformation->ControlState== STALLED)
{
vSetEPRxStatus(EP_RX_STALL);
vSetEPTxStatus(EP_TX_STALL);
}//将端点 0 的发送和接收都设置为: STALL,这种状态下只接受 SETUP 令牌包
4、从新地址获取设备描述符
因为过程类似,这里就简单说一下
1)上一阶段末尾的状态
端点 0的发送和接收都设置为: STALL,只接收 SETUP令牌包。
2)建立阶段:主机发令牌包、数据包、设备回应ACK
产生数据接收中断,且端点 0 的SETUP 置位,调用 Setup0_Process() 函数进行处理。在Setup0_Process()中,因为主机发送了请求数据 8 个字节。由调用Data_Setup0()函数进行处理。 首先是获取设备描述符的长度,描述符的起始地址,传送的最大字节数,根据这些参数确定本次能够传输的字节数,然后调用DataStageIn()函数进行实际的数据传输操作,设备描述符必须在本次中断中就写入发送缓冲区,因为很快就要进入数据阶段了。在函数处理的最后:
vSetEPTxStatus(EP_TX_VALID);
USB_StatusOut();/* 本来期待 IN令牌包,但用户可以取消数据阶段,一般不会用到 */
3)数据阶段:主机发IN包,设备返回数据,主机发ACK
本次操作会产生数据发送完成中断, 由 In0_Process(void)来处理中断, 它也调用DataStageIn()函数来进行处理。如果数据已经发送完:
ControlState = WAIT_STATUS_OUT;
vSetEPTxStatus(EP_TX_STALL);//转入状态阶段。
有可能的话:
Send0LengthData();
ControlState = LAST_IN_DATA;
Data_Mul_MaxPacketSize = FALSE;//这一次发送 0 个字节,状态转为最后输入阶段。
否则,继续准备数据,调整剩余字节数、发送指针位置,等待主机的下一个 IN令牌包。
4)状态阶段:主机发OUT包、 0字节包,设备发送ACK
数据发送完成中断, 调用 Out0_Process(void)函数进行处理,由于在数据阶段的末尾已经设置设备状态为: WAIT_STATUS_OUT, 所以处理函数基本上没有做什么事,就退出了。并将状态设为 STALLED
5、获取配置描述符
以后的过程大概类似就不做详细说明了。只做几点说明。按照网上的说法这次其实只是获取9字节的配置描述符,然后通过获取配置描述符的第二个字节(从0开始算)得知整个描述符集合(配置描述符、接口描述符、HID描述符(最后两个字节为下级描述符的长度(报告描述符))、端点描述符)共有多少个字节。然后获取整个描述符的集合。但是我在自己调试的时候,发现USB直接获取了整个描述符集合的数据。不知到为啥?
6、获取字符串描述符。
通过实际调试发现,获取三次字符串描述符,分别是,主机获取字符串序号描述符、主机获取字符串ID描述符、主机获取字符串产品描述符。
7、主机再次获取设备描述符和配置描述符集合
在这一步骤,主机进行了三次USB标准设备请求,分别为获取设备描述符、获取配置描述符、获取配置描述符集合。
8、主机设置配置
建立阶段:主机发 SETUP包、发请求数据包( DATA0 包)、用户发 ACK。
进入 CTR中断,用户调用 Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用NoData_Setup0()函数进行处理。判断为设置配置后, 调用Standard_SetInterface()函数将设备状态结构体的当前配置改为主机数据中的配置参数。同时调用用户的设置配置函数,将设备状态改为“ configured” 。退出时,将控制传输状态改为:ControlState= WAIT_STATUS_IN,进入状态阶段。
设备期待主机的IN令牌包,返回状态数据。状态阶段:主机发IN令牌、 设备返回0Setup0_Process()函数进行处理,取得请求数据后,由于没有数据传输阶段,该函数调用 NoData_Setup0()函数进行处理。设置空闲时一个类特殊请求, 其特征码为0x21, 2表示类请求而不是标准请求,1表示接收对象是接口而不是设备。USB的底层并不支持类特殊请求,它将调用上层函数提供的函数:
if (Result !=USB_SUCCESS)
{
Result =(*pProperty->Class_NoData_Setup)(RequestNo); //这里就是调用用户提供的类特殊请求的处理函数。 结果发现用户 提供的类特殊请求(针对无数据情况)只支持SET_PROTOCOL。针对有数据情况只支持: GET_PROTOCOL。
if ((Type_Recipient==(CLASS_REQUEST |INTERFACE_RECIPIENT))&& (RequestNo == SET_PROTOCOL)
{
return Joystick_SetProtocol();
}
}
9、主机获取报告描述符
建立阶段:主机发SETUP包、发请求数据包( DATA0包)、用户ACK。进入CTR中断,获取描述符是一个标准请求,但是报告描述符并不是需要通用实现的,所以在底层函数中没有实现。跟踪Setup0_Process(void)——进入Data_Setup(void)函数,它是这么处理的:
void Data_Setup0(void)
{
uint8_t *(*CopyRoutine)(uint16_t);
RESULT Result;
uint32_t Request_No = pInformation->USBbRequest;
uint32_t Related_Endpoint, Reserved;
uint32_t wOffset, Status;
CopyRoutine = NULL;
wOffset = 0;
/*GET DESCRIPTOR*/
if (Request_No == GET_DESCRIPTOR)
{
if (Type_Recipient== (STANDARD_REQUEST | DEVICE_RECIPIENT))
{//在这里面并没有获得有用的信息因为它并不是一个标准设备请求
uint8_t wValue1 =pInformation->USBwValue1;
if (wValue1 == DEVICE_DESCRIPTOR)
{
CopyRoutine =pProperty->GetDeviceDescriptor;
}
else if (wValue1 ==CONFIG_DESCRIPTOR)
{
CopyRoutine =pProperty->GetConfigDescriptor;
}
else if (wValue1 == STRING_DESCRIPTOR)
{
CopyRoutine =pProperty->GetStringDescriptor;
} /* End of GET_DESCRIPTOR */
}
}
/*GET STATUS*/
else if ((Request_No == GET_STATUS) &&(pInformation->USBwValue == 0)
&&(pInformation->USBwLength == 0x0002)
&&(pInformation->USBwIndex1 == 0))
{
/* GET STATUS for Device*/
if ((Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
&& (pInformation->USBwIndex== 0))
{
CopyRoutine = Standard_GetStatus;
}
/* GET STATUS for Interface*/
else if (Type_Recipient ==(STANDARD_REQUEST | INTERFACE_RECIPIENT))
{
if(((*pProperty->Class_Get_Interface_Setting)(pInformation->USBwIndex0, 0)== USB_SUCCESS)
&&(pInformation->Current_Configuration != 0))
{
CopyRoutine = Standard_GetStatus;
}
}
/* GET STATUS for EndPoint*/
else if (Type_Recipient ==(STANDARD_REQUEST | ENDPOINT_RECIPIENT))
{
Related_Endpoint =(pInformation->USBwIndex0 & 0x0f);
Reserved = pInformation->USBwIndex0& 0x70;
if (ValBit(pInformation->USBwIndex0,7))
{
/*Get Status of endpoint & stallthe request if the related_ENdpoint
is Disabled*/
Status =_GetEPTxStatus(Related_Endpoint);
}
else
{
Status =_GetEPRxStatus(Related_Endpoint);
}
if ((Related_Endpoint <Device_Table.Total_Endpoint) && (Reserved == 0)
&& (Status != 0))
{
CopyRoutine = Standard_GetStatus;
}
}
}
/*GET CONFIGURATION*/
else if (Request_No == GET_CONFIGURATION)
{
if (Type_Recipient == (STANDARD_REQUEST |DEVICE_RECIPIENT))
{
CopyRoutine = Standard_GetConfiguration;
}
}
/*GET INTERFACE*/
else if (Request_No == GET_INTERFACE)
{
if ((Type_Recipient == (STANDARD_REQUEST |INTERFACE_RECIPIENT))
&& (pInformation->Current_Configuration!= 0) && (pInformation->USBwValue == 0)
&& (pInformation->USBwIndex1== 0) && (pInformation->USBwLength == 0x0001)
&&((*pProperty->Class_Get_Interface_Setting)(pInformation->USBwIndex0, 0)== USB_SUCCESS))
{
CopyRoutine = Standard_GetInterface;
}
}
if (CopyRoutine)
{
pInformation->Ctrl_Info.Usb_wOffset =wOffset;
pInformation->Ctrl_Info.CopyData =CopyRoutine;
/* sb in the original the cast to word wasdirectly */
/* now the cast is made step by step */
(*CopyRoutine)(0);///这个函数这里调用的目的只是设置了 pInformation中需要写入的描述符的长度
Result = USB_SUCCESS;
}
else//最终通过调用用火的类特殊实现来获取报告描述符
{
Result =(*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
if (Result == USB_NOT_READY)
{
pInformation->ControlState = PAUSE;
return;
}
}
if (pInformation->Ctrl_Info.Usb_wLength == 0xFFFF)
{
/* Data is not ready, wait it */
pInformation->ControlState = PAUSE;
return;
}
if ((Result == USB_UNSUPPORT) || (pInformation->Ctrl_Info.Usb_wLength== 0))
{
/* Unsupported request */
pInformation->ControlState = STALLED;
return;
}
if (ValBit(pInformation->USBbmRequestType, 7))//设备到主机
{
/* Device ==> Host */
__IO uint32_t wLength =pInformation->USBwLength;
/* Restrict the data length to be the onehost asks for */
if (pInformation->Ctrl_Info.Usb_wLength> wLength)
{
pInformation->Ctrl_Info.Usb_wLength =wLength;
}
else if(pInformation->Ctrl_Info.Usb_wLength < pInformation->USBwLength)
{
if(pInformation->Ctrl_Info.Usb_wLength < pProperty->MaxPacketSize)
{
Data_Mul_MaxPacketSize = FALSE;
}
else if((pInformation->Ctrl_Info.Usb_wLength % pProperty->MaxPacketSize) == 0)
{
Data_Mul_MaxPacketSize = TRUE;
}
}
pInformation->Ctrl_Info.PacketSize =pProperty->MaxPacketSize;
DataStageIn();
}
else
{
pInformation->ControlState = OUT_DATA;
vSetEPRxStatus(EP_RX_VALID); /*enable for next data reception */
}
return;
}
可见核心函数只支持设备描述符、配置描述符以及字符串描述符。 最终该函数将调用:
Result= (*pProperty->Class_Data_Setup)(pInformation->USBbRequest);
调用用户的类特殊实现来获取报告描述符,它的函数本体是Joystick_Data_Setup函数,同时 HID 类描述符也是通过这种方式取得的,我们来看一下
RESULTJoystick_Data_Setup(uint8_t RequestNo)
{
uint8_t *(*CopyRoutine)(uint16_t);
CopyRoutine = NULL;
if ((RequestNo == GET_DESCRIPTOR)
&& (Type_Recipient ==(STANDARD_REQUEST | INTERFACE_RECIPIENT))
&& (pInformation->USBwIndex0== 0))
{
if (pInformation->USBwValue1== REPORT_DESCRIPTOR)
{
CopyRoutine = Joystick_GetReportDescriptor;
}
else if (pInformation->USBwValue1 ==HID_DESCRIPTOR_TYPE)
{
CopyRoutine = Joystick_GetHIDDescriptor;
}
} /* End of GET_DESCRIPTOR */
/*** GET_PROTOCOL ***/
else if ((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT))
&& RequestNo ==GET_PROTOCOL)
{
CopyRoutine = Joystick_GetProtocolValue;
}
if (CopyRoutine == NULL)
{
return USB_UNSUPPORT;
}
pInformation->Ctrl_Info.CopyData =CopyRoutine;
pInformation->Ctrl_Info.Usb_wOffset = 0;
(*CopyRoutine)(0);
return USB_SUCCESS;
}
最后通过DataStageIn做发送准备。主机发送IN令牌包时,数据就被发送出去了
下面贴一下我自己调试时,发送的一些串口的数据。
usb触摸鼠标。
RESET中断。
SUSP中断。
SUSP中断。
WKUP中断。
RESET中断。
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x40//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
RESET中断。
标准请求:
0x0 0x5 0x2 0x0 0x0 0x0 0x0 0x0//主机设置地址
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x12//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00xff//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32 0x9 0x4 0x0 0x0 0x1 0x3 0x1 0x2 0x0 0x9 0x21 0x10 0x1 0x0 0x1 0x22 0x4a0x0 0x7 0x5 0x81 0x3 0x4 0x0 0x20//设备发送配置描述符
标准请求:
0x80 0x6 0x3 0x3 0x9 0x4 0x00xff//主机获取字符串序号描述符
发送数据:
0x1a 0x3 0x34 0x0 0xff 0x0 0xd40x0 0x5 0x0 0x33 0x0 0x46 0x0 0x34 0x0 0x30 0x0 0x35 0x0 0x83 0x0 0x12 0x0 0x430x0//设备发送字符串序号描述符
标准请求:
0x80 0x6 0x0 0x3 0x0 0x0 0x00xff//主机获取字符串ID描述符
发送数据:
0x4 0x3 0x9 0x4 //设备发送字符串ID描述符
标准请求:
0x80 0x6 0x2 0x3 0x9 0x4 0x00xff//主机获取字符串产品描述符
发送数据:
0x1e 0x3 0x53 0x0 0x54 0x0 0x4d0x0 0x33 0x0 0x32 0x0 0x20 0x0 0x4a 0x0 0x6f 0x0 0x79 0x0 0x73 0x0 0x74 0x00x69 0x0 0x63 0x0 0x6b 0x0//设备发送字符串产品描述符
标准请求:
0x80 0x6 0x0 0x1 0x0 0x0 0x00x12//主机获取设备描述符
发送数据:
0x12 0x1 0x0 0x2 0x0 0x0 0x0 0x400x88 0x88 0x3 0x0 0x0 0x2 0x1 0x2 0x3 0x1//设备发送设备描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00x9//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32//设备发送配置描述符
标准请求:
0x80 0x6 0x0 0x2 0x0 0x0 0x00x22//主机获取配置描述符
发送数据:
0x9 0x2 0x22 0x0 0x1 0x1 0x0 0xa00x32 0x9 0x4 0x0 0x0 0x1 0x3 0x1 0x2 0x0 0x9 0x21 0x10 0x1 0x0 0x1 0x22 0x4a0x0 0x7 0x5 0x81 0x3 0x4 0x0 0x20 //设备发送配置描述符
标准请求:
0x0 0x9 0x1 0x0 0x0 0x0 0x00x0///主机设置配置
标准请求:
0x21 0xa 0x0 0x0 0x0 0x0 0x00x0//HID设置SET IDLE
标准请求:
0x81 0x6 0x0 0x22 0x0 0x0 0x00x8a//主机获取报告描述符
发送数据:
0x5 0x1 0x9 0x2 0xa1 0x1 0x9 0x10xa1 0x0 0x5 0x9 0x19 0x1 0x29 0x3 0x15 0x0 0x25 0x1 0x95 0x3 0x75 0x1 0x81 0x20x95
0x1 0x75 0x5 0x81 0x1 0x5 0x1 0x90x30 0x9 0x31 0x9 0x38 0x15 0x81 0x25 0x7f 0x75 0x8 0x95 0x3 0x81 0x6 0xc0 0x90x3c
0x5 0xff 0x9 0x1 0x15 0x0 0x250x1 0x75 0x1 0x95//设备发送报告描述符
发送数据:
0x2 0xb1 0x22 0x75 0x6 0x95 0x10xb1 0x1 0xc0//设备发送报告描述符
然后又用Bus Hound抓了一下USB的包数据,如下所示
看捕捉的数据,也是发现有不理解的地方,我们看USB主机与28通信的数据(42-47),是对USB设置进行枚举的过程,但是在与28通信的过程,并没有对于字符串描述符的获取,后来仔细查看,原来字符串描述符是在7端口获取(36-40),而且还在28端口进行数据通信之前。这个是为什么呢?
最后说明:文中大量引用网络资料,自己只是做了少量修改,引用资料包括:
1网络文档:USB基本知识(主要是http://www.usr.cc/thread-51423-1-1.html)
2网络文档:基于STM32的USBUSB程序开发笔记
3书籍《圈圈教你玩USB》